Terragrunt — When and Why
Terragrunt is a third-party wrapper around Terraform — a separate binary, maintained by Gruntwork, not HashiCorp — that addresses the gaps teams hit at scale: repeated backend and provider boilerplate, managing many environments DRYly, and orchestrating dependencies between separate states. It is widely used and genuinely useful. It is also another tool, another DSL, and another dependency, and an honest account has to weigh all three.
The thing to hold onto is that Terragrunt solves a real but specific problem. It is not a prerequisite for "real" Terraform, and adopting it on a small estate adds more complexity than the boilerplate it removes. Whether it pays off is a question of scale.
What Terragrunt Adds
Three capabilities. It generates the backend and provider blocks for each unit from one shared definition, so you write the S3-backend config once instead of pasting it into every root. It lets one unit declare a dependency on another's outputs, so it can order applies across separate states and pass values between them. And run --all applies many units in dependency order in a single command, which plain Terraform cannot do across separate states.
# root terragrunt.hcl — generated into each child unit remote_state { backend = "s3" config = { bucket = "acme-tfstate" key = "${path_relative_to_include()}/terraform.tfstate" region = "us-east-1" } } # child unit — reuses the parent backend, declares a dependency include "root" { path = find_in_parent_folders() } dependency "vpc" { config_path = "../vpc" }
The Problems It Targets
All three map to pain that grows with the number of environments and accounts. Backend boilerplate duplication is linear in the count of roots — fifty roots means fifty near-identical backend blocks. Dependency ordering between states is something Terraform does not do natively: separate states are separate runs, and Terraform will not apply the network state before the app state for you. And keeping many environments consistent — the same module versions, the same structure — is manual without a layer that enforces it. Terragrunt exists to absorb exactly these.
The Cost
Terragrunt is a second tool and a second configuration language layered on top of Terraform. New engineers now learn HCL plus Terragrunt's own functions and block types. Debugging means reasoning about what Terragrunt generated and then what Terraform did with it — two layers instead of one. And it is a dependency on a non-HashiCorp project with its own release cadence and its own breaking changes to track. None of this is disqualifying; all of it is real overhead that a small estate does not earn back.
What Native Terraform Now Covers
Terragrunt is older than several Terraform features that now overlap with it. Partial backend config via -backend-config already lets one backend block serve every environment by passing the bucket and key at init time, covering much of the boilerplate problem without a wrapper. Better module composition and pinned shared modules cover the consistency problem. What stays genuinely Terragrunt's domain is cross-state dependency ordering and run --all across many units — Terraform still has no native equivalent. So the case for Terragrunt has narrowed to orchestration, not boilerplate alone.
Plain Terraform — shared modules plus -backend-config handle small-to-medium estates with no extra tool, DSL, or dependency. Choose it by default and until backend boilerplate and cross-state orchestration are a measured, recurring pain.
Terraform + Terragrunt — generated backend and provider config, DRY multi-environment management, and run --all dependency ordering across many states. Adopt it when you have many environments and accounts with heavy backend boilerplate and real cross-state dependencies to orchestrate — for that scale, not as a starting point.
- Adopting Terragrunt on a small project, adding a tool and DSL whose complexity outweighs the boilerplate it removes.
- Using
run --allacross many states without understanding the dependency ordering, causing partial applies that leave the estate half-updated. - Treating Terragrunt as required for "real" Terraform, when native features now cover much of what it once uniquely solved.
- Mixing heavy Terragrunt logic with heavy Terraform logic until the two layers obscure each other and debugging means tracing both.
- Leaving the Terragrunt version unpinned, so a wrapper upgrade changes generated config out from under a working estate.
- Adopt Terragrunt when backend boilerplate and multi-environment, multi-state orchestration are a real, recurring pain — not by default.
- Keep Terragrunt config thin and let Terraform modules hold the infrastructure logic, so the two layers stay separable.
- Evaluate whether native features like
-backend-configand pinned shared modules already cover your need before adding the dependency. - Pin the Terragrunt version alongside the Terraform version so the whole team and CI generate identical config.
- Understand the dependency graph before running
run --all, so an ordered apply does not partially fail across states.
-backend-config and shared modules cover much of it
Terramate / Terraspace similar orchestration wrappers
HCP Terraform addresses some multi-workspace needs as a service
Knowledge Check
What does Terragrunt actually add over plain Terraform?
- Generated backend and provider config, DRY multi-environment management, and
run --alldependency ordering across separate states - A replacement HCL parser that runs every plan faster than HashiCorp's own engine does, swapping out the core binary so plan and apply finish in a fraction of the time
- Built-in state storage of its own that removes any need for an S3 backend at all, keeping every environment's state inside Terragrunt instead of any external remote backend
- A graphical console for clicking through and approving infrastructure changes visually, replacing the command line with a point-and-click view of every pending plan
What is the cost of adopting Terragrunt?
- A second tool and DSL, a learning curve, and a dependency on a non-HashiCorp project with its own breaking changes
- It forces every resource across all environments into one single shared state file, collapsing the per-environment isolation you started with into one monolithic state to manage
- It requires rewriting all of your existing Terraform modules in a brand-new language before they can be called, so every module has to be ported across to adopt the tool
- It removes the ability to run
terraform plandirectly against a module
At what scale does Terragrunt pay off?
- Many environments and accounts with heavy backend boilerplate and real cross-state dependencies to orchestrate
- Any project holding more than a single resource, since native Terraform simply cannot stay DRY once a second resource appears and needs a wrapper from that point on
- A single environment backed by one state, which is precisely where apply ordering matters most and Terragrunt's dependency graph earns back its overhead fastest
- Only projects that have abandoned shared modules entirely and inline everything into one root, since Terragrunt is meant as a replacement for modules rather than a complement
Which native Terraform feature now overlaps with a problem Terragrunt once uniquely solved?
- -backend-config, which lets one backend block serve every environment without per-root boilerplate
- Workspaces, which order applies across separate states automatically with no extra wiring, sequencing each environment's run behind the previous one just as run --all does
- Provisioners, which generate each root's backend config automatically at apply time, writing the bucket and key into every environment without any per-root boilerplate
- The
taintcommand, which keeps every environment consistent with the others by replacing drifted resources so each root converges on one shared backend setup
You got correct