Why Terragrunt
The idea in one sentence
Section titled “The idea in one sentence”Terraform workspaces solve reuse well but leave isolation weak, directory-per-environment solves isolation well but reintroduces duplicated boilerplate, and Terragrunt exists specifically to close the gap between them without asking you to give up either property.
Two halves of one problem, recap
Section titled “Two halves of one problem, recap”The last two lessons walked through Terraform’s two native answers to the multi-environment problem, and each one traded one goal for the other. A workspace keeps one .tf config and one backend/provider block, and just switches which state file is active:
# Workspaces: one directory, one backend, one provider, one subscription_id# for every environment, only the active workspace changesterraform workspace select prodterraform applyDirectory-per-environment goes the other way: real isolation through a genuinely separate directory, backend, and provider block per environment, at the cost of that backend/provider boilerplate being copy-pasted almost verbatim into every environment:
# Directory-per-environment: real isolation, separate backend and provider# per environment, but environments/dev, environments/staging, and# environments/prod each carry their own near-identical backend.tfcd environments/prodterraform initterraform applyNeither approach is wrong, and neither is a bug in Terraform — they are two different, honest trade-offs between the same two goals. What Terraform does not offer, on its own, is a single primitive that gives you strong isolation and zero configuration duplication across environments at the same time.
What Terragrunt actually is
Section titled “What Terragrunt actually is”Terragrunt is a thin wrapper and orchestrator that sits on top of the real terraform (or tofu) binary. It does not introduce a new resource syntax, and the module code you write to actually create Azure resources stays exactly what it already is: plain Terraform, in plain .tf files, unchanged.
What Terragrunt adds is a layer above that code, aimed squarely at the duplication that directory-per-environment reintroduces. Instead of writing out a near-identical backend.tf and provider.tf in every environment directory by hand, Terragrunt lets you define the backend and provider configuration once, centrally, and generate the per-environment specifics — the state key, the subscription_id, the resource group — from that one definition:
# terragrunt.hcl (a conceptual preview only; the actual syntax is the# subject of the next module, Terragrunt Fundamentals)# One central place that generates the backend and provider blocks for# every environment, instead of copy-pasting them into environments/dev,# environments/staging, and environments/prod by handremote_state { backend = "azurerm"}The same idea extends to supplying each environment’s input variables and, once infrastructure is split across multiple independently-applied units, to understanding which unit depends on which so that Terragrunt applies them in the right order.
What Terragrunt is not
Section titled “What Terragrunt is not”Terragrunt is not a fork of Terraform, it does not compile to a different infrastructure-as-code language, and it is not a replacement for the terraform binary. Every Terragrunt command ultimately shells out to a real terraform or tofu command underneath, running against the exact same providers and the exact same state formats Terraform already understands:
# Every terragrunt command wraps a real terraform (or tofu) commandterragrunt plan # generates config, then runs a real `terraform init` and `terraform plan`terragrunt apply # generates config, then runs a real `terraform apply`That distinction matters for one more reason beyond reducing boilerplate. A single Terraform configuration already has its own internal dependency graph between resources, but that graph stops at the edge of one terraform apply. Once an environment’s infrastructure is deliberately split into multiple, independently-applied units — a network unit, a database unit, an application unit, each with its own state — nothing in Terraform itself knows that the application unit needs the database unit’s outputs first. That is the second kind of problem Terragrunt is built to solve: ordering and passing outputs between separately-applied units, which the next module covers in detail.
flowchart LR workspaces["Terraform workspaces: DRY, weak isolation"] --> gap["The gap: no native primitive gives both at once"] dirPerEnv["Directory-per-environment: isolated, duplicated"] --> gap gap --> terragrunt["Terragrunt: generates config, removes duplication"] terragrunt --> realTf["Real terraform / tofu binary actually runs"]