Skip to content

Dependencies Between Units

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:

compute/terragrunt.hcl
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 {
source = "git::[email protected]:acme/infrastructure-modules.git//compute?ref=v1.2.0"
}
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
vpc unit outputs flowing into a compute unit dependency block and inputs

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):

root.hcl
remote_state {
backend = "local"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
path = "${get_terragrunt_dir()}/terraform.tfstate"
}
}
vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "."
}
vpc/main.tf
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:

compute/main.tf
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 -- plan
INFO The following units will be run, starting with dependencies and then their dependents:
╰── vpc
╰── compute
WARN [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-id

The 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 config
STDOUT terraform: launched into network-0123456789abcdef0

Same 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-list
ERROR 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

compute/terragrunt.hcl
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.

  • mock_outputs_allowed_terraform_commands including "apply" — proven above, that specific safety refusal exists precisely to stop apply from running against fake data.
  • mock_outputs present with no mock_outputs_allowed_terraform_commands at all — the mock is never used, so a fresh environment’s first plan fails with the exact “detected no outputs” error proven above.
  • A dependencies { paths = [...] } block where code then tries to read dependencies.vpc.something — that attribute never exists on a dependencies block; only dependency (singular) exposes .outputs.

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.

ChoiceBenefitCost
dependency blocks instead of one shared Terraform stateIndependent state per unit; run --all still gets correct ordering, proven aboveAn extra terraform output shell-out per dependency per run
mock_outputs scoped by mock_outputs_allowed_terraform_commandsplan/CI on a partially-applied environment doesn’t hard-failProven above: getting the allow-list wrong silently trades a loud, correct failure for a quiet, wrong one
  • “A dependency block links two units’ state together.” Proven above: Terraform never sees vpc at all when planning compute. Terragrunt resolves the value first, via a separate terraform output call.
  • “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.
Why is a plain Terraform module composition pattern not enough to connect a vpc unit to a compute unit in Terragrunt?
What is mock_outputs used for in a dependency block?
How does Terragrunt determine the correct order to apply multiple dependent units in?
Inside the compute unit inputs block, what does dependency.vpc.outputs.network_id refer to?
mock_outputs_allowed_terraform_commands is set to ["plan", "apply"] instead of just ["plan"]. What real, proven-above consequence does this have?
A dependency real state already has a real network_id output on disk, and compute has already been planned once against the mock. What does the next real terragrunt run --all -- plan show, proven above?