A backend block repeated in every Terraform folder compared with one remote_state block in a Terragrunt root file.
Terraform & OpenTofu

Terragrunt vs Terraform: What Terragrunt Adds, and When You Need It

Terragrunt is a thin wrapper that runs Terraform or OpenTofu across many small root modules. What it adds, what Terraform now does alone, what the extra layer costs, and where Crossplane fits.

Reviewed for technical accuracy: October 13, 2026

  • Terragrunt wraps Terraform and OpenTofu to share backend config, wire outputs between units, and run many units in dependency order
  • Plain Terraform with modules is simpler until you manage dozens of root modules across environments and accounts
  • Terraform Stacks (HCP Terraform) and OpenTofu 1.8 backend variables now cover part of what Terragrunt was built for
  • Crossplane continuously corrects cloud resources from Kubernetes; Terraform changes them only through a reviewed plan and apply

Terragrunt is a thin wrapper around Terraform and OpenTofu, built by Gruntwork. Every plan and apply still runs through the terraform or tofu binary underneath. What Terragrunt adds is orchestration across many small root modules (a root module is the directory where you run terraform plan, with its own state file). It writes the backend configuration for each one, passes outputs from one to the next, runs commands across all of them in dependency order, and runs your scripts before and after each command.

The short verdict: you need Terragrunt when you manage dozens of root modules spread over accounts and regions, and the copied backend blocks and cross-module lookups have become your main source of mistakes. With a handful of root modules, plain Terraform or OpenTofu with shared modules is simpler. Terragrunt 1.0 shipped on March 30, 2026 with a compatibility promise for its CLI flags and configuration, so it is a safer tool to pin than it was during the 0.x years.

What Terragrunt Adds to Terraform

Terragrunt has two terms worth learning first. A unit is a directory containing a terragrunt.hcl file, and it represents a single piece of infrastructure, such as a VPC or a database. A stack is a collection of units managed together, such as everything that makes up staging.

  • One backend definition for every unit. Terraform backend blocks cannot refer to variables or locals, so each root module carries its own hard-coded copy, with a unique state key typed in. Terragrunt's remote_state block lives once in a parent file, builds each unit's key from its folder path with path_relative_to_include(), and writes a backend.tf file into the unit before Terraform runs. With terragrunt backend bootstrap, it can also create the S3 bucket and DynamoDB lock table if they do not exist.
  • Generated files. A generate block writes any file into a unit before each run, which teams use to stamp the same provider configuration into every unit.
  • Dependencies between units. A dependency block reads another unit's outputs, so the app unit can take vpc_id from the VPC unit. mock_outputs supplies placeholder values so you can plan before the dependency has been applied.
  • Runs across many units. terragrunt run --all plan runs a command in every unit under the current directory, ordered by the dependency graph and in parallel where the graph allows. In 1.0, the run command replaced the older run-all.
  • Explicit stacks. A terragrunt.stack.hcl file lists unit blocks with a source, a path, and values. terragrunt stack generate writes the units into a .terragrunt-stack directory, and terragrunt stack run apply deploys them.
  • Hooks. before_hook, after_hook, and error_hook run commands around Terraform commands, for example a linter before every plan.

What Terraform Covers on Its Own

