Terragrunt for DRY Terraform modules
Problem
For three environments (dev, staging, prod) Terraform spawns three directories, each with a copy-pasted main.tf, backend.tf, providers.tf. Changing a provider takes three nearly identical PRs. Backend configs drift apart, and terraform init accidentally writes state into another environment. It all smells, but it still works.
Solution
Terragrunt is "a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale". For this case it gives two things:
- DRY configuration. The backend, providers and shared variables are described once in the root
terragrunt.hcl. Childterragrunt.hclfiles inherit them viainclude. - Backend derived from the path. The S3/Azure Blob state key is generated from the path in the repository — mixing up environments becomes physically impossible.
Example
Layout:
infra/
├── terragrunt.hcl # root: remote state, providers
├── modules/
│ └── vpc/
│ ├── main.tf
│ └── variables.tf
└── environments/
├── dev/
│ └── vpc/terragrunt.hcl
├── staging/
│ └── vpc/terragrunt.hcl
└── prod/
└── vpc/terragrunt.hcl
The root terragrunt.hcl — defined once:
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "acme-tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "eu-central-1"
encrypt = true
dynamodb_table = "acme-tfstate-lock"
}
}
generate "providers" {
path = "providers.tf"
if_exists = "overwrite_terragrunt"
contents = file("${get_terragrunt_dir()}/../../_shared/providers.tf")
}
environments/prod/vpc/terragrunt.hcl — all the environment-specific code:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/vpc"
}
inputs = {
cidr_block = "10.10.0.0/16"
env = "prod"
}
Run it like regular Terraform, just through terragrunt:
cd environments/prod/vpc
terragrunt plan
terragrunt apply
# Run all environments at once
cd environments
terragrunt run-all plan
In recent Terragrunt releases run-all is replaced by terragrunt run --all plan; check the syntax against the version you use.
When to use
- More than one environment, and configs are already drifting.
- Several teams share one IaC repository.
- You want "prod accidentally wrote state into the dev bucket" prevented by the tool, not by review.
Source
- gruntwork-io/terragrunt
- bregman-arie/devops-resources — the Terraform section
How to apply it yourself
- Extract shared code from the existing environments into
modules/and_shared/. - Create a root
terragrunt.hclwithremote_stateandgenerate. - Rewrite each environment as a small
terragrunt.hclwithinclude+inputs. - Manually delete the duplicated
backend.tf/providers.tffrom the environments. - A
terragrunt run-all planwith no resource changes = the migration went cleanly → status Studied.