Skip to main content

Migrating from Terragrunt

Terragrunt and Atmos solve similar problems—managing Terraform at scale with DRY configurations. If you're coming from Terragrunt, this guide will help you understand the differences and migrate your infrastructure.

Key Differences at a Glance

ConceptTerragruntAtmos
Configuration FormatHCL (terragrunt.hcl)YAML (.yaml)
Reuse Mechanisminclude {} blocksimport: with deep merge
Dependenciesdependency blockssettings.depends_on or !terraform.output
Variable Passinginputs = {}vars: with inheritance
Module Sourceterraform { source = "..." }metadata.component
CLIterragrunt plan/applyatmos terraform plan/apply
State BackendPer-directory backend {}Centralized in stack config
EnvironmentsDirectory structureStack files (YAML)
UnitsDirectory with terragrunt.hclComponent instance in a stack
StacksCollection of units (terragrunt.stack.hcl)Stack file (e.g., prod.yaml)

What Atmos Has That Terragrunt Doesn't

Beyond the conceptual differences, Atmos provides several capabilities that don't exist in Terragrunt:

FeatureDescription
Native AuthenticationBuilt-in multi-cloud auth with SAML, SSO, OIDC, and GitHub Actions. No separate tools needed—atmos auth login handles it all.
VendoringPull and version external modules locally with atmos vendor pull. Customize vendored code while tracking upstream.
Custom CommandsDefine your own CLI commands in YAML. No scripting needed—integrate team-specific workflows directly into Atmos.
WorkflowsOrchestrate multi-step operations across components and stacks. Chain commands, add conditions, run in parallel.
Terraform Shellatmos terraform shell vpc -s prod drops you into a configured shell for native Terraform debugging. All vars and backend pre-configured.
Affected Detectionatmos describe affected analyzes Git changes to find impacted components—purpose-built for CI/CD.
Component ValidationJSON Schema and OPA policy validation for stack configurations before deployment.
Stack Describeatmos describe component shows the fully-resolved configuration for any component in any stack.
Configuration Provenanceatmos describe component --provenance traces where every value came from across the import hierarchy.

Directory Structure Comparison

Terragrunt uses HCL syntax for configuration, which might feel familiar if you're accustomed to Terraform—but it can also be confusing since it's not actually Terraform code. You're writing HCL that looks like Terraform but behaves differently, with proprietary functions like find_in_parent_folders() and dependency blocks that don't exist in Terraform.

infrastructure/
├── terragrunt.hcl # Root config (HCL, not Terraform)
├── _envcommon/
│ ├── vpc.hcl # Shared config (HCL, not Terraform)
│ └── eks.hcl
├── prod/
│ ├── us-east-1/
│ │ ├── vpc/
│ │ │ └── terragrunt.hcl # More HCL config
│ │ └── eks/
│ │ └── terragrunt.hcl
│ └── terragrunt.hcl
└── dev/
└── us-east-1/
├── vpc/
│ └── terragrunt.hcl
└── eks/
└── terragrunt.hcl

Characteristics:

  • HCL config files scattered across directories
  • Environment structure baked into folder hierarchy
  • Each component needs its own terragrunt.hcl
  • Easy to confuse Terragrunt HCL with actual Terraform code

Key difference: Atmos cleanly separates components (native Terraform code) from stacks (YAML configuration). With Terragrunt, HCL configuration files are scattered throughout your directory structure alongside—and sometimes confused with—actual Terraform code.

Configuration Lives in YAML, Not the Filesystem

Atmos philosophically treats the filesystem as organization only—it doesn't influence behavior. All configuration semantics live in YAML stack files and their imports. This means:

  • You can query any component's fully-resolved configuration with atmos describe component
  • You can trace where any value came from with atmos describe component --provenance
  • You can output configuration as JSON/YAML for tooling integration
  • Directory structure is purely for human organization, not runtime behavior

This is fundamentally different from Terragrunt, where the directory hierarchy is the configuration—path_relative_to_include() and find_in_parent_folders() derive meaning from filesystem location.

Concept Mapping

