run-all across many units
Apply a whole environment.
A production environment is rarely one Terraform module. There is a VPC (virtual private cloud, your own fenced-off slice of a cloud provider's network), subnets carved out of it, a database that needs those subnets, an app that needs the database's address, and a monitoring stack watching all of it. Deploy that by hand and you are walking folder to folder in exactly the right order, trusting your memory for what that order is. A busy kitchen has the same problem and solves it with a head chef: one voice calls the whole line, and because the chef read the tickets first, the sauce goes on before the plate goes out. terragrunt run --all is that voice for your infrastructure.
What run --all actually does
Point Terragrunt at a directory and run --all walks everything beneath it looking for terragrunt.hcl files. Every directory holding one is a unit: one folder, one state file, one deployable thing. Terragrunt reads the dependency blocks inside those units and builds a DAG (directed acyclic graph, a set of one-way arrows between units where no path ever loops back to where it started). The arrows decide the order.
What Terragrunt does not do is melt your infrastructure into one giant apply. Every unit still gets its own OpenTofu process (OpenTofu is the open source fork of Terraform), its own state file, its own state lock. That matters when something goes wrong. A failure is scoped to one unit. A lock is held on one state file. The credentials in play are whatever that unit's provider block resolves to, not some merged superset of every unit's access. Terragrunt runs tofu from your PATH by default, and --tf-path points it at a different binary if you are still on Terraform.
cd ~/infra/live/prod# every unit Terragrunt can see from hereterragrunt list --tree
.├── app├── db├── monitoring╰── vpc
The arrows come from the config, not from the folder names. Here is the database unit saying out loud that it needs the VPC, and pulling two real values out of it once the VPC has been applied.
include "root" {path = find_in_parent_folders("root.hcl")}terraform {source = "${get_repo_root()}//modules/rds"}dependency "vpc" {config_path = "../vpc"# Stand-in values, used only while ../vpc has no outputs yetmock_outputs = {vpc_id = "vpc-00000000000000000"private_subnet_ids = ["subnet-0000000000000000a", "subnet-0000000000000000b"]}# Fake ids must never reach a real applymock_outputs_allowed_terraform_commands = ["plan", "validate"]}inputs = {vpc_id = dependency.vpc.outputs.vpc_idsubnet_ids = dependency.vpc.outputs.private_subnet_ids}
Read the graph before you run it
Before a controlled demolition, someone walks the building and marks which walls hold the roof up. Do the same with your infrastructure before an apply. Terragrunt will hand you the graph it is about to obey, and reading it costs about five seconds.
terragrunt find --dag prints the units in the order they would run. terragrunt list --dag --long --dependencies prints the same set as a table with a column for what each unit waits on. terragrunt dag graph emits the graph in DOT (a plain text language for describing graphs, which the Graphviz tool turns into a picture). That last one is an alias for list --format=dot --dependencies --external, so it also pulls in units that live outside your current folder.
For a defender, that DOT output is a blast radius map. A new dependency block that quietly reaches into another account or another environment shows up as an extra node with an extra arrow, and a changed picture is far easier to catch in review than one added line of HCL (HashiCorp Configuration Language, the syntax these files are written in).
# units in the order Terragrunt would run themterragrunt find --dag# the same set, plus what each unit waits onterragrunt list --dag --long --dependencies# machine readable graph, including units outside this folderterragrunt dag graph | dot -Tsvg -o /tmp/prod-graph.svg
vpcdbmonitoringappType Path Dependenciesunit vpcunit db vpcunit monitoring vpcunit app db, vpc
One command, the whole environment
With the graph settled, one invocation brings the whole environment to the state your code describes. run --all init creates backends and downloads providers for every unit. run --all apply walks the graph forwards. run --all destroy walks it backwards, so the things sitting on top come down before the things holding them up. Log lines carry the unit name in brackets, so four concurrent runs in one terminal are still readable, and a run summary lands at the end. The older spelling, terragrunt run-all apply, still works and means the same thing, but run --all is the form the current CLI is built around and the one that takes the flags in the rest of this lesson.
# create backends and download providers everywhereterragrunt run --all init# converge the environment in dependency orderterragrunt run --all apply# reverse order teardown (read the warning below first)terragrunt run --all destroy# older spelling, same behaviourterragrunt run-all apply
16:04:11.204 INFO [vpc] tofu: Initializing the backend...16:04:18.902 STDOUT [vpc] tofu: Apply complete! Resources: 9 added, 0 changed, 0 destroyed.16:04:19.113 INFO [db] tofu: Initializing the backend...16:04:19.115 INFO [monitoring] tofu: Initializing the backend...16:05:44.870 STDOUT [db] tofu: Apply complete! Resources: 6 added, 0 changed, 0 destroyed.16:05:45.001 STDOUT [monitoring] tofu: Apply complete! Resources: 4 added, 0 changed, 0 destroyed.16:06:02.559 STDOUT [app] tofu: Apply complete! Resources: 11 added, 0 changed, 0 destroyed.❯❯ Run Summary 4 units 1m51s────────────────────────────Succeeded 4
Units with no arrow between them start at the same time. That is what turns a fifty unit estate from an hour of waiting into a few minutes, and it is the first dial you turn down when a cloud API starts answering with HTTP 429 (too many requests, the polite way an API tells you to slow down) or when interleaved logs from eight units stop being readable. --parallelism 4 caps how many units run at once. Left alone there is no cap at all: Terragrunt starts every unit whose dependencies have finished.
Two more knobs matter on a big estate. Do not set TF_PLUGIN_CACHE_DIR for an --all run. That cache was never built for concurrent writers, and parallel units end up racing each other writing the same provider files into one directory. Terragrunt ships its own provider cache server (--provider-cache) that is built for exactly this. The other knob is --dependency-fetch-output-from-state, which reads a dependency's outputs straight out of the state file instead of starting a separate tofu output run for every arrow in the graph. Much faster on a wide graph, with three strings attached: it only supports the S3 backend (Amazon's object storage), whoever runs it needs direct read access on the state objects themselves, and it does not work with OpenTofu state encryption.
Nobody asks you to type yes
Run tofu apply by hand and it stops and waits for you to type yes. Under --all that prompt has nowhere to land, because several units may be mid-run and there is one keyboard between them, so Terragrunt silently appends -auto-approve on apply and destroy. Say that out loud before you type it into a production shell: terragrunt run --all apply changes every resource in every unit below your current directory without pausing once. --no-auto-approve switches that behaviour off, and it only makes sense paired with --parallelism 1, or you are back to several processes fighting over one keyboard.
--non-interactive is a different thing, and people mix the two up constantly. It answers Terragrunt's own questions rather than OpenTofu's: whether to create a missing state bucket, or whether to go ahead with the dependent units it spotted during a destroy. A CI (continuous integration, the automation that runs your pipeline) job needs it because there is no terminal there to answer with. Which is also why a CI job is the worst possible place for an unanswered destroy prompt to turn into a silent yes.
run --all destroy destroys the dependencies of the units under your current working directory in addition to the units themselves, by default. Stand in live/prod/app, run it, and the database and the VPC can go down with the app. During a destroy Terragrunt tries to find the dependent units it would break and shows a confirmation prompt listing them, and --non-interactive answers that prompt for you. So print the real reach first with terragrunt find --dag --external, keep destroy out of any pipeline that fires on merge, and if you must have a teardown job, make a human type the environment name before it starts.Save the plans, check them, then apply exactly those
A plan you read on screen and then throw away is a verbal quote from a builder. What you want is a written one, signed, that cannot change on the way to the job. Under --all you can keep every unit's plan as a file. --out-dir writes each unit's binary plan as tfplan.tfplan, mirroring the unit's path underneath the directory you name. --json-out-dir runs show -json after each plan and writes tfplan.json in the same layout, in JSON (JavaScript Object Notation, a plain text format for structured data) that policy tools read directly.
That gives you the gate a security team actually wants. Machine-check the JSON for an unencrypted database, a security group open to 0.0.0.0/0 (the whole internet), or an IAM (identity and access management, the rules saying who may do what in a cloud account) policy carrying "Action": "*". Only then hand the saved binary plans to apply. Point apply at the same --out-dir and each unit uses the plan you already read, so nothing drifts between the review and the change.
# plan once, keep both formatsterragrunt run --all \--out-dir /tmp/plans \--json-out-dir /tmp/plans-json \-- planfind /tmp/plans /tmp/plans-json -type f | sort# gate on the JSON before anything is appliedconftest test --policy policy/ /tmp/plans-json/db/tfplan.json# apply the exact plans that passedterragrunt run --all --out-dir /tmp/plans -- apply
❯❯ Run Summary 4 units 46s────────────────────────────Succeeded 4/tmp/plans/app/tfplan.tfplan/tmp/plans/db/tfplan.tfplan/tmp/plans/monitoring/tfplan.tfplan/tmp/plans/vpc/tfplan.tfplan/tmp/plans-json/app/tfplan.json/tmp/plans-json/db/tfplan.json/tmp/plans-json/monitoring/tfplan.json/tmp/plans-json/vpc/tfplan.jsonFAIL - /tmp/plans-json/db/tfplan.json - main - RDS instance orders-prod has storage_encrypted set to false1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
conftest is a command line policy checker built on Open Policy Agent, and it exits non-zero when a rule fails. Under set -e in a pipeline script the apply line therefore never runs, and the encryption rule stops a real change to a real RDS instance (Amazon's managed relational database service) before it reaches the account. One rule about the second half of that script: if you narrowed the plan with a filter, use the identical filter on the apply. Otherwise Terragrunt discovers the wider set of units, finds no plan file inside --out-dir for the extra ones, and errors out.
Cutting the run down to a slice
You rarely want to touch a whole environment at once. --queue-include-dir and --queue-exclude-dir take Unix-style globs relative to where you started, and current Terragrunt is strict about inclusion: only the directories you match enter the queue, and a matched unit's upstream dependencies are not dragged in behind it. The old --queue-strict-include flag existed to force that behaviour and is deprecated now, because it became the default. Worth writing on your hand: if your slice depends on a tier outside the filter, apply that tier first, or it will not be in the run at all. The cheapest scoping tool is still your shell, since discovery starts at the current directory, so cd live/prod/db shrinks the blast radius to that branch with no flags.
Two filters earn their keep in security and operations work. --queue-include-units-reading FILE runs only the units that actually read a given file, which answers the question you ask right after editing a shared config: who does this touch? Current Terragrunt expresses the same idea as a query, --filter 'reading=_env/common.hcl', and git-flavoured queries such as --filter '[main...HEAD]' narrow the run to what your branch changed. --filter-affected is the shorthand for that same comparison against the repository's default branch, which is the sane default for a pull request pipeline. For a unit you want out of routine runs no matter what flags somebody types, put an exclude block in the unit itself, so the decision lives in code review instead of in one person's shell history.
# two tiers only; strict inclusion means vpc does NOT come alongterragrunt run --all \--queue-include-dir "db" --queue-include-dir "app" \-- plan# which units read the shared config you just edited?terragrunt run --all --filter 'reading=_env/common.hcl' -- plan# in a pull request: only what this branch changedterragrunt run --all --filter-affected -- plan
❯❯ Run Summary 2 units 11s────────────────────────────Succeeded 2❯❯ Run Summary 3 units 21s────────────────────────────Succeeded 3❯❯ Run Summary 2 units 15s────────────────────────────Succeeded 2
# Keep this unit out of routine runs while it is being rebuilt.# It still appears in the run report as "excluded", so the skip stays# visible to whoever reads the pipeline artifact three weeks from now.exclude {if = get_env("TG_INCLUDE_MONITORING", "false") != "true"actions = ["plan", "apply"]exclude_dependencies = false}
When one unit fails
Terragrunt stops the branch, not the world. If db errors, everything downstream of db never starts, while units on other branches carry on to the end. The run report gives those states different names, and the difference is what you want at 2am staring at a red pipeline: failed means the unit ran and broke, early exit means it never got a turn because an ancestor failed, excluded means a filter or an exclude block kept it out.
--queue-ignore-errors changes that rule: dependent units run even though the thing they depend on failed. It is genuinely useful for shaking out every error in one pass on a plan. On an apply or a destroy it is a fine way to build an app against a database that was never created, so treat it as a diagnostic flag and not a pipeline setting. Write the whole run to disk with --report-file (the format is taken from the file extension, .json or .csv) and you get a per-unit record of what ran, when it started, when it ended, and why it did not: an artifact worth keeping next to the plan JSON.
terragrunt run --all --report-file /tmp/run.json -- apply
16:41:02.318 ERROR [db] tofu: Error: creating RDS DB Instance (orders-prod): InvalidParameterValue: Cannot find version 15.3 for postgres16:41:02.901 ERROR [db] Run failed with exit code 116:41:02.905 WARN [app] Skipping run: dependency [db] failed❯❯ Run Summary 4 units 38s────────────────────────────Succeeded 2Failed 1Early Exits 1
# jq is a command line JSON reader; @tsv lays the fields out in columnsjq -r '.[] | [.Name, .Result, .Reason] | @tsv' /tmp/run.json
vpc succeededmonitoring succeededdb failed run errorapp early exit ancestor error
One more thing about plans, and it bites hardest on day one. On a brand new environment nothing has been applied, so upstream units have no outputs sitting in remote state. run --all plan cannot fetch real values, so it falls back to the mock_outputs on each dependency block, or fails outright if you never wrote any. A mock output is an understudy reading the lines: close enough for a rehearsal, not the person the audience came to see. The plan you are reading is built on placeholders, so resource counts, generated names, and anything behind a conditional can be badly wrong, and a clean aggregate plan on an empty environment is no proof it will apply. Apply the foundation tiers first, then re-plan the rest against real state. Pin mock_outputs_allowed_terraform_commands = ["plan", "validate"] on every dependency too, because with that field unset the mocks are legal for every command, apply included, and an apply that swallows a fake id will happily build real resources wired to vpc-00000000000000000.
live/prod has been applied yet. From that folder you run terragrunt run --all --queue-include-dir "db" -- apply, and the db unit's dependency block on ../vpc defines mock_outputs but leaves mock_outputs_allowed_terraform_commands unset. What happens?vpc, apply vpc in its own run first.--, destroy included. Nothing about them is limited to read-only work, which is exactly why a filtered apply deserves a second look.vpc out of the run, dependency resolution still happens and falls back to the stand-in values, and with the allowed-commands field missing those values are accepted at apply time. You end up with a real database wired to vpc-00000000000000000.vpc lives inside the folder you started from, so nothing about it counts as external, and your own filter is what removed it. Terragrunt will not offer to put it back.run --all and the --non-interactive flag as one switch. Which description keeps them apart correctly?--non-interactive covers a different set of prompts, such as creating a missing state bucket or carrying on with a destroy that reaches dependents. --no-auto-approve switches the first one off, and it only pairs sensibly with --parallelism 1.-auto-approve answers, and Terragrunt appends that flag itself, while the state bucket question is Terragrunt's own.run-all instead of run --all, but that is the command name rather than the flag.--parallelism, which you turn down when a cloud API starts answering with HTTP 429 or when logs from eight units stop being readable. It has nothing to do with prompts.live/prod/app runs terragrunt run --all destroy with --non-interactive. Why is that combination dangerous?--parallelism only decides how many units run at the same time, and a destroy with no cap simply tears down more of them at once.terragrunt find --dag --external first, and keep teardown out of any pipeline that fires on merge.The pipeline shape that holds up: one job runs terragrunt run --all --filter-affected --out-dir plans --json-out-dir plans-json --report-file report.json -- plan and uploads all three as artifacts. A second job, gated behind a human approval and reusing the identical filter, runs terragrunt run --all --out-dir plans -- apply. The plans that were reviewed are the plans that get applied, and the report names every unit that moved.
Try this
Run terragrunt list --tree on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: run --all destroy reaches past the folder you are standing in. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.