Terragrunt — When and Why
Terragrunt is a thin wrapper around Terraform that exists to remove the boilerplate native Terraform leaves you to repeat. It generates the backend and provider blocks every stack would otherwise duplicate, wires dependencies between stacks so one's outputs feed another's inputs, and runs apply across many stacks at once in the right order. None of that is new capability — it is the same Terraform underneath — but it deletes toil that grows with the number of stacks.
So the question is never "is Terragrunt good" but "do I have enough stacks for it to pay off." It earns its keep when you have dozens of stacks — many Hatch projects, each a root module — and the per-stack backend duplication and ordered cross-stack applies become real work. Below that scale it is a second tool, a second config dialect, and a license to weigh, bought for a problem the native patterns from the last topic already solved.
What Terragrunt Adds
Two things, mainly. First, DRY generation of the backend and provider blocks every stack would otherwise repeat — you declare the backend pattern once in a root terragrunt.hcl and Terragrunt generates the per-stack block at runtime, so there is no -backend-config ceremony per stack. Second, dependency blocks that pass one stack's outputs into another's inputs, replacing hand-written terraform_remote_state lookups with explicit, mockable dependencies.
These map directly onto the native patterns from Topic 67. The partial backend config you supplied with -backend-config becomes a generated block; the terraform_remote_state data source you wired by hand becomes a dependency. Terragrunt is automation of the same DRY discipline, not a different idea — which is exactly why it only pays off once that discipline is repeated across many stacks.
run --all Across Stacks
terragrunt run --all apply plans and applies every stack in a tree in dependency order. Standing up hatch-net-host and then the service projects that attach to its Shared VPC becomes one command instead of a hand-ordered sequence of cd and apply per stack. Terragrunt reads the dependency graph across stacks and walks it for you, the way Terraform walks the resource graph within a single stack.
One naming note worth carrying: the older run-all subcommand is now spelled run --all. The behavior is the same, but a Terragrunt upgrade will deprecate the old form, so write run --all in scripts and CI from the start to avoid the rename biting later.
Dependency Wiring
A dependency "network" block reads the Shared VPC host project's outputs and feeds them to a service project that attaches to it. This replaces the brittle data "terraform_remote_state" lookups you would otherwise copy between every consuming stack — each one reaching into another stack's state by hand and breaking silently when an output is renamed.
# Terragrunt: explicit, mockable dependency dependency "network" { config_path = "../hatch-net-host" } inputs = { network_id = dependency.network.outputs.network_id }
The dependency is named, version-checkable, and can be mocked for a plan before the upstream stack exists. A renamed output now breaks at plan time with a clear error pointing at the dependency, instead of failing deep inside a consuming stack that quietly read stale state.
When Terragrunt Earns Its Keep
The signal is scale and ordering. Many stacks with repetitive backend and provider config, real cross-stack dependency ordering, and a team tired of -backend-config ceremony — that is where Terragrunt pays for itself. A multi-project Hatch org with dozens of root modules and a network host that everything depends on is the shape it was built for.
At five stacks it is not. There, the native patterns from Topic 67 — shared modules, per-environment .tfvars, and partial backend config — are simpler and add no extra tool. The crossover is roughly where hand-maintaining backend blocks and a hand-ordered apply script stops being a minor chore and becomes a recurring tax; below it, native Terraform wins on simplicity.
The Cost of Adopting It
Terragrunt is a second tool to install, pin, and learn, with its own HCL dialect layered over Terraform's. A small estate pays that learning cost up front and never collects the payoff, because there was little duplication to delete. The honest framing is that Terragrunt trades a fixed adoption cost for a per-stack saving — worth it only when the stack count makes the saving large.
A license note worth getting right: Terragrunt itself remains MIT-licensed and fully open source — it did not follow Terraform's 2023 move to the BUSL, and Gruntwork explicitly pivoted Terragrunt to support OpenTofu. So an OpenTofu-first organization that chose OpenTofu for its open-source license can add Terragrunt without undercutting that choice; the cost to weigh is the second tool and dependency, not its license. Pin the Terragrunt version alongside Terraform, and decide on it the same way — once, at the org level.
Native Terraform — shared modules, per-env .tfvars, and -backend-config, with terraform_remote_state for cross-stack reads. No extra tool, no extra license. The right choice for a handful of stacks where the duplication is small.
Terragrunt — generates backend and provider blocks, wires stacks with dependency blocks, and runs ordered multi-stack applies with run --all. Earns its keep at dozens of stacks with real cross-stack ordering; below that it is a second tool and dependency bought for a problem you do not yet have. It stays MIT-licensed, so the cost is the extra tooling, not the license.
- Adopting Terragrunt at five stacks to "do it properly," then carrying a second tool and its config dialect for a problem native
.tfvarsand-backend-configalready solved. - Refusing Terragrunt at fifty stacks and hand-maintaining fifty near-identical backend blocks and a fragile hand-ordered apply script — the toil Terragrunt was built to delete.
- Wiring stacks together with copy-pasted
terraform_remote_statedata sources instead ofdependencyblocks, so a renamed output breaks consumers silently. - Still typing the retired
run-allform and hitting deprecation after a Terragrunt upgrade, rather than the currentrun --all. - Rejecting Terragrunt on an OpenTofu-first estate by assuming it inherited Terraform's BUSL, when Terragrunt is in fact MIT-licensed and supports OpenTofu.
- Adopt Terragrunt once the stack count and cross-stack ordering make native backend and provider duplication real toil — typically a multi-project org, not a handful of stacks.
- Use
dependencyblocks to pass outputs between stacks instead of hand-wiredterraform_remote_statedata sources. - Run
terragrunt run --all applyfor ordered multi-stack rollouts, and userun --all planin CI to see the whole tree before applying. - Pin the Terragrunt version alongside Terraform and weigh its license against an OpenTofu-first policy before standardizing on it.
- Stay on native Terraform's DRY patterns until the per-stack duplication is a recurring tax, since the wrapper only pays off at scale.
-backend-config and terraform_remote_state, the no-extra-tool baseline
Pulumi stacks stack references for the same dependency wiring, built in
Knowledge Check
What does Terragrunt generate that native Terraform makes you repeat per stack?
- The backend and provider blocks, declared once and generated per stack, plus dependency wiring between stacks
- The resource blocks themselves, generated once so you never have to write the same resource twice across stacks
- The state file itself, which it stores in its own proprietary Terragrunt-specific format
- The provider plugins, which it downloads and caches faster than a plain terraform init does
What does terragrunt run --all apply do, and what was it called before?
- It applies every stack in a tree in dependency order; it was previously spelled
run-all, now deprecated - It re-applies one stack repeatedly in a loop until it converges; it was previously called
loop-apply - It validates all stacks in the tree without applying any of them; it replaced
terraform validate - It applies stacks strictly in alphabetical order by directory name; the subcommand name has never once changed
Why prefer a dependency block over a terraform_remote_state data source?
- It is an explicit, mockable dependency, so a renamed upstream output fails clearly at plan time instead of silently breaking consumers
- It encrypts the upstream stack's state object before reading any value out of it
- It lets two separate stacks safely share one single state file so you save on an extra bucket prefix and all of the setup it would need
- It is the only supported way to read another stack's outputs from a GCS backend
At roughly what point does Terragrunt earn the cost of being a second tool?
- When dozens of stacks and real cross-stack ordering make native backend duplication a recurring tax
- At the very first stack, since native Terraform on its own cannot manage remote state in GCS
- Only once a single estate exceeds a thousand managed resources, regardless of how many stacks there are
- Never — Terragrunt is always strictly heavier than native patterns at every possible scale
You got correct