If you're familiar with Terragrunt, the concepts below will help you translate what you already know into Atmos equivalents. Each section shows a side-by-side comparison so you can see exactly how Terragrunt patterns map to Atmos.

Include → Import

Terragrunt uses include blocks with proprietary functions like find_in_parent_folders() to locate and merge parent configurations. You must specify whether to expose included values, and the path resolution can be complex with nested directory structures.

terragrunt.hcl

include "root" {
path = find_in_parent_folders()
}

include "envcommon" {
path = "${dirname(find_in_parent_folders())}/_envcommon/vpc.hcl"
expose = true
}

Dependency → Remote State

Terragrunt uses dependency blocks to reference outputs from other modules. You specify a relative path to the dependent module's directory, and Terragrunt reads its state file to extract outputs. This creates implicit ordering and requires the dependency to be applied first. Since it's all HCL, the line between "configuration" and "business logic" can blur—you're writing code that looks like Terraform but isn't.

eks/terragrunt.hcl

dependency "vpc" {
config_path = "../vpc"
}

inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}

Inputs → Vars

Terragrunt passes variables to Terraform using the inputs block. These values become -var arguments when Terraform runs. The inputs block uses HCL syntax with equals signs and curly braces.

vpc/terragrunt.hcl

inputs = {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
environment = "production"
}

Terraform Source → Component + Vendoring

Terragrunt specifies the Terraform module source directly in each terragrunt.hcl file. The source is fetched at runtime, and you manage versions by changing the ref parameter. Each environment directory needs its own copy of this source reference.

vpc/terragrunt.hcl

terraform {
source = "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git?ref=v5.0.0"
}

Generate Blocks → Purpose-Built Generation

Terragrunt uses generate blocks to create arbitrary files before Terraform runs. You write file contents as heredoc strings within HCL, specifying the path and overwrite behavior. This gives you full templating power, but that flexibility comes with complexity—you're essentially writing a code generator in HCL.

terragrunt.hcl

generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${local.aws_region}"
}
EOF
}

Remote State Configuration

Terragrunt configures remote state in the root terragrunt.hcl using a remote_state block. The path_relative_to_include() function dynamically generates state keys based on directory structure. Child configurations inherit this through include blocks.

terragrunt.hcl (root)

remote_state {
backend = "s3"
config = {
bucket = "terraform-state"
region = "us-east-1"
key = "${path_relative_to_include()}/terraform.tfstate"
}
}

Locals → Vars with Inheritance

Terragrunt uses locals blocks for computed values and string interpolation. You define local variables with HCL syntax and reference them using local.variable_name. These are scoped to the current file and its includes.

terragrunt.hcl

locals {
account_id = "123456789012"
region = "us-east-1"
cluster_name = "${local.account_id}-${local.region}-eks"
}

inputs = {
cluster_name = local.cluster_name
}

run_cmd → YAML Functions

Terragrunt's run_cmd() function executes shell commands and captures their output. You pass the command and arguments as separate strings, and the result can be assigned to a local variable.

terragrunt.hcl

locals {
account_id = run_cmd("aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text")
}

extra_arguments → Component Settings

Terragrunt's extra_arguments block lets you append additional CLI arguments to Terraform commands. You specify which commands (plan, apply, etc.) receive the extra arguments and what those arguments should be.

terragrunt.hcl

terraform {
extra_arguments "common_vars" {
commands = ["plan", "apply"]
arguments = ["-var-file=common.tfvars"]
}
}

Units and Stacks

Terragrunt recently introduced formal Units and Stacks concepts (GA in v0.78.0, May 2025). Here's how they map to Atmos:

In Terragrunt terminology:

  • Unit: A directory containing a terragrunt.hcl file—a single instance of infrastructure with its own state. Each unit represents one deployment of a Terraform module.

  • Stack: A collection of units that can be managed together. Terragrunt supports both implicit stacks (directory-based) and explicit stacks (defined in terragrunt.stack.hcl files).

terragrunt.stack.hcl

# Explicit stack definition
unit "vpc" {
source = "../units/vpc"
values = {
environment = "prod"
cidr_block = "10.0.0.0/16"
}
}

