Skip to content

Dependencies Between Units

A dependency block lets one unit read another unit’s outputs across the state boundary that separates them, and Terragrunt uses those same blocks to work out the order units must be applied in.

Why module composition is not enough anymore

Section titled “Why module composition is not enough anymore”

In the Modules module you saw output-to-input composition: one module’s output feeding straight into another module’s input argument, inside a single main.tf, evaluated as one Terraform plan against one state file. That works because both modules are part of the same apply.

Terragrunt units break that assumption on purpose — a vpc unit and an ec2 unit each have their own terragrunt.hcl and their own separate state file, applied independently. module.vpc.outputs.vpc_id, referenced directly the way it was inside a single Terraform config, simply does not exist from the ec2 unit’s point of view; there is no shared configuration file for that reference to live inside. Something has to read across that state boundary explicitly, and that something is a dependency block.

dependency: reading across the state boundary

Section titled “dependency: reading across the state boundary”
ec2/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "git::[email protected]:acme/infrastructure-modules.git//ec2?ref=v1.4.0"
}
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "mock-vpc-id"
}
mock_outputs_allowed_terraform_commands = ["plan"]
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}

config_path points at the other unit’s directory — not a Terraform module, a Terragrunt unit. Before running, Terragrunt runs terraform output against that unit’s real state and exposes every output it finds as dependency.vpc.outputs.NAME, available anywhere in this unit’s config, including inside inputs.

mock_outputs: letting plan succeed before the dependency exists

Section titled “mock_outputs: letting plan succeed before the dependency exists”

dependency.vpc.outputs.vpc_id only has a real value once the vpc unit has actually been applied at least once. Two situations break that assumption: standing up a brand-new environment for the first time, where nothing has been applied yet, and CI validating a pull request that only touched the ec2 unit’s files, where nobody wants a plan to fail just because some unrelated unit has not been applied in that particular environment.

mock_outputs covers exactly this gap. It supplies placeholder values Terragrunt substitutes in whenever the real outputs are not available, and mock_outputs_allowed_terraform_commands restricts which commands are allowed to fall back to them — ["plan"] above means a plan on ec2 succeeds using vpc_id = "mock-vpc-id" even with no real vpc state yet, while an apply still refuses to run against a fake value and forces the real dependency to be applied first.

Terraform already computes an execution order inside a single state — its own dependency graph between individual resources. A dependency block hands Terragrunt the equivalent information one level higher, between whole units: since ec2 declares a dependency on vpc, Terragrunt knows vpc has to be applied first. Multiply this across a real tree of a dozen units, and Terragrunt builds a full graph from every dependency block it finds, then walks it in the correct order for any command that touches more than one unit at once — covered fully in the next lesson — without anyone maintaining that ordering by hand.

flowchart LR
  vpcstate["vpc unit state (applied separately)"] --> vpcout["vpc outputs: vpc_id"]
  vpcout --> dep["ec2 terragrunt.hcl: dependency vpc block"]
  dep --> ecin["ec2 inputs: vpc_id = dependency.vpc.outputs.vpc_id"]
  mock["mock_outputs: vpc_id = mock-vpc-id"] -.->|lets plan succeed before vpc is applied| dep
A dependency block passing vpc outputs into ec2 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”

This is a real Terragrunt 1.1.0 tree with a local backend (no AWS needed to prove the mechanism — vpc outputs a vpc_id string the same way aws_vpc.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}/vpc-id.txt"
content = "vpc-0123456789abcdef0\n"
}
output "vpc_id" {
value = "vpc-0123456789abcdef0"
}

ec2/terragrunt.hcl is exactly the dependency block shown above (config_path = "../vpc", mock_outputs_allowed_terraform_commands = ["plan"]); its module:

