With twenty environment folders, the same backend block, provider block and module call are copied twenty times. Terragrunt is a thin wrapper that generates that boilerplate and runs units in dependency order. It drives tofu or terraform underneath.
flowchart TB ROOT["root.hcl: remote_state + provider generate"] --> NET["live/dev/network/terragrunt.hcl"] ROOT --> APP["live/dev/app/terragrunt.hcl"] NET -->|"dependency outputs"| APP RA["terragrunt run --all apply"] --> NET RA --> APP %% step ROOT: The root file defines the backend and provider once; the state key comes from each unit's folder path. %% step NET: A unit is a folder with a terragrunt.hcl that points at a module and sets its inputs. %% step APP: The app unit reads the network unit's outputs through a dependency block. %% step RA: run --all walks every unit below the current folder in dependency order.
Layout
live/
root.hcl
dev/
network/terragrunt.hcl
app/terragrunt.hcl
prod/
network/terragrunt.hcl
app/terragrunt.hcl
# live/root.hcl
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite_terragrunt" }
config = {
bucket = "shop-tfstate"
key = "${path_relative_to_include()}/terraform.tfstate" # dev/app/terraform.tfstate
region = "us-east-1"
use_lockfile = true
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = "provider \"docker\" {}"
}
# live/dev/app/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "git::https://github.com/example-org/tf-modules.git//app?ref=v1.6.0"
}
dependency "network" {
config_path = "../network"
mock_outputs = { network_name = "mock-net" } # lets plan work before network exists
}
inputs = {
env = "dev"
network_name = dependency.network.outputs.network_name
}
Note the backend key: because each unit has its own folder, every unit automatically gets its own state.
Running it
| Command | Effect |
|---|---|
terragrunt plan |
Plan one unit (in its folder) |
terragrunt run --all plan |
Plan every unit below the current folder, in order |
terragrunt run --all apply |
Apply all, network before app |
terragrunt dag graph |
Show the dependency graph |
Older Terragrunt versions used terragrunt run-all plan; current versions use run --all.
When it is worth it
Use Terragrunt when you have many environments or accounts with the same building blocks. For two or three environments, plain folders and a module are simpler. Newer Terragrunt versions also offer stacks (terragrunt.stack.hcl) to generate whole sets of units.
Traps: run --all apply across prod applies many states in one command, so pipelines usually run it per environment with approval; and mock_outputs used during apply would wire fake values into real resources, so restrict them to plan/validate with mock_outputs_allowed_terraform_commands.