unit "eks" {
source = "../units/eks"
values = {
environment = "prod"
cluster_name = "main"
}
}
Terragrunt ConceptAtmos EquivalentNotes
Unit (terragrunt.hcl)Component instance in components.terraformSame concept: single deployable with own state
Implicit Stack (directory)Stack fileAtmos uses explicit YAML files, not directories
Explicit Stack (terragrunt.stack.hcl)Stack file (.yaml)Both define collections of units/components
unit {} blockComponent entry under components.terraformAtmos uses YAML; Terragrunt uses HCL
values = {}vars:Both pass variables to the underlying module
Atmos Was Stack-First From the Start

Terragrunt evolved from a directory-per-unit model and later added explicit stack support. Atmos was designed stack-first—every deployment is defined in a stack file, making the relationship between components explicit and queryable from day one.

Function Mapping

Terragrunt exposes many built-in functions in HCL. Here's how they map to Atmos equivalents:

Path and Directory Functions

Terragrunt FunctionAtmos EquivalentNotes
find_in_parent_folders()import:Atmos uses explicit imports instead of searching
path_relative_to_include()AutomaticState keys generated from stack/component names
path_relative_from_include()AutomaticNot needed—Atmos handles paths
get_terragrunt_dir()N/AComponents are always in components/terraform/
get_working_dir()N/AAtmos manages working directories
get_parent_terragrunt_dir()N/AFlat imports replace hierarchy
get_repo_root()!repo-rootYAML function returns repo root path
get_path_from_repo_root()!repo-rootCombine with path manipulation

Environment and Platform Functions

Terragrunt FunctionAtmos EquivalentNotes
get_env("VAR", "default")!env VARYAML function for environment variables
get_platform()N/AUse !exec if needed
get_aws_account_id()!exec!exec aws sts get-caller-identity --query Account
get_aws_account_alias()!execRun AWS CLI command
get_aws_caller_identity_arn()!execRun AWS CLI command
get_aws_caller_identity_user_id()!execRun AWS CLI command

Command Execution

Terragrunt FunctionAtmos EquivalentNotes
run_cmd("cmd", "arg1", ...)!exec!exec cmd arg1 (inline syntax)

Configuration Reading

Terragrunt FunctionAtmos EquivalentNotes
read_terragrunt_config()import:Import other stack configs
read_tfvars_file()!includevars: !include path/to/file.tfvars

State and Outputs

Terragrunt FunctionAtmos EquivalentNotes
dependency.X.outputs.Y!terraform.output!terraform.output component.output_name
N/A!terraform.stateRead arbitrary state attributes

Secret Management

Terragrunt FunctionAtmos EquivalentNotes
sops_decrypt_file()!storeUse store integrations (SSM, Secrets Manager, etc.)
YAML Functions vs Templates

Atmos offers two ways to access dynamic values:

  • YAML Functions (!exec, !env, !terraform.output) — Preferred. Validated at parse time, readable, works with YAML tooling.
  • Go Templates ({{ env "VAR" }}, {{ exec "cmd" }}) — Escape hatch. Use when YAML functions don't cover your use case.

See YAML Functions for the complete reference.

CLI Command Comparison

Terragrunt commands are directory-based—you cd into a component's directory and run commands there. For multi-component operations, run-all traverses the directory tree and executes in dependency order.

# Plan a single component
cd prod/us-east-1/vpc
terragrunt plan

# Apply a single component
cd prod/us-east-1/vpc
terragrunt apply

# Plan all components
terragrunt run-all plan

# Apply all components
terragrunt run-all apply

# Show outputs
terragrunt output

Migration Steps

Step 1: Convert terragrunt.hcl to Stack YAML

Each Terragrunt directory has its own terragrunt.hcl with include paths, source references, and inputs. The configuration is scattered across the directory tree with implicit relationships based on folder hierarchy.

prod/us-east-1/vpc/terragrunt.hcl

include "root" {
path = find_in_parent_folders()
}

terraform {
source = "../../../../modules/vpc"
}

