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 blocksdependencies.components for ordering and !terraform.output for values
Variable Passinginputs = {}vars: with inheritance
Localslocals {}locals
Module Sourceterraform { source = "..." }source: for JIT provisioning or vendor.yaml for vendoring
CLIterragrunt plan/applyatmos terraform plan/apply
Hooksbefore_hook, after_hook, error_hookhooks
State Backendremote_state with backend creationCentralized backend config with provision.backend
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 → Code 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
}

Hooks → Hooks

Terragrunt supports hooks inside terragrunt.hcl using before_hook, after_hook, and error_hook blocks. Hooks run commands around selected Terraform operations.

terragrunt.hcl
terraform {
before_hook "fmt" {
commands = ["plan", "apply"]
execute = ["terraform", "fmt", "-check"]
}

after_hook "notify" {
commands = ["apply"]
execute = ["./scripts/notify.sh"]
}
}

Remote State Configuration

Terragrunt configures remote state in the root terragrunt.hcl using a remote_state block and can create the configured backend resources when they do not exist. 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 → Locals

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 HCL functions for path discovery, environment lookup, command execution, dependency wiring, and file reading. Atmos supports the same workflows, but many of them move from inline functions to explicit YAML configuration, YAML functions, or stack templating. The table below audits the functions in the Terragrunt function reference.

Path and Directory Functions

Terragrunt FunctionAtmos EquivalentNotes
OpenTofu/Terraform built-insGo templates with Sprig/Gomplate, or YAML functionsAtmos does not expose Terraform's HCL function set directly. Use YAML functions for data access and templates for string, path, list, map, and semver helpers.
find_in_parent_folders()import: or !includeAtmos uses explicit imports and includes instead of walking parent directories.
path_relative_to_include()Explicit backend.key or templatesAtmos does not derive state keys from include paths. Configure state keys directly from stack/component context.
path_relative_from_include()Not usually neededAtmos imports, component base paths, and source provisioning use configured paths instead of include-relative module paths.
get_repo_root()!repo-rootYAML function returns repo root path
get_path_from_repo_root()No direct scalar functionUsually not needed; use configured stack/component paths, or combine !repo-root with templates when an absolute repo path is required.
get_path_to_repo_root()No direct scalar functionUse !repo-root for the absolute root. Atmos generally avoids relative path hops back to repo root.
get_terragrunt_dir()Manifest-relative paths, component base paths./ and ../ in !include resolve relative to the manifest file. Component paths come from components.terraform.base_path plus the component name.
get_working_dir()!cwdYAML function returns the directory where Atmos is executed.
get_parent_terragrunt_dir()import:Explicit imports replace parent-config discovery.
get_original_terragrunt_dir()!cwdAtmos is not directory-per-unit; use the command working directory when you need the invocation path.

Environment and Platform Functions

Terragrunt FunctionAtmos EquivalentNotes
get_env("VAR", "default")!env VAR, defaultYAML function for environment variables
get_platform()No native equivalentSprig and Gomplate can read environment variables, but do not expose Go runtime.GOOS/runtime.GOARCH. Use !exec if needed.
get_aws_account_id()!aws.account_idYAML function backed by AWS STS
get_aws_account_alias()!execNo dedicated YAML function; run an AWS CLI command
get_aws_caller_identity_arn()!aws.caller_identity_arnYAML function backed by AWS STS
get_aws_caller_identity_user_id()!aws.caller_identity_user_idYAML function backed by AWS STS

Terraform Command Helpers

Terragrunt FunctionAtmos EquivalentNotes
get_terraform_commands_that_need_vars()Built inAtmos generates and passes component varfiles automatically for Terraform commands that need them.
get_terraform_commands_that_need_input()settings.terraform.argsConfigure -input=false or related flags explicitly.
get_terraform_commands_that_need_locking()settings.terraform.argsConfigure lock flags explicitly when needed.
get_terraform_commands_that_need_parallelism()settings.terraform.argsConfigure -parallelism explicitly when needed.
get_terraform_command()No direct functionAtmos receives the Terraform subcommand from atmos terraform <command>. Use hooks or custom commands when behavior must vary by operation.
get_terraform_cli_args()settings.terraform.args, env, or pass-through argsUse component settings for durable args, TF_CLI_ARGS* env vars for Terraform-native behavior, or -- for one-off pass-through args.
get_default_retryable_errors()retry.conditionsAtmos has component retry configuration instead of a helper returning default regexes.

Command Execution

Terragrunt FunctionAtmos EquivalentNotes
run_cmd("cmd", "arg1", ...)!exec!exec cmd arg1 executes shell commands in YAML functions.
run_cmd("--terragrunt-quiet", ...)No direct flagUse Atmos secret masking and avoid printing sensitive command output.
run_cmd("--terragrunt-global-cache", ...)No direct flagAtmos does not expose a command-output cache control for !exec.
run_cmd("--terragrunt-no-cache", ...)Default !exec behavior!exec runs when functions are processed; there is no Terragrunt-style cache toggle.

Configuration Reading

Terragrunt FunctionAtmos EquivalentNotes
deep_merge()Stack imports and inheritanceAtmos deep-merges imported YAML configuration by design. Use Sprig/Gomplate template merge helpers only when inline expression merging is unavoidable.
read_terragrunt_config()import:Import reusable Atmos stack YAML instead of reading HCL into a local value.
read_tfvars_file()!include!include can load .tfvars and .tfvars.json into YAML values.
sops_decrypt_file()!exec or storesNo native SOPS YAML function. Use !exec sops ... for local encrypted files, or move secrets to a configured store and read with !store/!store.get.
get_terragrunt_source_cli_flag()No direct equivalentAtmos source provisioning uses source.uri and source.version in stack config. Use !env for local override patterns if you need them.

State and Outputs

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

Change Tracking and Constraints

Terragrunt FunctionAtmos EquivalentNotes
mark_as_read()dependencies.components with kind: fileDeclare explicit file dependencies so describe affected can track changes.
mark_glob_as_read()dependencies.components with kind: folderDeclare folder dependencies instead of imperatively marking glob reads.
constraint_check()Sprig semverCompare or tool dependency constraintsUse semverCompare in templates for conditional config, or Atmos toolchain dependency constraints for tool versions.
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" }}, {{ semverCompare ">=2.0.0" .vars.version }}) — 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: