# 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 | [`dependencies.components`](/stacks/dependencies/components) for ordering and [`!terraform.output`](/functions/yaml/terraform.output) for values |
| **Variable Passing** | `inputs = {}` | `vars:` with inheritance |
| **Locals** | `locals {}` | [`locals`](/stacks/locals) |
| **Module Source** | `terraform { source = "..." }` | `source:` for JIT provisioning or `vendor.yaml` for vendoring |
| **CLI** | `terragrunt plan/apply` | `atmos terraform plan/apply` |
| **Hooks** | `before_hook`, `after_hook`, `error_hook` | [`hooks`](/stacks/hooks) |
| **State Backend** | `remote_state` with backend creation | Centralized backend config with [`provision.backend`](/stacks/components/provision/backend) |
| **Environments** | Directory structure | Stack files (YAML) |
| **[Units](https://terragrunt.gruntwork.io/docs/getting-started/terminology/)** | Directory with `terragrunt.hcl` | Component instance in a stack |
| **[Stacks](https://terragrunt.gruntwork.io/docs/features/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](/cli/commands/auth/usage)** | Built-in multi-cloud auth with SAML, SSO, OIDC, and GitHub Actions. No separate tools needed—`atmos auth login` handles it all. |
| **[Vendoring](/cli/commands/vendor/pull)** | Pull and version external modules locally with `atmos vendor pull`. Customize vendored code while tracking upstream. |
| **[Custom Commands](/cli/configuration/commands)** | Define your own CLI commands in YAML. No scripting needed—integrate team-specific workflows directly into Atmos. |
| **[Workflows](/workflows)** | Orchestrate multi-step operations across components and stacks. Chain commands, add conditions, run in parallel. |
| **[Terraform Shell](/cli/commands/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](/cli/commands/describe/affected)** | `atmos describe affected` analyzes Git changes to find impacted components—purpose-built for CI/CD. |
| **[Component Validation](/cli/commands/validate/stacks)** | JSON Schema and OPA policy validation for stack configurations before deployment. |
| **[Stack Describe](/cli/commands/describe/component)** | `atmos describe component` shows the fully-resolved configuration for any component in any stack. |
| **[Configuration Provenance](/cli/commands/describe/component)** | `atmos describe component --provenance` traces where every value came from across the import hierarchy. |

## Directory Structure Comparison

### Terragrunt

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.

```plaintext
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

### Atmos

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.

```plaintext
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
```

:::tip Flexible Design Patterns
Atmos supports many [design patterns](/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.

:::info 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](/stacks/imports)

### Terragrunt

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.

**File:** `terragrunt.hcl`

```hcl
include "root" {
  path = find_in_parent_folders()
}

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

### Atmos

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.

**File:** `stacks/prod.yaml`

```yaml
import:
  - _defaults/globals
  - _defaults/vpc-defaults
```

### Dependency → [Remote State](/stacks/remote-state)

### Terragrunt

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.

**File:** `eks/terragrunt.hcl`

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

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

### Atmos

Atmos separates dependency ordering from value lookup. Use [`dependencies.components`](/stacks/dependencies/components) to declare explicit component ordering, and use [`!terraform.output`](/functions/yaml/terraform.output) to read outputs from another component's remote state.

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.

**File:** `stacks/prod.yaml`

```yaml
components:
  terraform:
    eks:
      dependencies:
        components:
          - component: vpc
      vars:
        vpc_id: !terraform.output vpc.vpc_id
        subnet_ids: !terraform.output vpc.private_subnet_ids
```

### Inputs → Vars

### Terragrunt

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.

**File:** `vpc/terragrunt.hcl`

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

### Atmos

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.

**File:** `stacks/prod.yaml`

```yaml
components:
  terraform:
    vpc:
      vars:
        cidr_block: "10.0.0.0/16"
        enable_dns_hostnames: true
        environment: production
```

### Terraform Source → Component + [Vendoring](/vendor/)

### Terragrunt

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.

**File:** `vpc/terragrunt.hcl`

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

### Atmos Source Provisioning

Atmos source provisioning is the closest equivalent to Terragrunt's `terraform.source`: declare the remote module source in stack configuration, and Atmos fetches it when the component runs.

**File:** `stacks/prod.yaml`

```yaml
components:
  terraform:
    vpc:
      source:
        uri: "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git"
        version: "v5.0.0"
        ttl: "7d"  # Optional: refresh cached sources after this duration
      provision:
        workdir:
          enabled: true
```

Then run Terraform normally. Atmos automatically provisions the source before Terraform runs:

```shell
atmos terraform plan vpc --stack prod
```

You can also pull the source explicitly when you want to prefetch or refresh it manually:

```shell
atmos terraform source pull vpc --stack prod
```

The `ttl` setting is an Atmos-specific addition that controls when cached sources are refreshed, which is especially useful for floating refs like branches. Set `ttl: "0s"` to disable source caching and pull every time. Terragrunt does not provide an equivalent source cache TTL.

Workdir provisioning is strongly recommended with source provisioning. It stages each component instance under `.workdir/`, so Terraform runs with isolated `.terraform/` directories, generated varfiles, and backend files.

### Atmos Vendoring

Atmos vendoring is the workflow for explicitly pulling a local copy of remote module code. Define sources in `vendor.yaml`, pull them with `atmos vendor pull`, and reference the local component in stacks. Teams often use this when they want vendored code committed for review, auditability, offline use, or local customization.

First, define the vendor source:

**File:** `vendor.yaml`

```yaml
apiVersion: atmos/v1
kind: AtmosVendorConfig
spec:
  sources:
    - source: "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git?ref=v5.0.0"
      targets:
        - "components/terraform/vpc"
```

Then pull it: `atmos vendor pull`

Finally, reference in your stack:

**File:** `stacks/prod.yaml`

```yaml
components:
  terraform:
    vpc:
      metadata:
        component: vpc  # Points to components/terraform/vpc/
```

### Generate Blocks → [Code Generation](/stacks/generate)

### Terragrunt

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.

**File:** `terragrunt.hcl`

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

### Atmos

Atmos supports declarative file generation with the [`generate`](/stacks/generate) section. This is the closest equivalent to Terragrunt `generate` blocks: define the files in YAML, and Atmos writes them before Terraform runs when `auto_generate_files` is enabled, or when you run `atmos terraform generate files`.

You can generate HCL, JSON, YAML, Markdown, or any text file. Backend and provider generation remain common built-in workflows, but `generate` covers custom auxiliary files too.

### Stack Config (YAML)

Define generated files declaratively in YAML. String values are treated as Go templates, so stack variables can be rendered into the generated file.

**File:** `stacks/prod.yaml`

```yaml
vars:
  aws_region: us-east-1

terraform:
  auto_generate_files: true

components:
  terraform:
    vpc:
      generate:
        provider.tf: |
          provider "aws" {
            region = "{{ .vars.aws_region }}"
          }
```

### Generated provider.tf

Atmos generates this file before Terraform runs.

**File:** `components/terraform/vpc/provider.tf`

```hcl
provider "aws" {
  region = "us-east-1"
}
```

### Hooks → [Hooks](/stacks/hooks)

### Terragrunt

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

**File:** `terragrunt.hcl`

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

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

### Atmos

Atmos supports lifecycle hooks in stack configuration. Hooks can run before or after Terraform `init`, `plan`, `apply`, and `deploy`. Atmos also includes native hook kinds for common workflows: `infracost` for cost estimates, `trivy`, `checkov`, and `kics` for security and policy scanning, plus `command` for custom scripts.

**File:** `stacks/prod.yaml`

```yaml
components:
  terraform:
    vpc:
      hooks:
        cost:
          events: [after-terraform-plan]
          kind: infracost

        security:
          events: [after-terraform-plan]
          kind: trivy

        policy:
          events: [after-terraform-plan]
          kind: checkov

        kics:
          events: [after-terraform-plan]
          kind: kics

        notify:
          events: [after-terraform-apply]
          kind: command
          command: ./scripts/notify.sh
```

### Remote State Configuration

### Terragrunt

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.

**File:** `terragrunt.hcl (root)`

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

### Atmos

Atmos centralizes backend configuration in YAML and supports automatic backend provisioning with [`provision.backend`](/stacks/components/provision/backend). The `backend:` block describes where state lives, and `provision.backend.enabled` tells Atmos to create that backend if it does not exist.

**File:** `stacks/catalog/vpc.yaml`

```yaml
components:
  terraform:
    vpc:
      backend_type: s3
      backend:
        bucket: terraform-state
        key: vpc/terraform.tfstate
        region: us-east-1
        use_lockfile: true

      provision:
        backend:
          enabled: true
```

Before Terraform runs, Atmos checks whether the backend exists, provisions it if needed, and then continues with `terraform init`.

### Locals → [Locals](/stacks/locals)

### Terragrunt

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.

**File:** `terragrunt.hcl`

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

inputs = {
  cluster_name = local.cluster_name
}
```

### Atmos

Atmos supports `locals` for file-scoped computed values and `vars` for values passed into components. Use locals to reduce repetition or build derived values, then reference them with `.locals.*` in `vars`, `settings`, `env`, and other templated sections.

**File:** `stacks/prod-us-east-1.yaml`

```yaml
locals:
  account_id: "123456789012"
  region: us-east-1
  cluster_name: "{{ .locals.account_id }}-{{ .locals.region }}-eks"

components:
  terraform:
    eks:
      vars:
        account_id: "{{ .locals.account_id }}"
        region: "{{ .locals.region }}"
        cluster_name: "{{ .locals.cluster_name }}"
```

Atmos locals are scoped to the file where they are defined. Use `vars` when you need inherited values across imports, and use `locals` when you need temporary computed values inside one stack file.

### run\_cmd → YAML Functions

### Terragrunt

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.

**File:** `terragrunt.hcl`

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

### Atmos

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.

**File:** `stacks/prod-us-east-1.yaml`

```yaml
vars:
  account_id: !exec aws sts get-caller-identity --query Account --output text
```

:::tip Why YAML functions over templates?
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

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.

**File:** `terragrunt.hcl`

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

### Atmos

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.

**File:** `stacks/prod-us-east-1.yaml`

```yaml
components:
  terraform:
    vpc:
      settings:
        terraform:
          args:
            - "-var-file=common.tfvars"
```

### Units and Stacks

Terragrunt recently introduced formal [Units and Stacks](https://terragrunt.gruntwork.io/docs/getting-started/terminology/) concepts (GA in v0.78.0, May 2025). Here's how they map to Atmos:

### Terragrunt

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).

**File:** `terragrunt.stack.hcl`

```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"
  }
}
```

### Atmos

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.terraform` section, 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.

