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 destroyINFORMATIONTip 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 |