RSS

What is Terragrunt? How It Simplifies Terraform and Where It Hits Its Limits

Terragrunt Terraform Infrastructure as Code

Terragrunt isn't infrastructure as code so much as code about your infrastructure as code: a layer that exists to help Terraform's state model reason across its own directory boundaries. As a free tool, Terragrunt can be genuinely useful, as it simplifies IaC. However, it has limitations.

TL;DR
$ cat what-is-terragrunt.tldr
• What Terragrunt actually is and what it isn't.
• The specific Terraform limitations, hardcoded backends, missing cross-module dependencies, and config duplication that Terragrunt exists to patch.
• What Terragrunt is used for day-to-day: DRY configuration, dependency ordering, multi-environment layouts, and orchestrated pipeline runs.
• Best practices for scaling it safely, and where its model eventually hits the limits of its potential.

As it so often comes up in Terraform conversations, IaC engineers often have an opinion on Terragrunt before they've actually used it. Some of that reputation is earned, some of it comes secondhand from a comment thread rather than actual experience writing Terragrunt configuration in a terragrunt.hcl file.

In this guide, we cover what Terragrunt is, the specific Terraform limitations it was built to patch, the tasks it's actually used for day to day, and how it behaves once it's running inside a real CI/CD pipeline instead of a laptop.

We close with a set of best practices grounded in Terragrunt's own features, and a look at where its model runs out of room: understanding a tool well enough to adopt it means understanding when you'll outgrow it too.

What is Terragrunt?

Terragrunt is a thin wrapper, built by Gruntwork in open source, that runs around Terraform or OpenTofu as a CLI tool. It does not replace them. Every terragrunt command you type ultimately shells out to terraform or tofu underneath; you still write ordinary Terraform code in .tf modules, and Terragrunt adds a configuration layer, typically a terragrunt.hcl file that governs how those modules get invoked, in what order, and with what shared configuration.

So, Terragrunt vs. Terraform is not really a competition. The two aren’t mutually exclusive, because you can’t use Terragrunt without Terraform (or OpenTofu). Terraform is the engine that talks to cloud providers and provisions resources. Terragrunt sits a level above it, handling the DRY configuration inheritance, generated backend and provider files, and cross-module dependency ordering that Terraform's engine doesn't provide on its own. Think of it less as an alternative and more as the part of your toolchain that decides how Terraform gets run, not what it runs.

You can compare a Terraform workflow with Terragrunt to one without. Here is our full Terraform and Terragrunt comparison if you want the detailed side-by-side.

Why use Terragrunt with Terraform?

Terraform is excellent at describing a single root module's worth of infrastructure. It gets noticeably less effective when your infrastructure code spans more than one environment, account, or region. A few specific limitations in how Terraform configurations are structured cause most of that friction.

Firstly, the backend configuration block can't take variables. Terraform has to resolve the backend before anything else in your configuration loads, and while Terraform supports passing some backend values in at init time, in most setups every environment's bucket name and state key still end up hardcoded, copied by hand from one directory to the next, right down to the region:

# environments/dev/backend.tf
terraform {
backend "s3" {
bucket = "acme-dev-tfstate"
key = "payments/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
}
}

The prod version of that file is nearly identical, just acme-prod-tfstate swapped in for the bucket, which is exactly the kind of near-identical, error-prone repetition that invites a copy-paste mistake (a dev key pointed at the prod bucket, for example).

Provider blocks have a related issue: they can't use for_each or count, so dynamic, per-environment provider configuration has to be hand-maintained too.

A deeper limitation compounds both. Terraform builds a dependency graph beautifully within a single root module, but that graph stops at the module's own boundary. Split your infrastructure into separate state files for blast-radius reasons (a common and sensible decision at any real scale), and Terraform has no native concept that one module's outputs feed another's inputs, or that they need to run in a specific order.

You either wire that by hand with terraform_remote_state data sources that read another module’s remote state, and remember the sequence every time, or write a script that keeps track of it for you. Terragrunt steps in when a team gets tired of maintaining the script.

What Terragrunt is actually used for

Terragrunt is not a one-size-fits-all solution. Different engineers and teams usually use it to solve one of three problems, each addressing a limitation above.

Keeping configuration DRY across environments

A root terragrunt.hcl (or root.hcl in newer versions) defines the shared backend and provider generation once, along with the inputs every environment reuses. Every environment then includes that root configuration instead of duplicating it:

# root.hcl
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "acme-tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}
inputs = {
tags = {
ManagedBy = "terragrunt"
}
}

The path_relative_to_include() function automatically generates a unique, predictable state key per environment, which keeps state management consistent and removes the copy-paste issue the hardcoded backend block above creates. Each child terragrunt.hcl shrinks down to an include block, a module source, and a handful of environment-specific inputs that override the shared values.

Managing dependencies between modules

A dependency block is how you define that one unit of infrastructure needs another unit's outputs before it can run:

# environments/dev/cache/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
dependency "networking" {
config_path = "../networking"
mock_outputs = {
private_subnet_id = "subnet-00000000000000000"
}
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}
terraform {
source = "../../../modules//redis-cluster"
}
inputs = {
subnet_id = dependency.networking.outputs.private_subnet_id
node_type = "cache.r6g.large"
}

Terragrunt resolves the dependency by running terragrunt output against the networking unit first, then exposes the result to cache as dependency.networking.outputs.private_subnet_id.

It builds a directed acyclic graph from these references across every unit in a stack, which is what lets it resolve the ordering question that plain Terraform can't.

Managing multiple environments, regions, or accounts

Rather than relying on Terraform workspaces, which share one backend and one configuration, each environment is just a directory that includes the shared root configuration. Adding a new region or account is mostly a case of copying a folder and changing a handful of inputs.