ec2/main.tf
terraform {
required_providers {
local = { source = "hashicorp/local", version = "~> 2.5" }
}
}
variable "vpc_id" { type = string }
resource "local_file" "ec2" {
filename = "${path.module}/ec2-vpc-ref.txt"
content = "launched into ${var.vpc_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
╰── ec2
WARN [ec2] Config .../vpc/terragrunt.hcl is a dependency of .../ec2/terragrunt.hcl
that has no outputs, but mock outputs provided and returning those in
dependency output.
STDOUT [ec2] terraform: # local_file.ec2 will be created
STDOUT [ec2] terraform: + content = <<-EOT
launched into mock-vpc-id
EOT
❯❯ Run Summary 2 units 2s
Succeeded 2

Two things proven at once: the ordering log (“starting with dependencies and then their dependents”) lists vpc before ec2 purely from the dependency "vpc" block — nothing in either unit’s config says “run me second” — and the plan content itself, launched into mock-vpc-id, is the literal mock_outputs value, proving mock_outputs_allowed_terraform_commands = ["plan"] is what let this succeed with zero real vpc state on disk.

Now apply vpc for real (still fully offline — local_file, no cloud) and re-run ec2’s plan with no config changes at all:

$ terragrunt run --non-interactive -- apply -auto-approve # inside vpc/
Outputs:
vpc_id = "vpc-0123456789abcdef0"
$ terragrunt run --non-interactive -- plan # inside ec2/, unchanged config
STDOUT terraform: + content = <<-EOT
launched into vpc-0123456789abcdef0
EOT

Same ec2/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, with zero edits on the ec2 side. This is the entire mechanism dependency.vpc.outputs.vpc_id runs on: Terragrunt shells out to terraform output -json against vpc’s state, and only falls back to mock_outputs when that comes back empty (and the current command is in mock_outputs_allowed_terraform_commands).

The real error when a dependency has no outputs and the command isn’t in the allow-list

Section titled “The real error when a dependency has no outputs and the command isn’t in the allow-list”

apply was deliberately left out of mock_outputs_allowed_terraform_commands = ["plan"] above. Running terragrunt apply against ec2 before vpc has been applied — the real Terragrunt 1.1.0 error, not paraphrased:

$ terragrunt apply --non-interactive # ec2/, before vpc has been applied, apply not in the allow-list
ERROR resolving dependency "vpc" outputs: .../vpc/terragrunt.hcl is a dependency of
.../ec2/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:
...
If you do not require outputs from your dependency, consider using the
dependencies block instead:
https://docs.terragrunt.com/reference/config-blocks-and-attributes/#dependencies

This is the guardrail mock_outputs_allowed_terraform_commands exists to enforce: a plan may proceed on a placeholder, but apply is refused outright rather than silently applying ec2 against a fake vpc_id — the exact behavior described earlier in this lesson, now shown as Terragrunt’s real error text instead of an assertion.

skip_outputs: forcing mocks even when the real dependency is already applied

Section titled “skip_outputs: forcing mocks even when the real dependency is already applied”

skip_outputs = true on a dependency block means “never call terraform output at all, always use mock_outputs if set.” Proof — same ec2 unit, vpc already has real applied state from above, skip_outputs = true added to the dependency "vpc" block, no other change:

$ terragrunt run --non-interactive -- plan # skip_outputs = true, vpc already applied for real
STDOUT terraform: + content = <<-EOT
launched into mock-vpc-id
EOT

Even though vpc’s real output is sitting right there on disk (vpc_id = "vpc-0123456789abcdef0", proven above), skip_outputs skips reading it entirely and the mock wins. This is the documented behavior for exactly one situation: CI with remote_state.disable_init set, where you don’t want Terragrunt attempting to read any backend for a dependency, real or mock-fallback — you want the mock, unconditionally, every time.

dependencies (plural) vs dependency (singular): ordering-only vs outputs

Section titled “dependencies (plural) vs dependency (singular): ordering-only vs outputs”

A dependencies block is a different, older mechanism that only affects ordering for run --all — it never exposes outputs:

dependencies {
paths = ["../vpc", "../rds"]
}

There is no dependencies.vpc.outputs anywhere — the block is a bare list of paths whose only job is telling run --all “apply these units first,” with none of dependency’s output-passing, mock_outputs, or per-command allow-listing. Use dependencies only for a unit that needs ordering but genuinely has no config value to read from the other unit; the moment you need even one real value out of another unit, that has to be a named dependency block instead — dependencies has no equivalent of dependency.NAME.outputs.OUTPUT to read from at all.

When reviewing generated dependency blocks, check for:

  • mock_outputs_allowed_terraform_commands including "apply" — proven above, that specific safety refusal exists precisely to stop apply from running against fake data; adding "apply" to the allow-list defeats it entirely and silently.
  • mock_outputs present with no mock_outputs_allowed_terraform_commands at all — the mock is simply never used (the allow-list defaults to empty, not “always”), so a fresh environment’s first plan fails with the exact “detected no outputs” error proven above, and the unused mock_outputs block looks like it should have prevented that.
  • A dependencies { paths = [...] } block where the code inside then tries to read dependencies.vpc.something — proven above, that attribute never exists on a dependencies block; only dependency (singular) exposes .outputs.
  • skip_outputs = true paired with no mock_outputs at all — proven by the docs quoted above, outputs becomes an empty map in that case, and any code referencing a specific key under it fails at evaluation time, not with a clear “you forgot mock_outputs” message.

🐛 Spot the bug

ec2/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "mock-vpc-id"
}
mock_outputs_allowed_terraform_commands = ["plan", "apply"]
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_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 ec2 succeeds silently using vpc_id = "mock-vpc-id", creating real infrastructure wired to a placeholder value instead of the real VPC. The fix is exactly the config shown earlier in this lesson: leave apply out of the allow-list so it hits the “detected no outputs” error and forces vpc to be applied first.

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 literally shelling out to terraform output -json against the other unit and substituting the result in before this unit’s own plan/apply ever starts. Nothing is shared or linked at the Terraform level; by the time Terraform itself runs, dependency.vpc.outputs.vpc_id has already been resolved to a plain string and baked into inputs. mock_outputs exists because that terraform output call can come back empty, and Terragrunt needs an explicit, command-scoped answer for “what do I do then” rather than either always failing or always guessing.

