Skip to content

Running Many Units at Once

terragrunt run --all apply walks every unit under the current directory, applies them in the order the dependency graph from the previous lesson requires, and --filter narrows that walk to a subset of the tree without changing how the ordering is computed.

run —all: operating on every unit in the tree

Section titled “run —all: operating on every unit in the tree”

Once a directory contains more than one unit, running terragrunt plan or terragrunt apply inside each one by hand does not scale. run --all fans a single command out across every unit found under the current directory:

Terminal window
terragrunt run --all plan
terragrunt run --all apply

Be careful with search results and older tutorials here — this is a module where advice goes stale fast. terragrunt run-all plan and terragrunt run-all apply are the old syntax and have been removed as well — on Terragrunt 1.1 they fail with unknown command, exactly like the even older terragrunt plan-all and terragrunt apply-all; run --all is the only form that works on a current install. If a guide you are reading tells you to run apply-all, treat that as a sign the rest of the guide may be out of date too.

Running every unit in an entire repository is not always what you want — sometimes you only touched one subscription’s units and want to plan just those. The --filter flag scopes a run --all to a subset of the tree using a glob pattern:

Terminal window
terragrunt run --all --filter './subscription-a/**' -- plan

--filter is the current, documented way to scope a run — but be precise about what happened to --queue-include-dir/--queue-exclude-dir, the flags it replaced: they are soft-deprecated, not removed, and that distinction matters because it sits right next to a genuinely-removed command family in this same lesson. Real proof, running the old flag directly against this course’s installed Terragrunt 1.1.0:

$ terragrunt run --all --queue-include-dir './database' -- plan
INFO The following units will be run, starting with dependencies and then their dependents:
╰── database
INFO [database] terraform: Initializing the backend...

No unknown flag error, no rejection — the flag is accepted and the run proceeds exactly like --filter './database' would. Compare that against terragrunt run-all/plan-all/apply-all a few paragraphs above, which fail immediately with a real unknown command error — those are hard-removed. Per the Terragrunt CLI-redesign migration guide, --queue-include-dir/--queue-exclude-dir/--queue-strict-include are described as flags to migrate away from, not flags that were deleted. Prefer --filter for new work, but an existing script using the old flag is not broken — check with terragrunt run --help before assuming otherwise. The -- before plan separates Terragrunt’s own flags from the command being run across the units, which matters once a run --all invocation starts combining flags like --filter with the underlying Terraform command.

For CI pipelines, where nothing should ever pause waiting for a human to confirm a prompt, add --non-interactive:

Terminal window
terragrunt run --all --non-interactive -- apply

--non-interactive replaced the older --terragrunt-non-interactive flag — another naming pattern to watch for in older material, since several flags used to carry a terragrunt- prefix that current Terragrunt has dropped.

before_hook and after_hook: hooking into a unit run

Section titled “before_hook and after_hook: hooking into a unit run”

A run --all tells Terragrunt which units to touch and in what order, but sometimes a unit needs a side effect wrapped around its own plan or apply — a validation script beforehand, or a notification afterward. before_hook and after_hook blocks, declared inside a unit’s terraform block, run an arbitrary shell command at exactly those points:

vm/terragrunt.hcl
terraform {
before_hook "validate_inputs" {
commands = ["plan", "apply"]
execute = ["./scripts/validate-inputs.sh"]
}
after_hook "notify_slack" {
commands = ["apply"]
execute = ["./scripts/notify-slack.sh", "vm apply finished"]
run_on_error = true
}
}

commands lists which Terragrunt commands trigger the hook, execute is the actual command Terragrunt runs, and run_on_error = true on the after_hook above means the Slack notification fires even if the apply itself failed — useful when you want to hear about failures, not just successes. Because these hooks live inside the unit’s own terragrunt.hcl, they run whether that unit is applied on its own or as part of a larger run --all apply sweeping through many units.

flowchart LR
  cmd["terragrunt run --all apply"]
  cmd --> vnet["vnet unit"]
  vnet --> vm["vm unit (depends on vnet)"]
  vnet --> database["database unit (depends on vnet)"]
  vm --> hook["after_hook notify_slack"]
run --all apply fanning out in dependency order, with a hook firing after one unit completes

Proof: real ordering, find/list, and --filter on a three-unit tree

Section titled “Proof: real ordering, find/list, and --filter on a three-unit tree”

