

Managing Terraform configurations across multiple environments is manageable when you have two. When you have twelve (dev, staging, prod, across three regions, under multiple AWS accounts), the copy-paste starts to accumulate. Backend configuration blocks duplicate. Output files drift. A change to one environment requires hunting down every other environment that shares the same pattern.
Terragrunt was built for exactly that problem. After nearly a decade of development and over 900 releases, Gruntwork shipped Terragrunt 1.0 on March 30, 2026, the first version with an explicit backwards compatibility commitment. It's a good time to get familiar with what Terragrunt does, what changed in 1.0, and how to put it to use.
At a glance Terragrunt is an open-source orchestration tool by Gruntwork that sits on top of Terraform or OpenTofu. Current version: v1.0.1 (April 13, 2026). Terragrunt keeps Infrastructure as Code (IaC) configurations DRY across environments, manages remote state automatically, and orchestrates multi-module deployments through dependency graphs. It supports both Terraform and OpenTofu as execution backends.
What is Terragrunt?
Terragrunt is a thin wrapper for Terraform and OpenTofu. Where Terraform gives you the IaC language and execution engine, Terragrunt gives you the scaffolding to run it at scale: configuration inheritance, automatic remote state creation, dependency ordering, and a mechanism to deploy changes across dozens of modules without writing the same backend block forty times.
The core idea is the DRY principle applied to infrastructure. Don't Repeat Yourself. With vanilla Terraform, every environment folder needs its own backend configuration, its own provider block, and its own copy of module inputs. Terragrunt replaces that repetition with a hierarchy of terragrunt.hcl files: one root file that defines what's shared, and smaller per-environment files that define only what differs.
Terragrunt is not a replacement for Terraform or OpenTofu. It's a layer above them. You still write .tf files. Terragrunt calls the underlying IaC tool on your behalf, injecting additional logic before and after each command runs.
Related reading: Terraform vs. OpenTofu: what changed after the license fork. Terragrunt supports both, so understanding the differences helps you pick the right execution backend.
What changed in Terragrunt 1.0
Terragrunt 1.0 shipped on March 30, 2026. The version number isn't cosmetic: it comes with a formal backwards compatibility commitment covering CLI flags, HCL configuration syntax, serialized output formats, and logging. Teams can upgrade within the 1.x series without expecting workflow breaks.
Several things changed materially:
Units and stacks. Terragrunt 1.0 introduced formal terminology for two concepts that existed informally before. A unit is a single directory containing a terragrunt.hcl file, representing one deployable piece of infrastructure. A stack is a collection of units managed together: your full dev environment, or your entire production account. The run --all command operates on stacks. Stacks can be implicit (defined by directory structure) or explicit (defined via terragrunt.stack.hcl files). These names matter because the documentation, error messages, and community discussion now use them consistently. Gruntwork's 1.0 announcement describes teams eliminating thousands of lines of repetitive configuration after migrating to explicit stacks.
An explicit stack file looks like this:
# terragrunt.stack.hcl
unit "vpc" {
source = "./vpc"
}
unit "app" {
source = "./app"
dependencies = [unit.vpc]
}
unit "database" {
source = "./database"
dependencies = [unit.vpc]
}
Running terragrunt run --all -- plan from the directory containing terragrunt.stack.hcl respects this dependency graph automatically.
The CLI was redesigned significantly. The most important change is run, which replaces direct subcommand invocation. Instead of terragrunt apply --terragrunt-non-interactive, you write terragrunt run --all -- apply --non-interactive. The --terragrunt- prefix is gone. Five new top-level commands were added: run, exec, find, list, and backend. The exec command runs arbitrary shell commands across all units in a stack. The find and list commands are inspection tools for understanding which units exist and how they relate. backend lets you manage backend configuration independently of a full run.
Seven legacy targeting flags were collapsed into a single --filter flag. Where you previously had separate flags for path matching, dependency filtering, and Git-changed file targeting, --filter handles all of them through one interface. You can now write --filter-affected as a shorthand for "only units changed since the default branch," or pass a path glob directly.
Structured run reports are new in 1.0. After a run --all command, Terragrunt can emit JSON or CSV output capturing which units succeeded, failed, or were skipped. Before this, CI pipelines had to parse log output to figure out deployment outcomes. The --report-file and --report-format flags control where the report lands and what format it takes.
On the documentation side: the official docs migrated from terragrunt.gruntwork.io to docs.terragrunt.com. Old URLs redirect, but update your bookmarks. When OpenTofu 1.10+ is installed alongside Terragrunt, provider caching now works across units automatically, which meaningfully cuts init time on large stacks.
Terragrunt features
Keep your configuration DRY
The foundational feature. Terragrunt's include block lets a child terragrunt.hcl inherit configuration from a parent. A typical root file defines the backend, the provider, and common inputs. Each environment file then uses include to pull in the root config and overrides only what it needs to change.
Before Terragrunt, environment-specific backend keys were a common source of mistakes. It's easy to accidentally reference the prod state file from dev. Terragrunt generates the state key automatically from the directory path, eliminating that class of error.
Remote state management
Terragrunt can create the remote state backend resources for you. Point it at an S3 bucket and DynamoDB table configuration, and Terragrunt will provision them if they don't exist before running init. No manual setup, no chicken-and-egg problem. The Terraform state file itself is still a standard .tfstate; Terragrunt just manages where it lives.
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "my-lock-table"
}
}
The path_relative_to_include() function is the key piece. It generates a unique state key for each unit based on its directory path, so dev and prod can never overwrite each other's state.
Dependency management
Terragrunt models dependencies between units as a Directed Acyclic Graph (DAG). You declare that unit B depends on unit A, and Terragrunt guarantees A runs first and makes its outputs available to B.
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}
During plan, when the upstream unit hasn't been applied yet, you can define mock_outputs to provide placeholder values so the plan doesn't fail. This makes CI pipelines against new environments work without requiring a full apply first.
Hooks
Terragrunt supports before_hook and after_hook blocks that execute shell commands at specific points in the Terraform lifecycle.
before_hook "validate" {
commands = ["apply", "plan"]
execute = ["tflint", "--chdir", "."]
}
Hooks are useful for running linting, secret injection, or notification steps without modifying your .tf files or CI configuration.
Run across multiple modules
The run --all command (formerly run-all) applies commands across an entire stack in dependency order. Running terragrunt run --all apply in a root directory traverses the unit graph, applies units with no dependencies first, then proceeds in order. The underlying [terraform plan](https://www.env0.com/blog/terraform-plan) and apply behavior is identical to running Terraform directly.
Combined with the new filter system, you can narrow execution to only the units affected by recent Git changes, which is useful for large stacks where running everything on every PR would be expensive.
Multi-account AWS support
Terragrunt can assume IAM roles per unit, enabling multi-account deployments without storing cross-account credentials in plaintext:
iam_role = "arn:aws:iam::123456789012:role/TerragruntDeployRole"
Terragrunt calls sts assume-role automatically and exposes the temporary credentials as environment variables for the underlying Terraform/OpenTofu run.
Related reading: A guide to Terraform backends: local vs. remote options. Remote state management is foundational to multi-environment Terragrunt setups.
Related reading: Terraform state file explained: storage, locking, and recovery. Understand what Terragrunt is automatically managing on your behalf.
Getting started with Terragrunt
Installation
Download the binary from the Terragrunt releases page and add it to your PATH. On macOS with Homebrew:
brew install terragrunt
Verify the installation:
terragrunt --version
# terragrunt version v1.0.1
Terragrunt requires an existing Terraform or OpenTofu installation. It delegates all IaC execution to whichever tool it finds on your PATH. When Terragrunt runs terraform init on your behalf, it also handles backend initialization automatically.
Basic commands
In Terragrunt 1.0, the primary pattern is terragrunt run -- <command> for single-unit operations and terragrunt run --all -- <command> for stack-wide operations. The -- separator distinguishes Terragrunt flags from the underlying OpenTofu/Terraform command and its flags. Terragrunt also accepts shorthand (terragrunt plan, terragrunt apply) for backwards compatibility:
# Single unit
terragrunt run -- plan
terragrunt run -- apply
terragrunt run -- destroy
# Entire stack (runs in dependency order)
terragrunt run --all -- plan
terragrunt run --all -- apply
# Shorthand (backwards compatible)
terragrunt plan
terragrunt apply
A basic terragrunt.hcl file
# root terragrunt.hcl
locals {
aws_region = "us-east-1"
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "my-company-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = local.aws_region
encrypt = true
dynamodb_table = "terraform-locks"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "${local.aws_region}"
}
EOF
}
Each environment's terragrunt.hcl then points back to this root:
# environments/dev/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules//app"
}
inputs = {
environment = "dev"
instance_type = "t3.small"
}
Terragrunt use cases and examples
DRY Terraform across dev and prod
The classic Terragrunt motivating example: two environments that share 90% of their configuration. Without Terragrunt, you maintain two near-identical directory trees. With Terragrunt, you maintain one module and two small environment files.
Below is the folder structure using Terragrunt:
├── terragrunt.hcl # root config (backend, provider)
├── environments
│ ├── dev
│ │ └── terragrunt.hcl # dev-specific inputs only
│ └── prod
│ └── terragrunt.hcl # prod-specific inputs only
└── modules
└── wordpress
├── main.tf
├── outputs.tf
└── variables.tf
Compare this to the Terraform-only structure, where main.tf and outputs.tf are duplicated in both environment folders. The backend configuration block alone is a copy-paste landmine: one wrong key value and dev applies over prod's state.
The dev terragrunt.hcl in this structure:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules//wordpress"
}
inputs = {
database_password = "dev-password"
region = "us-east-1"
instance_type = "t2.micro"
instance_class = "db.t2.micro"
}
The prod file is identical in structure, with different values for region, instance_type, instance_class, and database_password. The backend block doesn't appear in either. Terragrunt generates it from the root config, with the state key derived automatically from the directory path.
Deploying the full stack
Once both environment files are in place, deploying both environments is a single command from the environments/ directory:
terragrunt run --all -- apply
Terragrunt discovers all units in the directory tree, builds the dependency graph, and applies them in order. If you only need dev:
cd environments/dev && terragrunt run -- apply
Or from root using the global --working-dir flag:
terragrunt --working-dir environments/dev run -- apply
Related reading: Terraform modules guide: best practices for reusable IaC. Terragrunt is most effective when your Terraform code is already modular.
Adopting Terragrunt in an existing Terraform codebase
You don't need to refactor everything before getting value from Terragrunt. The most useful entry point is remote state: add a terragrunt.hcl alongside your existing .tf files and move the backend block into a remote_state configuration. Nothing else changes. No module refactoring, no new file layout. You get automatic state key generation from the directory path, which is the single most common source of backend-related mistakes in plain Terraform setups.
From there, extracting the provider block is a low-risk next step. A generate "provider" block in your root terragrunt.hcl creates the provider configuration at runtime and eliminates the need to maintain it in each module. Provider configuration is deterministic, so this change is safe to make incrementally across modules.
Dependency declarations come later, as you identify modules that consume outputs from other modules. Start with the most obvious pairs: networking before compute, database before application layer. The dependency graph only needs to be complete for run --all to apply correctly. Partially declaring dependencies still helps Terragrunt order the units it knows about.
Explicit stacks via terragrunt.stack.hcl are the last step. Once your units are well-defined, a stack file makes the dependency graph visible and removes the implicit directory structure dependency. A lot of teams stop before this step. Backend consolidation and provider generation alone are enough to justify the migration for codebases with three or more environments.
Terragrunt vs. Terraform alone
Yevgeniy Brikman, co-founder of Gruntwork, compared three multi-environment approaches: Terraform workspaces, Git branches, and Terragrunt. The conclusion was that workspaces conflate execution isolation with configuration isolation, and Git branches create merge overhead. Terragrunt separates concerns cleanly.
That said, Terragrunt adds a layer to your toolchain. For teams with a single environment or a small number of Terraform modules, the overhead of learning and maintaining Terragrunt configuration may not be worth it. The break-even point is somewhere around three environments with shared modules. At that scale, the DRY benefits start to outweigh the setup cost.
The table below (from Brikman's analysis) captures the comparison:

Adopting Terragrunt means adding another binary to install and version-pin in your CI environment. Debugging failures requires reading error output from both Terragrunt and the underlying Terraform or OpenTofu run, which adds a layer of complexity. Teams migrating from older Terragrunt versions still face migration work since backwards compatibility wasn't guaranteed until 1.0.
Terragrunt in CI/CD
The most common pattern is running run --all -- plan on pull requests and run --all -- apply on merge to the default branch. Here's a minimal GitHub Actions workflow:
name: Terragrunt
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gruntwork-io/terragrunt-action@v2
with:
tf_version: '1.10.0'
tg_version: '1.0.1'
tg_command: 'run --all -- plan'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
apply:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gruntwork-io/terragrunt-action@v2
with:
tf_version: '1.10.0'
tg_version: '1.0.1'
tg_command: 'run --all -- apply --non-interactive'
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
For large stacks, use --filter-affected on PRs so plan only runs against units changed in the branch:
terragrunt run --all --filter-affected -- plan
Add --report-file report.json --report-format json to the apply step to get structured output showing which units succeeded or failed, useful for posting deployment summaries back to the PR.
One thing CI doesn't give you out of the box: enforcement of who can trigger apply in production, or visibility across all environments when multiple teams are deploying simultaneously. That's where a governance layer matters. The env zero Terragrunt integration handles run orchestration natively so you don't need to wire this yourself.
Common Terragrunt pitfalls
The most frequent issue on large stacks is state locking conflicts during parallel runs. When run --all -- apply fires across many units, multiple units race to acquire the same DynamoDB lock. Cap parallelism to avoid it:
terragrunt run --all --parallelism 4 -- apply
The default is unlimited, which is fine for small stacks but causes contention problems in CI environments where multiple pipelines may run simultaneously. Four is a reasonable starting point; tune based on your lock table throughput.
Slow init is another common frustration. Each unit downloads providers independently by default. The fix is setting TF_PLUGIN_CACHE_DIR to a shared directory before running Terragrunt. On OpenTofu 1.10+, Terragrunt's automatic provider caching extends this further, sharing the cache across units without any shell configuration:
export TF_PLUGIN_CACHE_DIR="$HOME/.terraform.d/plugin-cache"
mkdir -p $TF_PLUGIN_CACHE_DIR
terragrunt run --all -- init
Path resolution problems are less obvious. If find_in_parent_folders() resolves to the wrong directory, it usually means the working directory changed mid-execution. The other path trap: using string literals for environment names in remote_state config instead of path_relative_to_include(). The function generates a unique state key from the directory path. Hard-coded strings don't, and that's how dev accidentally overwrites prod's state.
If run --all applies fewer units than you expected, run terragrunt find before debugging anything else. It shows exactly which units Terragrunt discovered and which were filtered out. Much faster than adding log verbosity to a failing apply.
Memory problems surface at 50+ units. The cause is typically O(n²) locals evaluation complexity in pre-1.0 configurations where locals reference other locals across many files. Migrating to explicit terragrunt.stack.hcl files resolves it. If you're not ready for that migration, splitting the stack into smaller sub-stacks applied in sequence is a workable interim approach.
Best practices for Terragrunt at scale
Pin both your Terragrunt version and the Terraform/OpenTofu version explicitly. Use terraform_version_constraint in your root terragrunt.hcl and pin the exact Terragrunt binary version in your CI tooling. Version drift between team members is a reliable source of subtle bugs that are annoying to diagnose.
Use generate blocks for provider and backend configuration rather than committing standalone files. Terragrunt creates .tf files at runtime from generate blocks. When you check those generated files into version control, you create ambiguity about what's authoritative and what's generated. Keep the generator in terragrunt.hcl and gitignore the output.
Keep your Terraform modules independent of Terragrunt. They should work with a direct terraform apply without any Terragrunt wrapper. This makes them testable in isolation and avoids a common coupling mistake where module code assumes Terragrunt-specific path functions exist.
For CI plan runs, define mock_outputs for every dependency output your modules consume. When an upstream unit hasn't been applied yet, plan fails without them. Mocks let plan work cleanly against any environment state, which matters when you're adding new environments or running plan against branches that haven't deployed yet.
Directory structure is a permanent decision that's painful to change later. Organize by environment boundaries (environments/dev/networking, environments/dev/compute) rather than resource type (networking/dev, networking/prod). The environment-first layout makes run --all more predictable and limits blast radius to one environment at a time.
In large stacks, run --all -- apply on every change is expensive. Use --filter-affected to apply only units changed since the default branch, or pass a path glob like --filter './environments/dev/**' to scope a deployment to a specific environment. The filter system is one of the most underused features in Terragrunt. The --report-file and --report-format flags are similarly worth enabling in CI: they produce structured JSON or CSV you can parse to build deployment summaries and send notifications without parsing log lines.
Related reading: Terraform workspaces guide: commands, examples, and best practices. Understanding workspaces helps clarify when Terragrunt's directory-based approach is preferable.
Terragrunt with env zero
Terragrunt covers the configuration layer. The gap it doesn't close is governance, visibility, and auditability across the teams running these stacks.
The pattern we see in enterprises adopting Terragrunt at scale: a platform team writes the modules and the root terragrunt.hcl. Application teams manage their environment-specific files. This works until the question becomes: who ran run --all apply last Thursday at 11pm, which environments did it touch, were the OPA policies evaluated before it ran, and does the current live infrastructure match the last successful apply?
env zero supports Terragrunt natively. It's one of the few platforms that handles run --all orchestration rather than treating each Terragrunt unit as a separate CI job.
Drift detection runs continuously across your entire Terragrunt stack, not just individual units. If someone makes a manual change in AWS that diverges from your Terragrunt state, env zero surfaces it before it causes a problem. env zero also supports Pulumi natively alongside Terraform and Terragrunt — our Pulumi guide covers how governance, drift detection, and approval workflows apply uniformly across all three frameworks. OPA policy enforcement applies before any run --all -- apply executes, so governance rules apply consistently whether a deployment runs from a developer's laptop or a CI pipeline.
Every run is captured with full context: who triggered it, what changed, which policies were evaluated, and what the state looked like before and after. RBAC lets platform teams restrict which application teams can trigger deployments in which environments, without requiring them to manage IAM roles per team.
For teams running dozens of Terragrunt units across multiple accounts, env zero replaces the combination of custom CI scripts, manual state audits, and spreadsheet-tracked approvals with a single governed interface.
See how env zero handles Terragrunt
Related reading: Cloud governance and risk management with env zero. Policy enforcement and drift detection are the two capabilities Terragrunt alone doesn't provide.
Try it with env zero
Terragrunt solves the configuration repetition problem. env zero extends that foundation with the governance layer: drift detection across your full Terragrunt stack, OPA policy enforcement before every apply, and a complete audit trail of every deployment.
Start a free trial or book a demo.
References
- Terragrunt 1.0 release announcement. Gruntwork, March 2026
- Terragrunt official documentation. docs.terragrunt.com
- Terragrunt GitHub releases. Latest: v1.0.1, April 13, 2026
- gruntwork-io/terragrunt-action. Official GitHub Actions integration for Terragrunt
- How to manage multiple environments with Terraform. Yevgeniy Brikman, Gruntwork
- Terraform S3 backend configuration. HashiCorp documentation for remote state with S3 and DynamoDB
- env zero Terragrunt integration
FAQ
What is Terragrunt? Terragrunt is an open-source tool by Gruntwork that sits on top of Terraform or OpenTofu. It keeps IaC configurations DRY across environments by providing configuration inheritance, automatic remote state management, dependency ordering, and stack-wide deployment commands. The current stable version is v1.0.1 (April 2026).
What changed in Terragrunt 1.0?
Terragrunt 1.0 introduced formal unit and stack terminology, a revised CLI (run, exec, find, list), a unified --filter system replacing seven legacy targeting flags, structured run reports, automatic provider caching with OpenTofu 1.10+, and an official backwards compatibility commitment for the 1.x series. The docs also moved to docs.terragrunt.com.
Does Terragrunt work with OpenTofu?
Yes. Terragrunt supports both Terraform and OpenTofu as execution backends. Automatic provider caching introduced in 1.0 specifically targets OpenTofu 1.10+. You can switch between backends by changing the terraform binary reference in your environment.
When should I use Terragrunt vs. Terraform workspaces? Terraform workspaces share the same module code across environments but maintain separate state files. They work for simple cases but conflate execution and configuration isolation. Terragrunt's directory-based approach is preferable when environments need meaningfully different configurations, not just different variable values. The practical break-even is around three environments with shared modules.
Can I use Terragrunt without restructuring my existing Terraform code?
Yes, with some caveats. Terragrunt can wrap existing Terraform code without requiring you to refactor it into modules. You'll get the most value from Terragrunt's DRY features once your code is modular, but remote_state and dependency management work with flat Terraform code too. Start with remote state automation as a low-friction entry point.
Does Terragrunt support drift detection? No. Terragrunt itself does not. It orchestrates IaC execution but has no mechanism to compare live infrastructure state against your code continuously. Drift detection requires a governance layer on top: env zero runs drift detection across Terragrunt stacks continuously and surfaces deviations before they cause incidents.
How do I run Terragrunt in GitHub Actions?
Use the official gruntwork-io/terragrunt-action with pinned tf_version and tg_version fields. On pull requests, run run --all --filter-affected -- plan to limit execution to changed units. On merge, run run --all -- apply --non-interactive. Add --report-file to generate structured deployment summaries. See the CI/CD section above for a complete workflow example.
Why is my run --all slow?
The most common causes are provider downloads (each unit runs init independently by default) and high parallelism causing lock contention. Enable provider caching via TF_PLUGIN_CACHE_DIR, cap parallelism with --parallelism, and use --filter-affected on PRs to avoid running every unit on every change.
What's the difference between run --all and run-all?
They are functionally equivalent: run-all is the pre-1.0 command and run --all is the 1.0 CLI style. Terragrunt maintains backwards compatibility with the old syntax, so existing scripts don't break. New code should use the run --all form.

.webp)

![Using Open Policy Agent (OPA) with Terraform: Tutorial and Examples [2026]](https://cdn.prod.website-files.com/63eb9bf7fa9e2724829607c1/69d6a3bde2ffe415812d9782_post_th.png)