Stage 4 · Provision
Modules & Composition
Composition with Terragrunt
include blocks, dependency outputs, generate blocks, and when Terragrunt adds operational risk — DRY Terraform at scale.
What Is Terragrunt?
Terragrunt is a thin wrapper for Terraform that provides DRY configuration, dependency management, and environment promotion. It reduces boilerplate in multi-environment setups by generating backend and provider configurations.
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
bucket = "my-terraform-state-${get_aws_account_id()}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
generate "provider" {
path = "provider.tf"
if_exists = "overwrite"
contents = <<EOF
provider "aws" {
region = "us-east-1"
}
EOF
}The root terragrunt.hcl generates backend and provider configurations. path_relative_to_include() creates a unique state file path per module. The generate block creates files automatically.
Include Blocks
# Simple include
include "root" {
path = find_in_parent_folders()
}
# Merge strategy
include "root" {
path = find_in_parent_folders()
merge_strategy = "deep"
}
# Environment-specific include
include "env" {
path = "../env.hcl"
merge_strategy = "shallow"
}include inherits configuration from a parent terragrunt.hcl. find_in_parent_folders() locates the root config. merge_strategy determines how configurations are combined: shallow replaces, deep merges.
Dependency Outputs
# terragrunt/prod/eks/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
}
dependency "security" {
config_path = "../security"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
kms_key_id = dependency.security.outputs.kms_key_id
}dependency blocks declare inter-module dependencies. Terragrunt ensures dependencies are applied first and passes their outputs. This eliminates manual wiring between modules.
Generate Blocks
generate "backend" {
path = "backend.tf"
if_exists = "overwrite"
contents = <<EOF
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
EOF
}
generate "versions" {
path = "versions.tf"
if_exists = "overwrite"
contents = <<EOF
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
EOF
}Generate blocks create .tf files automatically. if_exists controls behavior when the file already exists: overwrite replaces it, skip preserves it. Generated files should not be committed to Git.
Terragrunt Structure
terragrunt/
terragrunt.hcl # Root config
_envcommon/ # Shared module configs
vpc.hcl
eks.hcl
rds.hcl
dev/
env.hcl # Dev-specific values
vpc/
terragrunt.hcl
eks/
terragrunt.hcl
staging/
env.hcl
vpc/
terragrunt.hcl
prod/
env.hcl
vpc/
terragrunt.hcl
eks/
terragrunt.hclThe _envcommon directory contains shared module definitions. Per-environment directories override values. The env.hcl file defines environment-specific variables. This eliminates duplication while maintaining isolation.
When Terragrunt Adds Risk
- Extra abstraction layer — debugging requires understanding both Terragrunt and Terraform.
- Generated files — if generation fails, the wrong configuration may be used.
- Dependency ordering — complex dependency graphs can cause unexpected apply order.
- Learning curve — new team members must learn Terragrunt syntax.
- Version coupling — Terragrunt and Terraform versions must be compatible.
$ terragrunt plan # Plan all modules
$ terragrunt apply # Apply all modules
$ terragrunt run-all plan # Plan with dependency ordering
$ terragrunt run-all apply # Apply with dependency ordering
# Target specific module
$ terragrunt plan --terragrunt-config-dir prod/vpcrun-all applies modules in dependency order. The --terragrunt-config-dir flag targets a specific module. Use targeted commands for debugging and specific environments.
Terragrunt adds value when you have multiple environments and shared modules. For fewer than 5 environments, directory-per-environment with raw Terraform is simpler.
Generated files should not be committed to Git. They are recreated by Terragrunt on every run. Add generated file patterns to .gitignore.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.