Skip to content

Dependencies Between Units

A dependency block lets one unit read another unit’s outputs across the state boundary that separates them, with mock_outputs standing in for the real values until that other unit has actually been applied.

Why module composition is not enough anymore

Section titled “Why module composition is not enough anymore”

The Modules module showed one module’s output feeding straight into another module’s input, all inside a single main.tf, resolved by Terraform in one plan against one shared state. That worked because both modules were part of the same Terraform run — Terraform could see the whole dependency graph at once.

Units break that assumption on purpose. A vnet unit and a vm unit each have their own terragrunt.hcl and their own separate state file, applied independently. The vm unit still needs the subnet ID that vnet creates, but there is no longer a single Terraform run that sees both — the two units are, from Terraform’s point of view, completely unrelated projects. Terragrunt needs its own mechanism to hand a value from one unit’s state across that boundary into another unit’s inputs, and that mechanism is the dependency block.

The dependency block: crossing the state boundary

Section titled “The dependency block: crossing the state boundary”

A dependency block names another unit by its directory and exposes everything that unit outputs:

vm/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
dependency "vnet" {
config_path = "../vnet"
}
inputs = {
subnet_id = dependency.vnet.outputs.subnet_id
}

config_path = "../vnet" points at the vnet unit’s directory. Behind the scenes, Terragrunt runs terragrunt output against that unit and exposes the result under dependency.vnet.outputs, so dependency.vnet.outputs.subnet_id reaches straight into whatever the vnet unit’s Terraform module declared as an output "subnet_id". From the vm unit’s own terragrunt.hcl, this reads almost exactly like consuming a module output inside a single main.tf — the difference is invisible in the syntax, even though a completely separate apply and a completely separate state file sit behind dependency.vnet.outputs.

There is an obvious problem with the setup above: the very first time anyone runs terragrunt plan on the vm unit, the vnet unit might not have been applied yet, so dependency.vnet.outputs.subnet_id has no real value to read. mock_outputs solves exactly that:

dependency "vnet" {
config_path = "../vnet"
mock_outputs = {
subnet_id = "mock-subnet-id"
}
mock_outputs_allowed_terraform_commands = ["plan"]
}

When the real output is not yet available, Terragrunt substitutes the values in mock_outputs instead of failing outright, letting plan show what the vm unit would create even before vnet exists. mock_outputs_allowed_terraform_commands restricts exactly which commands are allowed to fall back to the mock — listing only plan here means an apply still fails loudly if the real dependency has not been applied yet, which is exactly the safety net you want: it is fine to preview a plan against a placeholder subnet ID, but never fine to actually create a virtual machine against one.

Nothing in the vm unit’s terragrunt.hcl says “apply vnet first.” Terragrunt works that out on its own: every dependency block it finds while scanning a tree of units becomes an edge in a dependency graph, and when you run an operation across multiple units at once, Terragrunt applies that graph as an ordering constraint — vnet is guaranteed to finish applying before vm starts, because vm declares a dependency on it. You never hard-code that ordering or remember to run commands in the right sequence by hand; the next lesson covers the command that actually triggers a multi-unit run.

flowchart LR
  vnetunit["vnet unit (own state)"]
  vnetout["outputs.subnet_id"]
  dep["vm/terragrunt.hcl dependency vnet"]
  vminputs["vm inputs.subnet_id"]
  vnetunit --> vnetout --> dep --> vminputs
  mock["mock_outputs (plan only, before vnet is applied)"] -.-> dep
vnet outputs flowing into vm through a dependency block, with mock_outputs bridging the gap before vnet is applied
Why is module composition from a single main.tf not enough once vnet and vm are separate Terragrunt units
What does mock_outputs let a unit do
How does Terragrunt determine the correct apply order across multiple dependent units
In dependency "vnet" { config_path = "../vnet" }, what does config_path point at