backend "s3" and provider "aws" blocks into 150 different directories. This violates the DRY (Don’t Repeat Yourself) principle. Let’s fix this using Terragrunt.1. The WET (We Enjoy Typing) Problem#
In Episode 11, we established that Directory Isolation is the safest way to manage multiple environments. However, look at the code required for a typical Staging folder:
# staging/main.tf
terraform {
backend "s3" {
bucket = "my-company-state"
key = "staging/app1/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
}
}
provider "aws" {
region = "us-east-1"
}
module "app" {
source = "../../modules/web-app"
env = "staging"
}Now imagine the production/main.tf file. The only things that change are the key path and the env variable. The rest is pure boilerplate.
If your Security Team decides to move the state bucket to a new AWS Account, you have to manually open 150 files and change the bucket name. This is unmanageable at an enterprise scale.
2. Introducing Terragrunt#
Terragrunt is an open-source, thin wrapper for Terraform created by Gruntwork.
Instead of running terraform apply, you run terragrunt apply. Terragrunt intercepts the command, dynamically generates the boilerplate (backend, providers) on the fly, injects it into the directory, and then passes the execution down to the core Terraform binary.
Installing Terragrunt#
Terragrunt is a single Go binary, just like Terraform.
# macOS
brew install terragrunt
# Linux
curl -LO https://github.com/gruntwork-io/terragrunt/releases/download/v0.50.14/terragrunt_linux_amd64
chmod +x terragrunt_linux_amd64
sudo mv terragrunt_linux_amd64 /usr/local/bin/terragrunt
# Verify
terragrunt --version3. Practice: Refactoring to a DRY Architecture#
Let’s rebuild our environment structure using Terragrunt. We replace the static main.tf files with dynamic terragrunt.hcl files.
Step 3.1: The New Directory Structure#
terragrunt-environments/
├── terragrunt.hcl # <--- The Global Configuration (Root)
├── staging/
│ └── app-1/
│ └── terragrunt.hcl # <--- The Child Configuration
└── production/
└── app-1/
└── terragrunt.hcl # <--- The Child ConfigurationStep 3.2: The Global Configuration (Root)#
In the root terragrunt.hcl file, we define the configuration that is identical across ALL environments.
# terragrunt-environments/terragrunt.hcl
# 1. Dynamically generate the Provider block
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}
# 2. Dynamically generate the Remote Backend block
remote_state {
backend = "s3"
# Automatically generate the S3 key path based on the folder structure!
config = {
bucket = "my-company-state-terragrunt"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}Notice the ${path_relative_to_include()} function. If Terragrunt is executed from the staging/app-1/ folder, it will automatically set the S3 key to staging/app-1/terraform.tfstate. Absolute magic!
Step 3.3: The Child Configurations#
Now, navigate into the Staging folder.
# terragrunt-environments/staging/app-1/terragrunt.hcl
# 1. Inherit the Global Configuration (Provider & Backend)
include {
path = find_in_parent_folders()
}
# 2. Point to the Terraform Module
terraform {
source = "git::https://github.com/my-company/modules.git//web-app?ref=v1.0.0"
}
# 3. Pass in the environment-specific variables
inputs = {
env = "staging"
instance_type = "t3.micro"
}This is incredibly clean. The Staging folder only contains the data that makes Staging unique.
4. Execution and Observation#
Navigate into the staging directory and run Terragrunt.
cd terragrunt-environments/staging/app-1/
terragrunt planWhat Terragrunt does behind the scenes:
- It reads the local
terragrunt.hcl. - It sees the
includeblock, traverses up the directory tree, and finds the rootterragrunt.hcl. - It downloads the Terraform module from the
sourceURL into a hidden.terragrunt-cache/folder. - It generates the
provider.tffile and injects thebackend "s3"configuration into the cache. - It runs
terraform planinside the cache, passing theinputsas CLI variables.
Expected Terminal Output:
[terragrunt] [/staging/app-1] Running command: terraform plan
...
Plan: 1 to add, 0 to change, 0 to destroy.If you ever need to change the S3 bucket name in the future, you change it exactly once in the root terragrunt.hcl, and all 150 environments will automatically inherit the update.
Troubleshooting & Common Errors#
terragrunt.hcl not found in parent folders- Root Cause: In the child
terragrunt.hcl, thefind_in_parent_folders()function traversed all the way up to your computer’s root directory (/) and could not find the masterterragrunt.hclfile. - Solution: Ensure the master
terragrunt.hclis located in a directory above your current working directory.
- Root Cause: In the child
Error: Unreadable module directory- Root Cause: Terragrunt failed to download the module specified in the
sourceblock because of a typo in the URL, or you lack SSH/Git credentials to clone a private repository. - Solution: Verify the
sourceURL format carefully, especially the double slash//syntax used to specify a subdirectory within a Git repository.
- Root Cause: Terragrunt failed to download the module specified in the
Conclusion & Next Steps#
Terragrunt is the ultimate weapon against boilerplate configuration. By implementing Terragrunt, your Infrastructure as Code repositories will remain pristine, manageable, and highly scalable, no matter how many microservices your company deploys.
You have mastered the architecture. But how do you ensure the code you wrote actually works before deploying it to Production?
In the final episode of this series, Episode 15: Infrastructure Unit Testing with Terratest (Golang), we will write automated Go tests that actually provision your modules, assert that the AWS resources are configured correctly, and then tear them down.

