Terragrunt
Terragrunt = wrapper ของ Terraform ที่ทำให้ DRY + จัด multi-state ง่าย — โดย Gruntwork
ทำไมต้อง Terragrunt?
Pure Terraform ใน multi-env ใหญ่ๆ มีปัญหา:
❌ Duplicate Backend Config
dev/network/main.tf
terraform {
backend "s3" {
bucket = "my-tfstate"
key = "dev/network/terraform.tfstate" # ← repeated everywhere
region = "ap-southeast-1"
}
}
prod/network/main.tf
terraform {
backend "s3" {
bucket = "my-tfstate"
key = "prod/network/terraform.tfstate" # ← repeated
region = "ap-southeast-1"
}
}
❌ Duplicate Module Calls
dev/network/main.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
# ... config ...
}
prod/network/main.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
# ... near-identical config ...
}
❌ Manual Apply Order
cd dev/network && terraform apply
cd ../data && terraform apply
cd ../compute && terraform apply
# repeat for staging, prod
→ Terragrunt แก้ทุกข้อนี้
Install
brew install terragrunt
# Or via tenv
tenv tg install latest
# Verify
terragrunt --version
Project Structure
infra/
├── terragrunt.hcl # root config (shared)
├── modules/ # Terraform modules
│ ├── vpc/
│ ├── eks/
│ └── rds/
└── live/ # actual deployments
├── dev/
│ ├── account.hcl
│ ├── network/
│ │ └── terragrunt.hcl
│ ├── data/
│ │ └── terragrunt.hcl
│ └── compute/
│ └── terragrunt.hcl
├── staging/
│ └── ... (same)
└── prod/
└── ... (same)
Root terragrunt.hcl (DRY Backend)
terragrunt.hcl
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "my-tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}
# Shared provider config
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<-EOF
provider "aws" {
region = "ap-southeast-1"
default_tags {
tags = {
ManagedBy = "terragrunt"
}
}
}
EOF
}
→ ทุก stack ใต้ folder นี้ใช้ backend + provider เดียวกัน
- key auto-generated:
dev/network/terraform.tfstate,prod/data/terraform.tfstate, ...
Stack terragrunt.hcl
live/dev/network/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/vpc"
}
inputs = {
name = "dev-vpc"
cidr = "10.10.0.0/16"
azs = ["ap-southeast-1a", "ap-southeast-1b"]
env_tags = {
Environment = "dev"
}
}
Apply
cd live/dev/network
terragrunt apply
→ Terragrunt:
- Generate
backend.tf+provider.tf - Symlink module from
../../../modules/vpc - Run
terraform init - Run
terraform applywith inputs
Run-All (Apply Multiple Stacks)
cd live/dev
terragrunt run-all apply
→ Apply ทุก stack ใน live/dev/ ตามลำดับ dependency
Dependencies
live/dev/compute/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/compute"
}
dependency "network" {
config_path = "../network"
}
dependency "data" {
config_path = "../data"
}
inputs = {
name = "dev-compute"
vpc_id = dependency.network.outputs.vpc_id
subnet_ids = dependency.network.outputs.private_subnet_ids
db_endpoint = dependency.data.outputs.endpoint
}
→ Terragrunt apply network + data ก่อน compute อัตโนมัติ
Environment-Specific Inputs
live/dev/account.hcl
locals {
account_id = "111111111111"
env = "dev"
region = "ap-southeast-1"
}
live/dev/network/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
locals {
account = read_terragrunt_config(find_in_parent_folders("account.hcl"))
}
terraform {
source = "../../../modules/vpc"
}
inputs = {
name = "${local.account.locals.env}-vpc"
env = local.account.locals.env
cidr = local.account.locals.env == "prod" ? "10.0.0.0/16" : "10.10.0.0/16"
}
Hook Pre/Post Commands
terraform {
before_hook "format" {
commands = ["plan", "apply"]
execute = ["terraform", "fmt"]
}
after_hook "notify" {
commands = ["apply"]
execute = ["./scripts/notify-slack.sh"]
run_on_error = true
}
}
Multiple Includes
include "root" {
path = find_in_parent_folders()
}
include "env" {
path = find_in_parent_folders("env.hcl")
}
include "tags" {
path = find_in_parent_folders("tags.hcl")
expose = true
merge_strategy = "deep"
}
Run Specific Stacks
# Apply เฉพาะ network
terragrunt apply
# Apply ทุกอย่างใน dev
terragrunt run-all apply
# Apply เฉพาะ stacks ที่ change
terragrunt run-all apply --terragrunt-include-dir live/dev/network
# Exclude
terragrunt run-all apply --terragrunt-exclude-dir live/dev/legacy
Plan All
cd live/dev
terragrunt run-all plan
→ Run plan ทุก stack — ดู diff ก่อน apply
Destroy All
cd live/dev
terragrunt run-all destroy
→ Destroy ตาม reverse dependency order (compute → data → network)
ตัวอย่าง: Real Project
infra/
├── terragrunt.hcl # root: backend + provider
├── _envcommon/
│ ├── network.hcl # shared network config
│ └── compute.hcl # shared compute config
├── modules/
│ ├── network/
│ ├── eks/
│ ├── rds/
│ └── monitoring/
└── live/
├── dev/
│ ├── account.hcl
│ ├── network/terragrunt.hcl
│ ├── data/terragrunt.hcl
│ ├── platform/terragrunt.hcl
│ └── apps/
│ ├── auth/terragrunt.hcl
│ └── api/terragrunt.hcl
├── staging/
│ └── ...
└── prod/
└── ...
Variables Inheritance
live/dev/account.hcl
locals {
env = "dev"
account_id = "111111111111"
}
live/dev/network/terragrunt.hcl
locals {
account = read_terragrunt_config(find_in_parent_folders("account.hcl"))
}
inputs = {
account_id = local.account.locals.account_id
env = local.account.locals.env
}
→ DRY: env config ที่ root, override per-stack ถ้าต้องการ
CI/CD Integration
.github/workflows/terragrunt.yml
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Setup Terragrunt
uses: autero1/action-terragrunt@v3
with:
terragrunt-version: 0.62.0
- name: Plan All
working-directory: live/${{ inputs.env }}
run: terragrunt run-all plan
- name: Apply All
working-directory: live/${{ inputs.env }}
run: terragrunt run-all apply --terragrunt-non-interactive
Best Practices
✅ DO:
- ใช้ Terragrunt สำหรับ multi-env, multi-stack
- DRY backend + provider ใน root terragrunt.hcl
- Folder structure clear (live/ vs modules/)
- Pin Terraform + Terragrunt + module versions
- Use dependencies block สำหรับ wiring
❌ DON'T:
- ห้ามใช้ Terragrunt ใน project เล็ก (overkill)
- ห้ามมี dependency cycles
- ห้าม hard-code account ID (use account.hcl)
- ห้าม run apply โดยไม่ plan ก่อน
เปรียบเทียบ: Terraform vs Terragrunt
| Feature | Terraform | Terragrunt |
|---|---|---|
| DRY backend | ❌ Manual | ✅ Auto |
| Multi-stack apply | ❌ Manual | ✅ run-all |
| Dependency wiring | ❌ Manual remote_state | ✅ dependency block |
| Code generation | ❌ | ✅ generate block |
| Hooks | ❌ | ✅ before/after hooks |
| Learning curve | Low | Higher |
ทางเลือก
- Atmos — similar tool โดย Cloud Posse
- Terraspace — Ruby-based wrapper
- Pulumi — different IaC paradigm (programming language)
- Native Terraform 1.x stacks (preview) — HashiCorp's own multi-stack solution
สรุป
- Terragrunt = wrapper ทำให้ multi-state DRY
- Root config = shared backend + provider
- Stack config = source + inputs
- Dependencies = wire stacks together
- run-all = apply/plan/destroy ทุก stack
- เหมาะกับ multi-env + multi-stack project (10+ stacks)
- เกิน overkill สำหรับ project เล็ก
ต่อไป → Infracost