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 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:
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.
mock_outputs: letting plan succeed early
Section titled “mock_outputs: letting plan succeed early”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.
Apply order is computed automatically
Section titled “Apply order is computed automatically”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