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
| Concept | Terragrunt | Atmos |
|---|---|---|
| Configuration Format | HCL (terragrunt.hcl) | YAML (.yaml) |
| Reuse Mechanism | include {} blocks | import: with deep merge |
| Dependencies | dependency blocks | settings.depends_on or !terraform.output |
| Variable Passing | inputs = {} | vars: with inheritance |
| Module Source | terraform { source = "..." } | metadata.component |
| CLI | terragrunt plan/apply | atmos terraform plan/apply |
| State Backend | Per-directory backend {} | Centralized in stack config |
| Environments | Directory structure | Stack files (YAML) |
| Units | Directory with terragrunt.hcl | Component instance in a stack |
| Stacks | Collection 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:
| Feature | Description |
|---|---|
| Native Authentication | Built-in multi-cloud auth with SAML, SSO, OIDC, and GitHub Actions. No separate tools needed—atmos auth login handles it all. |
| Vendoring | Pull and version external modules locally with atmos vendor pull. Customize vendored code while tracking upstream. |
| Custom Commands | Define your own CLI commands in YAML. No scripting needed—integrate team-specific workflows directly into Atmos. |
| Workflows | Orchestrate multi-step operations across components and stacks. Chain commands, add conditions, run in parallel. |
| Terraform Shell | atmos terraform shell vpc -s prod drops you into a configured shell for native Terraform debugging. All vars and backend pre-configured. |
| Affected Detection | atmos describe affected analyzes Git changes to find impacted components—purpose-built for CI/CD. |
| Component Validation | JSON Schema and OPA policy validation for stack configurations before deployment. |
| Stack Describe | atmos describe component shows the fully-resolved configuration for any component in any stack. |
| Configuration Provenance | atmos describe component --provenance traces where every value came from across the import hierarchy. |
Directory Structure Comparison
- Terragrunt
- Atmos
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
With Atmos, the distinction is clear: YAML for configuration, Terraform for code. Your stack configurations are pure YAML—no Terraform domain knowledge required—while all components are native Terraform modules, unchanged and portable.
infrastructure/
├── atmos.yaml # Atmos config (YAML)
├── components/
│ └── terraform/
│ ├── vpc/
│ │ ├── main.tf # Native Terraform
│ │ └── variables.tf # Native Terraform
│ └── eks/
│ ├── main.tf
│ └── variables.tf
└── stacks/
├── _defaults/
│ ├── globals.yaml # Shared defaults
│ └── vpc-defaults.yaml
├── dev/
│ ├── us-east-1.yaml # Dev in us-east-1
│ └── us-west-2.yaml # Dev in us-west-2
├── staging/
│ └── us-east-1.yaml # Staging in us-east-1
└── prod/
├── us-east-1.yaml # Prod in us-east-1
└── us-west-2.yaml # Prod in us-west-2
Atmos supports many design patterns for organizing your stacks—from simple flat structures for startups to enterprise-scale patterns with multi-tenant, multi-region hierarchies. Choose the pattern that fits your needs and evolve as you grow.
Characteristics:
- Code (Terraform) and config (YAML) cleanly separated
- One stack file per environment—no per-component config files
- Components are native Terraform—work with or without Atmos
- Anyone can read YAML; no HCL knowledge needed
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.
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
- Atmos
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
Atmos uses simple import statements—just list the files you want to include. All imports are automatically deep-merged in order, with later values overriding earlier ones. No special functions or expose flags needed.
stacks/prod.yaml
Dependency → Remote State
- Terragrunt
- Atmos
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
Atmos provides the !terraform.output YAML function to read outputs directly from another component's remote state. Simply reference the component name and output—Atmos handles the state backend lookup automatically.
Because Atmos uses YAML for configuration and keeps Terraform for code, there's a clear separation: stacks are configuration, components are code. Your configuration (what to deploy, with what values) stays in YAML. Your business logic (how resources are created) stays in Terraform. This separation makes it easier to reason about your infrastructure.
stacks/prod.yaml
Inputs → Vars
- Terragrunt
- Atmos
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
Atmos uses the vars section under each component. Variables are defined in YAML and automatically passed to Terraform. You can define vars at multiple levels (global, component) and they deep-merge, allowing inheritance and overrides.
stacks/prod.yaml
Terraform Source → Component + Vendoring
- Terragrunt
- Atmos
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
Atmos separates module sourcing from stack configuration using vendoring. You define sources in vendor.yaml, pull them locally with atmos vendor pull, and reference the local component in stacks. This gives you version control, offline capability, and the ability to customize vendored modules.
First, define the vendor source:
vendor.yaml
Then pull it: atmos vendor pull
Finally, reference in your stack:
stacks/prod.yaml
Generate Blocks → Purpose-Built Generation
- Terragrunt
- Atmos
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
Atmos intentionally does not provide full-fledged arbitrary file generation—that approach encourages complexity. Instead, Atmos offers purpose-built generation for the two most common use cases: backends and providers.
You express these as YAML in your stack configs. Atmos deep-merges the configuration across imports and generates the required JSON files at runtime. This keeps configuration declarative while solving the real problems.
- Stack Config (YAML)
- Generated backend.tf.json
- Generated providers.tf.json
Define your backend and provider configuration declaratively in YAML. Atmos deep-merges this configuration across imports and handles all the generation for you.
stacks/prod.yaml
Atmos generates this file at runtime before running Terraform. The backend configuration is serialized from your YAML stack config into the JSON format Terraform expects.
components/terraform/vpc/backend.tf.json
Provider configuration is also generated at runtime. This ensures your Terraform components remain environment-agnostic while Atmos injects the correct provider settings per stack.
components/terraform/vpc/providers.tf.json
Remote State Configuration
- Terragrunt
- Atmos
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)
Atmos centralizes backend configuration in a defaults file that all stacks import. The backend settings are defined once in YAML, and Atmos automatically generates unique state keys based on component and stack names.
stacks/_defaults/globals.yaml
Then import in all stacks:
stacks/prod-us-east-1.yaml
Locals → Vars with Inheritance
- Terragrunt
- Atmos
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
Atmos uses vars with deep-merge inheritance. Define variables at any level—they flow down through imports and can be overridden. For most cases, you don't need string interpolation at all; just pass the individual values to your Terraform module and let Terraform handle the composition.
stacks/prod-us-east-1.yaml
If you truly need string interpolation in YAML, Go templates work but should be used sparingly—they make configurations harder to read and validate.
run_cmd → YAML Functions
- Terragrunt
- Atmos
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
Atmos provides YAML functions (like !exec) as the preferred way to run commands or fetch dynamic values. YAML functions are validated at parse time, produce readable configurations, and integrate naturally with YAML syntax.
stacks/prod-us-east-1.yaml
Atmos also supports Go templates ({{ exec "..." }}), but we recommend YAML functions because:
- Validation: YAML functions are validated at parse time; templates can only be validated after generation
- Readability: Templates turn YAML into string soup that's hard to read and debug
- Tooling: YAML functions work with standard YAML tooling; templates break syntax highlighting and linting
Use templates only as an escape hatch when YAML functions don't cover your use case.
extra_arguments → Component Settings
- Terragrunt
- Atmos
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
Atmos uses the settings.terraform.args section to pass additional arguments to Terraform. These arguments are appended to all Terraform commands for that component and can be defined at multiple levels with inheritance.
stacks/prod-us-east-1.yaml
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:
- Terragrunt
- Atmos
In Terragrunt terminology:
-
Unit: A directory containing a
terragrunt.hclfile—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.hclfiles).
terragrunt.stack.hcl
Atmos has always had these concepts, just with different names:
-
Unit → Component Instance: In Atmos, when you define a component in a stack's
components.terraformsection, that's a unit—a single deployable instance with its own state. -
Stack → Stack File: An Atmos stack file (e.g.,
prod-us-east-1.yaml) is exactly what Terragrunt now calls a stack—a collection of component instances managed together.
stacks/prod-us-east-1.yaml
The key difference: Atmos has used YAML for this since day one, while Terragrunt's terragrunt.stack.hcl is a newer HCL-based approach to the same problem.
| Terragrunt Concept | Atmos Equivalent | Notes |
|---|---|---|
Unit (terragrunt.hcl) | Component instance in components.terraform | Same concept: single deployable with own state |
| Implicit Stack (directory) | Stack file | Atmos uses explicit YAML files, not directories |
Explicit Stack (terragrunt.stack.hcl) | Stack file (.yaml) | Both define collections of units/components |
unit {} block | Component entry under components.terraform | Atmos uses YAML; Terragrunt uses HCL |
values = {} | vars: | Both pass variables to the underlying module |
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 Function | Atmos Equivalent | Notes |
|---|---|---|
find_in_parent_folders() | import: | Atmos uses explicit imports instead of searching |
path_relative_to_include() | Automatic | State keys generated from stack/component names |
path_relative_from_include() | Automatic | Not needed—Atmos handles paths |
get_terragrunt_dir() | N/A | Components are always in components/terraform/ |
get_working_dir() | N/A | Atmos manages working directories |
get_parent_terragrunt_dir() | N/A | Flat imports replace hierarchy |
get_repo_root() | !repo-root | YAML function returns repo root path |
get_path_from_repo_root() | !repo-root | Combine with path manipulation |
Environment and Platform Functions
| Terragrunt Function | Atmos Equivalent | Notes |
|---|---|---|
get_env("VAR", "default") | !env VAR | YAML function for environment variables |
get_platform() | N/A | Use !exec if needed |
get_aws_account_id() | !exec | !exec aws sts get-caller-identity --query Account |
get_aws_account_alias() | !exec | Run AWS CLI command |
get_aws_caller_identity_arn() | !exec | Run AWS CLI command |
get_aws_caller_identity_user_id() | !exec | Run AWS CLI command |
Command Execution
| Terragrunt Function | Atmos Equivalent | Notes |
|---|---|---|
run_cmd("cmd", "arg1", ...) | !exec | !exec cmd arg1 (inline syntax) |
Configuration Reading
| Terragrunt Function | Atmos Equivalent | Notes |
|---|---|---|
read_terragrunt_config() | import: | Import other stack configs |
read_tfvars_file() | !include | vars: !include path/to/file.tfvars |
State and Outputs
| Terragrunt Function | Atmos Equivalent | Notes |
|---|---|---|
dependency.X.outputs.Y | !terraform.output | !terraform.output component.output_name |
| N/A | !terraform.state | Read arbitrary state attributes |
Secret Management
| Terragrunt Function | Atmos Equivalent | Notes |
|---|---|---|
sops_decrypt_file() | !store | Use store integrations (SSM, Secrets Manager, etc.) |
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
- Atmos
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
Atmos commands run from anywhere within a configured Atmos repository—you never need to cd anywhere. Just specify the component and stack, and Atmos finds everything automatically. This makes scripts and CI/CD pipelines simpler and eliminates directory management entirely.
# Plan a single component (from repo root)
atmos terraform plan vpc -s prod-us-east-1
# Apply a single component
atmos terraform apply vpc -s prod-us-east-1
# Plan ALL components in ALL stacks
atmos terraform plan --all
# Apply ALL components in ALL stacks
atmos terraform apply --all
# Show outputs
atmos terraform output vpc -s prod-us-east-1
# List all stacks
atmos list stacks
# Describe a component
atmos describe component vpc -s prod-us-east-1
GitOps-native: Atmos goes beyond simple "run all" with intelligent change detection:
# See what changed between commits (compares current branch to main)
atmos describe affected
# Plan ONLY components affected by changes in current branch
atmos terraform plan --affected
# Apply ONLY affected components
atmos terraform apply --affected
# Compare against a specific branch or commit
atmos describe affected --ref refs/heads/feature-branch
atmos describe affected --sha abc123def
# Include dependent components (if vpc changed, also plan eks that depends on it)
atmos describe affected --include-dependents=true
This is purpose-built for CI/CD: instead of planning everything on every PR, Atmos analyzes Git changes to determine exactly which components and stacks are affected—including changes to stack configs, component code, and even local Terraform modules.
Migration Steps
Step 1: Convert terragrunt.hcl to Stack YAML
- Before (Terragrunt)
- After (Atmos)
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
The Atmos stack file consolidates all configuration in one place. Imports bring in shared defaults, vars set environment-level values, and components define what to deploy. Each component points to a local Terraform module.
stacks/prod-us-east-1.yaml
Step 2: Move Terraform Root Modules
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.
- Before (Terragrunt)
- After (Atmos)
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
Atmos expects root modules (components) in components/terraform/. The modules themselves are unchanged—just move or rename the directory. Your Terraform code remains pure and portable.
components/terraform/
├── vpc/ # Component = root module
│ ├── main.tf
│ └── variables.tf
└── eks/ # Component = root module
├── main.tf
└── variables.tf
Simply rename modules/ to components/terraform/.
Step 3: Update Backend Configuration
- Before (Terragrunt)
- After (Atmos)
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)
Atmos defines backend settings in a shared defaults file. All stacks import this file and automatically get consistent backend configuration. State keys are generated from stack and component names, with optional per-component overrides.
stacks/_defaults/globals.yaml
Component-specific key prefixes:
stacks/prod-us-east-1.yaml
Migration Checklist
- Install Atmos CLI (Installation Guide)
- Create
atmos.yamlconfiguration - Move Terraform root modules to
components/terraform/ - Convert
terragrunt.hclfiles to stack YAML - Extract common config to
_defaults/ - Convert
dependencyblocks 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:
Option 1: Use name_template (Recommended for Consistent Patterns)
If you have consistent context variables across all your stacks, configure name_template in atmos.yaml to programmatically compute stack names:
atmos.yaml
This works well when:
- You have consistent
vars(likeenvironment,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
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
includeblocks - 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
generateblocks 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:
- Slack Community - Ask migration questions
- Office Hours - Live support for complex migrations
- GitHub Discussions - Share your migration story
Next Steps
Now that you understand the migration path:
- Learn YAML in Atmos - YAML is more powerful than you might think. Learn how Atmos uses deep merging, scope, and inheritance
- Explore YAML Functions - YAML functions like
!terraform.output,!env, and!execare first-class YAML features (technically explicit tags) that give your configuration superpowers - Try the Quick Start - Get hands-on with Atmos
- Read Core Concepts - Understand Atmos deeply
- Explore Stack Configuration - Advanced YAML features