ChoiceBenefitCost
dependency blocks instead of one shared Terraform stateIndependent state per unit (previous lesson); run --all still gets correct ordering, proven aboveAn extra terraform output shell-out per dependency per run — a real, measurable cost at dozens of units, addressed in the next lesson’s --parallelism
mock_outputs scoped by mock_outputs_allowed_terraform_commandsplan/CI on a partially-applied environment doesn’t hard-fail — proven aboveProven above too: getting the allow-list wrong (including apply) silently trades a loud, correct failure for a quiet, wrong one
skip_outputsDeterministic mocks with zero backend reads, useful when remote_state.disable_init is set in CIProven above: it ignores real applied state even when present, so a stale mock can mask real drift in a plan meant to catch it
dependencies (paths only) over dependencySimpler when a unit only needs ordering, no valuesNo .outputs access at all — the moment one real value is needed, this has to become a dependency block instead
  • “A dependency block links two units’ state together — Terraform sees both at once.” Proven above: Terraform never sees vpc at all when planning ec2. Terragrunt resolves dependency.vpc.outputs.vpc_id to a plain string before invoking Terraform, via a separate terraform output call against vpc’s own state.
  • “mock_outputs are used automatically whenever a dependency’s real state is missing.” Proven above with the real “detected no outputs” apply error: mocks are only used for commands explicitly listed in mock_outputs_allowed_terraform_commands. An empty or missing list means mocks are never used, and the command fails instead.
  • “skip_outputs just means ‘skip mocking, use the real thing.’” The opposite, proven above: skip_outputs = true means always use mock_outputs (never call terraform output), even when real applied state exists.
  • “dependencies and dependency are the same block with two valid spellings.” Proven above: dependencies { paths = [...] } only affects run --all ordering and exposes nothing; dependency "name" { ... } (singular, with a label) is what exposes .outputs. They are not interchangeable.
Why is output-to-input module composition from the Modules module not directly enough for two Terragrunt units?
What does mock_outputs provide?
How does Terragrunt determine the correct apply order across multiple dependent units?
What does mock_outputs_allowed_terraform_commands control?
A dependency's real state already exists on disk with a real output, but its terragrunt.hcl also sets skip_outputs = true. What value does the consuming unit actually see in a plan?
What is the actual difference between a dependencies { paths = [...] } block and a dependency "name" { ... } block?