Terragrunt: Keep Terraform DRY

Khimananda Oli 8 min read Database
Terragrunt: Keep Terraform DRY

By Khimananda Oli | Last reviewed: August 2026

Managing dozens of environments with vanilla Terraform inevitably leads to massive code duplication and fragile state configurations. If you are copying backend blocks or provider configs across fifty directories, you are violating the core principle that Terragrunt: Keep Terraform DRY was designed to enforce. This wrapper solves the scalability gap by introducing configuration inheritance, automatic backend management, and orchestration primitives that native HCL lacks. For teams building production-grade Infrastructure as Code, adopting this tool is often the turning point between unmanageable sprawl and a scalable platform.

Root terragrunt.hclEnvironment (prod/staging)VPC ModuleEKS ModuleShared configs flow down; no duplication in leaf nodesTerragrunt merges configs at runtime before calling Terraform
Hierarchical inheritance model demonstrating how Terragrunt keeps Terraform DRY across environments

How does Terragrunt keep Terraform DRY in practice?

The primary mechanism for maintaining DRY (Don't Repeat Yourself) principles is hierarchical configuration inheritance. Unlike standard Terraform, which treats each directory as an isolated unit requiring explicit backend and provider definitions, Terragrunt allows you to define these once in a root terragrunt.hcl file. Child configurations use the include block to pull in these parent settings, overriding only what is specific to that component.

Eliminating Backend Configuration Duplication

In a typical multi-account AWS setup, every single state file needs a unique S3 key but shares the same bucket, region, encryption settings, and DynamoDB lock table. Without Terragrunt, you copy-paste this 10-line block hundreds of times. With Terragrunt, you define it once:

# Root terragrunt.hcl
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket         = "my-org-terraform-state"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    key            = "${path_relative_to_include()}/terraform.tfstate"
  }
}

The key attribute uses the built-in path_relative_to_include() function to automatically derive a unique state path based on the directory structure. When you run terragrunt apply in prod/us-east-1/vpc, it generates a backend.tf with the correct key without you ever typing it manually. This single feature prevents the most common source of state corruption in large-scale cloud infrastructure projects.

Centralizing Provider Configuration

Provider versions and default tags should be standardized across your organization. Terragrunt’s generate block creates a provider.tf in the working directory at runtime, ensuring every module uses the exact same provider constraints and default tagging strategy for cost allocation and compliance auditing.

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.80"
    }
  }
}

provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      ManagedBy   = "terragrunt"
      Environment = var.env
      CostCenter  = var.cost_center
    }
  }
}
EOF
}

What is the difference between Terragrunt and Terraform modules?

A common mistake among engineers new to this ecosystem is confusing reusable modules with orchestration. They solve fundamentally different problems. Modules package infrastructure logic; Terragrunt packages deployment context. Understanding this distinction is critical when deciding which IaC approach fits your team.

CriteriaTerraform ModulesTerragrunt Orchestration
Primary PurposeEncapsulate resource logic and create reusable componentsManage state, backends, dependencies, and environment context
State ManagementPassive; relies on caller to configure backendActive; auto-creates buckets, locks, and unique keys
Configuration ScopeInput variables define the interfaceHierarchical inheritance defines the deployment context
Dependency HandlingImplicit via resource references within the moduleExplicit dependency blocks across separate state files
DRY MechanismCode reuse through abstractionConfig reuse through inheritance and generation
Execution ModelSingle plan/apply per module callOrchestrated runs with run-all and dependency ordering

In practice, you use both together. Your Terraform modules define what to build (e.g., an EKS cluster). Your Terragrunt configuration defines where and how to deploy it (e.g., prod us-east-1 with specific VPC outputs injected). Never put backend configuration or cross-module dependency logic inside a Terraform module; that is Terragrunt’s job.

How do you manage cross-stack dependencies without coupling?

Real infrastructure has dependencies: your EKS cluster needs the VPC ID, your RDS instance needs the security group, your app needs the database endpoint. In monolithic Terraform, these are simple resource references. In modular, DRY architectures with separate state files, you need a safe way to pass outputs between stacks.

