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
# WITHOUT Terragrunt — this exact block, with only the "key" changed,# has to be copy-pasted into every single environment folderterraform { 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 }}# WITH Terragrunt — write this ONCE at the repo rootremote_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
# 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
# This module needs the VPC module's outputs before it can rundependency "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:
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
# Plans every Terragrunt module under the current directory, in dependency orderterragrunt run-all plan # Applies every module under the current directory — VPC before ECS service, automaticallyterragrunt run-all apply # Destroys in REVERSE dependency order — ECS service before VPC, so nothing is orphanedterragrunt run-all destroyNoteTip from a senior engineer:
run-all applyis powerful and dangerous in the same breath. On a large repo it can apply twenty modules unattended. Always runrun-all planfirst and actually read the combined output before trustingrun-all applyagainst 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.