Terragrunt explained: what it adds on top of OpenTofu and Terraform, how units, includes, dependencies and stacks work, what changed in 1.0 and when it is worth adopting.
Terragrunt is an open source orchestration tool for OpenTofu and Terraform. You keep writing infrastructure as HCL modules, and Terragrunt handles what plain root modules make repetitive: the same backend and provider blocks in every directory, outputs passed from one root to another, and running plan and apply across dozens of them in the right order.
It shows up once an infrastructure repository has several accounts, regions or environments, each with its own state. This guide explains how Terragrunt works and what changed with 1.0. For a side-by-side with plain Terraform, see Terragrunt vs Terraform, and for other ways to solve the same problem, see Terragrunt alternatives.
The Terragrunt terminology guide describes it as an orchestrator for OpenTofu and Terraform rather than a tool that provisions infrastructure itself. Each unit points at a module, Terragrunt generates the files that module needs, such as backend.tf and provider.tf, sets the inputs and then runs OpenTofu or Terraform in that directory. If both binaries are installed, it defaults to OpenTofu.
Three problems drive most adoption:
dependency block reads another unit's outputs and also tells Terragrunt to apply that unit first.terragrunt run --all plan walks every unit below the current directory and runs them in dependency order, in parallel where the graph allows.Splitting infrastructure into small units also keeps the blast radius of each change small, which is a large part of why teams with big estates adopt it.
A unit is a directory with a terragrunt.hcl file. This one deploys a Postgres module into a staging account and reads the network from a sibling unit:
# live/staging/database/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "git::git@github.com:acme/infra-modules.git//postgres?ref=v2.3.0"
}
dependency "network" {
config_path = "../network"
mock_outputs = {
vpc_id = "vpc-mock"
private_subnet_ids = ["subnet-mock"]
}
}
inputs = {
name = "orders"
instance_class = "db.t4g.medium"
vpc_id = dependency.network.outputs.vpc_id
private_subnet_ids = dependency.network.outputs.private_subnet_ids
}
The include pulls in root.hcl, which typically holds a remote_state block and a generate block for providers. mock_outputs lets plan run before the network unit has been applied. Production gets a copy of this directory under live/production/database with different inputs, and that copy per environment is the structure Terragrunt is built around. Terragrunt now recommends root.hcl as the name of the shared file, replacing the older pattern of a root terragrunt.hcl.
A stack is a set of units managed together. The original form is implicit: any directory of units is a stack, and terragrunt run --all apply inside live/staging applies everything below it. That works, but every environment still has its own copy of every unit directory.
Explicit stacks replace those copies with a blueprint. A terragrunt.stack.hcl file lists unit blocks, each with a source, a path and values:
# live/staging/terragrunt.stack.hcl
unit "network" {
source = "git::git@github.com:acme/infra-catalog.git//units/network?ref=v1.4.0"
path = "network"
values = {
cidr = "10.20.0.0/16"
}
}
unit "database" {
source = "git::git@github.com:acme/infra-catalog.git//units/postgres?ref=v1.4.0"
path = "database"
values = {
name = "orders"
instance_class = "db.t4g.medium"
network_path = "../network"
}
}
terragrunt stack generate writes the units into a .terragrunt-stack directory, which you add to .gitignore, and terragrunt stack run apply generates and applies them in one step. stack blocks nest whole patterns, so dev and prod can each be one block pointing at an environment stack in a versioned catalog, and terragrunt stack output returns the outputs of every unit as one object.
Terragrunt 1.0 was released on March 30, 2026, after nearly a decade of 0.x releases. The headline is a backwards compatibility guarantee: CLI flags, HCL configuration and serialized output such as find results and the run report are stable across the 1.x series.
The CLI redesign that led up to it is what changes day-to-day use. run-all became terragrunt run --all, arbitrary commands go through terragrunt run -- <command>, find and list show which components Terragrunt discovers, and --filter expressions narrow the run queue to paths or names before anything is applied. Older blog posts and CI scripts still use the 0.x forms.
Terragrunt pays off when the same modules are deployed into many accounts, regions or environments, each with separate state, and a platform team owns that estate. Shared root configuration, dependency ordering and stack blueprints remove a lot of copy-and-paste at that scale.
It is a weaker fit for one or two environments, where plain OpenTofu or Terraform with modules is easier to follow, and for application teams whose infrastructure is mostly the database, queue and bucket their own service needs. In that case each new resource means a module change, a unit per environment and a dependency on the network unit, all maintained in a separate repository from the code that uses them.
Encore combines an open source infrastructure SDK with a platform that provisions and deploys into your own AWS or GCP account. It is an alternative to writing Terraform units for application infrastructure: the service declares the database, bucket and secrets it uses, and every environment provisions its own copies from those declarations, an approach often called infrastructure from code.