Terraform has closed part of the gap since Terragrunt first appeared.

  • Modules package reusable code, so each environment's root module can call the same module with different inputs.
  • CLI workspaces keep several named states for one configuration in one backend. HashiCorp says workspaces are not appropriate for system decomposition or deployments requiring separate credentials and access controls, so they rarely replace per-environment folders.
  • The terraform_remote_state data source reads outputs from another root module's state, which is what Terragrunt's dependency block does for you.
  • Partial backend configuration leaves values out of the backend block and passes them at terraform init with -backend-config, usually from a pipeline script.
  • Terraform Stacks, generally available in HCP Terraform (HashiCorp's hosted service), organize modules into components (.tfcomponent.hcl files) and repeat them as deployments (.tfdeploy.hcl files), each with its own isolated state. The documentation describes Stacks as an HCP Terraform feature, so they help only if you run there.
  • OpenTofu 1.8 added early evaluation of variables and locals in backend configuration and module sources, which removes one of the oldest reasons to wrap. See OpenTofu vs Terraform.

The Same Unit in Plain Terraform and in Terragrunt

Here is a staging app that needs the VPC ID from a separate VPC root module, in plain Terraform:

HCL
# live/stage/app/main.tf
terraform {
  backend "s3" {
    bucket         = "acme-tofu-state"
    key            = "stage/app/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "acme-lock-table"
  }
}

data "terraform_remote_state" "vpc" {
  backend = "s3"
  config = {
    bucket = "acme-tofu-state"
    key    = "stage/vpc/terraform.tfstate"
    region = "us-east-1"
  }
}

module "app" {
  source         = "git::git@github.com:acme/modules.git//app?ref=v0.0.3"
  vpc_id         = data.terraform_remote_state.vpc.outputs.vpc_id
  instance_count = 3
  instance_type  = "t4g.micro"
}

Production gets a copy of this file with stage changed to prod in two keys. Miss one, and prod reads staging's VPC. The Terragrunt version puts the backend in one parent file:

HCL
# live/root.hcl
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket         = "acme-tofu-state"
    key            = "${path_relative_to_include()}/tofu.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "acme-lock-table"
  }
}

Each unit then holds only what is different about it:

HCL
# live/stage/app/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "git::git@github.com:acme/modules.git//app?ref=v0.0.3"
}

dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  vpc_id         = dependency.vpc.outputs.vpc_id
  instance_count = 3
  instance_type  = "t4g.micro"
}

find_in_parent_folders searches up the directory tree for root.hcl, so every unit inherits the same backend, and the state key follows the folder path. Ten environments means ten small files and one backend definition, and terragrunt run --all apply from live/stage applies the VPC before the app.

What Terragrunt Costs

  • Another tool to install and pin. Every laptop and CI runner needs matching Terragrunt and Terraform or OpenTofu versions.
  • More layers to debug. Terragrunt copies module code into a .terragrunt-cache folder and writes generated files there. A failure can come from the include chain, a generated backend.tf, or the module itself.
  • Mock outputs can mislead. A plan built on placeholder values does not show what the real apply will do.
  • Wide applies. run --all apply can change many units in one command. It asks for confirmation, and --non-interactive skips that prompt, which is common in CI.
  • More to teach. New engineers learn Terragrunt functions and folder conventions on top of Terraform itself.

Terragrunt or Plain Terraform: Decision Table

Your situationPlain Terraform or OpenTofuTerragrunt
A few root modules, one or two environmentsSimpler, fewer moving partsExtra layer with little payoff
Same modules across many accounts, regions, environmentsCopied backend blocks or -backend-config scriptsOne remote_state block, keys from folder paths
Outputs passed between root modulesterraform_remote_state with typed-in keysdependency blocks with ordering built in
Applying many root modules in orderOrdering written into pipeline scriptsrun --all follows the dependency graph
Already on HCP TerraformTerraform Stacks cover repetitionOptional
On OpenTofu 1.8 or laterVariables allowed in backend configStill useful for dependencies and run --all

Crossplane vs Terraform

Crossplane answers a different question. It is a control plane framework, a graduated Cloud Native Computing Foundation project, that runs inside Kubernetes. You declare cloud resources as managed resources: Kubernetes objects whose spec.forProvider describes a real cloud resource, such as an EC2 instance. A Crossplane provider creates the resource and keeps checking it, and Crossplane overrides any changes made to it outside Crossplane. Compositions let a platform team publish its own APIs, such as a Database type that creates an RDS instance, subnet group, and security group together.

Terraform changes infrastructure only when someone runs plan and apply. Drift sits until the next plan shows it, and a person reads the diff before anything changes.

  • Choose Crossplane when Kubernetes is already your platform hub, you want app teams to request infrastructure through kubectl or GitOps, and continuous correction is the behavior you want.
  • Choose Terraform or OpenTofu when you want a reviewable plan before every change, the widest provider coverage, and no cluster to operate for your infrastructure tooling.

One trade-off decides it for many teams: Crossplane reverts an emergency console fix on its next check, while Terraform leaves it in place and shows it in the next plan.

Running Many Terraform Projects in ops0

Terragrunt usually arrives once per-environment backends and cross-project outputs pile up into wiring someone has to keep straight. ops0 takes over that wiring for the engineers who own the modules, while the code stays plain Terraform, OpenTofu, or CloudFormation in your Git repository.

  • Any engine. Each project in ops0 runs Terraform, OpenTofu, Oxid (ops0's open-source engine with PostgreSQL state), or AWS CloudFormation.
  • State backends in a click. Each project gets its own backend: Amazon S3 with locking, Google Cloud Storage, Azure Blob Storage, or Oracle Object Storage, in your own bucket or managed by ops0. State can live in a different account or cloud from the resources it tracks.
  • One variable hierarchy. Org variables apply to all projects or selected ones, and project values override them. Secrets are pulled from AWS Secrets Manager, Google Secret Manager, Azure Key Vault, or OCI Vault at deploy time, and the effective view shows where every value came from.
  • An org-wide dependency map. ops0 shows which projects consume which outputs, which share a variable with conflicting values, and which hard-code a resource ID another project manages, with a jump to the file and line behind each link.
  • Output references. Pick another project's output, and ops0 generates the remote-state block for you.

Every deployment still goes through plan review, policy checks, and an approval step where the person who starts a deployment cannot approve it. See IaC in ops0 and Deployments.

Quick answers

Does Terragrunt replace Terraform?

No. Terragrunt calls the terraform or tofu binary for every plan and apply. It adds a layer on top: shared backend configuration, dependency blocks that pass outputs between units, run --all to run many units in order, and hooks around each command.

Is Terragrunt still needed with Terraform Stacks?

If you run on HCP Terraform, Stacks cover much of the same need: components built from modules, repeated as deployments with isolated state. Stacks are documented as an HCP Terraform feature, so teams running Terraform or OpenTofu elsewhere still use Terragrunt or pipeline scripts for the same job.

What is a unit in Terragrunt?

A unit is a directory containing a terragrunt.hcl file, and it represents a single piece of infrastructure, such as a VPC or a database. A stack is a collection of units managed together, defined either by folder layout or in a terragrunt.stack.hcl file.

Does Terragrunt work with OpenTofu?

Yes. Terragrunt runs either OpenTofu or Terraform, and its own documentation uses OpenTofu in most examples. OpenTofu 1.8 also allows variables in backend configuration, which removes one reason to adopt Terragrunt, though dependency ordering and run --all still apply.

What is the difference between Crossplane and Terraform?

Crossplane runs inside Kubernetes and keeps checking cloud resources, reverting changes made outside it to match the declared spec. Terraform changes resources only when someone runs plan and apply, and shows the diff for review first. Crossplane fits Kubernetes-centered platform teams; Terraform fits teams that want a reviewed plan before every change.

Sources
  • State Backend: Terragrunt. remote_state, generated backend.tf, path_relative_to_include, and backend bootstrap.
  • Explicit Stacks: Terragrunt. terragrunt.stack.hcl, unit blocks, stack generate and stack run.
  • Terragrunt 1.0 Released: Gruntwork. 1.0 compatibility promise and run replacing run-all.
  • Backend block configuration: HashiCorp. Backend blocks cannot refer to variables; partial configuration.
  • Terraform Stacks: HashiCorp. Components, deployments, and isolated state in HCP Terraform.
  • What's Crossplane?: Crossplane. Control plane model, drift correction, and CNCF status.
From article to workflow

Turn infrastructure context into a reviewed change.

Review generated infrastructure code with policy checks, cost context, approval, and audit evidence before deployment.

Related articles

All articles