A real three-unit tree (vnet, and vm/database both depending on vnet, local backend). terragrunt find --dag and terragrunt list --tree --dag — discovery commands, no plan/apply — read the same dependency blocks a run --all would:

$ terragrunt find --dag
vnet
database
vm
$ terragrunt list --tree --dag
.
╰── vnet
├── database
╰── vm

And the ordering log from an actual run --all plan, identical shape:

$ terragrunt run --all --non-interactive -- plan
INFO The following units will be run, starting with dependencies and then their dependents:
╰── vnet
├── database
╰── vm

--filter scoping to just database — only that one unit runs, vnet is not pulled in since mock_outputs_allowed_terraform_commands = ["plan"] lets database succeed without needing vnet’s real state:

$ terragrunt run --all --filter './database' --non-interactive -- plan
INFO The following units will be run, starting with dependencies and then their dependents:
╰── database
❯❯ Run Summary 1 units
Succeeded 1

errors { retry / ignore }: the current block name

Section titled “errors { retry / ignore }: the current block name”

Current Terragrunt calls this block errors, with retry and ignore sub-blocks:

terragrunt.hcl
errors {
ignore "missing_file" {
ignorable_errors = [".*no file exists at.*"]
message = "Ignoring missing source file for this demo unit"
}
}

Proof the ignore block genuinely changes the exit code — a unit whose plan fails for real (file() referencing a file that does not exist), with a matching ignore block:

$ terragrunt run --non-interactive -- plan
STDERR terraform: Error: Invalid function argument
WARN Ignoring error, reason: Ignoring missing source file for this demo unit
$ echo $?
0

Without the ignore block, this exact failing plan exits non-zero. With it, the matched error is downgraded to a warning and the run reports success — a regex broader than intended silently swallows failures a CI pipeline was supposed to catch.

When reviewing generated multi-unit run configs, check for:

  • plan-all/apply-all/run-all anywhere in a script or CI pipeline — proven above, all three fail immediately with unknown command on current Terragrunt.
  • terragrunt run --all --queue-include-dir ... -- plan treated as broken or rewritten with a comment like “removed in new Terragrunt” — proven above, this flag still runs correctly; only --filter is the current recommendation, not the only working one.
  • An ignorable_errors regex that looks plausible but doesn’t match the actual provider error text — test the regex against a real captured error, never a guessed one.

🐛 Spot the bug

errors {
ignore "flaky_dns" {
ignorable_errors = [".*DNS resolution failed.*"]
message = "Known flaky DNS in this CI runner, ignoring"
}
}
Show the answer

Not a syntax bug — a scope bug. ignorable_errors regexes match against any error text produced during that unit’s run, not specifically DNS-resolution failures. A real provider bug that happens to mention “DNS resolution failed” as one line inside a much larger, unrelated error gets silently ignored too, because regex matching has no concept of “this is the root cause vs. incidental text.”

run --all builds a DAG (proven above via find --dag/list --dag returning the identical order a real run --all uses), then walks it. --filter restricts which nodes get visited at all. The errors block sits one layer below that walk, deciding per-unit whether a failure is fatal to the whole run or gets logged and treated as a (dangerous, if the regex is too broad) success.

ChoiceBenefitCost
--filter over --queue-include-dirCurrent, documented syntax, a real path-glob dialectExisting scripts using --queue-include-dir are not broken (proven above) — rewriting them is a choice, not a requirement
errors { ignore }A known, understood failure mode does not halt an entire run --allProven above: a regex broader than intended silently swallows unrelated real failures
  • “Any flag that isn’t in the current docs must be broken.” Proven above: --queue-include-dir still runs a real init+plan with zero errors on this course’s installed 1.1.0 — soft-deprecated is not the same as removed.
  • “An errors { ignore } block only matches the exact error scenario I wrote it for.” Proven above with the SpotTheBug: it matches any error text the regex happens to hit.
What is the current command for running plan across every unit under a directory
What happened to plan-all and apply-all, and what happened to run-all
What is a before_hook or after_hook used for
What determines the order units are processed in during a run --all apply
On this course's installed Terragrunt 1.1.0, what happens when you run terragrunt run --all --queue-include-dir './database' -- plan, the flag --filter is documented to have replaced?
An errors { ignore "x" { ignorable_errors = [".*no such file or directory.*"] } } block is added to catch a real Terraform error whose actual text is 'no file exists at'. What happens?