Dependencies Between Units
The idea in one sentence
Section titled “The idea in one sentence”A dependency block lets one unit read another unit’s outputs even though the two units have entirely separate Terraform state, and mock_outputs lets that dependent unit still plan before the unit it depends on has actually been applied.
Crossing the state boundary: why module composition is not enough
Section titled “Crossing the state boundary: why module composition is not enough”The Modules module showed one Terraform module’s output feeding straight into another module’s input — a network_id produced by a vpc module, passed as an argument into a compute module, both wired together inside a single main.tf and applied together as one terraform apply against one state file. That composition works because both modules live inside the same Terraform run: Terraform can see the whole dependency graph and resolve it in one pass.
Terragrunt units break that assumption on purpose. A vpc unit and a compute unit each have their own terragrunt.hcl, their own state file, and their own independent terraform apply. Nothing about a plain Terraform module block reaches across that boundary, because as far as either unit’s own Terraform run is concerned, the other unit’s resources do not exist. Wiring a vpc unit’s network into a compute unit therefore needs a mechanism built for exactly this gap — a dependency block.
The dependency block: reading another unit outputs
Section titled “The dependency block: reading another unit outputs”A dependency block names another unit by its directory and exposes that unit’s outputs as values this unit can use directly inside its own inputs:
include "root" { path = find_in_parent_folders("root.hcl")}
dependency "vpc" { config_path = "../vpc"
mock_outputs = { network_id = "mock-network-id" }
mock_outputs_allowed_terraform_commands = ["plan"]}
terraform {}
inputs = { network_id = dependency.vpc.outputs.network_id}config_path = "../vpc" points at the vpc unit’s directory. Under the hood, Terragrunt runs the equivalent of terraform output against that unit’s state and exposes the result as dependency.vpc.outputs, so dependency.vpc.outputs.network_id reaches into the vpc unit’s actual applied state and pulls out the value of its network_id output — the same output name the vpc unit’s own Terraform module would have declared with an output block. That value then flows straight into this unit’s inputs, exactly like any other input value.
mock_outputs: letting plan succeed before the dependency is applied
Section titled “mock_outputs: letting plan succeed before the dependency is applied”Reading a real output only works once the vpc unit has actually been applied and has real state to read from. Before that, a plan on compute would fail outright, since dependency.vpc.outputs.network_id would have nothing to resolve to. mock_outputs solves that by supplying a placeholder value Terragrunt substitutes in whenever the dependency has not been applied yet, and mock_outputs_allowed_terraform_commands restricts exactly which commands are allowed to use that placeholder. In the example above, mock-network-id stands in for the real value only during plan — a genuine apply still requires the vpc unit’s real output, so a placeholder value can never accidentally get written into real infrastructure.
Apply order: computed automatically from dependency blocks
Section titled “Apply order: computed automatically from dependency blocks”Because a dependency block names another unit explicitly, Terragrunt can see the full graph of which units depend on which, across an entire directory tree. When you run an operation across many units at once — the subject of the next lesson — Terragrunt uses that graph to compute the correct order on its own: the vpc unit gets applied before the compute unit that reads its outputs, without anyone hard-coding that ordering or remembering to run the units in the right sequence by hand.
flowchart LR vpc["vpc unit (own state)"] -->|terraform output| dep["dependency vpc: outputs.network_id"] dep --> inputsBlock["compute unit inputs: network_id"] mock["mock_outputs: network_id = mock-network-id"] -.->|used only for plan, before vpc is applied| dep
Proof: a real two-unit tree, run --all plan before vpc is ever applied
Section titled “Proof: a real two-unit tree, run --all plan before vpc is ever applied”A real Terragrunt 1.1.0 tree with a local backend (no GCP needed to prove the mechanism — vpc outputs a network_id string the same way google_compute_network.this.id would):
remote_state { backend = "local" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { path = "${get_terragrunt_dir()}/terraform.tfstate" }}include "root" { path = find_in_parent_folders("root.hcl")}
terraform { source = "."}terraform { required_providers { local = { source = "hashicorp/local", version = "~> 2.5" } }}
resource "local_file" "vpc" { filename = "${path.module}/network-id.txt" content = "network-0123456789abcdef0\n"}
output "network_id" { value = "network-0123456789abcdef0"}compute/terragrunt.hcl is exactly the dependency block shown above; its module:
terraform { required_providers { local = { source = "hashicorp/local", version = "~> 2.5" } }}
variable "network_id" { type = string }
resource "local_file" "compute" { filename = "${path.module}/compute-network-ref.txt" content = "launched into ${var.network_id}\n"}Before vpc has ever been applied, running terragrunt run --all -- plan from the tree root:
$ terragrunt run --all --non-interactive -- planINFO The following units will be run, starting with dependencies and then their dependents:╰── vpc ╰── computeWARN [compute] Config .../vpc/terragrunt.hcl is a dependency of .../compute/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output.STDOUT [compute] terraform: launched into mock-network-idThe ordering log lists vpc before compute purely from the dependency "vpc" block, and the plan content, launched into mock-network-id, is the literal mock_outputs value. Now apply vpc for real (still fully offline) and re-run compute’s plan with no config changes at all:
$ terragrunt run --non-interactive -- apply -auto-approve # inside vpc/Outputs:network_id = "network-0123456789abcdef0"
$ terragrunt run --non-interactive -- plan # inside compute/, unchanged configSTDOUT terraform: launched into network-0123456789abcdef0Same compute/terragrunt.hcl, same dependency block — the plan content flips from the mock value to the real terraform output value the instant vpc has real state.
The real error when a dependency has no outputs and the command isn’t allow-listed
Section titled “The real error when a dependency has no outputs and the command isn’t allow-listed”apply was deliberately left out of mock_outputs_allowed_terraform_commands = ["plan"] above. Running terragrunt apply against compute before vpc has been applied — the real Terragrunt 1.1.0 error:
$ terragrunt apply --non-interactive # compute/, before vpc has been applied, apply not in the allow-listERROR resolving dependency "vpc" outputs: .../vpc/terragrunt.hcl is a dependency of .../compute/terragrunt.hcl but detected no outputs. Either the target module has not been applied yet, or the module has no outputs.
If this dependency is accessed before the outputs are ready (which can happen during the planning phase of an unapplied stack), consider using mock_outputs: ...🐛 Spot the bug
dependency "vpc" {config_path = "../vpc"
mock_outputs = { network_id = "mock-network-id"}
mock_outputs_allowed_terraform_commands = ["plan", "apply"]}
inputs = {network_id = dependency.vpc.outputs.network_id}Show the answer
The bug is "apply" sitting in mock_outputs_allowed_terraform_commands alongside "plan". Proven above, restricting the list to ["plan"] is what makes apply refuse to run with a real error until vpc has genuinely been applied — with "apply" included, terragrunt apply on compute succeeds silently using network_id = "mock-network-id", wiring real GCP infrastructure to a placeholder value instead of the real network.
Reading AI-generated code
Section titled “Reading AI-generated code”mock_outputs_allowed_terraform_commandsincluding"apply"— proven above, that specific safety refusal exists precisely to stopapplyfrom running against fake data.mock_outputspresent with nomock_outputs_allowed_terraform_commandsat all — the mock is never used, so a fresh environment’s firstplanfails with the exact “detected no outputs” error proven above.- A
dependencies { paths = [...] }block where code then tries to readdependencies.vpc.something— that attribute never exists on adependenciesblock; onlydependency(singular) exposes.outputs.
Mental Model
Section titled “Mental Model”A dependency block is Terragrunt’s stand-in for the cross-module reference Terraform gives you for free inside one state — proven above, it works by shelling out to terraform output -json against the other unit before this unit’s own plan/apply ever starts.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
dependency blocks instead of one shared Terraform state | Independent state per unit; run --all still gets correct ordering, proven above | An extra terraform output shell-out per dependency per run |
mock_outputs scoped by mock_outputs_allowed_terraform_commands | plan/CI on a partially-applied environment doesn’t hard-fail | Proven above: getting the allow-list wrong silently trades a loud, correct failure for a quiet, wrong one |
Common Misconceptions
Section titled “Common Misconceptions”- “A dependency block links two units’ state together.” Proven above: Terraform never sees
vpcat all when planningcompute. Terragrunt resolves the value first, via a separateterraform outputcall. - “mock_outputs are used automatically whenever a dependency’s real state is missing.” Proven above with the real apply error: mocks are only used for commands explicitly listed in
mock_outputs_allowed_terraform_commands.