Terragrunt
Terragrunt is a thin wrapper for Terraform that provides extra tools for keeping your Terraform configurations DRY (Don’t Repeat Yourself), working with multiple modules, and managing remote state.
What is Terragrunt?
Terragrunt addresses common pain points when working with Terraform at scale:
┌──────────────────────────────────────────────────────────────┐
│ PROBLEMS TERRAGRUNT SOLVES │
├──────────────────────────────────────────────────────────────┤
│ │
│ PROBLEM 1: Backend Configuration Duplication │
│ • Every environment needs the same backend config │
│ • Copy-pasting backend blocks across environments │
│ • Easy to make mistakes with state file paths │
│ │
│ PROBLEM 2: Provider Configuration Duplication │
│ • Same provider config repeated in every module │
│ • Region, account, and other settings duplicated │
│ │
│ PROBLEM 3: Module Source Duplication │
│ • Same module called with slightly different variables │
│ • Difficult to update module versions across environments │
│ │
│ PROBLEM 4: Working with Multiple Modules │
│ • Need to run terraform apply in multiple directories │
│ • Managing dependencies between modules manually │
│ • No easy way to apply changes across all modules │
│ │
│ PROBLEM 5: Environment-Specific Variables │
│ • Managing different variable values per environment │
│ • No inheritance or composition of configurations │
│ │
└──────────────────────────────────────────────────────────────┘Installation
# macOS (Homebrew)
brew install terragrunt
# Linux (download binary)
TERRAGRUNT_VERSION="v0.55.0"
wget https://github.com/gruntwork-io/terragrunt/releases/download/${TERRAGRUNT_VERSION}/terragrunt_linux_amd64
chmod +x terragrunt_linux_amd64
sudo mv terragrunt_linux_amd64 /usr/local/bin/terragrunt
# Verify installation
terragrunt --versionBasic Concepts
The terragrunt.hcl File
Terragrunt uses terragrunt.hcl configuration files (HCL2 format) to define configuration that wraps your Terraform code.
# terragrunt.hcl - Basic structure
# Specify the Terraform source
terraform {
source = "../modules/vpc"
}
# Configure remote state
remote_state {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
}
}
# Pass inputs to Terraform
inputs = {
environment = "production"
cidr_block = "10.0.0.0/16"
}Directory Structure Convention
Terragrunt encourages a specific directory structure:
terraform/
├── terragrunt.hcl # Root configuration
├── modules/ # Reusable Terraform modules
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ec2/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── rds/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
│
└── environments/ # Environment-specific configurations
├── dev/
│ ├── terragrunt.hcl # Dev environment config
│ ├── vpc/
│ │ └── terragrunt.hcl
│ ├── ec2/
│ │ └── terragrunt.hcl
│ └── rds/
│ └── terragrunt.hcl
│
├── staging/
│ ├── terragrunt.hcl
│ ├── vpc/
│ │ └── terragrunt.hcl
│ ├── ec2/
│ │ └── terragrunt.hcl
│ └── rds/
│ └── terragrunt.hcl
│
└── production/
├── terragrunt.hcl
├── vpc/
│ └── terragrunt.hcl
├── ec2/
│ └── terragrunt.hcl
└── rds/
└── terragrunt.hclDRY Configuration
Root terragrunt.hcl
Create a root configuration that child configurations can inherit from:
# terraform/terragrunt.hcl (root)
# Configure remote state backend (inherited by all children)
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "my-terraform-state-bucket"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# Generate provider configuration (inherited by all children)
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}
# Common inputs for all environments
inputs = {
aws_region = "us-east-1"
common_tags = {
ManagedBy = "Terragrunt"
Project = "MyProject"
}
}Environment-Level Configuration
# environments/production/terragrunt.hcl
# Include root configuration
include "root" {
path = find_in_parent_folders()
}
# Environment-specific inputs
inputs = {
environment = "production"
# Merge with common_tags from root
tags = merge(
local.common_tags,
{
Environment = "production"
}
)
}
# Locals for reuse
locals {
account_id = "333333333333"
aws_region = "us-east-1"
}Module-Level Configuration
# environments/production/vpc/terragrunt.hcl
# Include root configuration
include "root" {
path = find_in_parent_folders()
}
# Include environment configuration
include "env" {
path = find_in_parent_folders("env.hcl")
expose = true
}
# Specify Terraform module source
terraform {
source = "../../../modules//vpc"
}
# Module-specific inputs
inputs = {
cidr_block = "10.0.0.0/16"
# Reference parent configs
environment = include.env.inputs.environment
# Add VPC-specific tags
tags = merge(
include.env.inputs.tags,
{
Name = "production-vpc"
}
)
}Remote State Management
Automatic Backend Generation
Terragrunt can automatically generate backend configuration:
# Root terragrunt.hcl
remote_state {
backend = "s3"
# Generate backend.tf file
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "my-terraform-state-${get_aws_account_id()}"
# Automatic path based on directory structure
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
# S3 bucket versioning
s3_bucket_tags = {
Name = "Terraform State"
Environment = "all"
}
}
}This generates a backend.tf in each module directory:
# Auto-generated backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state-333333333333"
key = "environments/production/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}Dynamic State Path
# Use directory path as state key
key = "${path_relative_to_include()}/terraform.tfstate"
# Examples:
# environments/dev/vpc/terragrunt.hcl
# → state key: environments/dev/vpc/terraform.tfstate
#
# environments/prod/rds/terragrunt.hcl
# → state key: environments/prod/rds/terraform.tfstateDependencies Between Modules
Terragrunt can manage dependencies between modules and pass outputs from one module as inputs to another.
Defining Dependencies
# environments/production/ec2/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules//ec2"
}
# Declare dependencies
dependency "vpc" {
config_path = "../vpc"
# Mock outputs for terraform validate/plan
mock_outputs = {
vpc_id = "vpc-12345678"
subnet_ids = ["subnet-12345678"]
}
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}
dependency "security_group" {
config_path = "../security-group"
mock_outputs = {
security_group_id = "sg-12345678"
}
}
# Use outputs from dependencies
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_id = dependency.vpc.outputs.subnet_ids[0]
security_group_id = dependency.security_group.outputs.security_group_id
instance_type = "t3.micro"
ami_id = "ami-12345678"
}Dependency Graph
┌──────────────────────────────────────────────────────────────┐
│ DEPENDENCY EXAMPLE │
└──────────────────────────────────────────────────────────────┘
┌─────────────┐
│ VPC │
└──────┬──────┘
│
├──────────────────┬──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Subnets │ │ Security │ │ Route │
│ │ │ Groups │ │ Tables │
└──────┬──────┘ └──────┬──────┘ └─────────────┘
│ │
└────────┬─────────┘
▼
┌─────────────────┐
│ EC2 Instance │
└─────────┬───────┘
│
▼
┌─────────────────┐
│ RDS Database │
└─────────────────┘
Terragrunt ensures resources are created in dependency orderWorking with Multiple Modules
Run-all Commands
Terragrunt provides commands to work with multiple modules at once:
# Plan all modules in the current directory and subdirectories
terragrunt run-all plan
# Apply all modules (respecting dependencies)
terragrunt run-all apply
# Destroy all modules (in reverse dependency order)
terragrunt run-all destroy
# Validate all modules
terragrunt run-all validate
# Show output from all modules
terragrunt run-all outputExecution Flow
┌──────────────────────────────────────────────────────────────────┐
│ TERRAGRUNT RUN-ALL EXECUTION │
└──────────────────────────────────────────────────────────────────┘
$ terragrunt run-all apply
Step 1: Scan Directory Tree
└─ Find all terragrunt.hcl files
└─ environments/production/
├─ vpc/terragrunt.hcl
├─ security-group/terragrunt.hcl
├─ ec2/terragrunt.hcl
└─ rds/terragrunt.hcl
Step 2: Build Dependency Graph
└─ Parse dependency blocks
└─ vpc (no dependencies)
├─ security-group (depends on vpc)
├─ ec2 (depends on vpc, security-group)
└─ rds (depends on vpc, security-group)
Step 3: Execute in Order
1. Apply vpc/ ✓
2. Apply security-group/ ✓ (after vpc)
3. Apply ec2/ and rds/ ✓ (in parallel, after dependencies)Parallelism
# Control parallelism (default is unlimited)
terragrunt run-all apply --terragrunt-parallelism 2
# Run modules that can be run in parallel
# (those without interdependencies)Built-in Functions
Terragrunt provides many built-in functions:
Path Functions
# Find parent folder containing a file
path = find_in_parent_folders("terragrunt.hcl")
# Get relative path from include
key = "${path_relative_to_include()}/terraform.tfstate"
# Get relative path from current directory
path_relative_from_include()
# Get absolute path
get_terragrunt_dir()
get_parent_terragrunt_dir()
get_original_terragrunt_dir()AWS Functions
# Get AWS account ID
bucket = "my-state-${get_aws_account_id()}"
# Get AWS caller identity
get_aws_caller_identity_arn()
get_aws_caller_identity_user_id()
# Get available AZs
availability_zones = get_aws_availability_zones()Environment Functions
# Get environment variables
database_password = get_env("DB_PASSWORD", "default-value")
# Check if environment variable is set
run_cmd("echo", get_env("USER"))Other Functions
# Read file contents
ssh_key = read_terragrunt_config("ssh_key.txt")
# Run shell command
git_branch = run_cmd("git", "rev-parse", "--abbrev-ref", "HEAD")
# Find files
tfvars_files = find_in_parent_folders("*.tfvars")Complete Example
Project Structure
infrastructure/
├── terragrunt.hcl # Root config
├── modules/ # Terraform modules
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── app/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
│
└── live/ # Live environments
├── _envcommon/ # Shared configs
│ ├── vpc.hcl
│ └── app.hcl
│
├── dev/
│ ├── account.hcl # Account-level config
│ ├── env.hcl # Environment-level config
│ ├── vpc/
│ │ └── terragrunt.hcl
│ └── app/
│ └── terragrunt.hcl
│
└── prod/
├── account.hcl
├── env.hcl
├── vpc/
│ └── terragrunt.hcl
└── app/
└── terragrunt.hclRoot Configuration
# infrastructure/terragrunt.hcl
locals {
# Parse account configuration
account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))
# Parse environment configuration
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
# Extract values
account_id = local.account_vars.locals.account_id
aws_region = local.account_vars.locals.aws_region
environment = local.env_vars.locals.environment
}
# Remote state configuration
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "terraform-state-${local.account_id}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = local.aws_region
encrypt = true
dynamodb_table = "terraform-locks-${local.environment}"
s3_bucket_tags = {
Name = "Terraform State ${local.environment}"
Environment = local.environment
}
}
}
# Generate provider
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "${local.aws_region}"
default_tags {
tags = {
Environment = "${local.environment}"
ManagedBy = "Terragrunt"
}
}
}
EOF
}
# Common inputs
inputs = merge(
local.account_vars.locals,
local.env_vars.locals,
{}
)Account Configuration
# live/dev/account.hcl
locals {
account_id = "111111111111"
aws_region = "us-east-1"
}Environment Configuration
# live/dev/env.hcl
locals {
environment = "dev"
# Environment-specific settings
instance_type = "t3.micro"
db_instance_class = "db.t3.micro"
}Shared Module Configuration
# live/_envcommon/vpc.hcl
terraform {
source = "${get_repo_root()}/modules//vpc"
}
inputs = {
enable_nat_gateway = true
enable_dns_hostnames = true
# These will be overridden per environment
cidr_block = ""
}Module Configuration (Dev)
# live/dev/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
include "envcommon" {
path = "${dirname(find_in_parent_folders())}/_envcommon/vpc.hcl"
expose = true
}
inputs = {
cidr_block = "10.0.0.0/16"
vpc_name = "dev-vpc"
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.10.0/24", "10.0.11.0/24"]
}Module Configuration (Prod)
# live/prod/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
include "envcommon" {
path = "${dirname(find_in_parent_folders())}/_envcommon/vpc.hcl"
expose = true
}
inputs = {
cidr_block = "10.1.0.0/16"
vpc_name = "prod-vpc"
public_subnets = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
private_subnets = ["10.1.10.0/24", "10.1.11.0/24", "10.1.12.0/24"]
}Common Commands
# Initialize and download Terraform modules
terragrunt init
# Plan changes
terragrunt plan
# Apply changes
terragrunt apply
# Destroy resources
terragrunt destroy
# Show output values
terragrunt output
# Run arbitrary Terraform command
terragrunt <terraform-command>
# Work with multiple modules
terragrunt run-all plan
terragrunt run-all apply
terragrunt run-all destroy
# Show dependency graph
terragrunt graph-dependencies
# Validate all configurations
terragrunt run-all validate
# Format all terragrunt.hcl files
terragrunt hclfmt
# Show rendered configuration
terragrunt render-jsonHooks
Terragrunt supports before/after hooks for running commands:
terraform {
source = "../../../modules//vpc"
# Run before any terraform command
before_hook "before_hook" {
commands = ["apply", "plan"]
execute = ["echo", "Running Terraform"]
}
# Run after successful apply
after_hook "after_hook" {
commands = ["apply"]
execute = ["echo", "Apply completed successfully"]
run_on_error = false
}
# Run even on error
error_hook "error_hook" {
commands = ["apply", "destroy"]
execute = ["echo", "Command failed"]
run_on_error = true
}
}Common Hook Use Cases
# Initialize Terraform plugins
before_hook "init" {
commands = ["plan", "apply"]
execute = ["terraform", "init", "-upgrade"]
}
# Validate before applying
before_hook "validate" {
commands = ["apply"]
execute = ["terraform", "validate"]
}
# Run tflint
before_hook "tflint" {
commands = ["plan", "apply"]
execute = ["tflint"]
}
# Notify on completion
after_hook "notify" {
commands = ["apply"]
execute = ["./scripts/notify-slack.sh", "Apply completed"]
}
# Backup state before destroy
before_hook "backup_state" {
commands = ["destroy"]
execute = ["./scripts/backup-state.sh"]
}Advanced Patterns
Multi-Region Deployment
infrastructure/
└── live/
└── prod/
├── us-east-1/
│ ├── region.hcl
│ ├── vpc/
│ └── app/
└── us-west-2/
├── region.hcl
├── vpc/
└── app/# live/prod/us-east-1/region.hcl
locals {
aws_region = "us-east-1"
}
# live/prod/us-east-1/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
locals {
region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))
}
terraform {
source = "${get_repo_root()}/modules//vpc"
}
inputs = {
aws_region = local.region_vars.locals.aws_region
cidr_block = "10.0.0.0/16"
}Conditional Configuration
# Enable/disable features per environment
locals {
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
is_production = local.env_vars.locals.environment == "prod"
}
inputs = {
# Enable backups only in production
backup_retention_days = local.is_production ? 30 : 7
# Multi-AZ only in production
multi_az = local.is_production
# Different instance sizes
instance_type = local.is_production ? "t3.large" : "t3.micro"
}Using External Data
# Read from JSON file
locals {
config = jsondecode(file("${get_terragrunt_dir()}/config.json"))
}
inputs = {
vpc_cidr = local.config.vpc_cidr
subnet_cidrs = local.config.subnet_cidrs
}
# Read from YAML file
locals {
config = yamldecode(file("${get_terragrunt_dir()}/config.yaml"))
}Best Practices
┌──────────────────────────────────────────────────────────────┐
│ TERRAGRUNT BEST PRACTICES │
├──────────────────────────────────────────────────────────────┤
│ │
│ STRUCTURE: │
│ ✅ Keep modules separate from live configurations │
│ ✅ Use consistent directory structure across environments │
│ ✅ Use _envcommon for shared configuration │
│ ✅ Separate account, region, and environment configs │
│ │
│ DRY PRINCIPLE: │
│ ✅ Use include blocks to inherit configuration │
│ ✅ Extract common values to parent configs │
│ ✅ Use locals for complex logic │
│ ✅ Leverage built-in functions │
│ │
│ DEPENDENCIES: │
│ ✅ Explicitly declare dependencies between modules │
│ ✅ Use mock_outputs for faster planning │
│ ✅ Keep dependency chains shallow │
│ ✅ Document dependency relationships │
│ │
│ STATE MANAGEMENT: │
│ ✅ Use remote state with S3 backend │
│ ✅ Enable encryption and versioning │
│ ✅ Use DynamoDB for state locking │
│ ✅ Organize state files by environment │
│ │
│ MODULES: │
│ ✅ Keep Terraform modules independent of Terragrunt │
│ ✅ Make modules reusable across environments │
│ ✅ Version your modules │
│ ✅ Test modules independently │
│ │
│ TESTING: │
│ ✅ Use terragrunt validate to check syntax │
│ ✅ Use run-all plan before run-all apply │
│ ✅ Test in dev before promoting to production │
│ ✅ Use mock_outputs for faster validation │
│ │
│ CI/CD: │
│ ✅ Run terragrunt run-all plan in CI │
│ ✅ Require approval for run-all apply │
│ ✅ Use hooks for validation and linting │
│ ✅ Cache .terragrunt-cache between runs │
│ │
└──────────────────────────────────────────────────────────────┘Common Pitfalls
1. Circular Dependencies
# ❌ BAD: Circular dependency
# moduleA/terragrunt.hcl
dependency "moduleB" {
config_path = "../moduleB"
}
# moduleB/terragrunt.hcl
dependency "moduleA" {
config_path = "../moduleA"
}
# ✅ GOOD: Break the cycle
# Create a third module or use data sources2. Forgetting mock_outputs
# ❌ BAD: No mock outputs
dependency "vpc" {
config_path = "../vpc"
}
# Can't run 'plan' until VPC is applied!
# ✅ GOOD: Provide mock outputs
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-mock"
}
mock_outputs_allowed_terraform_commands = ["validate", "plan"]
}3. Not Using include
# ❌ BAD: Duplicating backend config everywhere
remote_state {
backend = "s3"
config = {
# Repeated in every terragrunt.hcl
}
}
# ✅ GOOD: Use include to inherit
include "root" {
path = find_in_parent_folders()
}4. Hardcoding Paths
# ❌ BAD: Hardcoded paths
terraform {
source = "/home/user/projects/terraform/modules/vpc"
}
# ✅ GOOD: Use relative paths and functions
terraform {
source = "${get_repo_root()}/modules//vpc"
}Terragrunt vs. Alternatives
┌──────────────────────────────────────────────────────────────┐
│ TERRAGRUNT VS. OTHER APPROACHES │
├──────────────────────────────────────────────────────────────┤
│ │
│ TERRAGRUNT vs. WORKSPACES: │
│ • Terragrunt: Separate state files, better isolation │
│ • Workspaces: Single state file, easier state management │
│ • Use Terragrunt for: Multi-account, complex environments │
│ • Use Workspaces for: Simple environment separation │
│ │
│ TERRAGRUNT vs. TERRAFORM CLOUD: │
│ • Terragrunt: Open source, self-hosted │
│ • TF Cloud: Managed service, built-in features │
│ • Use Terragrunt for: Cost savings, full control │
│ • Use TF Cloud for: Managed experience, team features │
│ │
│ TERRAGRUNT vs. CUSTOM SCRIPTS: │
│ • Terragrunt: Standardized approach, community support │
│ • Scripts: Full customization, no dependencies │
│ • Use Terragrunt for: Standard workflows, DRY configs │
│ • Use Scripts for: Highly custom workflows │
│ │
│ TERRAGRUNT vs. MODULES ONLY: │
│ • Terragrunt: Less duplication, easier multi-env mgmt │
│ • Modules Only: Simpler, no extra tooling │
│ • Use Terragrunt for: Many environments, complex setups │
│ • Use Modules for: Simple projects, few environments │
│ │
└──────────────────────────────────────────────────────────────┘When to Use Terragrunt
Good Use Cases:
Multiple Environments
- Dev, staging, production with similar structure
- Need to keep configurations DRY
Multi-Account/Multi-Region
- AWS Organizations with multiple accounts
- Deploying same infrastructure to multiple regions
Complex Dependency Chains
- Many interdependent modules
- Need automatic dependency resolution
Large Teams
- Need consistent structure
- Want to reduce configuration drift
Heavy Module Reuse
- Same modules used across many environments
- Want centralized module versioning
When NOT to Use Terragrunt:
Simple Projects
- Single environment
- Few modules
Learning Terraform
- Adds complexity for beginners
- Master Terraform first
Using Terraform Cloud
- TF Cloud provides similar features
- May be redundant
Team Unfamiliar with HCL
- Additional learning curve
- Terragrunt has its own syntax
Troubleshooting
Common Issues
Issue 1: Module Not Found
Error: Module not found: ../../../modules//vpc# Solution: Check source path
terraform {
source = "${get_repo_root()}/modules//vpc"
}
# Or use relative path
terraform {
source = "../../../modules//vpc"
}Issue 2: Dependency Cycle
Error: Cycle detected in dependencies# Solution: Visualize dependencies
terragrunt graph-dependencies | dot -Tpng > graph.png
# Identify and break the cycleIssue 3: State Locking Error
Error: Error acquiring state lock# Solution: Check lock table
aws dynamodb scan \
--table-name terraform-locks-prod \
--filter-expression "LockID = :lockid"
# Force unlock if needed (careful!)
cd specific/module/directory
terragrunt force-unlock <lock-id>Issue 4: .terragrunt-cache Issues
# Clear Terragrunt cache
find . -type d -name ".terragrunt-cache" -prune -exec rm -rf {} \;
# Or for specific directory
rm -rf .terragrunt-cache
# Re-initialize
terragrunt initDebug Mode
# Enable debug logging
export TERRAGRUNT_DEBUG=true
terragrunt plan
# More verbose logging
export TF_LOG=DEBUG
export TERRAGRUNT_DEBUG=true
terragrunt apply
# Disable auto-init
export TERRAGRUNT_AUTO_INIT=falseMigration to Terragrunt
Step-by-Step Migration
Step 1: Organize Existing Terraform
# Current structure
terraform/
├── dev/
│ └── main.tf
├── staging/
│ └── main.tf
└── prod/
└── main.tf
# Move to modules
terraform/
├── modules/
│ └── app/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
├── dev/
├── staging/
└── prod/Step 2: Create Root Configuration
# terraform/terragrunt.hcl
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
}
}Step 3: Create Module Configurations
# environments/dev/app/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules//app"
}
inputs = {
environment = "dev"
# ... other variables
}Step 4: Migrate State
# For each environment
cd environments/dev/app
# Initialize Terragrunt
terragrunt init
# Import existing state
terragrunt state pull > /tmp/old-state.tfstate
terragrunt state push /tmp/old-state.tfstate
# Verify
terragrunt plan # Should show no changesStep 5: Test
# Test planning
terragrunt run-all plan
# Test in dev first
cd environments/dev
terragrunt run-all apply
# Then promote to other environmentsSummary
Terragrunt is a powerful wrapper for Terraform that solves common problems when managing infrastructure at scale:
Key Benefits:
- DRY Configuration: Reduce duplication through inheritance
- Remote State Management: Automatic backend configuration
- Module Dependencies: Automatic dependency resolution
- Multi-Module Operations: Run commands across multiple modules
- Environment Management: Easy management of multiple environments
When to Use:
- Multiple environments (dev, staging, prod)
- Multi-account or multi-region deployments
- Complex module dependencies
- Large teams needing consistency
- Heavy module reuse
Remember:
- Keep Terraform modules independent of Terragrunt
- Use
includeto avoid duplication - Define dependencies explicitly
- Provide
mock_outputsfor faster planning - Test in lower environments first
- Use
run-allcommands for multi-module operations
External Resources: