Git Expressions
Match units and stacks based on changes between Git references. This is useful for targeting infrastructure that has been modified, added, or removed between commits, branches, or tags.
Git-based expressions are written between [ and ] characters, and use the ... operator to indicate the range of changes to compare.
# Compare between two referencesterragrunt find --filter '[main...HEAD]'
# Shorthand: compare reference to HEADterragrunt find --filter '[main]'
# Compare between specific commitsterragrunt find --filter '[abc123...def456]'
# Compare between tagsterragrunt find --filter '[v1.0.0...v2.0.0]'
# Compare using relative referencesterragrunt find --filter '[HEAD~1...HEAD]'
# Compare between branchesterragrunt find --filter '[feature-branch...main]'Directory.
Directorymodified-unit <— Matched by [main…HEAD] (terragrunt.hcl was modified)
- terragrunt.hcl (modified)
Directorynew-unit <— Matched by [main…HEAD] (terragrunt.hcl was added)
- terragrunt.hcl (added)
Directoryremoved-unit <— Matched by [main…HEAD] (terragrunt.hcl was removed)
- (directory removed)
Directoryunchanged-unit
- terragrunt.hcl (unchanged)
How it works
Section titled “How it works”When evaluating a Git-based filter, Terragrunt will first generate a worktree for every reference that needs to be evaluated, and assess the diffs between Git references.
e.g. For a filter like [main...HEAD], Terragrunt will generate a worktree for main and one for HEAD in temporary directories, and use git diff to assess the diffs between the two references.
Then, for any unit that is discovered within those worktrees, Terragrunt will enqueue that unit for a run in the run queue in the worktree where it was discovered.
In the example above, the modified-unit will be discovered in a “to” temporary directory (e.g. /tmp/.../terragrunt-worktree-HEAD.../modified-unit), whereas the removed-unit would be discovered in the “from” temporary directory (e.g. /tmp/.../terragrunt-worktree-main.../removed-unit).
This is important to recognize, as it’s how destroys will be possible despite the unit no longer being present in the current working directory. As a consequence, however, you may find that paths don’t behave how you expect, as you will be performing runs in the temporary directories created for the relevant worktrees.
Interaction with the run command
Section titled “Interaction with the run command”When using Git-based expressions and the run command, you are required to use one of the plan or apply commands, and not the -destroy flag.
This is because whether a unit will be destroyed is determined by logic relevant to inspecting changes in Git.
When units are added or modified between two Git references, they will be planned or applied. When the units are removed between two Git references, they will be planned for destruction (with plan -destroy) or destroyed (with apply -destroy).
In the scenario above, running the following:
terragrunt run --all --filter '[main...HEAD]' -- planWill result in the following:
modified-unitwill be planned (tofu plan)new-unitwill be planned (tofu plan)removed-unitwill be planned for destruction (tofu plan -destroy)unchanged-unitwill be ignored
Destroying removed components
Section titled “Destroying removed components”A component is orphaned when it is removed from the configuration, or a stack stops generating it, while its infrastructure is still deployed. Terragrunt finds orphans in the diff between the two references and runs them with -destroy in the worktree for the “from” reference, as described in How it works.
Deleted units
Section titled “Deleted units”Deleting a unit’s terragrunt.hcl orphans that unit. e.g. a commit that deletes the live/cache unit:
Directorylive
Directorycache
- terragrunt.hcl
Directoryvpc
- terragrunt.hcl
git rm -r live/cachegit commit -m "Remove cache"terragrunt run --filter '[HEAD^...HEAD]' --filter-allow-destroy -- plan then runs the equivalent of terragrunt run -- plan -destroy in live/cache in the temporary worktree generated for HEAD^:
DirectoryHEAD^-worktree temporary worktree for
HEAD^Directorylive
Directorycache
- terragrunt.hcl
Directoryvpc
- terragrunt.hcl
Deleted stacks
Section titled “Deleted stacks”Deleting a terragrunt.stack.hcl orphans every component the stack generated. e.g. a commit that deletes the live stack:
Directorylive
- terragrunt.stack.hcl
Directory.terragrunt-stack
Directoryapp
- terragrunt.hcl
Directorydb
- terragrunt.hcl
git rm live/terragrunt.stack.hclgit commit -m "Remove live stack"terragrunt run --filter '[HEAD^...HEAD]' --filter-allow-destroy -- plan then generates the stack in the temporary worktree generated for HEAD^ and runs the equivalent of terragrunt run -- plan -destroy in every unit it generates:
DirectoryHEAD^-worktree temporary worktree for
HEAD^Directorylive
- terragrunt.stack.hcl
Directory.terragrunt-stack
Directoryapp
- terragrunt.hcl
Directorydb
- terragrunt.hcl
Changed stack files
Section titled “Changed stack files”When a stack file changes between references without being added or removed, Terragrunt generates the stack in both references and compares the results. e.g. a stack that generates one expanded unit per region:
locals { regions = ["us-east-1", "us-west-2", "eu-west-1"]}
unit "app" { expansion { for_each = toset(local.regions) }
source = "../units/app" path = "app/${each.key}"
values = { region = each.key }}Directoryunits
Directoryapp
- terragrunt.hcl
Directorylive
- terragrunt.stack.hcl
Directory.terragrunt-stack
Directoryapp
Directoryus-east-1
- terragrunt.hcl
Directoryus-west-2
- terragrunt.hcl
Directoryeu-west-1
- terragrunt.hcl
Removing "us-west-2" from local.regions:
locals { regions = ["us-east-1", "eu-west-1"]}The stack in HEAD^ generates app/us-west-2 and the stack in HEAD does not, so terragrunt run --filter '[HEAD^...HEAD]' --filter-allow-destroy --experiment block-iteration -- plan runs the equivalent of terragrunt run -- plan -destroy in app/us-west-2 in the temporary worktree generated for HEAD^:
DirectoryHEAD^-worktree temporary worktree for
HEAD^Directoryunits
Directoryapp
- terragrunt.hcl
Directorylive
- terragrunt.stack.hcl
Directory.terragrunt-stack
Directoryapp
Directoryus-east-1
- terragrunt.hcl
Directoryus-west-2
- terragrunt.hcl
Directoryeu-west-1
- terragrunt.hcl
Changed files read by a stack
Section titled “Changed files read by a stack”Terragrunt also compares the generated stacks when the stack file itself is unchanged but the diff includes a file the stack reads. e.g. a stack that reads its regions from regions.hcl:
Directoryunits
Directoryapp
- terragrunt.hcl
Directorylive
- terragrunt.stack.hcl
- regions.hcl
Directory.terragrunt-stack
Directoryapp
Directoryus-east-1
- terragrunt.hcl
Directoryus-west-2
- terragrunt.hcl
Directoryeu-west-1
- terragrunt.hcl
locals { regions = read_terragrunt_config("regions.hcl").locals.regions}Removing "us-west-2" from regions.hcl orphans app/us-west-2 in the same way, and terragrunt run --filter '[HEAD^...HEAD]' --filter-allow-destroy --experiment block-iteration -- plan runs the equivalent of terragrunt run -- plan -destroy in app/us-west-2 in the temporary worktree generated for HEAD^:
DirectoryHEAD^-worktree temporary worktree for
HEAD^Directoryunits
Directoryapp
- terragrunt.hcl
Directorylive
- terragrunt.stack.hcl
- regions.hcl
Directory.terragrunt-stack
Directoryapp
Directoryus-east-1
- terragrunt.hcl
Directoryus-west-2
- terragrunt.hcl
Directoryeu-west-1
- terragrunt.hcl
Changes outside the diff
Section titled “Changes outside the diff”If an expansion shrinks because of something outside the diff, Terragrunt cannot find the orphan. e.g. a stack that reads its regions from an environment variable:
Directoryunits
Directoryapp
- terragrunt.hcl
Directorylive
- terragrunt.stack.hcl
Directory.terragrunt-stack
Directoryapp
Directoryus-east-1
- terragrunt.hcl
Directoryus-west-2
- terragrunt.hcl
Directoryeu-west-1
- terragrunt.hcl
locals { regions = split(",", get_env("REGIONS"))}Changing REGIONS from us-east-1,us-west-2,eu-west-1 to us-east-1,eu-west-1 changes no file in the repository, so [HEAD^...HEAD] does not select app/us-west-2 and its infrastructure stays deployed. The same happens with a values file outside the repository. Destroy those components yourself.
Narrowing Git expressions
Section titled “Narrowing Git expressions”Consider a branch that removes "us-west-2" and "eu-west-1" from local.regions in the live stack, and deletes the staging/cache unit. [main...HEAD] alone selects the three orphans in bold:
Directoryunits
Directoryapp
- terragrunt.hcl
Directorylive
- terragrunt.stack.hcl
Directory.terragrunt-stack
Directoryapp
Directoryus-east-1
- terragrunt.hcl
Directoryus-west-2
- terragrunt.hcl
Directoryeu-west-1
- terragrunt.hcl
Directorystaging
Directorycache
- terragrunt.hcl
To limit the search to part of the repository, intersect the Git expression with another expression using | in the same --filter. The intersection applies to every component the Git expression selects, including orphans, which you can select by their generated path:
terragrunt run --all --filter '[main...HEAD] | ./live/**' --filter-allow-destroy --experiment block-iteration -- applyterragrunt run --all --filter '[main...HEAD] | ./live/.terragrunt-stack/app/us-west-2' --filter-allow-destroy --experiment block-iteration -- applyThe first command destroys app/us-west-2 and app/eu-west-1, and skips staging/cache. The second destroys only app/us-west-2.
Filters in a .terragrunt-filters file also apply in the worktree for each reference. e.g. a repository that keeps unit templates in catalog:
Directorycatalog
Directoryunits
Directoryapp
- terragrunt.hcl
Directorylive
- terragrunt.stack.hcl
!./catalog/**terragrunt run --filter '[main...HEAD]' --filter-allow-destroy -- plan skips everything under catalog, in both worktrees, even when the diff changes or deletes a unit there.
The --filter-affected flag
Section titled “The --filter-affected flag”For the common use case of comparing the default branch (typically main) with HEAD, you can use the --filter-affected flag as a convenient shorthand:
# Find components affected by changes between main and HEADterragrunt find --filter-affected
# Equivalent to:terragrunt find --filter '[main...HEAD]'The --filter-affected flag automatically detects your repository’s default branch and compares it with HEAD.