Running Many Units at Once
The idea in one sentence
Section titled “The idea in one sentence”terragrunt run --all plan and terragrunt run --all apply operate on every unit under the current directory tree at once, in the dependency order established by their dependency blocks.
run —all is the current command
Section titled “run —all is the current command”terragrunt run --all planterragrunt run --all applyrun --all discovers every unit under the current working directory, builds the dependency graph from their dependency blocks — covered in the previous lesson — and runs the given command against each one in the correct order. vpc applies before ec2 automatically; nobody has to remember or hard-code that sequencing.
A word about older syntax you will see in older material
Section titled “A word about older syntax you will see in older material”Terragrunt’s multi-unit command surface has changed, and a lot of existing blog posts and answers online still describe the old one. Two generations of history are worth knowing so stale advice does not trip you up:
terragrunt plan-all and terragrunt apply-all (along with destroy-all, output-all, and validate-all) have been completely removed from current Terragrunt. They do not exist anymore and will simply fail — do not use them, and treat any tutorial that leans on them as written for a much older version:
# Removed entirely — these commands no longer exist in current Terragruntterragrunt plan-allterragrunt apply-allterragrunt run-all COMMAND went through the same fate one release later: deprecated first, then removed entirely once the CLI redesign shipped in current Terragrunt. Running it today fails immediately with unknown command: "run-all", the same as plan-all:
# Removed in current Terragrunt — fails with "unknown command: run-all"; use run --all insteadterragrunt run-all planScoping a run —all, and running non-interactively
Section titled “Scoping a run —all, and running non-interactively”Sometimes you do not want every unit in the tree, only a subset — say, everything under one region while leaving the rest of the tree untouched. The --filter flag does that, matching units by a path glob. Flags placed before the command need a -- separator so Terragrunt knows where its own flags end and the per-unit command begins:
# Only plan units under ./networking (and anything depending on them)terragrunt run --all --filter './networking/**' -- planFor CI, prompting for a manual confirmation on every unit is not an option — --non-interactive skips those prompts so an apply can run unattended:
terragrunt run --all --non-interactive -- applybefore_hook and after_hook
Section titled “before_hook and after_hook”A terraform block inside a unit’s terragrunt.hcl can define before_hook and after_hook entries — arbitrary shell commands Terragrunt runs immediately before or after a given operation on that specific unit:
terraform { before_hook "validate_before_plan" { commands = ["plan"] execute = ["./scripts/validate-tags.sh"] }
after_hook "notify_slack" { commands = ["apply"] execute = ["./scripts/notify-slack.sh"] run_on_error = false }}commands lists which Terragrunt commands trigger the hook, execute is the actual command Terragrunt runs — as an argument list, not a shell string — and run_on_error controls whether an after_hook still fires if the operation it is attached to failed. During a run --all apply across many units, each unit’s own hooks fire around that unit’s own apply — the Slack notification above fires once per unit that successfully applies, not once for the whole run.
flowchart LR cmd["terragrunt run --all apply"] --> vpc["vpc unit applies first"] vpc --> ec2["ec2 unit applies (depends on vpc)"] vpc --> rds["rds unit applies (depends on vpc)"] vpc -->|after_hook| slack["notify-slack.sh"]
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 (vpc, ec2 and rds both depending on vpc, local backend, same shape as the previous lesson). terragrunt find --dag and terragrunt list --tree --dag — both discovery commands, no plan/apply involved — read the same dependency blocks a run --all would, real output:
$ terragrunt find --dagvpcec2rds
$ terragrunt list --tree --dag.╰── vpc ├── ec2 ╰── rdsAnd the ordering log from an actual run --all plan, identical shape, confirming find/list --dag is not a separate approximation but the real execution plan:
$ terragrunt run --all --non-interactive -- planINFO The following units will be run, starting with dependencies and then their dependents:╰── vpc ├── ec2 ╰── rds--filter scoping to just rds, real output — note only one unit runs, vpc is not pulled in for a plan because mock_outputs_allowed_terraform_commands = ["plan"] lets rds succeed without needing vpc’s real state at all:
$ terragrunt run --all --filter './rds' --non-interactive -- planINFO The following units will be run, starting with dependencies and then their dependents:╰── rds
❯❯ Run Summary 1 units Succeeded 1--queue-include-dir: soft-deprecated, not removed — do not confuse it with plan-all
Section titled “--queue-include-dir: soft-deprecated, not removed — do not confuse it with plan-all”This is worth being precise about, because getting it wrong reads as a contradiction of the removal story above. --queue-include-dir/--queue-exclude-dir are the older scoping flags --filter replaced — but “replaced” does not mean “removed.” Real proof, running the old flag directly against this course’s installed Terragrunt 1.1.0:
$ terragrunt run --all --queue-include-dir './rds' -- planINFO The following units will be run, starting with dependencies and then their dependents:╰── rdsINFO [rds] terraform: Initializing the backend...INFO [rds] terraform: Terraform has been successfully initialized!No unknown flag error, no rejection — the flag is accepted and the run proceeds exactly like --filter './rds' above. Compare that directly against the genuinely-removed surface from earlier in this lesson:
$ terragrunt run-all planERROR unknown command: "run-all". ...
$ terragrunt run --all --terragrunt-non-interactive -- planERROR flag `-terragrunt-non-interactive` is not a Terragrunt flag. ...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 — this course’s own installed 1.1.0 confirms that distinction with a real run, not just the docs’ wording. run-all/plan-all/apply-all and the old --terragrunt-* flag prefix are hard-removed and error immediately; --queue-include-dir is soft-deprecated and still works today. Prefer --filter for new work, but do not assume every old flag you meet in an existing codebase is broken — check with terragrunt run --help before rewriting it.
errors { retry / ignore }: the current block name
Section titled “errors { retry / ignore }: the current block name”The spec-sounding names error_hook/on_errors are not what current Terragrunt actually calls this — the real block is errors, with retry and ignore sub-blocks, each keyed by a label and a list of regexes matched against the error text:
errors { ignore "missing_file" { ignorable_errors = [".*no file exists at.*"] message = "Ignoring missing source file for this demo unit" }
retry "transient_network" { retryable_errors = [".*Error: transient network issue.*"] max_attempts = 3 sleep_interval_sec = 5 }}Proof the ignore block genuinely changes the exit code — a unit whose plan fails for real (file() referencing a file that does not exist, a real Invalid function argument error from Terraform), with an ignore block whose regex matches that exact error text:
$ terragrunt run --non-interactive -- planSTDERR terraform: Error: Invalid function argumentSTDERR terraform: Invalid value for "path" parameter: no file exists at "./does-not-exist.txt"; ...WARN Ignoring error, reason: Ignoring missing source file for this demo unit$ echo $?0Without the ignore block, this exact same failing plan exits non-zero and run --all stops at this unit (or continues past it only with --queue-ignore-errors, a blunter, whole-run tool). With it, the specific, matched error is downgraded to a warning and the run reports success — worth being careful with, since a regex just slightly wrong (matching more than intended) silently swallows failures a CI pipeline was supposed to catch.
flowchart TD
fail["A unit command fails"] --> check{"errors block present?"}
check -->|no| bubble["Failure bubbles up to run --all as-is"]
check -->|ignore matches| warn["Logged as WARN, unit reported succeeded"]
check -->|retry matches| again["Retried up to max_attempts, sleep_interval_sec apart"]
check -->|no match| bubble 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; this is the single most common stale-tutorial artifact in generated Terragrunt code.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_errors/retryable_errorsregex that looks plausible but doesn’t match the actual provider error text — proven above with a first attempt (.*no such file or directory.*) that silently failed to match Terraform’s real wording (no file exists at), meaning theignoreblock did nothing and the failure still propagated. Always test the regex against a real captured error, never a guessed one. --filterused with a glob that doesn’t include a leading./or trailing/**consistently — Terragrunt’s filter syntax is a real path-glob dialect, not shell globbing; copy the exact form fromterragrunt run --all --filter --helprather than guessing shell-glob syntax.
🐛 Spot the bug
# CI pipeline snippeterrors {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 elsewhere in the stack trace. A real provider bug that happens to mention “DNS resolution failed” as one line inside a much larger, unrelated error (a misconfigured resource whose error message also references a DNS name) gets silently ignored too, because regex matching has no concept of “this is the root cause vs. incidental text.” Proven above: the mechanism downgrades any matching error to a WARN and reports the unit as succeeded — a broad regex like this is exactly the failure mode described in the “Reading AI-generated code” list, just inverted (too broad instead of not matching at all).
Mental Model
Section titled “Mental Model”run --all is Terragrunt doing, at the unit level, exactly what terraform apply does at the resource level inside one state: build a DAG (proven above via find --dag/list --dag returning the identical order a real run --all uses), then walk it. --filter restricts which nodes of that graph get visited at all — proven above, filtering to rds alone did not pull in vpc, because mock_outputs meant rds’s plan needed no real vpc data. 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 |
|---|---|---|
run --all over N sequential manual runs | Correct dependency order computed automatically, proven above | A mistake in one unit’s dependency graph (or an over-broad --filter) affects every unit downstream in that same run |
--filter over --queue-include-dir | Current, documented syntax; a real path-glob dialect with union/AND semantics --queue-* never had | 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 (proven above: a genuinely broken file() call) does not halt an entire run --all | Proven above with the SpotTheBug: a regex broader than intended silently swallows unrelated real failures, with no distinction between “the exact known issue” and “any error containing similar text” |
before_hook/after_hook per unit | Per-unit side effects (tag validation, Slack notification) without a separate orchestration script | Hooks fire once per unit in a run --all, not once for the whole run — a notification hook fires N times across N units, which needs to be designed for |
Common Misconceptions
Section titled “Common Misconceptions”- “plan-all, apply-all, and run-all are all just older spellings of run —all that still work.” Proven above with real errors:
plan-all/apply-all/run-allall fail immediately withunknown command. Onlyrun --all(with the space, not the hyphen) works. - “Any flag that isn’t in the current docs must be broken.” Proven above:
--queue-include-diris absent from the headline--filterdocumentation but still 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, including unrelated failures that mention similar words. - “
errors { retry }anderrors { ignore }are the same thing with different names.” They are genuinely different actions on a match:retryre-runs the operation up tomax_attemptstimes,ignorelogs a warning and treats the unit as succeeded without re-running anything — picking the wrong one for a transient-vs-permanent failure produces very different (and sometimes very wrong) behavior.