Terragrunt is a thin orchestration layer around OpenTofu/Terraform — it generates backend and provider config, wires
dependencies between modules, and runs Terraform across many environments from one DRY source. It's not a
replacement for Terraform; every terragrunt command ultimately shells out to a real tofu/terraform binary.
Current CLI syntax
Recent Terragrunt versions (0.80+) reorganized the CLI around an explicit run command. The direct shortcuts
below (terragrunt plan, terragrunt apply) still work and shell out to run internally — use whichever reads
clearer; run is more explicit and is what --all orchestration is built on.
Core Commands
terragrunt initInitializes the working directory — same as Terraform's init, plus generating any backend/provider config Terragrunt is configured to inject.
terragrunt planShows what would change, for the single unit in the current directory.
terragrunt applyApplies the current unit.
terragrunt run -- planThe explicit form of the same plan command — useful when passing flags that would otherwise be ambiguous between Terragrunt's own options and the underlying Terraform command.
terragrunt destroyDestroys the infrastructure managed by the current unit.
terragrunt force-unlock LOCK_IDReleases a stuck state lock, same as Terraform's own force-unlock.
Running Across Multiple Units
terragrunt run --all -- planRuns plan across every unit in and below the current directory, in dependency order.
terragrunt run --all -- applyApplies every unit in and below the current directory, in dependency order. Review the plan output for each unit carefully — this touches everything in scope at once.
terragrunt run --all --parallelism 4 -- planLimits how many units run concurrently — useful against a backend or provider API with rate limits.
terragrunt.hcl — Remote State
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}path_relative_to_include()
This function computes each unit's own unique state key automatically from its position in the directory tree —
the core trick that lets one remote_state block (defined once, in a root terragrunt.hcl) serve every
environment without a copy-pasted, manually-edited key per environment.
include — DRY Root Config
# environments/prod/app/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules//app"
}
inputs = {
instance_count = 3
}The root terragrunt.hcl (found via find_in_parent_folders()) defines shared backend/provider config once;
every environment's unit includes it and only specifies what's actually different — its own inputs.
dependency — Wiring Units Together
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}terragrunt outputShows outputs for the current unit — what a dependency block reads from another unit.
mock_outputs
Add mock_outputs to a dependency block so terragrunt plan on a unit works even before its dependency has
ever been applied (e.g. in CI, or a fresh environment) — without it, planning a dependent unit fails outright if
the dependency has no real outputs yet.
Terraform vs. Terragrunt
Terraform
The provisioning engine — HCL resources, state, providers, the plan/apply graph. Works standalone for a single environment or a hand-rolled multi-environment setup.
Terragrunt
A wrapper that keeps backend config, provider config and environment-specific inputs DRY across many
environments, and orchestrates run --all across a whole tree of units in dependency order. Adds value once
you have enough environments that copy-pasted root modules become a real maintenance cost.