VPC Stackvpc_id, subnet_idsEKS StackNeeds vpc_idApp StackNeeds cluster_endpointread_output()read_output()Terragrunt resolves graph before Terraform executes
Cross-stack dependency resolution using Terragrunt read_output to maintain separation while sharing data

Using Dependency Blocks Safely

The dependency block reads outputs from another stack’s state file without creating a hard Terraform-level coupling. This keeps your modules pure and testable while allowing orchestrated deployments.

# eks/terragrunt.hcl
dependency "vpc" {
  config_path = "../vpc"

  # Mock outputs allow planning even if VPC hasn't been applied yet
  mock_outputs = {
    vpc_id     = "vpc-mock-12345"
    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.subnet_ids
}

The mock_outputs pattern is essential for developer experience. It allows team members to run terragrunt plan on the EKS stack without having deployed the VPC first, enabling parallel development and faster CI feedback loops. Without mocks, every plan requires the full dependency chain to exist, which kills productivity in large organizations.

When should you adopt Terragrunt over native Terraform?

Terragrunt adds operational complexity. You should not adopt it for a single-project hobby setup. However, for teams managing multiple environments, regions, or microservice stacks, the trade-off pays dividends quickly. The decision typically comes down to three factors: state management burden, team size, and compliance requirements.

  • Multi-environment parity: If you maintain dev, staging, and prod with identical structure but different inputs, Terragrunt’s inheritance eliminates drift caused by manual copying.
  • Compliance and audit readiness: For teams pursuing SOC 2 or ISO 27001 certification, centralized provider configs ensure mandatory tagging and encryption policies cannot be accidentally omitted in individual modules.
  • State isolation requirements: If your security model demands separate state files per component (to limit blast radius), Terragrunt makes this manageable. Native Terraform makes separate-state architectures painful to maintain.
  • Team autonomy: Platform teams can own the root configuration and enforce standards, while product teams own their component terragrunt.hcl files with minimal boilerplate.

Conversely, stick with native Terraform if you have fewer than five stacks, a single environment, or a team unfamiliar with HCL basics. Premature abstraction with Terragrunt can obscure debugging and increase onboarding friction. Start with solid Terraform fundamentals before adding the orchestration layer.

How do you structure a scalable Terragrunt repository?

Directory layout determines long-term maintainability. A proven pattern separates live infrastructure configurations from reusable modules, mirroring the organizational boundary between platform and product teams.

infrastructure-live/
├── terragrunt.hcl              # Root: backend, provider, global vars
├── _envcommon/                 # Shared env-specific overrides
│   ├── vpc.hcl
│   └── eks.hcl
├── prod/
│   ├── terragrunt.hcl          # Env-level: region, account ID, env name
│   ├── us-east-1/
│   │   ├── vpc/terragrunt.hcl
│   │   └── eks/terragrunt.hcl
│   └── eu-west-1/
│       └── vpc/terragrunt.hcl
└── staging/
    └── ... (mirrors prod structure)

This layout leverages find_in_parent_folders() for automatic config discovery. Each level adds specificity: root defines global standards, environment defines account/region context, and leaf nodes define component-specific inputs. The _envcommon directory holds shared component configs that can be included via include "envcommon" blocks, preventing duplication even across environments when component configs are identical except for the environment name.

Native Terraformprod/vpc/backend.tf (10 lines)prod/vpc/provider.tf (15 lines)staging/vpc/backend.tf (10 lines)staging/vpc/provider.tf (15 lines)× N environments × M componentsHigh duplication, drift riskWith TerragruntRoot terragrunt.hcl (ONCE)prod/terragrunt.hcl (env vars)prod/vpc/terragrunt.hcl (inputs)staging/vpc/terragrunt.hcl (inputs)Zero backend/provider duplicationInherited, generated at runtimeDRY
File count and duplication comparison demonstrating why Terragrunt keeps Terraform DRY at scale

Implementing Terragrunt for Production Workloads

Adopting Terragrunt is not just about writing less code; it is about establishing guardrails that prevent configuration drift and state corruption as your team grows. Start by migrating a single non-critical environment to validate your inheritance hierarchy and CI integration before converting production. Ensure your CI pipelines support Terragrunt commands natively, as standard Terraform CI actions will not handle the orchestration layer correctly. Remember that the goal is sustainable velocity: if your Terragrunt configuration becomes harder to understand than the duplicated Terraform it replaced, simplify the hierarchy. The best infrastructure code is the code your team can confidently modify at 2 AM during an incident.

If you are evaluating whether Terragrunt fits your current architecture or need help migrating a legacy Terraform codebase to a DRY, scalable structure, reach out to discuss your infrastructure challenges. I help teams design IaC foundations that survive growth, audits, and personnel changes without accumulating technical debt.

Frequently Asked Questions

Terragrunt is a thin wrapper for Terraform that eliminates configuration duplication. It uses inheritance, remote state management, and dependency handling to reuse backend configs, provider settings, and module inputs across multiple environments without repeating HCL code in every directory.

Install via Homebrew with brew install terragrunt or download the binary from GitHub releases. Ensure compatibility with your current Terraform version by checking the release notes. Verify installation by running terragrunt --version in your terminal to confirm the expected build is active.

Yes, Terragrunt wraps existing Terraform configurations non-destructively. Add terragrunt.hcl files alongside your main.tf to manage remote state and dependencies. Your original HCL remains unchanged, allowing gradual adoption without refactoring working infrastructure code or disrupting current CI/CD pipelines immediately.

Modules encapsulate reusable resource logic while Terragrunt manages orchestration and configuration reuse across directories. Use modules for component abstraction and Terragrunt for eliminating duplicate backend, provider, and variable definitions across staging, production, and regional deployments effectively.

Yes, Terragrunt integrates fully with Terraform Cloud and Enterprise backends. Configure cloud blocks in terragrunt.hcl using generate blocks to inject workspace mappings dynamically. This maintains DRY principles while leveraging remote operations, policy enforcement, and team collaboration features native to HashiCorp platforms.

Terragrunt auto-generates backend configuration per directory using remote_state blocks. Define S3, GCS, or Azure blob settings once in a parent terragrunt.hcl and inherit them downstream. This prevents hardcoded state paths and ensures consistent locking across all environment stacks automatically.

Yes, Terragrunt supports OpenTofu as a drop-in replacement. Set the TERRAGRUNT_TFPATH environment variable to point to the tofu binary. All DRY features including include, dependency, and generate blocks function identically with OpenTofu's HCL parser and state format.

Run terragrunt render-json to output the final merged configuration before execution. Use TF_LOG=DEBUG with terragrunt apply to trace HCL evaluation. Check terragrunt.hcl syntax with terragrunt validate-inputs to catch missing variables or type mismatches early in development cycles.

Yes.

Define dependency blocks in terragrunt.hcl to reference outputs from other directories. Terragrunt automatically determines execution order and passes values as inputs. This eliminates manual output copying and ensures correct apply sequencing across interconnected infrastructure components safely.

Yes, use generate blocks to create provider.tf, backend.tf, or locals.tf at runtime. Content is written to .terragrunt-cache before execution and never committed to version control. This keeps source directories clean while injecting environment-specific configurations programmatically during plan and apply phases.

Avoid deep nesting beyond three levels, overusing globals, and generating excessive dynamic HCL. Keep include chains shallow and explicit. Prefer passing inputs directly rather than relying on implicit variable resolution. Complex hierarchies reduce readability and make debugging significantly harder for new team members.

Terragrunt enables targeted runs using run-all with dependency-aware filtering. Only changed directories execute during PR checks. Parallel execution respects dependency graphs automatically. This reduces pipeline duration from hours to minutes for large multi-environment repositories without custom scripting or external orchestration tools.

Minimal.

Start by extracting backend configs into root terragrunt.hcl files. Add include blocks incrementally per environment. Validate each migration step with terragrunt plan before applying. Maintain parallel Terraform runs during transition to verify output parity and ensure zero drift occurs during adoption.