Skip to main content

Terraform Ep 14: Achieving DRY Architecture with Terragrunt

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
terraform - This article is part of a series.
Part 14: This Article
Terraform is an incredible tool, but it lacks native support for dynamic backend configurations. If you have 50 microservices deployed across 3 environments (Dev, Staging, Prod), you must manually copy-paste the 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 --version

3. 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 Configuration

Step 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 plan

What Terragrunt does behind the scenes:

  1. It reads the local terragrunt.hcl.
  2. It sees the include block, traverses up the directory tree, and finds the root terragrunt.hcl.
  3. It downloads the Terraform module from the source URL into a hidden .terragrunt-cache/ folder.
  4. It generates the provider.tf file and injects the backend "s3" configuration into the cache.
  5. It runs terraform plan inside the cache, passing the inputs as 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
#

  1. terragrunt.hcl not found in parent folders

    • Root Cause: In the child terragrunt.hcl, the find_in_parent_folders() function traversed all the way up to your computer’s root directory (/) and could not find the master terragrunt.hcl file.
    • Solution: Ensure the master terragrunt.hcl is located in a directory above your current working directory.
  2. Error: Unreadable module directory

    • Root Cause: Terragrunt failed to download the module specified in the source block because of a typo in the URL, or you lack SSH/Git credentials to clone a private repository.
    • Solution: Verify the source URL format carefully, especially the double slash // syntax used to specify a subdirectory within a Git repository.

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.

terraform - This article is part of a series.
Part 14: This Article