All skills
Skillintermediate

Terragrunt Best Practices and Common Patterns

This reference document provides best practices, common patterns, and anti-patterns for Terragrunt configurations. Use this as a guide when validating or creating Terragrunt code.

Claude Code Knowledge Pack7/10/2026

Overview

Terragrunt Best Practices and Common Patterns

Overview

This reference document provides best practices, common patterns, and anti-patterns for Terragrunt configurations. Use this as a guide when validating or creating Terragrunt code.

Directory Structure

Recommended Structure

infrastructure/
├── root.hcl                    # Root Terragrunt config (Terragrunt 0.93+)
├── common.hcl                  # Shared configuration
├── prod/
│   ├── env.hcl                # Environment-level config
│   ├── vpc/
│   │   └── terragrunt.hcl     # Module-specific config
│   ├── database/
│   │   └── terragrunt.hcl
│   └── app/
│       └── terragrunt.hcl
├── staging/
│   └── ... (similar structure)
└── dev/
    └── ... (similar structure)

Anti-Pattern: Flat Structure

❌ Avoid flat structures without environment separation:

infrastructure/
├── vpc.hcl
├── database.hcl
├── app.hcl

DRY Principles

Use include for Shared Configuration

Good Practice:

# Root root.hcl (Terragrunt 0.93+)
remote_state {
  backend = "s3"
  config = {
    bucket         = "my-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

# Child terragrunt.hcl
include "root" {
  path = find_in_parent_folders("root.hcl")
}

Use read_terragrunt_config for Shared Variables

Good Practice:

# common.hcl
locals {
  region = "us-east-1"
  environment = "prod"
  tags = {
    Terraform   = "true"
    Environment = local.environment
  }
}

# terragrunt.hcl
locals {
  common = read_terragrunt_config(find_in_parent_folders("common.hcl"))
}

inputs = {
  region = local.common.locals.region
  tags   = local.common.locals.tags
}

Dependencies

Explicit Dependencies

Good Practice:

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

dependency "database" {
  config_path = "../database"

  # Mock outputs for validation
  mock_outputs = {
    endpoint = "mock-db-endpoint"
    port     = 5432
  }

  # Allow mock outputs during plan
  mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}

inputs = {
  vpc_id          = dependency.vpc.outputs.vpc_id
  database_endpoint = dependency.database.outputs.endpoint
}

Anti-Pattern: Implicit Dependencies via Remote State

❌ Avoid accessing remote state directly:

# This makes dependencies unclear
inputs = {
  vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id
}

Mock Outputs for Testing

Provide Mock Outputs

Good Practice:

dependency "network" {
  config_path = "../network"

  mock_outputs = {
    vpc_id     = "vpc-mock123"
    subnet_ids = ["subnet-mock1", "subnet-mock2"]
  }

  mock_outputs_allowed_terraform_commands = ["validate", "plan", "init"]
  mock_outputs_merge_strategy_with_state  = "shallow"
}

This allows running terragrunt plan without deploying dependencies first.

Generate Blocks

Use generate for Provider Configuration

Good Practice:

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
EOF
}

Error Handling

Use get_env with Defaults

Good Practice:

locals {
  account_id = get_env("AWS_ACCOUNT_ID", "")
}

# Validate required environment variables
inputs = {
  account_id = local.account_id != "" ? local.account_id : run_cmd("--terragrunt-quiet", "aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text")
}

Use try for Optional Values

Good Practice:

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

  # Safely access potentially missing values
  instance_type = try(local.env_config.locals.instance_type, "t3.micro")
}

Common Anti-Patterns

1. Hardcoding Values

Bad:

inputs = {
  region = "us-east-1"  # Hardcoded
  account_id = "123456789012"  # Hardcoded
}

Good:

locals {
  region     = get_env("AWS_REGION", "us-east-1")
  account_id = get_aws_account_id()
}

inputs = {
  region     = local.region
  account_id = local.account_id
}

2. Not Using Mock Outputs

Bad:

dependency "vpc" {
  config_path = "../vpc"
  # No mock outputs - can't validate without deploying vpc
}

3. Deep Nesting

Bad:

infrastructure/
└── prod/
    └── us-east-1/
        └── vpc/
            └── public/
                └── subnet-1/
                    └── terragrunt.hcl

Good:

infrastructure/
└── prod/
    └── vpc/
        └── terragrunt.hcl  # Configure all subnets here

4. Not Using Functions

Bad:

# Manually maintaining paths
remote_state {
  config = {
    key = "prod/vpc/terraform.tfstate"
  }
}

Good:

remote_state {
  config = {
    key = "${path_relative_to_include()}/terraform.tfstate"
  }
}

Security Best Practices

1. Enable State Encryption

remote_state {
  backend = "s3"
  config = {
    encrypt = true
    kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/..."
  }
}

2. Use IAM Roles for Authentication

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = < **Note:** `run-all` is deprecated. Use `run --all` instead.

```bash
# Validate all modules
terragrunt run --all validate

# Plan all modules
terragrunt run --all plan

# Apply all modules
terragrunt run --all apply

# With strict mode (errors on deprecated features)
terragrunt --strict-mode run --all plan

# Or via environment variable
TG_STRICT_MODE=true terragrunt run --all plan

Performance Optimization

1. Use Shallow Dependencies

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

  # Only fetch specific outputs
  mock_outputs_merge_strategy_with_state = "shallow"
}

2. Parallelize Operations

# Run operations in parallel (new syntax)
terragrunt run --all apply --parallelism 4

# Legacy syntax (deprecated)
# terragrunt run-all apply --terragrunt-parallelism=4

3. Use Caching

# Cache downloaded modules
terraform {
  source = "tfr:///terraform-aws-modules/vpc/aws?version=5.1.0"
}

Troubleshooting Common Issues

Issue: Circular Dependencies

Symptom: "Cycle detected in dependency graph"

Solution:

  • Review dependency chain
  • Separate tightly coupled resources into single module
  • Use data sources instead of dependencies where appropriate

Issue: State Locking Errors

Symptom: "Error acquiring the state lock"

Solution:

# Force unlock (use with caution)
terragrunt force-unlock <LOCK_ID>

Issue: Module Not Found

Symptom: "Module not found"

Solution:

# Clear cache and reinitialize
rm -rf .terragrunt-cache
terragrunt init

Version Compatibility

Terragrunt Version Constraints

Specify minimum Terragrunt version:

# For new CLI features (recommended)
terragrunt_version_constraint = ">= 0.93.0"

# For backwards compatibility with older features
# terragrunt_version_constraint = ">= 0.48.0"

Terraform Version Constraints

terraform_version_constraint = ">= 1.6.0, < 2.0.0"

References