docs/src/data/faq/why-use-terragrunt-instead-of-plain-opentofu.mdx
Terragrunt does not replace OpenTofu/Terraform. It orchestrates them, and extends what they do.
The two tools have different jobs. OpenTofu/Terraform are deliberately narrow: they turn configuration into infrastructure, with as few side effects as possible. That restraint is why they are predictable, and it is also why they leave a set of problems unsolved. Terragrunt takes on the messier side of the work: networks are flaky, APIs return transient errors, credentials differ between environments, and operational procedures have to run somewhere.
If you have one or two root modules and no dependencies between them, using OpenTofu/Terraform alone might be fine for you. The problems below show up as the number of units, environments, and people grows.
Each Terragrunt unit has its own state, and a root configuration shared by every unit can derive each one's backend key from its path with path_relative_to_include(). You get per-unit state without configuring a backend by hand for every root module.
The obvious benefit is blast radius: your current working directory is what you can break. The less obvious one matters more on a team. One state file means one lock, so two engineers touching unrelated infrastructure still queue behind each other. Segmented state gives each unit its own lock, and unrelated work proceeds in parallel. A monolithic state file is an organizational bottleneck as much as a technical one, and it produces a Terralith: slow plans, risky changes, and diffs nobody wants to review.
Per-unit state also makes least-privilege practical. Because each unit has its own state and its own run, you can scope credentials to that unit rather than granting one role broad enough to apply everything.
A unit points at one module through its terraform block and supplies inputs. The fact that this is the lowest friction way to use Terragrunt is an advantage by design. There is a persistent temptation to write new infrastructure by hand, and to quietly bolt one more resource onto a root module that was supposed to be a clean instantiation of a shared pattern. When instantiating a module is the easiest thing to do, reuse is explicit, and drift from the pattern shows up in review.
The dependency block passes outputs from one unit into another and places both in a DAG, so run --all apply visits them in the right order and destroys them in reverse. mock_outputs lets you plan a unit whose dependencies have not been applied yet. Wiring the same thing together in plain OpenTofu/Terraform means either a single large state file or manual coordination.
terragrunt run --all plan runs the command across every unit in the run queue, in parallel where the DAG allows it, with per-unit prefixes on the output and a summary of what succeeded, failed, or was skipped.
Running everything is rarely what you want. The --filter flag is a small query language for selecting components, and it composes:
type=unit or external=trueterragrunt run --all --filter-affected -- plan
Backend configuration, provider blocks, and common inputs live in a root configuration file that units pull in with an include block. generate blocks write files into the module before a run, so you can inject a provider or backend into a module you do not control. You write these once rather than in every root module.
Promoting a change through environments usually means either applying it everywhere at once or opening a pull request per environment to bump versions one at a time. Feature flags offer a third option: codify both configurations, then control which one is active at runtime with the feature block or the --feature flag. Shipping the change and turning it on become separate steps.
The catalog gives you a searchable terminal UI over the modules in your repositories, and scaffold generates a unit's terragrunt.hcl from a chosen module using boilerplate templates, with support for your own. A developer who is not fluent in IaC can provision from a pre-approved pattern without waiting on the platform team.
Large runs fail for boring reasons: rate limits, eventual consistency, an API having a bad minute. The errors block makes that configuration rather than a shell retry loop. retry matches errors by regex and retries with a bounded attempt count and sleep interval; ignore lets known-safe errors through without failing the run.
Before, after, and error hooks run commands around an OpenTofu/Terraform invocation. Building and pushing a container image before applying the service that runs it, taking a database backup before a risky change, running smoke tests after: these have to happen somewhere, and putting them in the module makes the module less reusable. Hooks keep the generic infrastructure pattern generic and the implementation-specific steps next to the unit that needs them.
Terragrunt can acquire credentials as part of a run rather than expecting them to already exist in the environment. It can assume an IAM role via --iam-assume-role or the iam_role attribute, including through OIDC. For anything more involved, --auth-provider-cmd runs a program of your choosing to produce credentials at runtime, which is how you give different units, accounts, and pipeline stages different roles: a read-only role on pull requests and a read-write role on merge, for example.
At scale, most of a run is spent fetching things you already have. Terragrunt shares them:
The exec command runs an arbitrary program with the unit's inputs in scope as TF_VAR_ environment variables, so tooling that needs the same configuration as your infrastructure code can read it from one place. IaC engines go further and let you change how OpenTofu/Terraform is invoked at all.