---
model_tier: medium
name: terragrunt
description: "Use when working with Terragrunt — DRY multi-env configs, module dependencies, remote state orchestration — even when the user just says 'deploy this to staging and prod' without naming Terragrunt."
domain: devops
workspaces:
  - engineering
packs:
  - engineering-base
---

# terragrunt

## When to use

Use this skill when working with Terragrunt configurations (`.hcl` files), managing environment-specific settings, or orchestrating multi-module deployments.

## Procedure: Write Terragrunt config

1. Read the `root.hcl` in the target env (`environments/pro/root.hcl` or `environments/sta/root.hcl`).
2. Check existing `terragrunt.hcl` files in sibling directories for patterns.
3. Read the target module's `variables.tf` to understand required inputs.

## Project structure

```
environments/
├── pro/
│   ├── root.hcl                        # Root config (backend, providers)
│   ├── core/
│   │   ├── terragrunt.hcl              # Core module config
│   │   └── {service}.yaml               # Module-specific variables
│   ├── {service}/
│   │   ├── terragrunt.hcl              # Service module config
│   │   └── {service}.yaml              # Service-specific variables
│   └── ...
└── sta/
    ├── root.hcl
    └── ...
```

## Root configuration (`root.hcl`)

The root HCL defines shared settings for all modules in an env:

### Environment variables

```hcl
locals {
  env_files = merge(
    yamldecode(file(".env.yaml")),           # Base env vars
    try(yamldecode(file(".env.local.yaml")), {})  # Local overrides
  )
  env = { for k, v in local.env_files : k => get_env(k, v) }
}
```

- `.env.yaml` — committed, shared env config
- `.env.local.yaml` — gitignored, local overrides (AWS profiles, etc.)
- Real env variables take precedence over file values.

### Remote state

```hcl
remote_state {
  backend = "s3"
  config = {
    bucket         = "${local.env.aws_account_name}-terraform-remote-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    dynamodb_table = "${local.env.aws_account_name}-terraform-remote-state"
    profile        = local.env.aws_profile
    region         = local.env.aws_region
    encrypt        = true
  }
}
```

### Provider generation

Providers are auto-generated by Terragrunt (not manually written):

```hcl
generate "provider" {
  path      = "provider-aws.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  allowed_account_ids = ["${local.env.aws_account_id}"]
  profile             = "${local.env.aws_profile}"
  region              = "${local.env.aws_region}"
}
EOF
}
```

### Terraform binary

```hcl
terraform_binary = "terraform"  # Explicitly NOT OpenTofu
```

## Module configuration (`terragrunt.hcl`)

Each module directory contains a `terragrunt.hcl` that:

1. **Loads module-specific variables** from a YAML file
2. **Includes the root config** for backend and providers
3. **Points to the Terraform module source**
4. **Declares deps** on other modules
5. **Passes inputs** by merging dependency outputs with local variables

### Example pattern

```hcl
locals {
  project = yamldecode(file("{service}.yaml"))
}

include {
  path   = find_in_parent_folders("root.hcl")
  expose = true
}

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

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

inputs = merge(
  dependency.core.outputs,
  local.project
)
```

## Conventions

### Variable files

- Use **YAML files** (not HCL) for module-specific variables.
- Name them after the module (e.g., `my-service.yaml`).
- Keep them in the same directory as `terragrunt.hcl`.

### Dependencies

- Use `dependency` blocks to reference other modules.
- Core module outputs (VPC, DNS zones, etc.) are passed via `dependency.core.outputs`.
- Merge dependency outputs with local variables in `inputs`.

### Additional providers

Some modules need extra providers (e.g., New Relic). Generate them in the module's `terragrunt.hcl`:

```hcl
generate "provider_newrelic" {
  path      = "provider-newrelic.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "newrelic" {
  account_id = "${include.locals.env.newrelic_account_id}"
  api_key    = "${get_env("TF_VAR_newrelic_api_key", include.locals.env.newrelic_api_key)}"
}
EOF
}
```

## Development tools

The project uses **devbox** for tool management:

```json
{
  "packages": ["terragrunt", "awscli2", "terraform@latest"]
}
```

### Quick commands (devbox scripts)

```bash
devbox run i           # terragrunt init
devbox run p           # terragrunt plan
devbox run a           # terragrunt apply
devbox run d           # terragrunt destroy
```


## Output format

1. Terragrunt HCL files with DRY env configuration
2. Dependency graph and remote state references

## Auto-trigger keywords

- Terragrunt
- multi-env
- DRY config
- remote state

## Gotcha

- Terragrunt `dependency` blocks create implicit ordering — circular deps cause cryptic errors.
- Don't duplicate Terraform variables in terragrunt.hcl — use `inputs` to pass them through.
- The model tends to hardcode values that should come from `include` blocks — use DRY patterns.

## Do NOT

- Do NOT switch to OpenTofu — the project explicitly uses `terraform_binary = "terraform"`.
- Do NOT hardcode AWS credentials — use AWS profiles from `.env.yaml`.
- Do NOT commit `.env.local.yaml` — it contains local AWS profile overrides.
- Do NOT modify `root.hcl` without understanding the impact on all modules.
- Do NOT remove `dependency` blocks — they ensure correct apply order.
- Do NOT use `terraform` commands directly — always use `terragrunt` as the wrapper.
