Terragrunt Discovery and DAG
The idea in one sentence
Section titled “The idea in one sentence”Terragrunt builds its own dependency graph — one node per unit (directory), edges from real dependency/dependencies blocks, completely separate from any single unit’s own Terraform graph (The Graph and the Walk) — and run --all walks that graph the same way Terraform walks its own: dependencies before dependents, proven below with a real, timestamped, strictly-serial log.
A real 3-unit live tree
Section titled “A real 3-unit live tree”variable "name" { type = string }resource "local_file" "vpc_marker" { filename = "${path.module}/${var.name}.vpc.json" content = jsonencode({ vpc_id = "vpc-${var.name}" })}output "vpc_id" { value = "vpc-${var.name}" }remote_state { backend = "local" generate = { path = "backend.tf" if_exists = "overwrite" } config = { path = "${get_terragrunt_dir()}/terraform.tfstate" }}generate "provider" { path = "provider.tf" if_exists = "overwrite" contents = "terraform {\n required_version = \">= 1.11.0\"\n}\n"}include "root" { path = find_in_parent_folders("root.hcl")}terraform { source = "../../../modules/vpc"}inputs = { name = "dev"}include "root" { path = find_in_parent_folders("root.hcl")}terraform { source = "../../../modules/vpc"}dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "vpc-mock" } mock_outputs_allowed_terraform_commands = ["plan", "validate"]}inputs = { name = "compute-in-${dependency.vpc.outputs.vpc_id}"}include "root" { path = find_in_parent_folders("root.hcl")}terraform { source = "../../../modules/vpc"}dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "vpc-mock" } mock_outputs_allowed_terraform_commands = ["plan", "validate"]}inputs = { name = "db-in-${dependency.vpc.outputs.vpc_id}"}Two independent dependents (compute, db), both depending on the same vpc unit — deliberately, so the real logs below can distinguish “runs after its dependency” from “runs in some other fixed order.”
terragrunt find: real discovery, no DAG ordering
Section titled “terragrunt find: real discovery, no DAG ordering”$ terragrunt findlive/dev/computelive/dev/dblive/dev/vpcPlain find lists units it discovers (real, --no-hidden-aware directory scanning for terragrunt.hcl/stack files) in lexical order — compute before vpc, which is not a valid apply order for this tree (compute depends on vpc). Discovery is a separate concern from ordering.
terragrunt list --dag / --dependencies: real dependency-aware output
Section titled “terragrunt list --dag / --dependencies: real dependency-aware output”$ terragrunt list --daglive/dev/vpc live/dev/compute live/dev/db
$ terragrunt list --long --dependenciesType Path Dependenciesunit live/dev/compute live/dev/vpcunit live/dev/db live/dev/vpcunit live/dev/vpc
$ terragrunt find --dag --dependencies --format json[ { "type": "unit", "path": "live/dev/vpc" }, { "type": "unit", "path": "live/dev/compute", "dependencies": ["live/dev/vpc"] }, { "type": "unit", "path": "live/dev/db", "dependencies": ["live/dev/vpc"] }]--dag reorders list’s output so vpc (no dependencies) comes first; --dependencies (on list --long, or find --format json) makes the real edges explicit per unit. This is Terragrunt’s own dependency graph, built entirely from the dependency/dependencies blocks in these .hcl files — no relationship to what any individual unit’s own terraform graph would show inside modules/vpc.
run --all --parallelism 1: a real, ordered, timestamped log
Section titled “run --all --parallelism 1: a real, ordered, timestamped log”$ terragrunt run --all --parallelism 1 --non-interactive -- planReal log lines, one per unit, filtered to the first mention of each:
04:07:40.998 STDOUT [live/dev/vpc] terraform: Terraform used the selected providers to generate the following execution04:07:41.031 WARN [live/dev/compute] Config .../live/dev/vpc/terragrunt.hcl is a dependency of .../live/dev/compute/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output.04:07:41.134 WARN [live/dev/db] Config .../live/dev/vpc/terragrunt.hcl is a dependency of .../live/dev/db/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output.
❯❯ Run Summary 3 units 322ms ──────────────────────────── Succeeded 3Three real facts in these timestamps: vpc’s own plan starts and completes (04:07:40.998) before compute’s dependency-resolution line even appears (04:07:41.031); compute and db — genuinely independent of each other, both only depending on vpc — still run strictly one after another (.031 then .134) because --parallelism 1 was set, proving Terragrunt’s own --parallelism bounds concurrent units the same way Terraform’s -parallelism bounds concurrent resources (The Graph and the Walk) — two separate knobs, two separate graphs, same underlying idea.
mock_outputs during plan: the real warning, proven
Section titled “mock_outputs during plan: the real warning, proven”Every dependency block above sets mock_outputs_allowed_terraform_commands = ["plan", "validate"]. The real log line proves exactly when and why the mock is used: vpc has never been applied in this tree (no real state, no real output), so compute and db’s plan — a command in the allowed list — proceeds using vpc_id = "vpc-mock" instead of failing outright. Without vpc ever having real outputs and without mock_outputs, the same plan would fail immediately needing a real dependency output that doesn’t exist yet; this is precisely the mechanism that lets a multi-unit tree’s plan be reviewed unit-by-unit or all-at-once before anything is ever applied.
.terragrunt-cache: the real layout
Section titled “.terragrunt-cache: the real layout”$ find live/dev/vpc/.terragrunt-cache -maxdepth 4live/dev/vpc/.terragrunt-cachelive/dev/vpc/.terragrunt-cache/agVjb7UHwdx6LnyffjKdnln63rklive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrMlive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/.terragrunt-module-manifestlive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/main.tflive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/terragrunt.hcllive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/.terraformlive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/.terragrunt-source-versionlive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/.terragrunt-source-manifestlive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/provider.tflive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/backend.tflive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/.terraform.lock.hcllive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/.terraform/terraform.tfstatelive/dev/vpc/.terragrunt-cache/.../SG_cjn-kou3UtRZrXhlds3NpcrM/.terraform/providersA real, two-level hashed directory (first level keyed on the source, second on the resolved module version/manifest) inside each unit — Terragrunt copies modules/vpc’s real main.tf into this cache directory alongside its generated files (provider.tf, backend.tf — the exact contents of root.hcl’s generate blocks above) and the real terragrunt.hcl that produced it, then runs Terraform there, not in live/dev/vpc/ itself. This is why the real .terraform/ (with providers/ and, for a local backend, terraform.tfstate) lives inside .terragrunt-cache/<hash>/<hash>/, not next to terragrunt.hcl.
--filter, --queue-*: real flags, from run --help
Section titled “--filter, --queue-*: real flags, from run --help”$ terragrunt run --help... --filter value Filter components using filter syntax. Can be specified multiple times for union (OR) semantics. [$TG_FILTER] --filter-affected Filter components affected by changes between main and HEAD. Equivalent to --filter=[main...HEAD]. [$TG_FILTER_AFFECTED] --queue-ignore-dag-order Ignore DAG order for --all commands. [$TG_QUEUE_IGNORE_DAG_ORDER] --queue-ignore-errors Continue processing Units even if a dependency fails. [$TG_QUEUE_IGNORE_ERRORS] --parallelism value Parallelism for --all commands. [$TG_PARALLELISM]--filter (real, documented “filter syntax,” repeatable for OR semantics — the real successor to the older --terragrunt-include-dir-style flags this course’s earlier terragrunt-fundamentals module does not use) scopes which units a run --all touches without editing the tree; --queue-ignore-dag-order is the real, explicit escape hatch for when DAG ordering (proven above) is not wanted — proof that the DAG order is a deliberate default, not an unavoidable constraint; --queue-ignore-errors changes failure propagation across the same queue. All three names cite the real 1.1.0 CLI --help output — never guessed.
Mental Model
Section titled “Mental Model”Terragrunt’s DAG is a second, coarser graph layered above Terraform’s own: one node per unit, edges from real dependency/dependencies blocks (proven above via list --dag/find --dependencies), walked by run --all the same dependencies-first way Terraform walks its own graph (The Graph and the Walk), bounded by its own --parallelism (proven above: strictly serial at 1, even for two units with no relationship to each other, because they share a common dependency still being resolved by the queue). mock_outputs exists because this walk can be asked to plan a unit whose real dependency hasn’t been applied yet — proven above with the real warning naming exactly that condition. The .terragrunt-cache layout (proven above) is where the abstraction becomes concrete: Terragrunt never runs Terraform in your source directory, it assembles a real, generated, hashed copy per unit and runs Terraform there.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
DAG-ordered run --all (default) | Proven above: a dependency always completes before its dependents start, even under high --parallelism | Independent units can still queue behind an unrelated one sharing a dependency, as proven above at --parallelism 1 |
--queue-ignore-dag-order | Escape hatch for genuinely independent applies that happen to share a dependency declaration for output values only | Loses the ordering guarantee entirely — a real footgun if the dependency is not actually output-only |
mock_outputs scoped to plan/validate only (this lesson’s tree) | plan across the whole tree works before anything is ever applied, proven above | apply is deliberately NOT in the allowed list here — a real, correct constraint stopping a mocked value from ever being written as if real |
Common Misconceptions
Section titled “Common Misconceptions”- “
terragrunt findalready returns units in a safe apply order.” Proven above: plainfindlists in lexical order (computebeforevpc), which is invalid for this tree — only--dag(onfind/list) orrun --allitself applies real dependency ordering. - “
--parallelismonrun --allonly matters when units have zero relationship to each other.” Proven above:computeanddbhave no dependency on each other directly, yet still ran strictly serially at--parallelism 1— the flag bounds the whole queue’s concurrency, not just directly-related pairs. - “
mock_outputsmakes Terragrunt skip the dependency check entirely.” Proven above: the real log still emits an explicit warning every time a mock is substituted, naming the exact unit and reason — it is a documented, visible substitution, not a silent skip. - “Terraform actually runs inside
live/dev/vpc/for that unit.” Proven above with a realfindof.terragrunt-cache: Terraform runs inside a generated, hashed cache directory containing copied source plus Terragrunt’s owngenerateblocks (provider.tf,backend.tf) — never the originalmodules/vpc/orlive/dev/vpc/directories directly.
Reading AI-generated code
Section titled “Reading AI-generated code”- A CI script or generated doc that assumes
terragrunt find(without--dag) is already apply-safe order — proven above, plainfindis lexical, not dependency-aware; piping it straight into a manual per-unit apply loop can genuinely apply a dependent before its dependency. - A “speed up CI” suggestion to blindly raise
--parallelismonrun --allwithout checking the tree’s actual dependency shape — proven above, units sharing a common dependency still serialize behind it regardless of--parallelism, so the real speedup ceiling is the tree’s shape, not just the flag value. - Code or docs assuming
mock_outputs_allowed_terraform_commandsincluding"plan"also implicitly permitsapply— proven above and by design,applyshould never be in that list for a dependency whose real value matters, since applying against a mocked value would silently write wrong data.
🐛 Spot the bug
# ci-deploy.sh -- AI-generated deploy scriptfor unit in $(terragrunt find --working-dir live/dev); do(cd "live/dev/$unit" && terragrunt apply -auto-approve)doneShow the answer
This loops over terragrunt find’s output directly and applies each unit in that order. Proven above, plain find returns units in lexical order (compute, db, vpc for this lesson’s tree) — not dependency order. Applying compute before vpc in a tree where compute genuinely needs vpc’s real (non-mocked) output would either fail outright or, worse, silently succeed against a stale/mocked value if mock_outputs_allowed_terraform_commands were misconfigured to include apply. The real fix, proven above, is terragrunt run --all -- apply (which walks the real DAG built from dependency/dependencies blocks, dependencies-first) or terragrunt find --dag to get a correctly-ordered list before looping manually.