A stack in Terragrunt is a collection of related units that can be managed together. Stacks provide a way to:
Terragrunt supports two approaches to defining stacks:
terragrunt.stack.hcl
blueprint filesThe simplest way to create a stack is to organize your units in a directory structure. When you have multiple units in a directory, Terragrunt automatically treats that directory as a stack.
Letβs say your infrastructure is defined across multiple OpenTofu/Terraform root modules:
root
βββ backend-app
β βββ main.tf
βββ frontend-app
β βββ main.tf
βββ mysql
β βββ main.tf
βββ valkey
β βββ main.tf
βββ vpc
βββ main.tf
To convert these to Terragrunt units, simply add a terragrunt.hcl
file to each directory:
root
βββ backend-app
β βββ main.tf
β βββ terragrunt.hcl
βββ frontend-app
β βββ main.tf
β βββ terragrunt.hcl
βββ mysql
β βββ main.tf
β βββ terragrunt.hcl
βββ valkey
β βββ main.tf
β βββ terragrunt.hcl
βββ vpc
βββ main.tf
βββ terragrunt.hcl
Now you have an implicit stack! The root
directory contains all your units and can be managed as a single stack.
Use the --all
flag to run an OpenTofu/Terraform command on all units in the implicit stack in the current working directory:
# Deploy all units discovered in the current working directory
terragrunt run --all apply
# Plan changes across all units discovered in the current working directory
terragrunt run --all plan
# Destroy all units discovered in the current working directory
terragrunt run --all destroy
# View outputs from all units discovered in the current working directory
terragrunt run --all output
You can also use the --graph
flag to run an OpenTofu/Terraform command on all units in the DAG of the unit in the current working directory.
# Run an OpenTofu/Terraform command on all units in the DAG of the unit in the current working directory
terragrunt run --graph apply
For a more modern approach, you can define stacks using terragrunt.stack.hcl
files. These are blueprints that programmatically generate units.
A terragrunt.stack.hcl
file is a blueprint that defines how to generate Terragrunt configuration programmatically. It tells Terragrunt:
unit
blocks - Define Individual Infrastructure Componentsterragrunt.hcl
file in the specified pathstack
blocks - Define Reusable Infrastructure Patternsterragrunt.stack.hcl
file that can contain more units or stacksAspect | unit block |
stack block |
---|---|---|
Purpose | Define a single infrastructure component | Define a reusable collection of components |
When to use | For specific, one-off infrastructure pieces | For patterns of infrastructure pieces that you want provisioned together |
Generated output | A directory with a single terragrunt.hcl file |
A directory with a terragrunt.stack.hcl file |
# terragrunt.stack.hcl
unit "vpc" {
source = "git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1"
path = "vpc"
values = {
vpc_name = "main"
cidr = "10.0.0.0/16"
}
}
unit "database" {
source = "git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1"
path = "database"
values = {
engine = "postgres"
version = "13"
vpc_path = "../vpc"
}
}
Running terragrunt stack generate
creates:
terragrunt.stack.hcl
.terragrunt-stack/
βββ vpc/
β βββ terragrunt.hcl
β βββ terragrunt.values.hcl
βββ database/
βββ terragrunt.hcl
βββ terragrunt.values.hcl
# terragrunt.stack.hcl
stack "dev" {
source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1"
path = "dev"
values = {
environment = "development"
cidr = "10.0.0.0/16"
}
}
stack "prod" {
source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1"
path = "prod"
values = {
environment = "production"
cidr = "10.1.0.0/16"
}
}
The referenced stack might contain:
# stacks/environment/terragrunt.stack.hcl
unit "vpc" {
source = "git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1"
path = "vpc"
values = {
vpc_name = values.environment
cidr = values.cidr
}
}
unit "database" {
source = "git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1"
path = "database"
values = {
environment = values.environment
vpc_path = "../vpc"
}
}
# Generate units from the `terragrunt.stack.hcl` file in the current working directory
terragrunt stack generate
# Deploy all generated units defined using the `terragrunt.stack.hcl` file in the current working directory (and any units generated by stacks in this file)
terragrunt stack run apply
# Or combine generation and deployment
terragrunt stack run apply # Automatically generates first
terragrunt.hcl
filesterragrunt run --all apply
to deploy all unitsterragrunt.hcl
files, terragrunt.stack.hcl
files, etc.) in a git repositoryterragrunt.stack.hcl
file with unit
and/or stack
blocksterragrunt stack generate
to create the actual units*terragrunt stack run apply
to deploy all units*** Multiple commands (like stack run
or run --all
) automatically generate units from terragrunt.stack.hcl
files for you.
** You can also just use run --all apply
to deploy all units in the stack like you can with implicit stacks.
Note: These are simplified examples that show common high-level patterns.
For more detailed examples, see the Gruntwork Terragrunt Infrastructure Catalog Stack Examples.
Create reusable environment patterns that can be instantiated for dev, staging, and production:
# terragrunt.stack.hcl
stack "dev" {
source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1"
path = "dev"
values = {
environment = "development"
cidr = "10.0.0.0/16"
}
}
stack "staging" {
source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1"
path = "staging"
values = {
environment = "staging"
cidr = "10.1.0.0/16"
}
}
stack "prod" {
source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1"
path = "prod"
values = {
environment = "production"
cidr = "10.2.0.0/16"
}
}
Define complete application patterns with all required components:
# terragrunt.stack.hcl
stack "web-app" {
source = "git::git@github.com:acme/infrastructure-catalog.git//stacks/web-application?ref=v0.0.1"
path = "web-app"
values = {
app_name = "my-web-app"
domain = "example.com"
environment = "production"
}
}
Consider the following file structure:
root
βββ backend-app
β βββ terragrunt.hcl
βββ mysql
β βββ terragrunt.hcl
βββ valkey
β βββ terragrunt.hcl
βββ vpc
βββ terragrunt.hcl
Suppose that you wanted to pass in the VPC ID of the VPC that is created from the vpc
unit in the folder structure above to the mysql
unit as an input variable. Or that you wanted to pass in the subnet IDs of the private subnet that is allocated as part of the vpc
unit.
You can use the dependency
block to extract those outputs and use them as inputs
to the mysql
unit.
For example, suppose the vpc
unit outputs the ID under the output named vpc_id
. To access that output, you would specify in mysql/terragrunt.hcl
:
# mysql/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
When you apply this unit, the output will be read from the vpc
unit and passed in as an input to the mysql
unit right before calling tofu apply
/terraform apply
.
You can also specify multiple dependency
blocks to access the outputs of multiple units.
For example, in the above folder structure, you might want to reference the domain
output of the valkey
and mysql
units for use as inputs
in the backend-app
unit. To access those outputs, you would specify the following in backend-app/terragrunt.hcl
:
# backend-app/terragrunt.hcl
dependency "mysql" {
config_path = "../mysql"
}
dependency "valkey" {
config_path = "../valkey"
}
inputs = {
mysql_url = dependency.mysql.outputs.domain
valkey_url = dependency.valkey.outputs.domain
}
Note that each dependency
block results in a relevant status in the Terragrunt DAG. This means that when you run run --all apply
on a config that has dependency
blocks, Terragrunt will not attempt to deploy the config until all the units referenced in dependency
blocks have been applied. So for the above example, the order for the run --all apply
command would be:
Deploy the VPC
Deploy MySQL and valkey in parallel
Deploy the backend-app
If any of the units failed to deploy, then Terragrunt will not attempt to deploy the units that depend on them.
Note: Not all blocks can access outputs passed by dependency
blocks. See the section on Configuration parsing order for more information.
Terragrunt will return an error if the unit referenced in a dependency
block has not been applied yet. This is because you cannot actually fetch outputs out of an unapplied unit, even if there are no resources being created in the unit.
This is most problematic when running commands that do not modify state (e.g run --all plan
and run --all validate
) on a completely new setup where no infrastructure has been deployed. You wonβt be able to plan
or validate
a unit if you canβt determine the inputs
. If the unit depends on the outputs of another unit that hasnβt been applied yet, you wonβt be able to compute the inputs
unless the dependencies are all applied.
Of course, in real life usage, you typically need the ability to run run --all validate
or run --all plan
on a completely new set of infrastructure.
To address this, you can provide mock outputs to use when a unit hasnβt been applied yet. This is configured using the mock_outputs
attribute on the dependency
block and it corresponds to a map that will be injected in place of the actual dependency outputs if the target config hasnβt been applied yet.
Using a mock output is typically the best solution here, as you typically donβt actually care that an accurate value is used for a given value at this stage, just that it will plan successfully. When you actually apply the unit, thatβs when you want to be sure that a real value is used.
For example, in the previous scenario with a mysql
unit and vpc
unit, suppose you wanted to mock a value for the vpc_id
during a run --all validate
for the mysql
unit.
You can specify that in mysql/terragrunt.hcl
:
# mysql/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "mock-vpc-id"
}
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
You can now run validate
on this config before the vpc
unit is applied because Terragrunt will use the map {vpc_id = "mock-vpc-id"}
as the outputs
attribute on the dependency instead of erroring out.
What if you wanted to restrict this behavior to only the validate
command? For example, you might not want to use the defaults for a plan
operation because the plan doesnβt give you any indication of what is actually going to be created.
You can use the mock_outputs_allowed_terraform_commands
attribute to indicate that the mock_outputs
should only be used when running those OpenTofu/Terraform commands. So to restrict the mock_outputs
to only when validate
is being run, you can modify the above terragrunt.hcl
file to:
# mysql/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "temporary-dummy-id"
}
mock_outputs_allowed_terraform_commands = ["validate"]
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
}
Note that indicating validate
means that the mock_outputs
will be used either with validate
or with run --all validate
.
You can also use skip_outputs
on the dependency
block to specify the dependency without pulling in the outputs:
# mysql/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
skip_outputs = true
}
When skip_outputs
is used with mock_outputs
, mocked outputs will be returned without attempting to load outputs from OpenTofu/Terraform.
This can be useful when you disable backend initialization (remote_state.disable_init
) in CI for example.
# mysql/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "temporary-dummy-id"
}
skip_outputs = true
}
You can also use mock_outputs_merge_strategy_with_state
on the dependency
block to merge mocked outputs and real outputs:
# mysql/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "temporary-dummy-id"
new_output = "temporary-dummy-value"
}
mock_outputs_merge_strategy_with_state = "shallow"
}
If real outputs only contain vpc_id
, dependency.outputs
will contain a real value for vpc_id
and a mocked value for new_output
.
When defining a stack using a terragrunt.stack.hcl
file, you also have the ability to interact with the aggregated outputs of all the units in the stack.
To do this, use the stack output
command (not the stack run output
command).
$ terragrunt stack output
backend_app = {
domain = "backend-app.example.com"
}
frontend_app = {
domain = "frontend-app.example.com"
}
mysql = {
endpoint = "terraform-20250504140737772400000001.abcdefghijkl.us-east-1.rds.amazonaws.com"
}
valkey = {
endpoint = "serverless-valkey-01.amazonaws.com"
}
vpc = {
vpc_id = "vpc-1234567890"
}
This will return a single aggregated value for all the outputs of all the units in the stack.
You can also specify dependencies without accessing any of the outputs of units. Consider the following file structure:
root
βββ backend-app
β βββ terragrunt.hcl
βββ frontend-app
β βββ terragrunt.hcl
βββ mysql
β βββ terragrunt.hcl
βββ valkey
β βββ terragrunt.hcl
βββ vpc
βββ terragrunt.hcl
Letβs assume you have the following dependencies between OpenTofu/Terraform units:
backend-app
depends on mysql
, valkey
, and vpc
frontend-app
depends on backend-app
and vpc
mysql
depends on vpc
valkey
depends on vpc
vpc
has no dependencies
You can express these dependencies in your terragrunt.hcl
config files using a dependencies
block. For example, in backend-app/terragrunt.hcl
you would specify:
# backend-app/terragrunt.hcl
dependencies {
paths = ["../vpc", "../mysql", "../valkey"]
}
Similarly, in frontend-app/terragrunt.hcl
, you would specify:
# frontend-app/terragrunt.hcl
dependencies {
paths = ["../vpc", "../backend-app"]
}
Once youβve specified these dependencies in each terragrunt.hcl
file, Terragrunt will be able to perform updates respecting the DAG of dependencies.
For the example at the start of this section, the order of runs for the run --all apply
command would be:
Deploy the VPC
Deploy MySQL and valkey in parallel
Deploy the backend-app
Deploy the frontend-app
Any error encountered in an individual unit during a run --all
command will prevent Terragrunt from proceeding with the deployment of any dependent units.
To check all of your dependencies and validate the code in them, you can use the run --all validate
command.
Note: During
destroy
runs, Terragrunt will try to find all dependent units and show a confirmation prompt with a list of detected dependencies.This is because Terragrunt knows that once resources in a dependency are destroyed, any commands run on dependent units may fail.
For example, if
destroy
was called on theValkey
unit, youβd be asked for confirmation, as thebackend-app
depends onValkey
. You can suppress the prompt by using the--non-interactive
flag.
To visualize the dependency graph you can use the dag graph
command (similar to the terraform graph
command).
The graph is output in DOT format. The typical program used to render this file format is GraphViz, but many web services are available that can do this as well.
terragrunt dag graph | dot -Tsvg > graph.svg
The example above generates the following graph:
Note that this graph shows the dependency relationship in the direction of the arrow, with the tip pointing to the dependency (e.g. frontend-app
depends on backend-app
).
For most commands, Terragrunt will run in the opposite direction, however (e.g. backend-app
would be applied before frontend-app
).
The exception to this rule is during the destroy
(and plan -destroy
) command, where Terragrunt will run in the direction of the arrow (e.g. frontend-app
would be destroyed before backend-app
).
If you are using Terragrunt to download remote OpenTofu/Terraform modules and all of your units have the source
parameter set to a Git URL, but you want to test with a local checkout of the code, you can use the --source
parameter to override that value:
terragrunt run --all plan --source /source/modules
If you set the --source
parameter, the run --all
command will assume that parameter is pointing to a folder on your local file system that has a local checkout of all of your OpenTofu/Terraform modules.
For each unit that is being processed via a run --all
command, Terragrunt will:
source
parameter in that unitβs terragrunt.hcl
file.--source
parameter to create the final local path for that unit.For example, consider the following terragrunt.hcl
file:
# terragrunt.hcl
terraform {
source = "git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1"
}
Running the following:
terragrunt run --all apply --source /source/infrastructure-modules
Will result in a unit with the configuration for the source above being resolved to /source/infrastructure-modules//networking/vpc
.
By default, Terragrunt will not impose a limit on the number of units it executes when it traverses the dependency graph, meaning that if it finds 5 units without dependencies, itβll run OpenTofu/Terraform 5 times in parallel, once in each unit.
Sometimes, this can create a problem if there are a lot of units in the dependency graph, like hitting a rate limit on a cloud provider.
To limit the maximum number of unit executions at any given time use the --parallelism [number]
flag
terragrunt run --all apply --parallelism 4
A powerful feature of OpenTofu/Terraform is the ability to save the result of a plan as a binary or JSON file using the -out flag.
Terragrunt provides special tooling in run --all
execution to ensure that the saved plan for a run --all
against a stack has
a corresponding entry for each unit in the stack in a directory structure that mirrors the stack structure.
To save plan against a stack, use the --out-dir
flag (or TG_OUT_DIR
environment variable) as demonstrated below:
terragrunt run --all plan --out-dir /tmp/tfplan
app1
βββ tfplan.tfplan
app2
βββ tfplan.tfplan
app3
βββ tfplan.tfplan
project-2
βββ project-2-app1
βββ tfplan.tfplan
terragrunt run --all --out-dir /tmp/tfplan apply
For planning a destroy operation, use the following commands:
terragrunt run --all --out-dir /tmp/tfplan plan -destroy
terragrunt run --all --out-dir /tmp/tfplan apply
To save plan in json format use --json-out-dir
flag (or TG_JSON_OUT_DIR
environment variable):
terragrunt run --all --json-out-dir /tmp/json plan
app1
βββ tfplan.json
app2
βββ tfplan.json
app3
βββ tfplan.json
project-2
βββ project-2-app1
βββ tfplan.json
terragrunt run --all --out-dir /tmp/all --json-out-dir /tmp/all plan
app1
βββ tfplan.json
βββ tfplan.tfplan
app2
βββ tfplan.json
βββ tfplan.tfplan
app3
βββ tfplan.json
βββ tfplan.tfplan
project-2
βββ project-2-app1
βββ tfplan.json
βββ tfplan.tfplan
To recap:
tfplan.tfplan
and tfplan.json
for plan JSON.terragrunt run --all apply
command, only binary plan files can be used.Note that you can also have nested stacks.
For example, consider the following file structure:
root
βββ us-east-1
β βββ app
β β βββ terragrunt.hcl
β βββ db
β βββ terragrunt.hcl
βββ us-west-2
βββ app
β βββ terragrunt.hcl
βββ db
βββ terragrunt.hcl
In this example, thereβs the root
stack, that contains all the infrastructure youβve defined so far,
and thereβs also the us-east-1
and us-west-2
stacks, that contain the infrastructure for the app
and db
units in those regions.
You can run run --all
commands at any depth of the stack to run the units in that stack and all of its children.
For example, to run all the units in the us-east-1
stack, you can run:
cd root/us-east-1
terragrunt run --all apply
Terragrunt will only include the units in the us-east-1
stack and its children in the queue of units to run (unless external dependencies are pulled in, as discussed in the run βall command).
Generally speaking, this is the primary tool Terragrunt users use to control the blast radius of their changes. For the most part, it is the current working directory that determines the blast radius of a run --all
command.
In addition to using your working directory to control whatβs included in a run queue, you can also use flags like βinclude-dir and βexclude-dir to explicitly control whatβs included in a run queue within a stack, or outside of it.
There are more flags that control the behavior of the run
command, which you can find in the run
docs.
When using Terragrunt Stacks, you might want to use local state files instead of remote state for development, testing, or specific use cases. However, this presents a challenge because the generated .terragrunt-stack
directory can be safely deleted and regenerated using terragrunt stack clean && terragrunt stack generate
, which would normally cause local state files to be lost.
To solve this problem, you can configure your stack to store state files outside of the .terragrunt-stack
directory, in a persistent location that survives stack regeneration.
Hereβs how to configure local state that persists across stack regeneration:
1. Create a root.hcl
file with local backend configuration:
# root.hcl
remote_state {
backend = "local"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
path = "${get_parent_terragrunt_dir()}/.terragrunt-local-state/${path_relative_to_include()}/tofu.tfstate"
}
}
2. Create your stack definition:
# live/terragrunt.stack.hcl
unit "vpc" {
source = "${find_in_parent_folders("units/vpc")}"
path = "vpc"
}
unit "database" {
source = "${find_in_parent_folders("units/database")}"
path = "database"
}
unit "app" {
source = "${find_in_parent_folders("units/app")}"
path = "app"
}
3. Configure your units to include the root configuration:
# units/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "."
}
4. Add a .gitignore
file to exclude state files from version control:
# .gitignore
.terragrunt-local-state
Important: Local state files should never be committed to version control as they may contain sensitive information and can cause conflicts when multiple developers work on the same infrastructure.
The key insight is using path_relative_to_include()
in the state path configuration. This function returns the relative path from each unit to the root.hcl
file, creating unique state file paths like:
.terragrunt-local-state/live/.terragrunt-stack/vpc/tofu.tfstate
.terragrunt-local-state/live/.terragrunt-stack/database/tofu.tfstate
.terragrunt-local-state/live/.terragrunt-stack/app/tofu.tfstate
Since these state files are stored in .terragrunt-local-state/
(outside of .terragrunt-stack/
), they persist when you run:
terragrunt stack clean && terragrunt stack generate
After running the stack, your directory structure will look like this:
.
βββ root.hcl
βββ .gitignore (Excludes .terragrunt-local-state)
βββ .terragrunt-local-state/ (Persistent state files - ignored by git)
β βββ live/
β βββ .terragrunt-stack/
β βββ vpc/
β β βββ tofu.tfstate
β βββ database/
β β βββ tofu.tfstate
β βββ app/
β βββ tofu.tfstate
βββ live/
β βββ terragrunt.stack.hcl
β βββ .terragrunt-stack/ (Generated stack - can be deleted)
β βββ vpc/
β β βββ terragrunt.hcl
β β βββ main.tf
β βββ database/
β β βββ terragrunt.hcl
β β βββ main.tf
β βββ app/
β βββ terragrunt.hcl
β βββ main.tf
βββ units/ (Reusable unit definitions)
βββ vpc/
βββ database/
βββ app/
This approach provides several advantages:
# Initial setup
terragrunt stack generate
terragrunt stack run apply
# Later, regenerate the stack without losing state
terragrunt stack clean
terragrunt stack generate
# Verify existing resources are recognized
terragrunt stack run plan # Should show "No changes"
This pattern is particularly useful for development environments, testing scenarios, or when you need to maintain local state for compliance or security reasons while still benefiting from Terragruntβs stack management capabilities.
Now that you understand both implicit and explicit stacks, you can:
unit
and stack
blocksPro Tip: Start with implicit stacks to get familiar with the concept, then gradually introduce explicit stacks for reusable patterns as your infrastructure grows.
If youβd like more advanced examples on stacks, check out the terragrunt-infrastructure-catalog-example repository. These have full-featured examples of stacks that deploy real, stateful infrastructure in an AWS account.