**File:** `stacks/prod-us-east-1.yaml`

```yaml
# Stack file = collection of component instances (units)
components:
  terraform:
    vpc:                    # Unit 1: VPC instance
      vars:
        environment: prod
        cidr_block: "10.0.0.0/16"

    eks:                    # Unit 2: EKS instance
      vars:
        environment: prod
        cluster_name: main
```

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 |

:::tip 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](https://docs.terragrunt.com/reference/hcl/functions/).

### Path and Directory Functions

| Terragrunt Function | Atmos Equivalent | Notes |
|---------------------|------------------|-------|
| OpenTofu/Terraform built-ins | Go templates with Sprig/Gomplate, or YAML functions | Atmos 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 `!include` | Atmos uses explicit imports and includes instead of walking parent directories. |
| `path_relative_to_include()` | Explicit `backend.key` or templates | Atmos does not derive state keys from include paths. Configure state keys directly from stack/component context. |
| `path_relative_from_include()` | Not usually needed | Atmos imports, component base paths, and source provisioning use configured paths instead of include-relative module paths. |
| `get_repo_root()` | `!repo-root` | YAML function returns repo root path |
| `get_path_from_repo_root()` | No direct scalar function | Usually 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 function | Use `!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()` | `!cwd` | YAML function returns the directory where Atmos is executed. |
| `get_parent_terragrunt_dir()` | `import:` | Explicit imports replace parent-config discovery. |
| `get_original_terragrunt_dir()` | `!cwd` | Atmos is not directory-per-unit; use the command working directory when you need the invocation path. |

### Environment and Platform Functions

| Terragrunt Function | Atmos Equivalent | Notes |
|---------------------|------------------|-------|
| `get_env("VAR", "default")` | `!env VAR, default` | YAML function for environment variables |
| `get_platform()` | No native equivalent | Sprig 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_id` | YAML function backed by AWS STS |
| `get_aws_account_alias()` | `!exec` | No dedicated YAML function; run an AWS CLI command |
| `get_aws_caller_identity_arn()` | `!aws.caller_identity_arn` | YAML function backed by AWS STS |
| `get_aws_caller_identity_user_id()` | `!aws.caller_identity_user_id` | YAML function backed by AWS STS |

### Terraform Command Helpers

| Terragrunt Function | Atmos Equivalent | Notes |
|---------------------|------------------|-------|
| `get_terraform_commands_that_need_vars()` | Built in | Atmos generates and passes component varfiles automatically for Terraform commands that need them. |
| `get_terraform_commands_that_need_input()` | `settings.terraform.args` | Configure `-input=false` or related flags explicitly. |
| `get_terraform_commands_that_need_locking()` | `settings.terraform.args` | Configure lock flags explicitly when needed. |
| `get_terraform_commands_that_need_parallelism()` | `settings.terraform.args` | Configure `-parallelism` explicitly when needed. |
| `get_terraform_command()` | No direct function | Atmos 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 args | Use 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.conditions` | Atmos has component retry configuration instead of a helper returning default regexes. |

### Command Execution

| Terragrunt Function | Atmos Equivalent | Notes |
|---------------------|------------------|-------|
| `run_cmd("cmd", "arg1", ...)` | `!exec` | `!exec cmd arg1` executes shell commands in YAML functions. |
| `run_cmd("--terragrunt-quiet", ...)` | No direct flag | Use Atmos secret masking and avoid printing sensitive command output. |
| `run_cmd("--terragrunt-global-cache", ...)` | No direct flag | Atmos 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 Function | Atmos Equivalent | Notes |
|---------------------|------------------|-------|
| `deep_merge()` | Stack imports and inheritance | Atmos 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 stores | No 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 equivalent | Atmos 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 Function | Atmos Equivalent | Notes |
|---------------------|------------------|-------|
| `dependency.X.outputs.Y` | `!terraform.output` | `!terraform.output component.output_name` |
| N/A | `!terraform.state` | Read arbitrary state attributes |

### Change Tracking and Constraints

| Terragrunt Function | Atmos Equivalent | Notes |
|---------------------|------------------|-------|
| `mark_as_read()` | `dependencies.components` with `kind: file` | Declare explicit file dependencies so `describe affected` can track changes. |
| `mark_glob_as_read()` | `dependencies.components` with `kind: folder` | Declare folder dependencies instead of imperatively marking glob reads. |
| `constraint_check()` | Sprig `semverCompare` or tool dependency constraints | Use `semverCompare` in templates for conditional config, or Atmos toolchain dependency constraints for tool versions. |

:::tip 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](/functions/yaml) for the complete reference.
:::

