Skip to main content

Terragrunt

Terragrunt is a thin wrapper around Terraform that adds features for DRY (Don't Repeat Yourself) configurations — automatically configuring remote state backends, enabling module dependency management, and reducing copy-paste across environments.

What Is Terragrunt?

Terragrunt is a small command-line wrapper that sits on top of Terraform — it does not replace Terraform, it calls Terraform for you with extra setup done automatically first. Its entire reason for existing is one observation: if you have five environments, you almost certainly have five nearly-identical backend.tf blocks, and copy-pasting that block five times means fixing a typo five times too.

Think of Terragrunt like a build script that runs before your actual build. Before it calls terraform apply, it generates the right backend configuration for whichever folder you are standing in, resolves any dependencies on other modules, and only then hands control to the real terraform binary underneath.

At CRED, the platform team manages 14 separate Terraform modules — networking, six microservices, two databases, monitoring, and a few shared IAM roles — across three environments. That is 42 potential backend.tf files if written natively. With Terragrunt, there is exactly one remote_state block at the root of the repository, and every one of those 42 folders inherits it automatically.

The Problem Terragrunt Solves

HCL
# WITHOUT Terragrunt — this exact block, with only the "key" changed,
# has to be copy-pasted into every single environment folder
terraform {
backend "s3" {
bucket = "cred-terraform-state"
key = "prod/ecs-service/terraform.tfstate" # only this line differs per folder
region = "ap-south-1"
dynamodb_table = "terraform-lock"
encrypt = true
}
}
HCL
# WITH Terragrunt — write this ONCE at the repo root
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite" # Terragrunt writes the backend.tf file for you, every time
}
config = {
bucket = "cred-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate" # auto-fills the path
region = "ap-south-1"
dynamodb_table = "terraform-lock"
encrypt = true
}
}

terragrunt.hcl Structure

HCL
# environments/prod/ecs-service/terragrunt.hcl
include "root" {
path = find_in_parent_folders() # pulls in the remote_state block from the repo root
}
terraform {
source = "../../../modules//ecs-service" # points at the actual Terraform module
}
inputs = {
service_name = "orders-api"
cpu = 1024
memory = 2048
}

The dependency Block

HCL
# This module needs the VPC module's outputs before it can run
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnets
}

The generate Block

Beyond backends, generate can write any file — commonly used for provider configuration that would otherwise be duplicated across every module:

HCL
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<-EOF
provider "aws" {
region = "ap-south-1"
default_tags {
tags = { ManagedBy = "terragrunt" }
}
}
EOF
}

run-all Commands

Bash
# Plans every Terragrunt module under the current directory, in dependency order
terragrunt run-all plan
# Applies every module under the current directory — VPC before ECS service, automatically
terragrunt run-all apply
# Destroys in REVERSE dependency order — ECS service before VPC, so nothing is orphaned
terragrunt run-all destroy
Note

Tip from a senior engineer: run-all apply is powerful and dangerous in the same breath. On a large repo it can apply twenty modules unattended. Always run run-all plan first and actually read the combined output before trusting run-all apply against production.

Quick Reference

Terragrunt Concept What It Does
remote_state Defines the backend once, generates backend.tf everywhere it's included
include Pulls configuration from a parent terragrunt.hcl
dependency Reads another module's outputs before this one runs
generate Writes any file (providers, backend) automatically per folder
inputs Passes variables into the underlying Terraform module
Error Root Cause Fix
Could not find terragrunt.hcl Running terragrunt from outside a Terragrunt-managed folder cd into a folder containing a terragrunt.hcl file
dependency.x.outputs is empty Dependency module not yet applied Use run-all apply so dependencies apply in correct order first
Version mismatch errors Terragrunt and Terraform versions incompatible Check Terragrunt's release notes for its tested Terraform version range
Generated backend.tf not updating Stale file from if_exists = "skip" Set if_exists = "overwrite" in the generate block

Frequently Asked Questions

What specific pain does Terragrunt solve that plain Terraform doesn't?

Plain Terraform has no native way to avoid repeating the same backend block, provider config, and module source/version across every environment folder — copy-pasting means updating N files whenever the backend bucket name or provider version changes. Terragrunt introduces a `terragrunt.hcl` per environment that inherits shared config from parent files, generates the backend and provider blocks automatically, and can run `apply` across a dependency graph of modules in the right order.

When should you reach for Terragrunt instead of native Terraform features?

Reach for it when you're maintaining many near-identical environments (e.g. 10+ AWS accounts with the same module set) and are fighting copy-paste drift — its `run-all` and dependency blocks genuinely help there. Skip it for a handful of environments where Terraform workspaces or a simple module + tfvars pattern suffice, since Terragrunt adds a second tool, its own state/config quirks, and a learning curve that isn't always worth it at small scale.