Running Many Units at Once
The idea in one sentence
Section titled “The idea in one sentence”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:
terragrunt run --all planterragrunt run --all applyBe 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.
Scoping with —filter
Section titled “Scoping with —filter”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:
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' -- planINFO The following units will be run, starting with dependencies and then their dependents:╰── databaseINFO [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:
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:
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"]
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 --dagvnetdatabasevm
$ terragrunt list --tree --dag.╰── vnet ├── database ╰── vmAnd the ordering log from an actual run --all plan, identical shape:
$ terragrunt run --all --non-interactive -- planINFO 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 -- planINFO The following units will be run, starting with dependencies and then their dependents:╰── database
❯❯ Run Summary 1 units Succeeded 1errors { 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:
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 -- planSTDERR terraform: Error: Invalid function argumentWARN Ignoring error, reason: Ignoring missing source file for this demo unit$ echo $?0Without 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.
Reading AI-generated code
Section titled “Reading AI-generated code”When reviewing generated multi-unit run configs, check for:
plan-all/apply-all/run-allanywhere in a script or CI pipeline — proven above, all three fail immediately withunknown commandon current Terragrunt.terragrunt run --all --queue-include-dir ... -- plantreated as broken or rewritten with a comment like “removed in new Terragrunt” — proven above, this flag still runs correctly; only--filteris the current recommendation, not the only working one.- An
ignorable_errorsregex 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.”
Mental Model
Section titled “Mental Model”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.
Trade-offs
Section titled “Trade-offs”| Choice | Benefit | Cost |
|---|---|---|
--filter over --queue-include-dir | Current, documented syntax, a real path-glob dialect | Existing 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 --all | Proven above: a regex broader than intended silently swallows unrelated real failures |
Common Misconceptions
Section titled “Common Misconceptions”- “Any flag that isn’t in the current docs must be broken.” Proven above:
--queue-include-dirstill runs a realinit+planwith 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.