## CLI Command Comparison

### Terragrunt

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.

```bash
# 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

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.

```bash
# 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:

```bash
# 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)

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.

**File:** `prod/us-east-1/vpc/terragrunt.hcl`

```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"
}
```

### After (Atmos)

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.

**File:** `stacks/prod-us-east-1.yaml`

```yaml
import:
  - _defaults/globals

vars:
  environment: prod
  region: us-east-1

components:
  terraform:
    vpc:
      metadata:
        component: vpc  # Points to components/terraform/vpc
      vars:
        cidr_block: "10.0.0.0/16"
```

### Step 2: Move Terraform Root Modules

:::info 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.
:::

### Before (Terragrunt)

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

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

### After (Atmos)

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.

```plaintext
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)

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.

**File:** `terragrunt.hcl (root)`

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

### After (Atmos)

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.

**File:** `stacks/_defaults/globals.yaml`

```yaml
terraform:
  backend_type: s3
  backend:
    s3:
      bucket: terraform-state
      key: terraform.tfstate
      region: us-east-1
```

Component-specific key prefixes:

**File:** `stacks/prod-us-east-1.yaml`

```yaml
components:
  terraform:
    vpc:
      backend:
        s3:
          workspace_key_prefix: vpc
```

## Migration Checklist

- \[ ] Install Atmos CLI ([Installation Guide](/install))
- \[ ] 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:

### 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:

**File:** `atmos.yaml`

```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:

**File:** `stacks/us-east-1/prod/vpc.yaml`

```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](/stacks/name).

## 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:

- **[Slack Community](/community/slack)** - Ask migration questions
- **[Office Hours](/community/office-hours)** - Live support for complex migrations
- **[GitHub Discussions](https://github.com/cloudposse/atmos/discussions)** - Share your migration story

## Next Steps

Now that you understand the migration path:

- **[Learn YAML in Atmos](/learn/yaml)** - YAML is more powerful than you might think. Learn how Atmos uses deep merging, scope, and inheritance
- **[Explore YAML Functions](/functions/yaml)** - YAML functions like `!terraform.output`, `!env`, and `!exec` are first-class YAML features (technically explicit tags) that give your configuration superpowers
- **[Try the Quick Start](/quick-start/simple)** - Get hands-on with Atmos
- **[Read Core Concepts](/learn/why-atmos)** - Understand Atmos deeply
- **[Explore Stack Configuration](/stacks/)** - Advanced YAML features
