Terra provides powerful parallel execution capabilities for any Terragrunt command using the --parallel=N flag, where N is the number of concurrent threads. This feature automatically discovers Terraform/Terragrunt modules in subdirectories and executes operations across them simultaneously, significantly reducing execution time.
For any command:
# Run init across all modules with 4 parallel threads
terra init --parallel=4 /path/to/infrastructure
# Run plan across all modules with 2 parallel threads
terra plan --parallel=2 /path/to/infrastructure
# Run apply across all modules with 8 parallel threads
terra apply --parallel=8 /path/to/infrastructureFor state commands:
# Import a resource across all modules in parallel (4 threads)
terra import --parallel=4 null_resource.example resource-id /path/to/infrastructure
# Remove a resource from state across all modules in parallel (2 threads)
terra state rm --parallel=2 null_resource.example /path/to/infrastructure
# Move a resource in state across all modules in parallel
terra state mv --parallel=4 old_resource.name new_resource.name /path/to/infrastructure
# Pull state from remote across all modules in parallel
terra state pull --parallel=4 /path/to/infrastructureUse the --only and --skip flags to control which subdirectories should be processed in parallel. This is useful when you only want to run commands on specific modules rather than all discovered modules.
Selecting specific directories with --only:
# Run init on specific folders (test1, test2, test3) within the target path
terra init --parallel=4 --only=test1,test2,test3 environments/xpto/prod
# This executes init on:
# - environments/xpto/prod/test1
# - environments/xpto/prod/test2
# - environments/xpto/prod/test3Skipping specific directories with --skip:
# Run apply on all folders except folder2
terra apply --parallel=4 --skip=folder2 /path/to/infrastructure
# Run plan on all folders except folder1 and folder3
terra plan --parallel=4 --skip=folder1,folder3 /path/to/infrastructureCombining --only and --skip:
# Select specific folders but skip a subset
terra init --parallel=4 --only=folder1,folder2,folder3 --skip=folder2 /path/to/infrastructureHow it works:
--only=: Values are concatenated with the target path usingfilepath.Join(). Only these directories are processed.--skip=: Matching directories are removed from processing.- When only
--skipis provided, all subdirectories are discovered first, then skipped modules are removed. - When both
--onlyand--skipare provided,--onlyis processed first, then--skipis applied. - Only directories that exist and are valid will be processed.
- Non-existent or invalid paths are logged as warnings and skipped.
- If the number of threads (
--parallel=N) exceeds the number of modules, the thread count is automatically reduced to match. - The same module cannot appear in both
--onlyand--skip(validation error).
Examples:
# Apply changes to specific environments only
terra apply --parallel=3 --only=dev,staging,prod /path/to/infrastructure
# Plan all modules except test environments
terra plan --parallel=2 --skip=test,testing /path/to/infrastructure
# Import resources in specific directories, skipping backup folders
terra import --parallel=4 --only=region1,region2 --skip=backup null_resource.example resource-id /path/to/infrastructureThread count optimization:
# If you specify --parallel=4 but only provide 3 items in --only,
# the thread count is automatically reduced to 3
terra init --parallel=4 --only=test1,test2,test3 /path
# Logs: "Reducing thread count to 3 (number of modules)"Terra's --parallel=N is separate from Terragrunt's native --all and --parallelism flags. They serve different purposes and own different filter flags:
| Flag | Owner | Purpose | Filter flags |
|---|---|---|---|
--parallel=N |
Terra | Terra manages goroutine workers across module directories | --only=mod1,mod2, --skip=mod1,mod2 |
--all |
Terragrunt | Terragrunt's native run-all behavior (forwarded as-is) | --filter, --queue-exclude-dir, --queue-include-dir |
--parallelism=N |
Terragrunt | Terragrunt's concurrency for --all (forwarded as-is) |
n/a |
--filter=query |
Terragrunt | Terragrunt's expressive module filter (forwarded as-is) | n/a |
# Terra-managed parallel execution (terra discovers modules, runs N workers)
terra plan --parallel=4 /path/to/infrastructure
terra plan --parallel=4 --skip=mod1,mod2 /path/to/infrastructure
# Terragrunt-managed run-all (forwarded directly to terragrunt)
terra apply --all /path/to/infrastructure
terra apply --all --filter='!mod1' /path/to/infrastructure
# Terragrunt-managed run-all with parallelism and filter
terra apply --all --parallelism=4 --filter='region-us-east' /path/to/infrastructureImportant: --parallel and --all cannot be used together -- they represent competing execution strategies.
Use this checklist to pick the right strategy before writing the command:
- State operation across multiple modules from a root directory? → must use
--parallel=N. Terragrunt's--alldoes not support state commands, and terra rejects that combination. Single-module state commands (e.g.,terra state rm <addr> /path/to/one/module) still work without--parallel— terra just forwards them directly to terragrunt. - Need terragrunt DAG ordering /
dependenciesblock awareness? → must use--all. Terra's worker pool runs modules in parallel without resolving dependencies between them. - Flat stack, want simple basename filtering? → either works.
--parallel=Nis slightly faster and its--only/--skipsyntax is shorter for flat stacks. - Need glob, graph, or git-diff filtering? → must use
--allwith terragrunt's--filter. Terra's--only/--skiponly match literal basenames.
--only/--skip (terra-managed) and --filter/--queue-exclude-dir (terragrunt-managed) are not interchangeable at runtime, but they have equivalents for common cases. Use the column that matches the strategy you picked above:
| Intent | With --parallel=N |
With --all |
|---|---|---|
| Skip one module | --skip=mod1 |
--filter='!mod1' |
| Skip multiple | --skip=mod1,mod2 |
--filter='!mod1' --filter='!mod2' |
| Only specific modules | --only=mod1,mod2 |
--filter='mod1' --filter='mod2' |
| Path glob | (not supported) | --filter='./prod/**' |
| Graph expression | (not supported) | --filter='service...' |
| Git-diff expression | (not supported) | --filter='[main...HEAD]' |
Terra deliberately does not translate --skip/--only into --queue-exclude-dir/--filter on the --all path, and does not translate terragrunt's filter flags into terra's worker-pool selection on the --parallel path. There are three reasons:
- Matching semantics differ. Terra's
--skip=lab3matches modules by basename anywhere in the subtree. Terragrunt's--queue-exclude-dir=lab3matches paths relative to the working directory and follows terragrunt-specific glob rules. Any automatic translation would either lie about semantics or require terra to walk the module tree itself, which defeats the purpose of--all. - Upstream parsing quirks. gruntwork-io/terragrunt#5124 documents that
--queue-exclude-dirstill parses excluded directories during dependency discovery. A module skipped by terra's native--skipis never touched; the same name forwarded to terragrunt's queue flag still goes through DAG parsing. Surfacing that divergence as a silent translation would cause subtle incidents in CI. --filteris strictly more expressive. Translating--skip=a,bto--queue-exclude-dirwould downgrade capability for--allusers who have access to graph and git-diff expressions.
Instead, terra provides discoverability: when you use --skip with --all, the validation error echoes your command and shows the exact --filter form you should type. When you use terragrunt's --filter/--queue-exclude-dir with --parallel=N, terra logs a warning pointing you at --only/--skip.
When using --parallel with apply or destroy, you must provide a confirmation flag because parallel workers cannot share a single stdin for interactive prompts. Terra translates these flags into native Terraform and Terragrunt flags before forwarding the command, so there is no PTY pattern matching involved:
--yes/-yinjects Terragrunt's--non-interactiveplus Terraform's-auto-approveforapply/destroy.--no/-ninjects only Terragrunt's--non-interactive, which causes Terraform's apply prompt to abort instead of proceeding -- matching a "no" answer.
# ERROR: apply prompts for confirmation, but parallel workers can't share stdin
terra apply --parallel=4 /path/to/infrastructure
# CORRECT: --yes maps to --non-interactive -auto-approve
terra apply --parallel=4 --yes /path/to/infrastructure
# Short form
terra destroy --parallel=4 -y /path/to/infrastructure
# Non-interactive, but abort instead of auto-approving
terra apply --parallel=4 --no /path/to/infrastructure
# OK: plan never prompts, so no confirmation flag is required
terra plan --parallel=4 /path/to/infrastructureNote: The legacy
--reply/-rflags still work and emit a deprecation warning.--reply=yand bare--replymap to--yes;--reply=nmaps to--no. Migrate to--yes/--noat your earliest convenience;--replywill be removed in a future release.
All Terragrunt commands support --parallel=N:
init- Initialize Terraform working directoryplan- Generate and show execution planapply- Build or change infrastructuredestroy- Destroy Terraform-managed infrastructurevalidate- Validate Terraform filesfmt- Format Terraform filesimport- Import existing infrastructure into Terraform statestate rm- Remove resources from statestate mv- Move resources in statestate pull- Pull remote statestate push- Push local state to remotestate show- Show attributes of a resource in state- And any other Terragrunt command
- Automatic Module Discovery: Scans subdirectories for
.tf,.tfvars, orterragrunt.hclfiles (unless--onlyis specified) - Selective Filtering: When
--onlyor--skipis used, only the matching subdirectories are processed - Parallel Execution: Runs N jobs concurrently (where N is specified in
--parallel=N, default is 5) - Thread Optimization: Automatically reduces thread count if it exceeds the number of modules to process
- Error Aggregation: Collects and reports errors from all parallel operations
- Progress Tracking: Provides real-time logging of module processing status
- Flag Filtering: Removes Terra-specific flags (
--parallel=N,--only=,--skip=) before passing to Terragrunt
Because --parallel=N runs several Terragrunt processes at once, terra captures each worker's stdout and stderr and prefixes every line with the module's directory name, so the interleaved output stays attributable:
[module-a] Initializing provider plugins...
[module-b] Initializing the backend...
[module-a] Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
[module-b] Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
- The label is the base name of the module directory terra is processing (the leaf of the path).
- When terra's stdout is an interactive terminal, each module's label is colorized with a stable per-module color so the streams are easy to tell apart. Colors are disabled automatically when the output is redirected to a file or a pipe, or when the
NO_COLORenvironment variable is set. - Lines from different modules are serialized through a shared lock, so a line from one module never splits a line from another.
This mirrors the attributable, prefixed output that Terragrunt's native --all produces, but for terra's own worker pool. The Terragrunt-managed --all path keeps its own native prefixing and is unaffected.
| Command | Behavior |
|---|---|
terra init --parallel=4 |
Terra handles parallel execution with 4 threads across all modules |
terra init --parallel=4 --only=test1,test2,test3 /path |
Terra handles parallel execution with 4 threads on specified folders only |
terra import --parallel=4 |
Terra handles parallel execution with 4 threads |
terra plan --all |
Terragrunt handles --all flag natively (forwarded by Terra) |
terra apply --all --parallelism=4 |
Both flags forwarded to Terragrunt |
terra apply --all --filter=mod1 |
Both flags forwarded to Terragrunt |
- Performance: Executes across multiple modules simultaneously instead of sequentially
- Flexibility: Control the number of parallel threads with
--parallel=N - Selective Execution: Use
--onlyand--skipto target specific directories instead of all discovered modules - Thread Optimization: Automatically adjusts thread count to match the number of modules
- Native Integration: No need for external tools like GNU parallel
- Error Handling: Comprehensive error reporting and aggregation
- Logging: Detailed progress and completion status for each module
- Clean Separation: Terra's
--paralleland Terragrunt's--all/--parallelism/--filterare independent and unambiguous; mixing them produces educational validation errors that show the correct form for your command rather than silently doing the wrong thing
- When using
--parallel=N, Terra automatically handles parallel execution for all commands, including state commands. - Regular Terragrunt commands with
--all(likeplan --all,apply --all) are forwarded to Terragrunt's native implementation and are not handled by Terra's parallel execution. --parallel=Nand--allcannot be used together.- When modules share Terragrunt dependencies, concurrent
terraform initmay trigger a Git ref backend race condition. See parallel-git-clone-race.md for details and workarounds.