The orders database and an invoice bucket from the Terragrunt example look like this in an Encore service:
Run in your terminal to get started locally.
// orders/orders.ts
import { api, APIError } from "encore.dev/api";
import { secret } from "encore.dev/config";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { Bucket } from "encore.dev/storage/objects";
const db = new SQLDatabase("orders", { migrations: "./migrations" });
export const invoices = new Bucket("invoices", { versioned: true });
// Set per environment type with `encore secret set --type prod,dev,local TaxAPIKey`.
const taxApiKey = secret("TaxAPIKey");
interface InvoiceParams {
orderId: number;
}
export const createInvoice = api(
{ expose: false, method: "POST", path: "/orders/:orderId/invoice" },
async ({ orderId }: InvoiceParams): Promise<{ key: string }> => {
const order = await db.queryRow<{ total_cents: number }>`
SELECT total_cents FROM orders WHERE id = ${orderId}
`;
if (!order) {
throw APIError.notFound("order not found");
}
const res = await fetch("https://tax.example.com/v1/quote", {
method: "POST",
headers: { Authorization: `Bearer ${taxApiKey()}` },
body: JSON.stringify({ amount: order.total_cents }),
});
const quote = await res.text();
const key = `invoices/${orderId}.json`;
await invoices.upload(key, Buffer.from(quote), { contentType: "application/json" });
return { key };
},
);
encore run starts Postgres locally, stores bucket objects on the local filesystem and resolves the secret from your local values, so the whole flow runs before any cloud account is involved. In each environment, the database becomes RDS or Cloud SQL, the bucket becomes S3 or Cloud Storage, and the secret is stored in AWS Secrets Manager or GCP Secret Manager in your account. Encore provisions an IAM role for the service on AWS or a service account on GCP.
Instance class, scaling and networking, the values that Terragrunt units pass as inputs, are configured per environment in Encore rather than in a directory per environment, and production can require an Admin to approve infrastructure changes before a deploy proceeds. If you are moving existing Terraform-managed resources, the migration guide from Terraform covers the steps.
For a coding agent, adding a bucket is an edit to the service it is already changing, tested under encore run, instead of a module change and a new unit in a second repository.
Best fit: application teams that want each environment's databases, buckets and secrets created in their own AWS or GCP account from the service code, with sizing set per environment.
Application teams that want databases, buckets and secrets created from the service code instead of another Terragrunt unit can start with Encore and set sizing per environment in their own AWS or GCP account.
Terragrunt orchestrates OpenTofu and Terraform across many units of infrastructure. It shares backend and provider configuration from one root file, passes outputs between units with dependency blocks, and runs plan or apply across a whole stack in dependency order with terragrunt run --all.
No. Terraform and OpenTofu define and create resources through providers and keep state. Terragrunt is a wrapper that prepares each unit's configuration, then invokes OpenTofu or Terraform to plan and apply it. Resources are still written in HCL modules.
A stack is a collection of units managed together. An implicit stack is a directory tree of units with terragrunt.hcl files. An explicit stack is a terragrunt.stack.hcl file whose unit and stack blocks generate those units into a .terragrunt-stack directory.
Declare the database with new SQLDatabase, the bucket with new Bucket and any credentials with secret() in your TypeScript service. Each Encore environment provisions its own RDS or Cloud SQL database and S3 or Cloud Storage bucket from those declarations, so there is no unit per environment to maintain.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.