The repetition problem in Terraform
After managing infrastructure across three environments with Terraform, your variables.tf, main.tf, and backend.tf start looking almost identical. You copy, paste, and tweak values—environment by environment, region by region, stack by stack. The friction compounds when you need to update a shared setting across twelve different directories. This is where Terragrunt basics: a Terraform & IaC guide for developers becomes essential. Terragrunt sits on top of Terraform, reducing boilerplate and enforcing consistency without sacrificing flexibility. By day 53 of building production infrastructure, you'll find that adopting Terragrunt transforms how your team manages environments and scales deployments.
Generate backend config automatically
One of the first pain points in multi-environment Terraform is managing remote state. Each environment needs its own S3 bucket (or equivalent), with a unique key path. You end up writing nearly identical backend.tf files in every directory:
terraform {
backend "s3" {
bucket = "my-prod-tfstate"
key = "prod/networking/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}Now repeat that for staging, develop, and each region variant. Terragrunt eliminates this repetition by generating backend configuration from a single source of truth. Create a terragrunt.hcl file at your root:
remote_state {
backend = "s3"
config = {
bucket = "my-tfstate-${get_aws_account_id()}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}The get_aws_account_id() and path_relative_to_include() functions are built-in helpers. When you run terragrunt plan in any subdirectory, Terragrunt automatically generates the correct backend.tf block before calling Terraform. This means you never manually write backend config again. If your AWS account ID, region, or bucket naming strategy changes, you update it once, and every stack inherits the change on the next run.
This approach scales cleanly. A team with fifty infrastructure stacks across four environments now maintains one backend template instead of two hundred boilerplate blocks.
Share inputs across environments
Terragrunt's inputs block lets you define variables hierarchically. Imagine your team deploys a microservice platform to dev, staging, and prod. Most settings—VPC CIDR, instance types, database version—differ slightly per environment, but some are shared globally.
Structure your directories like this:
infrastructure/
├── terragrunt.hcl (root config)
├── dev/
│ ├── terragrunt.hcl
│ ├── networking/
│ │ └── terragrunt.hcl
│ └── databases/
│ └── terragrunt.hcl
├── staging/
│ └── (similar structure)
└── prod/
└── (similar structure)In your root terragrunt.hcl, define globals:
inputs = {
team = "platform"
billing_code = "proj-2024-001"
tags = {
Environment = local.environment
ManagedBy = "Terragrunt"
}
}In dev/terragrunt.hcl, inherit and override:
include "root" {
path = find_in_parent_folders()
}
inputs = merge(
local.root_inputs,
{
instance_count = 2
instance_type = "t3.medium"
backup_retention_days = 7
}
)And in prod/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
inputs = merge(
local.root_inputs,
{
instance_count = 8
instance_type = "r6i.2xlarge"
backup_retention_days = 30
enable_multi_az = true
}
)When you run terragrunt apply in the dev directory, Terragrunt merges the root inputs with environment-specific overrides and passes everything to Terraform as variables. This pattern scales: add a new input to the root config, and it's immediately available to all environments. Change a prod-only setting, and dev remains unaffected.
Run many stacks with one command
One of Terragrunt's most powerful features is the ability to orchestrate multiple Terraform modules in a single operation. Suppose you have networking, databases, and application servers spread across separate directories. Applying changes across all three typically requires three separate terraform apply commands.
With Terragrunt, run:
terragrunt run-all planThis discovers all terragrunt.hcl files in the current directory tree and runs plan for each one. Terragrunt respects dependency order: if your application stack depends on the database stack, Terragrunt ensures the database is applied first.
Declare dependencies in a child terragrunt.hcl:
dependencies {
paths = ["../databases", "../networking"]
}
dependency "db" {
config_path = "../databases"
mock_outputs = {
db_endpoint = "localhost:5432"
}
}
inputs = {
database_host = dependency.db.outputs.db_endpoint
}When you run terragrunt run-all apply from the root, Terragrunt builds a directed acyclic graph of all stacks, respects the dependency chain, and applies changes in the correct order. If a stack fails, Terragrunt stops and reports the error. This is invaluable during disaster recovery or large infrastructure refactors where timing matters.
Be cautious: run-all apply without dry-run confirmation can modify production infrastructure quickly. Always run terragrunt run-all plan first and review the output carefully, or use terragrunt run-all apply --auto-approve=false to confirm each stack individually.
The cost: another tool to learn
Terragrunt adds a new layer of abstraction. Your team must understand not just Terraform syntax, but also Terragrunt's include, inputs, dependency, and path helper functions. Debugging becomes more complex: when a plan fails, is it a Terraform issue or a Terragrunt configuration error? The generated backend.tf and merged inputs are invisible until you run terragrunt init.
Training time increases. New engineers need to understand both tools. Documentation must cover both layers. And if Terragrunt introduces a bug or breaking change, your entire infrastructure workflow is affected.
For small teams or simple infrastructure (one or two stacks), Terragrunt may be overkill. The benefit only becomes clear once you're managing five or more stacks across multiple environments. That said, if repetition is causing errors and slowing your team, the investment pays off within weeks.
Common mistakes
- Circular dependencies: If stack A depends on B and B depends on A, Terragrunt cannot resolve the order. Always review your
dependenciesblocks during planning phases. - Forgetting to commit generated files: Terragrunt generates
backend.tf, but it should not be committed to version control. Add it to.gitignore. The source of truth isterragrunt.hcl. - Mixing hard-coded and dynamic values: If one environment uses a hard-coded security group ID and another uses a dynamic lookup, inconsistency breeds bugs. Standardize your input patterns across all environments.
- Running
run-allwithout understanding the plan: Terragrunt's batch operations are powerful but dangerous. Always preview the full plan output before applying, especially in production. - Ignoring mock_outputs: When dependencies are declared but the dependency stack hasn't been applied yet, mock_outputs provide sensible defaults. Omitting them causes confusing null reference errors.
Key takeaways
- Terragrunt eliminates backend configuration boilerplate by generating
backend.tfautomatically from a root template, reducing errors and maintenance burden. - Use hierarchical
inputsblocks to share global variables across environments while allowing per-environment overrides, keeping your configuration DRY. run-all planandrun-all applylet you manage multiple stacks as a unit, respecting dependency order and reducing manual orchestration.- Terragrunt adds complexity; adopt it when you have five or more stacks or when repetition is causing errors, not before.
- Always preview the full output of
run-allcommands before applying, especially in production, to avoid unintended large-scale changes. - Keep Terragrunt configuration version-controlled alongside your Terraform modules; generated files should never be committed.






Comments
Loading comments…