// Stay in touch?
Products
Encore CloudEncore Cloud
Encore.tsEncore.ts
Encore.goEncore.go
PricingPricing
Book a DemoBook a Demo
Use Cases
AI-Powered DevelopmentAI-Powered Development
Event-Driven SystemsEvent-Driven Systems
Distributed SystemsDistributed Systems
Case StudiesCase Studies
ShowcaseShowcase
Resources
DocsDocs
InstallInstall
Example AppsExample Apps
Demo videoDemo video
ArticlesArticles
GitHub ReleasesGitHub Releases
Systems Operational
Company
About UsAbout Us
Swag ShopSwag Shop
ContactContact
JobsJobs
PressPress
TermsTerms
Privacy PolicyPrivacy Policy
Data Processing AgreementData Processing Agreement
Enterprise SLAEnterprise SLA
Encore
© 2026 EncoreAll rights reserved
© 2026 Encore All Rights Reserved
GitHubDiscordYouTube

Terragrunt vs Terraform: When You Need the Wrapper

Terragrunt is Terraform plus scaffolding. Here's when the scaffolding earns its keep.

07/08/26
9 Min Read
Ivan Cernja
07/08/26

Terragrunt vs Terraform: When You Need the Wrapper

Terragrunt is Terraform plus scaffolding. Here's when the scaffolding earns its keep.

Ivan Cernja
9 Min Read

If you are asking whether to add Terragrunt, you have probably already written the same Terraform three times. Once for staging, once for production, and once for the EU region you spun up last quarter. The backend blocks are near-identical, the module calls differ by a handful of inputs, and you have started copy-pasting main.tf between directories and praying the diff stays small.

That copy-paste problem is exactly what Terragrunt exists to solve. Terragrunt is not a competing IaC engine. It is a wrapper that calls Terraform (or OpenTofu) for you and removes the repetition that plain HCL forces on multi-environment setups. So the real question has less to do with "Terragrunt or Terraform" and more to do with whether your infrastructure has grown big enough that hand-written HCL has started to hurt.

This article shows the exact repetition Terragrunt removes, the config that replaces it, and where the line sits.

What Terragrunt Is

Terragrunt is a binary that shells out to terraform. When you run terragrunt apply, it reads a terragrunt.hcl file, generates the backend and provider configuration into a temporary directory, injects inputs, and then runs terraform apply in that directory. Everything Terraform does, Terraform still does. Terragrunt sits one layer above.

It was built by Gruntwork to patch four specific gaps in raw Terraform:

  • Backend boilerplate. Every Terraform root module needs a backend block, and that block cannot use variables. So the bucket, key, and region get hardcoded per directory.
  • Input repetition. Common values (account ID, region, tags, VPC ID) get restated in every environment's terraform.tfvars.
  • Multi-module orchestration. Running plan across ten interdependent modules in the right order means either a shell script or a lot of manual cd.
  • Dependency wiring. Passing one module's output into another across separate state files means terraform_remote_state data sources everywhere.

None of these are bugs in Terraform. They are the natural friction of a tool designed around a single root module with a static backend. Terragrunt is the community's answer to running many root modules at scale.

Terragrunt vs Terraform: Feature Comparison

TerraformTerragrunt
What it isIaC engine (plan/apply against providers)Wrapper that invokes the Terraform/OpenTofu binary
Provisions resourcesYesNo, delegates to Terraform
Config languageHCLHCL (terragrunt.hcl), then generates Terraform HCL
Backend configStatic block per root moduleGenerated, DRY, one definition inherited by all
Multi-environmentWorkspaces or copied directoriesFile-tree of thin terragrunt.hcl units
Cross-module outputsterraform_remote_state data sourcesdependency blocks with mock outputs
Run across many modulesManual or scriptedterragrunt run-all plan/apply
Learning curveBaselineBaseline plus Terragrunt's own concepts
Extra dependencyNoneA second binary to install and version

How Terragrunt Reduces Configuration Repetition

Here is a fairly ordinary setup: one reusable module, deployed to staging and prod, each in its own state file.

Raw Terraform

You end up with a directory per environment, and most of the content is duplicated.

