Skip to main content

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:

  1. Generate backend.tf + provider.tf
  2. Symlink module from ../../../modules/vpc
  3. Run terraform init
  4. Run terraform apply with 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

FeatureTerraformTerragrunt
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 curveLowHigher

ทางเลือก

  • 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