Updated Oct 12, 20267 min read

Terragrunt vs Terraform: Differences and When to Use Each

Compare Terraform and Terragrunt for managing configuration across multiple environments.

Terraform provides the language and execution engine for defining infrastructure, whereas Terragrunt adds a layer for reusing configuration and coordinating multiple Terraform projects. But when environments tend to repeat the same backend, provider settings, module resources, and dependency wiring - that's when the difference starts to matter.

This guide compares the two tools through a multi-environment AWS project by covering shared configuration, remote state, dependencies, run order, CI, and the point at which Terragrunt removes enough repetition to justify another tool in the workflow.

We'll also look at how Encore reduces repeated configuration by letting you declare databases, queues, and other application resources directly in code.

What Terragrunt adds to Terraform

Terraform works from a root module, a backend, provider configuration, input values, and state. Ordinary variables are not available inside a backend block, and separate root modules do not automatically form one dependency graph. Those boundaries are manageable in a small project and become repetitive when the same module is deployed across several accounts, regions, or environments.

Terragrunt reads terragrunt.hcl, combines it with included configuration, generates any files the unit needs, and then invokes Terraform or OpenTofu in the prepared working directory. The underlying engine still talks to providers, calculates the plan, holds state, and changes the resources.

TerraformTerragrunt
Primary roleInfrastructure-as-code engineConfiguration and orchestration layer around Terraform or OpenTofu
Resource definitionsTerraform HCL and providersReuses Terraform modules; does not define a second resource model
Remote stateBackend configuration in each rootCan generate backend configuration from one shared definition
Environment reuseModules, variables, workspaces, or separate rootsIncludes and thin per-environment units
Cross-root dependenciesRemote-state data or external orchestrationdependency blocks and an inferred run graph
Multi-unit commandsCI or scripts select and order rootsterragrunt run --all plan and terragrunt run --all apply
Operational surfaceTerraform CLI, providers, and stateTerraform surface plus Terragrunt HCL, cache, CLI, and versioning

The extra layer only earns its place by removing meaningful duplication, which a terragrunt.hcl that forwards values into one root module and one state file does not do.

Build a multi-environment Terragrunt setup

A common layout keeps reusable Terraform modules separate from the live configuration that selects where each module runs:

infrastructure/ ├── modules/ │ ├── network/ │ └── service/ └── live/ ├── root.hcl ├── staging/ │ ├── env.hcl │ ├── network/terragrunt.hcl │ └── service/terragrunt.hcl └── production/ ├── env.hcl ├── network/terragrunt.hcl └── service/terragrunt.hcl

The shared root.hcl can generate one S3 backend configuration for every unit, deriving the state key from the unit's location in the directory tree:

# live/root.hcl remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { bucket = "acme-terraform-state" key = "${path_relative_to_include()}/terraform.tfstate" region = "eu-west-1" encrypt = true use_lockfile = true } }

path_relative_to_include() produces a different key for staging/network, staging/service, and their production equivalents. Native S3 locking through use_lockfile requires Terraform 1.10 or newer, and the state bucket still needs the access controls, encryption, versioning, and recovery process expected of any shared Terraform backend.

Environment values can live in one small file instead of being repeated in each unit:

# live/staging/env.hcl locals { environment = "staging" aws_account = "111111111111" aws_region = "eu-west-1" }

The service unit includes the shared backend, reads the closest environment file, selects the Terraform module, and passes only the inputs that belong to this deployment:

# live/staging/service/terragrunt.hcl include "root" { path = find_in_parent_folders("root.hcl") } locals { env = read_terragrunt_config(find_in_parent_folders("env.hcl")).locals } terraform { source = "../../../modules/service" } dependency "network" { config_path = "../network" mock_outputs_allowed_terraform_commands = ["validate", "plan"] mock_outputs = { vpc_id = "vpc-mock" private_subnet_ids = ["subnet-mock-a", "subnet-mock-b"] } } inputs = { environment = local.env.environment aws_account = local.env.aws_account aws_region = local.env.aws_region vpc_id = dependency.network.outputs.vpc_id subnet_ids = dependency.network.outputs.private_subnet_ids }

Once the network unit has been applied, the dependency block reads its real outputs. Mocks allow exploratory plans before those outputs exist, but a saved plan containing mock IDs must never be applied. Disallowing mocks for apply does not remove values already captured in a saved plan.

Run commands from one environment directory. For example, from infrastructure/, inspect staging with:

cd live/staging terragrunt run --all plan

This plan does not change resources or save an approved deployment artifact. Running from live/ would discover staging and production together.

For production, first deploy any missing dependencies separately, following the dependency order, and verify their real outputs. Then remove the example's mock_outputs and mock_outputs_allowed_terraform_commands settings from the production service unit. From infrastructure/, save a new set of production plans:

cd live/production plan_dir=$(mktemp -d) terragrunt run --all --out-dir "$plan_dir" -- plan