# environments/staging/main.tf terraform { backend "s3" { bucket = "acme-tfstate" key = "staging/services/main.tfstate" region = "eu-west-1" dynamodb_table = "acme-tf-locks" encrypt = true } } module "service" { source = "../../modules/service" environment = "staging" instance_type = "t3.small" min_size = 1 max_size = 2 account_id = "111111111111" tags = { team = "platform", env = "staging" } }
# environments/prod/main.tf terraform { backend "s3" { bucket = "acme-tfstate" key = "prod/services/main.tfstate" region = "eu-west-1" dynamodb_table = "acme-tf-locks" encrypt = true } } module "service" { source = "../../modules/service" environment = "prod" instance_type = "m5.large" min_size = 3 max_size = 10 account_id = "222222222222" tags = { team = "platform", env = "prod" } }

The backend block is identical except for the key. The account_id, region, and tags follow a mechanical pattern. Add a third environment and you copy this file again. Change the lock table name and you edit it in every directory. Terraform cannot interpolate variables into a backend block, so this stays hardcoded no matter how disciplined you are.

Terragrunt

Terragrunt splits the shared parts out. A single root terragrunt.hcl defines the backend once and generates it into each unit.

# terragrunt.hcl (repo root) remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite" } config = { bucket = "acme-tfstate" # path_relative_to_include() -> "staging/services", "prod/services", etc. key = "${path_relative_to_include()}/terraform.tfstate" region = "eu-west-1" dynamodb_table = "acme-tf-locks" encrypt = true } }

Environment-level common inputs live in one file per environment:

# staging/env.hcl locals { environment = "staging" account_id = "111111111111" }

And each deployed unit becomes small enough to read at a glance:

# staging/services/terragrunt.hcl include "root" { path = find_in_parent_folders() } locals { env = read_terragrunt_config(find_in_parent_folders("env.hcl")).locals } terraform { source = "../../modules/service" } inputs = { environment = local.env.environment account_id = local.env.account_id instance_type = "t3.small" min_size = 1 max_size = 2 tags = { team = "platform", env = local.env.environment } }

The prod/services/terragrunt.hcl is the same shape with different instance sizes. The backend block does not appear in it at all, because Terragrunt generates backend.tf from the root config and derives the state key from the folder path. Change the lock table name once in the root and every unit picks it up.

That is the whole value proposition. Less HCL, one source of truth for backend and shared inputs, and units that describe only what makes them different.

Dependencies Between Modules

The other pain Terragrunt targets is passing outputs between separately-stated modules. In raw Terraform you reach into another state file:

data "terraform_remote_state" "vpc" { backend = "s3" config = { bucket = "acme-tfstate" key = "prod/vpc/terraform.tfstate" region = "eu-west-1" } } module "service" { source = "../../modules/service" vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id subnet_ids = data.terraform_remote_state.vpc.outputs.private_subnet_ids }

Terragrunt replaces that with a dependency block, which also knows how to supply mock outputs during plan so you can plan the whole graph before anything is applied:

dependency "vpc" { config_path = "../vpc" mock_outputs = { vpc_id = "vpc-mock" private_subnet_ids = ["subnet-mock-a", "subnet-mock-b"] } mock_outputs_allowed_terraform_commands = ["plan", "validate"] } inputs = { vpc_id = dependency.vpc.outputs.vpc_id subnet_ids = dependency.vpc.outputs.private_subnet_ids }

Then terragrunt run-all apply walks the dependency graph and applies in the correct order. On a raw Terraform setup that ordering is a shell script or a human running apply in the right sequence and hoping.

When to Choose Terragrunt

Reaching for Terragrunt is a signal, and it is worth reading.

Choose Terragrunt when:

  • You have three or more environments (or regions, or accounts) that share the same module wiring, and the duplication is already real.
  • You want backend configuration defined once instead of hardcoded per directory.
  • You have interdependent modules across separate state files and are tired of terraform_remote_state plumbing.
  • You want run-all to plan and apply an ordered graph in one command.
  • Your team is committed to HCL and Terraform and wants to keep that investment while taming the sprawl.

Stay on plain Terraform when:

  • You have one or two environments and the repetition is not yet painful. A backend block plus a tfvars file per environment is fine, and it is one fewer binary to install, version, and teach.
  • You can get by with Terraform workspaces for environment separation (they share one backend and one module, which suits simpler cases).
  • You are on HCP Terraform and its Stacks feature covers your multi-environment orchestration.
  • You do not want a second tool in the chain when a failed run means debugging whether Terraform or Terragrunt produced the behavior.

Terragrunt is very good at what it does, and Gruntwork has kept it current, including OpenTofu support. But adding it means every engineer now needs to understand both the Terraform mental model and Terragrunt's generate-and-include model on top. That is a fair trade at scale and dead weight on a small project.