The backend key and provider region get generated per directory rather than hand-edited across files, along with whatever account-specific values that environment needs overridden, like the role Terraform assumes to access each account. Teams often adopt Terragrunt to manage multiple environments in this way.

How Terragrunt simplifies IaC

The mechanism behind all three use cases above is the same: less duplicated code across HCL files for a human to keep in sync.

A change to tagging policy, a bumped provider version, or a new backend region gets made once in the root configuration and inherited everywhere, instead of having to be hunted down across every environment folder individually.

Generated files (backend.tf and provider.tf) replace hand-maintained ones, and the dependency graph built from your dependency blocks replaces a difficult-to-remember run order, or a runbook that drifts out of date the first time the team reorganizes its Terraform modules.

The way Terraform uses its providers to provision infrastructure remains the same, but a human has to write and remember less of the configuration around it, which helps prevent errors from creeping into large infrastructure-as-code estates.

Terragrunt in Terraform workflows and pipelines

You expose different risks when running Terragrunt on a laptop compared to running it inside a CI pipeline. terragrunt run --all plan (the modern form; older releases used run-all, now deprecated in Terragrunt's own docs in favor of run --all as part of its CLI redesign) walks every unit in the current stack, resolves the dependency graph, and runs the commands for each unit in order, in parallel where the graph allows it.

It's convenient for a quick multi-module plan on your own machine; in a pipeline, the same command pointed at the wrong directory can cause plans, and potentially applies, to fan out across dozens of modules and services nobody intended to touch.

State locking is an additional risk: if two pipeline runs target overlapping units at once, Terragrunt relies on each unit's own backend for native state locking to prevent a conflicting write, but a lock held by a stuck or crashed job can block every subsequent run until a user clears it by hand.

The practical takeaway for pipeline design is to scope run --all deliberately, targeting specific directories for production rather than defaulting to "everything," and to treat the directory a pipeline job points at as the actual blast-radius boundary.

Terragrunt best practices for scalable infrastructure automation

Turn these best practices into habits to scale Terragrunt cleanly. None of these require anything beyond Terragrunt’s built-in functionality.

Where Terragrunt's model runs out of room

If you are considering whether to adopt Terragrunt or not, it is a decision worth making for the right team, not a universal upgrade: another CLI to install, another HCL dialect layered on the one you already know, and debugging that gets more indirect once Terragrunt is generating working directories and injecting configuration behind the scenes.

The deeper structural point is that every one of these capabilities lives in Terragrunt's own process, external to the Terraform or OpenTofu engine itself. The dependency graph it builds from your directory structure and dependency blocks doesn't exist inside Terraform's state; Terragrunt reconstructs it fresh, from your filesystem layout, on every run.

This setup is also what causes the run --all blast-radius risk: the ordering guarantee is only as good as the external graph Terragrunt infers, not a property the engine enforces for you.

Running Terragrunt alongside Stategraph

Terragrunt is a good solution to a common problem. It gives teams DRY configuration and dependency ordering that Terraform's flat, single-state model doesn't provide natively, and for a lot of teams in a lot of environments, that's worth the extra CLI and the learning curve it demands.

Terragrunt's dependency graph and generated backend config are a well-built patch on an external layer, not a change to the underlying engine, and teams that keep scaling tend to eventually feel that ceiling: more directories to keep consistent and more indirection when something breaks, plus a run --all blast radius that only gets harder to reason about as the stack grows.

Stategraph Orchestration is built to run alongside what you already have rather than force a rewrite. Its free tier explicitly supports Terraform, OpenTofu, Terragrunt, Pulumi, and CDKTF side by side, so a team can add pull-request plans and policy checks, plus drift detection, on top of an existing Terragrunt setup without touching a single terragrunt.hcl file.

From there, you can move away from Terragrunt entirely on your own schedule: Stategraph's Infrastructure as a Database stores your state as a dependency graph in PostgreSQL rather than a flat file per root module, so the ordering and cross-module wiring that Terragrunt's dependency blocks handle externally become native engine behavior instead.

stategraph import tf creates that state from an existing Terraform state file and its HCL in one step, so onboarding an already-running Terragrunt unit doesn't mean rebuilding it from scratch.

It's a migration, not a flag to flip, but it's one you can start today, for free, without giving up anything you've already built on Terragrunt.

Conclusion

Terragrunt is a good answer to backend configuration that can't take variables and provider blocks that can't loop, on top of a dependency graph that stops at a single root module's edge.

Knowing what it is and what it's actually used for should make the decision to adopt it, or not, a lot less reliant on secondhand opinion.

If you're already running it and want to see what pairing it with a pull-request workflow and real drift detection looks like, try Stategraph free and run it alongside what you've already built.

What is Terragrunt FAQs

Is Terragrunt free to use?

Yes. Terragrunt is open source under the MIT license, maintained by Gruntwork, with no cost to download, install, or run against your own Terraform or OpenTofu configurations. Gruntwork offers paid commercial support and additional tooling around it, but the core CLI itself carries no license fee.

Does Terragrunt work with OpenTofu?

Yes. Terragrunt shells out to whichever binary you configure, Terraform or OpenTofu, so switching the underlying engine is a configuration change rather than a complete rewrite of your terragrunt.hcl files. Most of Terragrunt's current documentation and examples already reference both interchangeably.

Do you need Terragrunt from day one on a new project?

Usually not. A single environment or an early-stage project with a handful of modules hasn't yet hit the duplication or dependency-ordering problems Terragrunt can solve, so adding it before you feel that pain just means you're maintaining a second tool with an opinionated directory structure for no immediate return.

Most teams consider it once they notice they're maintaining three or more near-identical environment folders – dev, staging, and prod – by hand.