Review every unit's saved plan and approve the deployment before continuing. Only then, from the same production directory, apply those exact artifacts:

terragrunt run --all --out-dir "$plan_dir" -- apply

The saved-plan workflow uses --out-dir for both commands. Keep the configuration, source revision, unit selection, and artifacts unchanged between approval and apply. In CI, transfer the plan directory securely between jobs and retain the same environment scope. Plans can contain sensitive data and should not be committed.

The current CLI uses run --all; older examples often show run-all. Multi-unit apply has no per-unit approval prompt, so the separate approval step matters. Use production credentials limited to the intended account, and re-plan if dependencies, configuration, or state change after review.

What Terragrunt adds to the workflow

Terragrunt makes backend configuration, shared inputs, and cross-unit orchestration easier to maintain by letting a platform team change a state bucket or common provider argument once while keeping each environment directory focused on what differs. The resulting file tree also provides an explicit inventory of deployed units.

Removing repetition still leaves engineers learning Terraform evaluation and state alongside Terragrunt includes, merge behavior, generated files, dependency outputs, cache directories, and its own CLI. A failure may originate in the wrapper, generated configuration, provider, backend, or Terraform itself, and upgrades need to keep both tools compatible.

Large dependency graphs create another trade-off. Separating infrastructure into smaller state files limits the impact of one apply, while run --all connects those states operationally again. Plans take longer, output becomes harder to review, and a shared change can fan out across many units. Terragrunt's filters and run queue help control the graph, but the repository still needs clear ownership boundaries rather than one command that habitually touches everything.

Plain Terraform remains the simpler choice when a project has one or two roots, little repeated backend configuration, and no need to coordinate many states. Modules and separate environment roots can go a long way before a wrapper is necessary, particularly when the team values a smaller toolchain more than perfectly DRY HCL.

Encore as an alternative for application infrastructure

Encore

Terragrunt is a good answer when the team has chosen Terraform and needs to operate the same modules across many environments. For a new backend, it is worth asking why application resources are being copied through a separate infrastructure tree in the first place.

Declaring databases, queues, buckets, caches, and scheduled jobs alongside the code that uses them lets Encore provide local infrastructure during development and provision the supported resources in deployed environments. Capacity, networking, and other environment settings remain outside the application code.

Terraform and Terragrunt can continue to manage shared networking, organization-wide IAM, accounts, clusters, and resources outside Encore's model. Encore is most useful at the application boundary, where it can remove repeated per-environment HCL without disturbing the wider IaC estate.

If the problem is new application teams repeatedly configuring databases, queues, buckets, and service wiring, Encore can remove more work than another layer around Terraform. Terragrunt is the more direct fit when the problem is coordinating a broad existing Terraform estate across accounts and regions.

Learn how Encore works →

Choosing between Terraform and Terragrunt

Stay with plain Terraform while the root configurations remain easy to understand independently. A backend file and environment-specific variables are not a problem merely because some lines repeat, and avoiding an abstraction is often cheaper than operating one before it is needed.

Terragrunt starts to earn its place when several environments share the same module wiring, backend configuration is copied across many roots, outputs pass between separate state files, or CI relies on custom scripts to select and order Terraform directories. Introduce it around one representative slice first, since converting the whole repository before the include and dependency model is settled makes the migration harder to review.

For new application infrastructure, compare the wrapper approach with declaring those resources from the application itself. A mixed estate may be the right result, with Encore handling application resources, Terraform retaining the wider cloud foundation, and Terragrunt used only where enough Terraform units remain to justify it.

Frequently asked questions

Is Terragrunt a replacement for Terraform?

No. Terragrunt wraps Terraform or OpenTofu and invokes the underlying engine for plan and apply. It reduces repeated configuration and coordinates multiple infrastructure units, but the resources, providers, and state still belong to Terraform or OpenTofu.

What is the main difference between Terraform and Terragrunt?

Terraform is the infrastructure-as-code engine that creates and changes resources. Terragrunt is an orchestration and configuration layer that can generate backend configuration, inherit common settings, pass inputs, connect dependencies, and run Terraform across several units.

Do small Terraform projects need Terragrunt?

Usually not. One or two root configurations can remain clearer as plain Terraform. Terragrunt becomes useful when the same backend, provider settings, and module wiring are repeated across several environments, regions, or accounts.

Does Terragrunt work with OpenTofu?

Yes. Terragrunt supports both OpenTofu and Terraform. The HCL layer and orchestration model are similar regardless of which engine performs the plan and apply.

Can Terragrunt manage dependencies between Terraform modules?

Yes. A dependency block reads outputs from another Terragrunt unit and makes them available as inputs. Terragrunt can then use the dependency graph to order commands run across several units.

How can I avoid repeating application infrastructure configuration across environments?

With Encore, databases, queues, and other application resources are declared once in code and provisioned for each environment. Region, sizing, and scaling are configured separately, so the application does not need a separate set of resource definitions for staging and production.

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.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders