
September 09, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
You copied the same S3 backend block into twelve Terraform folders. Then you changed the bucket name once and missed three stacks. Terragrunt: Keep Terraform DRY solves that by moving repeated config into parent terragrunt.hcl files that child modules inherit automatically. If you already use infrastructure as code with Terraform, Terragrunt is the thin wrapper that stops env-specific duplication from eating your week. This guide covers folder layout, remote state, dependencies, and the mistakes I see on real deployments.
terragrunt.hcl files. Child folders include that config and pass only env-specific values, so you edit one file instead of copying blocks across every Terraform module.What is Terragrunt and why does it help keep Terraform DRY?
Terragrunt is a CLI wrapper around Terraform published by Gruntwork. It does not replace Terraform. It orchestrates it.
Terraform modules already reduce duplication inside a single repo. Terragrunt reduces duplication across repos and environments. You define a remote backend once. You pin provider versions once. You set common tags once. Every child stack picks those up through include blocks.
On production systems I maintain, the same EC2 + RDS pattern repeats across staging and production. Without Terragrunt, each folder carries its own backend snippet, its own provider block, and its own copy-pasted variables. That is how drift starts.
The DRY principle in Terragrunt rests on three mechanisms documented in the official Terragrunt documentation:
- Include blocks — child
terragrunt.hclfiles pull in parent configs withinclude "root". - Remote state generation — Terragrunt writes the backend block into a temp folder before each run.
- Dependency blocks — one stack reads outputs from another without manual
terraform outputcopy-paste.
Terragrunt wraps Terraform commands. Run terragrunt plan instead of terraform plan. Under the hood it generates files, sets working directories, and runs Terraform in the right context. That is the whole trick.
How do you set up a Terragrunt project structure?
A live Terragrunt repo mirrors how your org thinks about environments and services. I prefer a layout that separates reusable modules from live infrastructure.
Recommended folder layout
infra-live/
├── root.hcl
├── _envcommon/
│ ├── vpc.hcl
│ ├── rds.hcl
│ └── ec2.hcl
├── staging/
│ ├── env.hcl
│ ├── vpc/terragrunt.hcl
│ ├── rds/terragrunt.hcl
│ └── ec2/terragrunt.hcl
└── production/
├── env.hcl
├── vpc/terragrunt.hcl
├── rds/terragrunt.hcl
└── ec2/terragrunt.hcl
infra-modules/
├── vpc/
├── rds/
└── ec2/ Keep modules in a separate repo or top-level folder. Point Terragrunt at them with a terraform { source = "..." } block. This mirrors how Terraform modules for reusable infrastructure work, but Terragrunt handles the wiring between envs.
Root configuration file
Your root root.hcl (or terragrunt.hcl at the repo root) holds everything repeated across stacks:
remote_state {
backend = "s3"
config = {
bucket = "my-org-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "ap-southeast-1"
default_tags {
tags = {
ManagedBy = "Terragrunt"
}
}
}
EOF
} Notice the path_relative_to_include() function. Terragrunt sets a unique state key per folder automatically. That alone removes dozens of duplicated backend blocks. For deeper state strategy, see how to manage Terraform state safely and remote backend configuration.
Environment-level inputs
Each environment gets an env.hcl with locals only:
locals {
environment = "staging"
vpc_cidr = "10.10.0.0/16"
instance_type = "t3.small"
} Child stacks read these through read_terragrunt_config(find_in_parent_folders("env.hcl")). You never hard-code staging CIDRs inside a VPC module folder.
Leaf stack example
A production VPC stack stays short because inheritance does the heavy work:
include "root" {
path = find_in_parent_folders("root.hcl")
}
include "envcommon" {
path = "${dirname(find_in_parent_folders("root.hcl"))}/_envcommon/vpc.hcl"
expose = true
}
locals {
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}
inputs = {
cidr_block = local.env_vars.locals.vpc_cidr
environment = local.env_vars.locals.environment
} Run terragrunt run-all plan from the staging/ folder to plan every stack in dependency order. That command alone saves hours on multi-stack envs. Validate generated JSON with a JSON formatter when debugging complex inputs maps.
How does Terragrunt manage remote state and dependencies?
Remote state is where Terragrunt earns its keep. Plain Terraform needs a backend "s3" {} block in every module. Terragrunt generates it from the root config and keeps keys unique per folder.
Automatic state key isolation
The expression ${path_relative_to_include()}/terraform.tfstate produces keys like staging/vpc/terraform.tfstate and production/rds/terraform.tfstate. No two stacks collide. You do not maintain a spreadsheet of state paths.
Locking through DynamoDB works the same as native Terraform. Terragrunt just writes the block for you. If you run OpenTofu instead of HashiCorp Terraform, the backend config is identical — see OpenTofu as the open Terraform fork for compatibility notes.
Dependency blocks
RDS needs a VPC ID. In raw Terraform you export outputs manually or use terraform_remote_state data sources with hard-coded keys. Terragrunt simplifies this:
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-mock"
private_subnets = ["subnet-mock"]
}
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnets
} Terragrunt reads the remote state of ../vpc before planning RDS. It also respects order in run-all apply. VPC applies first. RDS waits. That is declarative orchestration without a separate tool.
For multi-region or multi-cloud setups, the same pattern scales. Each account/region gets an env folder. Shared root config stays identical. Read managing multi-cloud state with Terraform for backend choices beyond a single S3 bucket.
How do Terragrunt and Terraform modules work together?
Terragrunt does not replace modules. It calls them. Think of Terragrunt as the deployment layer and Terraform modules as the building blocks.
The terraform { source = "..." } block tells Terragrunt where to fetch module code. Common patterns:
- Local path —
source = "../../../infra-modules/vpc"for monorepos. - Git ref —
source = "git::https://github.com/org/modules.git//vpc?ref=v2.1.0"for version pinning. - Registry —
source = "tfr:///terraform-aws-modules/vpc/aws?version=5.0.0"for public modules.
Pin module versions explicitly. Terragrunt caches downloaded modules in .terragrunt-cache. Add that folder to .gitignore. When you bump a ref, run terragrunt init -upgrade in affected stacks.
Provider version constraints still live in the module's versions.tf or in a Terragrunt generate block. Align this with Terraform provider version pinning practices so CI and local runs match.
DRY inputs with _envcommon
The _envcommon/ folder holds partial configs shared by every environment for a given module type. A vpc.hcl there might set:
terraform {
source = "git::https://github.com/myorg/terraform-modules.git//vpc?ref=v1.4.0"
}
inputs = {
enable_dns_hostnames = true
enable_dns_support = true
az_count = 2
} Staging and production leaf files only override what differs — CIDR, NAT gateway count, tags. That is DRY at the input level, not just the backend level.
| Approach | DRY level | Best for | Trade-off |
|---|---|---|---|
| Plain Terraform folders | Low — copy backend per folder | 1–3 simple stacks | Fast start, painful at scale |
| Terraform modules only | Medium — shared resource logic | Single env, many resources | Backend still duplicated per deploy target |
| Terraform workspaces | Medium — shared code, split state | Small teams, similar envs | Workspace sprawl, weak isolation story |
| Terragrunt + modules | High — backend, providers, inputs, deps | Multi-env, multi-account, 5+ stacks | Extra tool, learning curve |
For workspace-specific trade-offs, compare with Terraform workspaces and environments. Workspaces solve a narrower problem. Terragrunt solves org-scale repetition.
What are common Terragrunt mistakes to avoid?
I've debugged Terragrunt setups that worked on a laptop and failed in CI. Most failures fall into a short list.
Duplicating config instead of including it
Teams new to Terragrunt often copy the root remote_state block into every child file "just to be safe." That defeats the purpose. Use include and find_in_parent_folders(). One source of truth.
Skipping mock_outputs on dependencies
Without mock_outputs, terragrunt plan on a fresh clone fails because dependency state does not exist yet. Always allow mocks for validate and plan commands. Remove mocks only when you know the dependency is applied.
Deep nesting without run-all flags
Running terragrunt apply manually in fifteen folders is error-prone. Use terragrunt run-all apply with --terragrunt-non-interactive in CI. Pair it with Terraform CI/CD in GitHub Actions or your GitLab pipeline.
Committing .terragrunt-cache
The cache directory contains downloaded modules and generated files. It bloats the repo and causes merge conflicts. Ignore it.
Mixing Terragrunt and hand-written backend.tf
If a module folder already has a backend.tf, Terragrunt's generate block may conflict. Pick one approach per stack. Generated backends with if_exists = "overwrite_terragrunt" are the clean default.
When should you choose Terragrunt over plain Terraform?
Not every project needs Terragrunt. A single VPS provisioned once a year is fine with plain Terraform for VPS provisioning.
Add Terragrunt when you hit any of these thresholds:
- Three or more environments with identical backend requirements.
- Five or more Terraform stacks that share dependencies.
- Multiple AWS accounts, regions, or cloud providers under one repo.
- CI pipelines that must plan/apply stacks in order without custom scripts.
- A team where Terraform variables, locals, and outputs are duplicated across folders weekly.
For sister sites I deploy with Deployer 7 and GitLab CI on shared EC2, Terragrunt would fit if we moved infra to code at scale. Today those sites share a pipeline pattern, not a Terraform monorepo. The same DRY thinking applies — centralise what repeats. See the Adventure Third Pole Trek deployment for a live Laravel + Livewire stack that benefits from consistent ops patterns.
Install Terragrunt alongside Terraform on your workstation or CI runner:
brew install terragrunt
terragrunt --version
cd infra-live/staging/vpc
terragrunt init
terragrunt plan
terragrunt apply Terragrunt detects the Terraform binary on your PATH. It passes through most CLI flags. Use terragrunt hcl fmt to format config files the way terraform fmt formats .tf files.
If your team manages servers day-to-day rather than cloud APIs, compare Terraform vs Ansible for provisioning vs configuration. Terraform (with Terragrunt) builds infra. Ansible configures it afterward. Many production setups use both.
For certification prep that covers Terraform fundamentals Terragrunt assumes you know, see the HashiCorp Terraform Associate certification guide. Terragrunt itself has no official cert, but the Terraform base matters.
Official Terraform language reference for modules and backends lives in the HashiCorp Terraform documentation. Cross-check generated HCL against those specs when debugging.
Key Takeaways
- Define remote state, providers, and common tags once in a root
terragrunt.hcl— child stacks inherit viaincludeblocks. - Use
path_relative_to_include()for automatic per-stack state keys so stacks never overwrite each other. - Wire cross-stack values with
dependencyblocks andmock_outputsso plans work on fresh clones. - Keep reusable module logic in a separate repo; Terragrunt handles env-specific inputs through
_envcommonandenv.hcl. - Run
terragrunt run-all plan/applyin CI instead of applying stacks one folder at a time by hand. - Adopt Terragrunt when you have multiple envs and repeated backend or dependency wiring — not for a single one-off stack.
People Also Ask
Does Terragrunt replace Terraform?
No. Terragrunt is a wrapper that generates Terraform files, manages remote state config, and orchestrates multiple modules. You still write Terraform modules and run Terraform providers under the hood. Terragrunt calls terraform plan and terraform apply for you.
Can Terragrunt work with OpenTofu?
Yes. Set the environment variable TG_TF_PATH=tofu or configure terraform_binary in your terragrunt config. OpenTofu uses the same HCL syntax and backend blocks Terragrunt generates. Test in a non-production env first.
How is Terragrunt different from Terraform workspaces?
Workspaces split state inside one backend key prefix within a single folder. Terragrunt uses separate folders per env with inherited config and explicit dependency graphs. Terragrunt scales better when environments differ in inputs, accounts, or stack count.
Where should I store Terragrunt config secrets?
Never commit secrets in terragrunt.hcl. Use environment variables, AWS SSM Parameter Store, or HashiCorp Vault. Terragrunt supports get_env() and sops integration patterns. Keep secrets out of version control and inject them at CI runtime.
Ship DRY infrastructure without the copy-paste tax
Terragrunt: Keep Terraform DRY by treating repeated backend, provider, and input config as a hierarchy problem, not a copy-paste problem. Start with a root config, add env folders, wire dependencies, and let run-all handle orchestration. The first hour spent on layout saves days of drift fixes later.
If you want help structuring live infra repos, CI pipelines, or server hardening around your Terraform workflow, Linux system administration and ongoing support and maintenance cover the full deploy path. For greenfield apps that will eventually need this ops layer, review enterprise application development or kokil.com.np. Ready to talk through your stack? Contact us with your current folder layout and env count.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

