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
- Read the
root.hclin the target environment (environments/pro/root.hclorenvironments/sta/root.hcl). - Check existing
terragrunt.hclfiles in sibling directories for patterns. - Read the target module's
variables.tfto 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 environment:
Environment variables
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 environment config.env.local.yaml— gitignored, local overrides (AWS profiles, etc.)- Real environment variables take precedence over file values.
Remote state
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):
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
terraform_binary = "terraform" # Explicitly NOT OpenTofu
Module configuration (terragrunt.hcl)
Each module directory contains a terragrunt.hcl that:
- Loads module-specific variables from a YAML file
- Includes the root config for backend and providers
- Points to the Terraform module source
- Declares dependencies on other modules
- Passes inputs by merging dependency outputs with local variables
Example pattern
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
dependencyblocks 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:
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:
{
"packages": ["terragrunt", "awscli2", "terraform@latest"]
}
Quick commands (devbox scripts)
devbox run i # terragrunt init
devbox run p # terragrunt plan
devbox run a # terragrunt apply
devbox run d # terragrunt destroy
Output format
- Terragrunt HCL files with DRY environment configuration
- Dependency graph and remote state references
Auto-trigger keywords
- Terragrunt
- multi-environment
- DRY config
- remote state
Gotcha
- Terragrunt
dependencyblocks create implicit ordering — circular dependencies cause cryptic errors. - Don't duplicate Terraform variables in terragrunt.hcl — use
inputsto pass them through. - The model tends to hardcode values that should come from
includeblocks — 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.hclwithout understanding the impact on all modules. - Do NOT remove
dependencyblocks — they ensure correct apply order. - Do NOT use
terraformcommands directly — always useterragruntas the wrapper.