inputs = {
cidr_block = "10.0.0.0/16"
environment = "prod"
region = "us-east-1"
}

Step 2: Move Terraform Root Modules

What's a Root Module?

In Terraform, a root module is the top-level directory where you run terraform plan and terraform apply. It has its own state file. Child modules are reusable building blocks called from root modules—they don't have their own state.

In Atmos terminology, root modules are called components. Each component is a self-contained Terraform configuration that gets deployed independently with its own state.

Terragrunt typically references root modules from a modules/ directory (or pulls from remote sources). These are standard Terraform root modules that work independently.

modules/
├── vpc/ # Root module (has state)
│ ├── main.tf
│ └── variables.tf
└── eks/ # Root module (has state)
├── main.tf
└── variables.tf

Step 3: Update Backend Configuration

Terragrunt manages state backend configuration in the root terragrunt.hcl. The path_relative_to_include() function generates unique state keys based on the directory structure, ensuring each component has its own state file.

terragrunt.hcl (root)

remote_state {
backend = "s3"
config = {
bucket = "terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
}
}

Migration Checklist

  • Install Atmos CLI (Installation Guide)
  • Create atmos.yaml configuration
  • Move Terraform root modules to components/terraform/
  • Convert terragrunt.hcl files to stack YAML
  • Extract common config to _defaults/
  • Convert dependency blocks to remote state
  • Update backend configuration
  • Test with atmos terraform plan
  • Update CI/CD pipelines
  • Train team on new commands

Stack Naming for Migrations

Every Atmos command requires a stack name—whether you're running atmos terraform plan -s <stack>, listing stacks with atmos list stacks, or referencing dependencies. You need to define how Atmos determines these names.

Additionally, if you want Atmos to automatically determine Terraform workspaces, those workspace names should follow a consistent convention that Atmos can compute.

You have two options:

If you have consistent context variables across all your stacks, configure name_template in atmos.yaml to programmatically compute stack names:

atmos.yaml

stacks:
name_template: "{{ .vars.environment }}-{{ .vars.stage }}"

This works well when:

  • You have consistent vars (like environment, stage, tenant) across all stacks
  • You want programmatic, convention-based naming
  • Your Terraform workspaces should follow the same naming pattern

Option 2: Use Explicit name Field (For Inconsistent or Legacy Naming)

If your infrastructure doesn't follow a strict naming convention, use the name field in each stack manifest to explicitly specify the stack name:

stacks/us-east-1/prod/vpc.yaml

name: "prod-us-east-1-vpc"

import:
- catalog/vpc

components:
terraform:
vpc:
vars:
cidr: "10.0.0.0/16"

This is the right choice when:

  • Your stacks don't have consistent context variables
  • You're migrating infrastructure from multiple sources with different naming conventions
  • Workspace names are ad-hoc or don't follow a pattern you can express as a template

The name field takes precedence over name_template, so you can use both approaches—template for most stacks, explicit names for exceptions.

For complete documentation on stack naming, see Stack Names.

Why Migrate?

Advantages of Atmos

  • Clear separation of code and config - YAML for configuration, native Terraform for code. Components (code) and stacks (config) live in different directories—no "Terraform-like but not Terraform" confusion
  • Components are pure Terraform - Your modules work with or without Atmos, no vendor lock-in
  • YAML is universal - Every language and tool can parse it; no HCL knowledge needed for configuration
  • Deep merge semantics - More powerful than include blocks
  • Schema validation - JSON Schema + OPA policies for configuration validation
  • Multi-tool orchestration - Not just Terraform (Helmfile, Packer, etc.)
  • Active development - Regular releases, responsive community

When to Stay with Terragrunt

  • It's working for you - If your team knows Terragrunt well and has no pain points, there's no reason to change
  • Strong HCL preference - If your team prefers HCL over YAML and wants configuration in the same language as Terraform code
  • Heavy dynamic generation - If you rely extensively on generate blocks to create arbitrary Terraform files dynamically, Terragrunt's full templating power may be necessary

Get Help

Migrating a large codebase? We're here to help:

Next Steps

Now that you understand the migration path: