jellyace
← All posts
Cloud·3 min read

Terragrunt: keeping multi-environment Terraform DRY

How Terragrunt removes copy-pasted backend and provider config across environments and AWS accounts, with a practical folder layout and examples.

Terraform is great until you have several environments built from the same pieces. Then the same backend block, provider config and module call get copy-pasted into every folder, and the copies slowly drift apart.

Terragrunt is a thin wrapper around Terraform (and OpenTofu) that fixes this. You write your modules once, and each environment becomes a small file that says which module, with which inputs.

The problem with plain Terraform

The two common approaches both have trade-offs:

  • Workspaces keep environments in one directory, but it's easy to apply to the wrong one, and differences between environments are hidden in variables.
  • A folder per environment is explicit, but every folder repeats the same backend, provider and module boilerplate.

Terragrunt keeps the folder-per-environment clarity and removes the repetition.

Before and after: with plain Terraform, each environment folder repeats backend, provider, module call and inputs. With Terragrunt, backend and provider are written once in root.hcl, and each folder holds only a source and its inputs.

A typical layout

modules/
  vpc/
  ecs-service/
live/
  root.hcl
  dev/
    env.hcl
    vpc/terragrunt.hcl
    api/terragrunt.hcl
  staging/
    env.hcl
    vpc/terragrunt.hcl
    api/terragrunt.hcl
  production/
    env.hcl
    vpc/terragrunt.hcl
    api/terragrunt.hcl

modules/ holds normal Terraform modules. live/ describes what is deployed where.

Backend and provider config, written once

The root file generates the backend and provider for every module underneath it:

# 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
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "eu-west-1"
}
EOF
}

Every module gets its own state file automatically, keyed by its folder path, so production/vpc can never overwrite staging/vpc. use_lockfile turns on native S3 state locking, so you don't need a DynamoDB table. It needs Terraform 1.10+ or OpenTofu 1.10+.

Update (October 2026): Since Terragrunt v0.87.0 (5 September 2025), the state bucket is no longer created automatically. Create it once with terragrunt backend bootstrap. For one AWS account per environment, see our later post.

Each environment is a small diff

# live/production/vpc/terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

terraform {
  source = "../../../modules//vpc"
}

inputs = {
  cidr_block = "10.20.0.0/16"
  az_count   = 3
}

The dev and staging files are identical except for their inputs. Reviewing the difference between environments means comparing two short files.

Values shared across an environment — region, account ID, instance sizes — can live in env.hcl and be loaded with read_terragrunt_config(find_in_parent_folders("env.hcl")), so they're defined once.

Dependencies between modules

Modules often need each other's outputs. Instead of hard-coding IDs, declare a dependency:

# live/production/api/terragrunt.hcl
dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  vpc_id     = dependency.vpc.outputs.vpc_id
  subnet_ids = dependency.vpc.outputs.private_subnet_ids
}

Terragrunt now knows the API depends on the VPC, and applies them in the right order.

Dependency graph: vpc first, then rds and eks, which both read the VPC's outputs, then api, which waits for both.

Running many modules at once

Terragrunt can plan or apply a whole tree of modules in dependency order with terragrunt run --all. That's handy for standing up a new environment from scratch. To plan an environment that doesn't exist yet, give each dependency block mock_outputs, so units can be planned before the modules they depend on are applied. For production changes, we still prefer targeting individual modules from CI, with the plan reviewed before apply.

When is it worth it?

Terragrunt pays off when you have multiple environments or AWS accounts built from the same pieces. For a single environment and a small team, plain Terraform is simpler and perfectly fine.

One note if you're reading older tutorials: recent Terragrunt versions recommend naming the root file root.hcl rather than a root-level terragrunt.hcl, and some commands have been renamed. Check the docs for the version you're installing.

Want this done for you?See our Cloud services

Have something you need built?

Tell us a bit about your product and what you’re trying to get done. You’ll hear back from an engineer, not a sales team — no obligation.

hello@jellyace.net