Infrastructure From Code as an Alternative

Step back from the two tools for a second. The entire reason Terragrunt exists is that describing the same backend across N environments, in a language that cannot loop over environments, produces N copies of nearly-identical config. Terragrunt is a very capable de-duplicator for that problem.

It is worth noticing that the problem is structural, not accidental. Infrastructure lives in its own codebase, in HCL, against its own state files, with no knowledge of the application it supports. Every environment is a fresh copy of that description. Terragrunt reduces the copy count, but the model that generates copies in the first place stays put.

A different model avoids the repetition rather than de-duplicating it. Infrastructure from code means you declare what your application needs (a database, a Pub/Sub topic, a cron job, a bucket) directly in the application's own source, and the tooling provisions the matching cloud resources per environment from that single declaration. There is no per-environment HCL to keep DRY because there is no per-environment HCL at all.

Encore is the open-source framework built on this idea, for TypeScript and Go, with around 12,000 GitHub stars and production users including Groupon.

import { api } from "encore.dev/api"; import { SQLDatabase } from "encore.dev/storage/sqldb"; // One declaration. Encore provisions Postgres (RDS on AWS, Cloud SQL on GCP, // Docker locally) separately for every environment: local, each PR preview, // staging, and production. No per-environment config to keep in sync. const db = new SQLDatabase("users", { migrations: "./migrations" }); export const getUser = api( { method: "GET", path: "/users/:id", expose: true }, async ({ id }: { id: number }) => { return await db.queryRow`SELECT id, email FROM users WHERE id = ${id}`; }, );

Encore Cloud provisions the underlying AWS or GCP services in your own account, and the framework is open source, with encore build docker producing a standalone image if you want out. This is not a wholesale replacement for Terraform. Teams running large VM fleets, complex networking, or IAM at scale will keep using IaC, and Terragrunt with it. What infrastructure from code removes is exactly the per-environment configuration sprawl that Terragrunt is built to manage, which makes it most compelling for new backends rather than existing HCL estates.

If you are evaluating Terragrunt because your environment count has outgrown hand-written HCL, it is worth spending an hour on the alternative model before you commit to maintaining a second binary layered on top of Terraform.

Deploy with Encore

Provision Postgres per environment from one declaration, in your own AWS or GCP account. No per-environment HCL.

Deploy

Related Resources

  • Terraform alternatives
  • Terragrunt vs Terraform is one part of the wider IaC picture: infrastructure as code
  • Terraform modules explained
  • OpenTofu vs Terraform in 2026

Frequently asked questions

Is Terragrunt a replacement for Terraform?

No. Terragrunt is a thin wrapper that calls the Terraform (or OpenTofu) binary under the hood. It does not provision anything itself. It generates backend config, injects inputs, and orchestrates multiple Terraform modules. You still write Terraform.

Do I need Terragrunt for a small project?

Usually not. A single environment with a handful of resources works fine as plain Terraform with a backend block and a variables file. Terragrunt starts paying off once you have three or more environments repeating the same module wiring.

Does Terragrunt work with OpenTofu?

Yes. Current Terragrunt releases default to calling the tofu binary. You can point it at a specific binary with the TERRAGRUNT_TFPATH environment variable or the terraform_binary option. The wrapper is agnostic about which engine runs the plan.

What does Terragrunt add that Terraform 1.5+ does not?

Mainly generated backend configuration, DRY input inheritance across environments, dependency ordering with run-all, and mock outputs for planning. Terraform's own answers (workspaces, Stacks in HCP) overlap partially but do not fully replace Terragrunt's file-tree composition model.

Is Terragrunt still maintained in 2026?

Yes. Terragrunt is maintained by Gruntwork and remains actively developed, including support for OpenTofu and ongoing work on its unit and stack model.

Contents
What Terragrunt Is
Terragrunt vs Terraform: Feature Comparison
How Terragrunt Reduces Configuration Repetition
Raw Terraform
Terragrunt
Dependencies Between Modules
When to Choose Terragrunt
Infrastructure From Code as an Alternative
Related Resources
Encore

The backend platform for humans and agents

Build backends in Go or TypeScript and run them on your own AWS or GCP, with the guardrails your team and AI agents need to ship safely.

Ready to build your next backend?

Encore is the Open Source framework for building robust type-safe distributed systems with declarative infrastructure.