Every Terraform project eventually hits the same problem: three environments with nearly identical configuration, copy-pasted across directories with slight differences. Terragrunt is a thin wrapper around Terraform that eliminates this duplication.
The Problem Terragrunt Solves
Typical multi-environment Terraform layout:
environments/
āāā dev/
ā āāā main.tf # 90% identical to staging
ā āāā variables.tf
ā āāā backend.tf # Different state bucket
āāā staging/
ā āāā main.tf # 90% identical to production
ā āāā variables.tf
ā āāā backend.tf
āāā production/
āāā main.tf
āāā variables.tf
āāā backend.tfThree copies of nearly the same code. Change a module version? Update three files. Add a new resource? Three places.
Terragrunt Structure
live/
āāā terragrunt.hcl # Root config (backend, provider)
āāā dev/
ā āāā terragrunt.hcl # Environment-specific values
āāā staging/
ā āāā terragrunt.hcl
āāā production/
āāā terragrunt.hcl
modules/
āāā app-infrastructure/ # Single Terraform module
āāā main.tf
āāā variables.tf
āāā outputs.tfOne module. Three small config files. Zero duplication.
Master this topic with hands-on labs
Go beyond reading ā build real projects in sandboxed environments with expert video guidance.
Browse Courses āRoot Configuration
# live/terragrunt.hcl
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "myorg-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "eu-west-1"
}
EOF
}Every environment inherits this. The path_relative_to_include() function ensures each environment gets a unique state key.
Environment Configuration
# live/dev/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules/app-infrastructure"
}
inputs = {
environment = "dev"
instance_type = "t3.small"
instance_count = 1
enable_cdn = false
}# live/production/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules/app-infrastructure"
}
inputs = {
environment = "production"
instance_type = "t3.large"
instance_count = 3
enable_cdn = true
}The only differences are the values. The infrastructure code is shared.
Dependency Management
When one stack depends on another:
# live/production/app/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
}
dependency "database" {
config_path = "../database"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
db_url = dependency.database.outputs.connection_string
}# Apply everything in the right order
cd live/production
terragrunt run-all applyTerragrunt resolves the dependency graph: VPC first, then database, then app. Parallel execution where dependencies allow.
Get weekly IT automation tips
Docker, Ansible, Terraform, MLOps ā curated insights delivered to your inbox. No spam.
Subscribe Free āCommands
# Apply a single environment
cd live/dev
terragrunt apply
# Apply all environments
cd live
terragrunt run-all apply
# Plan across all environments
terragrunt run-all plan
# Destroy in reverse dependency order
terragrunt run-all destroyCommon Patterns
Environment-Specific Variables
# live/terragrunt.hcl
locals {
environment = basename(get_terragrunt_dir())
env_config = {
dev = {
instance_type = "t3.small"
min_size = 1
}
staging = {
instance_type = "t3.medium"
min_size = 2
}
production = {
instance_type = "t3.large"
min_size = 3
}
}
}
inputs = local.env_config[local.environment]Before and After Hooks
terraform {
before_hook "validate" {
commands = ["apply", "plan"]
execute = ["tflint", "--init"]
}
after_hook "notify" {
commands = ["apply"]
execute = ["slack-notify", "Terraform apply completed"]
}
}When to Use Terragrunt
Good fit: - 3+ environments with shared infrastructure code - Teams managing multiple AWS accounts or regions - Complex dependency chains between infrastructure stacks - Need for consistent remote state configuration
Not needed: - Single environment projects - Simple infrastructure managed by one team - Already using Terraform workspaces successfully
Terragrunt adds a layer of abstraction. If your Terraform is simple enough, that layer adds complexity without benefit. If you are maintaining copy-pasted Terraform across environments, Terragrunt pays for itself immediately.
---
Ready to go deeper? Master Terraform with hands-on courses at CopyPasteLearn.
Ready to learn by doing?
Stop reading tutorials ā start building. Expert video courses with hands-on labs in real sandboxed environments.
Related Articles
Ansible vs Terraform When to Use
Ansible and Terraform solve different infrastructure problems. Learn when to use each, when to use both together, and how they complement each other.
Terraform CDK vs HCL Comparison
Terraform CDK lets you write infrastructure in TypeScript, Python, or Go instead of HCL. Compare CDKTF and HCL for real-world use cases and learn when each.
Terraform Workspaces Multi-Env
Manage environments with Terraform workspaces. CLI workspaces, Terraform Cloud workspaces, directory isolation, and trade-offs.
Testkube Kubernetes Testing Guide
Testkube runs tests natively on Kubernetes using any testing framework. Learn how to run integration tests, load tests, and API tests inside your cluster.
Thanos Long-Term Prometheus Storage
Thanos extends Prometheus with unlimited retention, global querying across clusters, and downsampling. Learn how to deploy Thanos Sidecar and Store Gateway.
Tilt Kubernetes Dev Environment
Tilt automates the build-push-deploy loop for Kubernetes development. Learn how Tilt watches code changes, rebuilds containers, and updates deployments.
Explore topics
Browse more articles on the topics covered here.