Skip to content

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.

Terminal window
# Compare between two references
terragrunt find --filter '[main...HEAD]'
# Shorthand: compare reference to HEAD
terragrunt find --filter '[main]'
# Compare between specific commits
terragrunt find --filter '[abc123...def456]'
# Compare between tags
terragrunt find --filter '[v1.0.0...v2.0.0]'
# Compare using relative references
terragrunt find --filter '[HEAD~1...HEAD]'
# Compare between branches
terragrunt 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)

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.

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:

Terminal window
terragrunt run --all --filter '[main...HEAD]' -- plan

Will result in the following:

  • modified-unit will be planned (tofu plan)
  • new-unit will be planned (tofu plan)
  • removed-unit will be planned for destruction (tofu plan -destroy)
  • unchanged-unit will be ignored

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.

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
Terminal window
git rm -r live/cache
git 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

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
Terminal window
git rm live/terragrunt.stack.hcl
git 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

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:

live/terragrunt.stack.hcl
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:

live/terragrunt.stack.hcl
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

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
live/terragrunt.stack.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

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
live/terragrunt.stack.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.

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:

Terminal window
terragrunt run --all --filter '[main...HEAD] | ./live/**' --filter-allow-destroy --experiment block-iteration -- apply
terragrunt run --all --filter '[main...HEAD] | ./live/.terragrunt-stack/app/us-west-2' --filter-allow-destroy --experiment block-iteration -- apply

The 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
.terragrunt-filters
!./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.

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:

Terminal window
# Find components affected by changes between main and HEAD
terragrunt 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.