Terragrunt: Keeping Terraform DRY at Scale
Once you have more than a handful of Terraform root modules across environments, copy-pasted backend blocks and provider configs become a maintenance tax. Terragrunt removes that tax.
The problem Terragrunt solves
Terraform itself has no native concept of "this module, but once per environment, with environment-specific inputs, sharing a common backend configuration." Teams end up copying the same backend block, the same provider block, and the same remote state data source into every environment folder. Change your state bucket name and now you're editing fifteen files.
Terragrunt is a thin wrapper around the Terraform CLI that adds exactly this missing layer: DRY backend configuration, DRY provider generation, and explicit dependency ordering between modules — without touching your actual Terraform module code.
DRY backend configuration
Instead of repeating a backend block per environment, you define it once in a root terragrunt.hcl and let every child inherit and parameterise it:
# terragrunt.hcl (root)
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
bucket = "my-org-tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
Every environment's terragrunt.hcl just does include "root" { path = find_in_parent_folders() } and the state key is derived automatically from its folder path — no manual key management, no drift between environments.
path_relative_to_include() means moving or renaming a module folder automatically updates its state key convention — no manual backend edits required.Explicit dependencies between modules
Real infrastructure has ordering requirements: VPC before compute, IAM roles before the services that assume them. Terragrunt's dependency blocks let you reference outputs from another module's state directly, and Terragrunt resolves the execution order for you with run-all.
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}
terragrunt run-all apply walks the dependency graph and applies modules in the correct order, in parallel where safe. This replaces hand-maintained Makefiles or CI pipeline stages that call terraform apply in a hardcoded sequence.
Environment hierarchies without copy-paste
A typical layout separates account, region, and environment-specific inputs into their own terragrunt.hcl files, merged via include blocks:
live/
_envcommon/
vpc.hcl
prod/
eu-west-1/
vpc/
terragrunt.hcl # includes _envcommon/vpc.hcl, overrides cidr
staging/
eu-west-1/
vpc/
terragrunt.hcl # includes same _envcommon/vpc.hcl, different cidr
The actual module logic lives in one place (_envcommon or a versioned module source); environment folders only hold the inputs that differ. This is the single biggest win for teams managing more than two or three environments.
Things to watch out for
- Extra indirection. New engineers need to learn both Terraform and Terragrunt's HCL dialect — there's a real onboarding cost.
run-allblast radius. Runningrun-all applyfrom a high-level directory can touch more modules than intended. Scope your working directory carefully, especially in CI.- Version pinning. Pin both the Terragrunt binary and Terraform version explicitly in
terragrunt.hclviaterraform_version_constraint— mismatches between CI runners are a common source of "works on my machine."
Final thoughts
Terragrunt isn't a replacement for good Terraform module design — it's a layer that removes the boilerplate Terraform makes you repeat across environments. If you're maintaining more than two or three near-identical environment folders by hand, it's worth the learning curve.