Fast dependency graph generator for terragrunt monorepos. Outputs Atlantis, Digger, GitHub Actions matrix, JSON, or YAML.
cargo install --path .# Atlantis config
terragrunt-dag ./live/prod --format atlantis > atlantis.yaml
# Digger config
terragrunt-dag ./live/prod --format digger > digger.yaml
# GitHub Actions matrix (with change detection)
terragrunt-dag ./live/prod --format gha --base-ref origin/main > matrix.json
# JSON (for scripting)
terragrunt-dag ./live/prod --format json | jq '.projects[] | .name'terragrunt-dag <ROOT> [OPTIONS]
Options:
-f, --format <FORMAT> Output format [json|yaml|atlantis|digger|gha] (default: json)
-v, --verbose Debug output to stderr
--filter <PATTERN> Filter projects by glob (e.g., 'prod/*', '**/vpc')
--workflow <NAME> Workflow name (default: terragrunt)
--workspace <NAME> Override workspace for all projects
--autoplan Enable autoplan (default: true)
--automerge Enable automerge (default: false)
--parallel-apply Enable parallel apply (default: false)
--cascade-dependencies <BOOL> Include transitive dependencies (default: true)
--base-ref <REF> Git ref to diff against for `gha` change detection (e.g., origin/main)
--gha-filter-unchanged Drop unchanged units from the `gha` matrix (honors --cascade-dependencies)
--max-layers <N> For `gha`: fail non-zero if the DAG needs more than N layer buckets
--max-units-per-layer <N> For `gha`: split layers holding more than N matrix cells (default: 256, 0 disables)
--units-per-job <K> For `gha`: pack K units into one matrix cell (default: 1)
- Finds all
terragrunt.hclfiles - Parses
dependency,include,terraform.source,read_terragrunt_config(),file(),sops_decrypt_file() - Resolves
find_in_parent_folders(),get_repo_root(), path functions - Outputs projects with dependencies and watch files
Atlantis output includes execution_order_group for proper dependency ordering:
projects:
- name: vpc
dir: vpc
workspace: vpc
workflow: terragrunt
autoplan:
when_modified:
- '**/*.hcl'
- '**/*.tf'
- ../../../root.hcl
- ../../../_modules/vpc/**/*.tf
enabled: true
execution_order_group: 0
- name: app
dir: app
execution_order_group: 1 # runs after vpcThe gha format emits a matrix object ({"include":[...]}) ready to be consumed by fromJSON() in a downstream job. Each entry exposes name, working-directory, dependencies, layer (= execution_order_group), and changed.
jobs:
plan-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.gen.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: gen
run: |
echo "matrix=$(terragrunt-dag . --format gha --base-ref origin/main)" >> "$GITHUB_OUTPUT"
terragrunt:
needs: plan-matrix
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.plan-matrix.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- if: ${{ matrix.changed }}
working-directory: ${{ matrix.working-directory }}
run: terragrunt plandependencies and layer let callers chain layered jobs via needs: instead of per-step if: filtering. --base-ref requires git on PATH (standard in CI); on git failure it warns to stderr and marks all units unchanged. Use --max-layers <N> to fail-fast when the DAG exceeds the number of layer-jobs your workflow has hardcoded.
GitHub Actions caps a matrix at 256 jobs per workflow run, so a layer wider than that produces a matrix the consumer cannot expand. Units within a layer are mutually independent, so there are two safe ways to narrow one, and they cost different things:
--units-per-job <K>(default1) packs K units into one matrix cell. One job runs them back to back. Cell count drops, depth does not change.--max-units-per-layer <N>(default256) splits a layer holding more than N cells intoceil(cells/N)consecutive layers. This adds a barrier per split and charges the extra depth against--max-layers.
Batching is applied first, splitting to whatever is left over. Both chunk by sorted unit name, so a unit lands in the same cell across reruns; consumers keying caches or PR comments on position would otherwise see it move. --max-layers is measured after both.
The difference on a real 913-unit tree with layer profile [414, 238, 83, 67, 42, 40, 22, 7]:
| flags | depth | cells | widest layer |
|---|---|---|---|
--max-units-per-layer 0 (off) |
8 | 913 | 414 |
--max-units-per-layer 256 |
9 | 913 | 256 |
--max-units-per-layer 100 |
14 | 913 | 100 |
--max-units-per-layer 256 --units-per-job 2 |
8 | 458 | 207 |
--max-units-per-layer 100 --units-per-job 5 |
8 | 186 | 83 |
Splitting alone at a cap of 100 costs six extra layers; batching gets under the same cap at the original depth. Splitting is the backstop for what batching cannot absorb.
Because the documented cap is per run rather than per matrix, a workflow with several layer jobs may want a value below 256.
At --units-per-job 1 each entry describes one unit, as above. Above 1 a cell no longer maps to a single unit, so working-directory and dependencies are replaced by a units list and name becomes a job label:
{"name": "live_prod_vpc (+2 more)", "layer": 0, "changed": true,
"units": [{"name": "live_prod_vpc", "working-directory": "live/prod/vpc", "dependencies": [], "changed": true}]}The shape is decided by the flag, not per cell, so the matrix never has heterogeneous keys. A cell is changed if any unit in it is.
A unit is marked changed if any of its own source files changed, and (with --cascade-dependencies, the default) the change is propagated to its downstream dependents through the DAG.
Target: <500ms for 800 projects. Uses rayon for parallel processing and caches parsed configs.
| Feature | terragrunt-dag | terragrunt-atlantis-config |
|---|---|---|
| Language | Rust | Go |
| Output formats | Atlantis, Digger, JSON, YAML | Atlantis only |
| execution_order_group | Always computed | Opt-in (--execution-order-groups) |
| Parallel processing | Yes (rayon) | No |
| Config caching | Yes | No |
| autoplan default | true | false |
| create-workspace default | Per-project name | false (uses "default") |
| Dependency cascade | Yes (--cascade-dependencies) |
Yes (--cascade-dependencies) |
| locals overrides | No | Yes (atlantis_skip, atlantis_workflow, etc.) |
| extra_atlantis_dependencies | No | Yes |
| Project markers | No | Yes (--project-hcl-files) |
| Pre-workflow hook | Manual setup | Documented |
| preserve-workflows | No | Yes |
| apply-requirements | No | Yes |
- You need Digger or JSON/YAML output
- You want execution ordering by default
- You want fast builds (Rust binary, config caching)
- Simple setup without per-module overrides
- You need per-module locals overrides (
atlantis_skip,atlantis_workflow) - You need
extra_atlantis_dependenciesfor custom watch files - You need
--apply-requirementsor--preserve-workflows
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See LICENSE for the full text.