Terragrunt keeps large Terraform setups DRY and orchestrated, but small teams often pay its learning curve for little return.
Terragrunt shows up in a lot of infrastructure repos, and it shows up in a lot of arguments too. Some teams swear it saved them from copy-paste hell. Others rip it out after a year and wonder why they added a whole extra tool. Both camps are usually right, because Terragrunt earns its keep in specific conditions and becomes dead weight outside them.
This post walks through what Terragrunt is, the concrete problems it solves, what it costs you, and how to decide whether your setup is one where it pays off.
Terragrunt is a thin wrapper around Terraform (and now OpenTofu). It does not replace Terraform. Under the hood it still calls the same binary, generates the same plan, and manages the same state. What it adds is a layer that handles the boilerplate Terraform makes you repeat: backend configuration, provider blocks, variable wiring, and running commands across many modules at once.
If you have never hit that boilerplate as a real problem, that sentence probably sounds like a solution looking for a problem. That is the honest tension at the center of Terragrunt. It only makes sense once your repo is big enough that repetition genuinely hurts.
DRY backend config: In plain Terraform, every module that keeps remote state needs a backend block, and those blocks cannot use variables. So you end up hardcoding the same S3 bucket, region, and a slightly different key in dozens of places. Terragrunt generates that block for you from one shared definition, changing only the state key per module.
DRY provider config: The same story applies to provider blocks, required_providers, assume-role settings, and default tags. Terragrunt can generate a provider.tf into each module so you define the pattern once and inherit it everywhere.
Many environments and state files: When you run dev, staging, and prod as separate state files (which you should for blast-radius reasons), the input differences between them are usually small. Terragrunt lets you keep the module code in one place and feed each environment its own inputs, without duplicating the module per environment.
Dependencies between modules: Real infrastructure has ordering. The VPC has to exist before the database, the cluster before the app. Terragrunt models this with dependency blocks that pass one module's outputs into another's inputs and enforce apply order.
run-all across a stack: Once dependencies are declared, terragrunt run-all apply walks the whole graph and applies everything in the right order. For a platform team managing dozens of components, that single command is a big part of the appeal.
terragrunt.hcl model#Each leaf directory holds a small terragrunt.hcl that points at a module and supplies inputs. Shared settings live in a parent file that children inherit through include.
# live/prod/us-east-1/database/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::git@github.com:acme/modules.git//rds?ref=v1.4.0"
}
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
instance_class = "db.r6g.large"
subnet_ids = dependency.vpc.outputs.private_subnet_ids
environment = "prod"
}
The root file (found via find_in_parent_folders) holds the backend and provider generation that every module shares, so no leaf ever repeats it.
The folder layout is where the model clicks. You typically split immutable module code from the live configuration that instantiates it:
infra/
├── modules/ # reusable Terraform modules
│ ├── vpc/
│ ├── rds/
│ └── eks/
└── live/
├── terragrunt.hcl # root: backend + provider generation
├── dev/
│ └── us-east-1/
│ ├── vpc/terragrunt.hcl
│ ├── database/terragrunt.hcl
│ └── eks/terragrunt.hcl
└── prod/
└── us-east-1/
├── vpc/terragrunt.hcl
├── database/terragrunt.hcl
└── eks/terragrunt.hcl
Adding a new environment becomes a matter of copying a directory of small config files, not duplicating module logic.
Terragrunt is not free. The costs are real and worth naming.
Bold: It is another layer. You now debug across two tools. When something breaks, you have to work out whether the fault is in your Terraform, in the generated code, or in Terragrunt's orchestration. Generated backend.tf and provider.tf files are not in your source, which surprises people reading the repo for the first time.
Learning curve: HCL functions like find_in_parent_folders, read_terragrunt_config, and the dependency/mock_outputs dance take time to learn. New engineers who already know Terraform still have to learn a second mental model on top.
Overlap with native features: Terraform has closed some of the gap Terragrunt originally filled. Partial backend config via -backend-config files, provider default tags, and the newer stacks and test tooling all reduce the "you have no other option" argument. Terragrunt still does more, but the case is narrower than it was in 2019.
Native workspaces: Good for a handful of near-identical environments off one config. They share a backend key prefix and get awkward once environments diverge, so treat them as a light-touch option rather than a scaling strategy.
Terramate: A newer orchestration layer that generates Terraform code and adds change detection, so it only runs stacks that actually changed. Worth a look if orchestration is your main pain rather than DRY config.
Plain modules plus a wrapper script: A well-factored module library plus a short Makefile or shell script covers a surprising number of teams. You lose run-all and dependency graphs, but you keep the stack simple.
For where these sit against each other, see our roundup of the best Infrastructure-as-Code tools. If you are also weighing engines, Terraform vs OpenTofu is the companion read.
Ask a few blunt questions:
run-all is genuinely useful.If most answers point at "small and simple," native Terraform will serve you better and read more plainly to the next person.
Reach for Terragrunt when you are running many environments across many components with a platform team that owns the tooling. That is exactly the situation it was built for, and it will save you from a repo full of near-duplicate backend blocks and manual apply ordering.
Skip it when you have one or two environments, a modest number of state files, and no dedicated platform owner. In that world the extra layer costs more attention than it returns, and plain Terraform modules with a thin script will keep your setup easier to reason about. Start without it, and adopt it the day repetition and apply-ordering stop being annoyances and start being incidents.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
The observability market is huge and the pricing is a minefield. This is the map to the tools that matter, what each is best at, and how to avoid a runaway bill.
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
Explore more articles in this category
The IaC landscape fractured after the Terraform license change. This is the map to what each tool is actually best at, and how to choose without regret.
A practical comparison of Kubernetes-native continuous reconciliation against the classic CLI-driven, state-file IaC model for platform teams.
Every CI/CD platform claims to be fast and easy. The real differences are in pricing, self-hosting, and where each one falls apart at scale. This is the map.
Evergreen posts worth revisiting.