What Terragrunt is, and the problem it solves
Terraform is good at describing one stack. It is less comfortable when you have forty of them: a development, staging and production environment, across several accounts and regions, each needing the same backend settings, the same provider configuration and a particular order of deployment. Copy and paste works for a while, then someone forgets to change a state key and two environments share state.
Terragrunt is a thin wrapper that addresses exactly that. It reads small configuration files, generates the repetitive Terraform blocks, wires dependencies between stacks and runs Terraform or OpenTofu on your behalf. It does not change how Terraform plans or applies. Think of it as orchestration and templating around the tool, rather than a replacement for Terraform.
We use it where the scale justifies it, and we say so when it does not. Our broader Terraform infrastructure as code service covers the foundations that Terragrunt depends on.
How we lay out a Terragrunt repository
A clear layout is the whole game. We usually organise directories as account, then region, then stack, with shared configuration near the root:
# live/prod/eu-west-1/network/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "git::https://github.com/example-org/infra-modules.git//network?ref=v1.4.0"
}
inputs = {
cidr_block = "10.20.0.0/16"
environment = "prod"
}
The root file defines the remote state backend using the directory path to derive each key, so no one hand-writes a state location. The leaf file names a module version and passes inputs. Promotion between environments becomes a pull request that changes the version reference.
Versioned modules
Source modules live in their own repository and are pinned to a tag. This keeps infrastructure code and environment configuration separate, and means a module change reaches production only when someone updates a version number. The practice mirrors our guidance on module versioning and registries.
Shared includes, used sparingly
It is tempting to put everything in includes and generate half the repository. Resist that. Include what is genuinely identical everywhere: backend, provider, required versions and default tags. Anything environment-specific stays in leaf files so a reader can understand a stack by opening one directory.
Dependencies and ordering
Terragrunt lets a stack declare that it needs outputs from another, such as a database stack reading subnet identifiers from the network stack. It also derives an apply order from those declarations. Two details matter in practice.
First, mock outputs let a plan run before a dependency exists, which is useful on first deployment but dangerous if mocks leak into a real apply. We restrict mock use to specific commands.
Second, dependency graphs can hide coupling. If every stack depends on five others, a small change ripples widely. We keep the graph shallow and prefer data sources or well-known parameter names for loose coupling where it makes sense.
Pipelines and run-all
The run-all family of commands applies an operation across many stacks. It is convenient and also the fastest way to change a lot of production at once. Our pipelines detect which directories changed in a pull request, plan only those and their dependents, post the results for review, and apply from a controlled runner under approval. We cap parallelism so provider rate limits and lock contention do not cause failures. Pull requests link back to the plan so that approvers read what will change.
Security and access
Everything from Terraform applies here: short-lived federated pipeline credentials, separate plan and apply roles, encrypted and versioned state with narrow access, and static analysis such as Checkov in CI. Terragrunt adds one specific concern: generated files and before_hook or after_hook commands can execute arbitrary scripts, so we review hooks like any code that runs with deployment privileges and avoid them where a pipeline step would do. Pin the Terragrunt version along with Terraform or OpenTofu so everyone, including CI, runs the same binaries.
Cost and capacity
Terragrunt is free and open source. The real cost is another layer to learn and maintain, plus extra care with onboarding. Plan speed is dominated by Terraform, provider API calls and state size, so keeping stacks small helps more than any wrapper feature. We do not quote performance numbers; they depend on your providers and accounts.
Day-2 operations
Upgrades involve three moving parts: Terragrunt, the Terraform or OpenTofu binary and the providers. We pin all three, upgrade in a lower environment and read changelogs for configuration changes, since the wrapper’s configuration syntax has evolved over time. Drift checks run as scheduled plans per stack, as described in our drift detection guide. For multi-environment structure more generally, see Terraform multi-environment setup.
Common failure modes
- Adopting it too early. Three stacks do not need a wrapper.
- Over-generated configuration. Nobody can tell what a stack contains without running the tool.
- Deep dependency chains. Slow applies and fragile ordering.
- Unreviewed run-all. A broad command changes many environments together.
- Unpinned tooling. Engineers and CI on different versions produce different results.
When Terragrunt is the wrong choice
If you have a small number of stacks, use plain modules and a pipeline. If your pain is code review and locking rather than repetition, tools such as Atlantis address that more directly. If you want loops and abstraction in a real language, Pulumi is a different route altogether. And if you are AWS-only and prefer a native service, CloudFormation with StackSets covers multi-account rollout without any wrapper.
Terragrunt works with both Terraform and OpenTofu, so it does not force the licensing decision. If you are unsure whether it suits your estate, talk to us and we will look at your stack count and layout before recommending anything.
Official documentation: terragrunt.gruntwork.io