Beyond Terraform — Using Terragrunt to Manage Infrastructure at Scale

March 08, 2026  4 minute read  

Beyond Terraform — Using Terragrunt to Manage Infrastructure at Scale”

Terraform is excellent at provisioning infrastructure, but large Terraform codebases tend to accumulate problems:

  • Copy‑pasted backend configuration\
  • Repeated provider configuration\
  • Environment drift across dev/stage/prod\
  • Dependency ordering between stacks\
  • Painful module orchestration

Terragrunt solves these operational problems without replacing Terraform. It wraps Terraform and adds features for composition, DRY configuration, and orchestration of infrastructure stacks.

This guide explains how experienced engineers actually structure Terragrunt in production environments.


What Terragrunt Really Does

Terragrunt is a thin wrapper around Terraform that adds operational capabilities Terraform intentionally avoids.


Capability Why It Matters ———————————– ———————————– DRY Terraform configuration Avoid copy‑pasting backend and providers

Environment hierarchy Clean dev/stage/prod structure

Dependency management Apply stacks in correct order

Remote state automation Automatically configure state

Module orchestration Run apply across multiple modules ———————————————————————–

Terraform itself remains responsible for infrastructure provisioning. Terragrunt focuses on codebase management and orchestration.


Typical Terraform Scaling Problem

A real infrastructure repository often starts simple and gradually becomes messy.

terraform/
 ├── dev
 │   ├── vpc
 │   ├── eks
 │   └── rds
 ├── stage
 │   ├── vpc
 │   ├── eks
 │   └── rds
 └── prod
     ├── vpc
     ├── eks
     └── rds

Each directory typically contains:

  • backend configuration
  • provider configuration
  • repeated variables
  • identical module references

As the infrastructure grows, copy‑paste drift becomes inevitable.


Terragrunt Repository Layout

Terragrunt separates infrastructure modules from environment configuration.

infrastructure-live/
 ├── dev
 │   ├── vpc
 │   │   └── terragrunt.hcl
 │   ├── eks
 │   │   └── terragrunt.hcl
 │   └── rds
 │       └── terragrunt.hcl
 ├── stage
 └── prod

Terraform modules live in a separate repository:

infrastructure-modules/
 ├── vpc
 ├── eks
 └── rds

Terragrunt orchestrates module usage across environments.


Installing Terragrunt

brew install terragrunt

Or download the binary:

https://terragrunt.gruntwork.io/docs/getting-started/install/

Verify installation:

terragrunt --version

Minimal Terragrunt Configuration

Example terragrunt.hcl:

terraform {
  source = "git::ssh://git@github.com/company/infrastructure-modules.git//vpc"
}

inputs = {
  cidr_block = "10.0.0.0/16"
}

Execution:

terragrunt apply

Terragrunt downloads the module and executes Terraform internally.


The Most Important Feature: include

Large infrastructures require shared configuration. Terragrunt solves this with inheritance via include.

Root configuration:

infrastructure-live/terragrunt.hcl

Example:

remote_state {
  backend = "s3"

  config = {
    bucket         = "company-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

Child module:

dev/vpc/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

terraform {
  source = "git::ssh://git@github.com/company/infrastructure-modules.git//vpc"
}

inputs = {
  cidr_block = "10.0.0.0/16"
}

All modules inherit shared configuration automatically.


Remote State Without Duplication

Terraform normally requires each module to define its backend.

Terragrunt can generate it automatically.

remote_state {
  backend = "s3"

  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }

  config = {
    bucket = "company-terraform-state"
    key    = "${path_relative_to_include()}/terraform.tfstate"
    region = "us-east-1"
  }
}

When Terragrunt runs, backend.tf is generated dynamically.


Environment Configuration

Terragrunt supports environment‑level variables.

live/
 ├── terragrunt.hcl
 ├── dev
 │   └── env.hcl
 ├── stage
 │   └── env.hcl
 └── prod
     └── env.hcl

Example env.hcl:

locals {
  env = "dev"
}

Load it in a module:

locals {
  env_config = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}

inputs = {
  environment = local.env_config.locals.env
}

This allows environment‑aware configuration without repeating variables.


Managing Dependencies

Infrastructure stacks often depend on one another.

Example:

VPC -> EKS -> Application

Terragrunt provides a dependency block.

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

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

Terragrunt reads outputs from Terraform state automatically.


Running Multiple Modules

Instead of applying each module individually:

cd vpc
terraform apply

cd eks
terraform apply

Use Terragrunt orchestration:

terragrunt run-all apply

Terragrunt:

  1. Builds a dependency graph
  2. Applies modules in correct order
  3. Parallelizes independent stacks

Module Version Pinning

Modules are typically referenced from Git.

terraform {
  source = "git::ssh://git@github.com/company/infrastructure-modules.git//eks?ref=v1.4.0"
}

Benefits:

  • controlled upgrades
  • reproducible infrastructure
  • easy rollback

Provider Generation

Provider configuration can also be generated.

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"

  contents = <<EOF
provider "aws" {
  region = "us-east-1"
}
EOF
}

This prevents provider duplication across modules.


Terragrunt Repository Design (Production Layout)

Most teams eventually converge on a layered layout.

repo-root
 ├── infrastructure-modules
 │    ├── vpc
 │    ├── eks
 │    └── rds
 │
 └── infrastructure-live
      ├── terragrunt.hcl
      │
      ├── dev
      │   ├── env.hcl
      │   ├── vpc
      │   ├── eks
      │   └── rds
      │
      ├── stage
      │   └── ...
      │
      └── prod
          └── ...

Responsibilities:

Modules repository:

  • reusable Terraform modules
  • versioned releases
  • no environment values

Live repository:

  • environment configuration
  • Terragrunt orchestration
  • module version pinning

Multi‑Account / Multi‑Region Pattern

Terragrunt scales well to multi‑account architectures.

Example:

live/
 ├── prod
 │   ├── account.hcl
 │   ├── us-east-1
 │   │   ├── vpc
 │   │   └── eks
 │   └── us-west-2
 │       └── vpc

Example account.hcl:

locals {
  account_id = "123456789012"
}

Provider configuration can dynamically assume roles based on this configuration.


CI/CD Integration

Terragrunt works well inside pipelines.

Example CI stage:

terragrunt run-all plan --terragrunt-non-interactive

Typical automation tools:

  • Atlantis
  • Spacelift
  • GitHub Actions
  • GitLab CI
  • Azure DevOps

When Terragrunt Is Worth Using

Terragrunt becomes valuable when:

  • Terraform repositories exceed ~10 modules
  • multiple environments exist
  • backend duplication becomes painful
  • stack dependencies grow complex

For very small infrastructures, plain Terraform is simpler.


Key Takeaways

Terragrunt does not replace Terraform.

It solves the operational scaling problems Terraform intentionally avoids.

The most valuable capabilities are:

  1. DRY configuration
  2. dependency orchestration
  3. environment hierarchy
  4. remote state automation
  5. multi‑module execution

These features allow Terraform infrastructures to scale without turning into copy‑paste chaos.

Leave a comment