Introduction
Kalau Anda sudah pernah pakai Terraform untuk manage infrastruktur cloud, cepat atau lambat Anda akan merasakan beberapa hal ini:
- DRY violation — backend config, provider, tag, region diulang di setiap folder stack. Mau tambah env baru? Copy-paste lagi.
- State explosion — satu root module per env per region per cloud = puluhan folder dengan konfigurasi nyaris identik.
- Cross-stack dependency — VPC ID perlu di-passing ke EKS, EKS endpoint ke monitoring, dan seterusnya. Terraform native tidak punya cara elegan untuk ini.
- CI/CD drift — setiap stack harus di-plan/apply manual, atau Anda tulis orchestration sendiri.
Di sinilah Terragrunt masuk. Terragrunt adalah thin wrapper untuk Terraform yang menambahkan:

| Masalah | Solusi Terragrunt |
| Konfigurasi berulang | root.hcl + include block → config ditulis sekali |
| Backend boilerplate | remote_state block diwariskan ke semua stack |
| Cross-stack dependency | dependency block baca output stack lain |
| Multi-env orchestration | terragrunt run-all plan/apply |
| Provider generation | Generate block (dynamic provider per stack) |
▎ TL;DR: Terragrunt bukan replacement Terraform — Terragrunt tetap panggil terraform di belakang layar. Fungsinya adalah mengorkestrasi banyak root module Terraform dengan konfigurasi bersama.
iac/
├── modules/ # reusable, versioned Terraform modules
│ ├── aws/
│ │ ├── network/ # VPC + subnets + NAT + IGW
│ │ ├── sg/ # Security group reusable
│ │ └── ec2/ # EC2 instance (public/private variant)
└── live/ # Terragrunt stack tree
├── aws/
│ ├── root.hcl # region, provider, remote_state, common inputs
│ ├── dev/
│ │ ├── vpc/ # → uses modules/aws/network
│ │ ├── sg/ # → uses modules/aws/sg
│ │ ├── ec2-public/ # → uses modules/aws/ec2
│ │ └── ec2-private/ # → uses modules/aws/ec2
Kita coba untuk environment Dev
Konsep kunci:
- root.hcl di setiap cloud = single source of truth untuk region, account, remote state, dan provider.
- Setiap folder di bawah live/{env}/ adalah satu stack Terragrunt independen dengan state-nya sendiri.
- Module di modules/ adalah Terraform root module reusable, dipanggil via terraform { source = “../../../modules/aws/vpc” }.
Step 1 — Struktur Folder
Pisahkan module (reusable) dari live (env-specific):
mkdir -p iac/modules/aws/{network,sg,ec2}
mkdir -p iac/live/aws/dev/{vpc,sg,ec2-public,ec2-private}
mkdir -p iac/live/aws/prod/network
Prinsip: module = kode Terraform polos, tidak tahu env. Live = Terragrunt config, tahu env, region, akun.
Step 2 — Tulis Module (Terraform)
Module tidak perlu tahu soal Terragrunt. Contoh modules/aws/network/main.tf:
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
tags = merge(var.tags, {
Name = var.name
})
}
resource "aws_subnet" "public" {
count = length(var.public_subnets)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnets[count.index]
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, { Name = "${var.name}-public-${count.index + 1}" })
}
# ... dst: private subnet, IGW, NAT, route table
Output outputs.tf:
output "vpc_id" {
value = aws_vpc.this.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
▎ Module harus input-driven — semua yang berbeda antar env/vpc dilewatkan via var.*.
Step 3 — root.hcl
live/aws/root.hcl — single source of truth untuk AWS:
locals {
aws_region = "ap-southeast-3"
common_tags = {
ManagedBy = "terragrunt"
Project = "platform-engineer"
}
}
# Backend state (S3 + DynamoDB lock) — comment out dulu kalau masih local
# remote_state {
# backend = "s3"
# config = {
# bucket = "my-tf-state-ap-southeast-3"
# key = "${path_relative_to_include()}/terraform.tfstate"
# region = local.aws_region
# dynamodb_table = "my-tf-locks"
# encrypt = true
# }
# }
# Provider di-generate per stack
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "${local.aws_region}"
default_tags {
tags = ${jsonencode(local.common_tags)}
}
}
EOF
}
Step 4 — terragrunt.hcl per Stack
Setiap stack cukup include root, lalu isi input spesifik env.
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/aws/network"
}
inputs = {
name = "vpc-dev"
cidr_block = "10.10.0.0/16"
azs = ["ap-southeast-3a", "ap-southeast-3b"]
public_subnets = ["10.10.1.0/24", "10.10.2.0/24"]
private_subnets = ["10.10.11.0/24", "10.10.12.0/24"]
tags = {
Environment = "dev"
}
}
live/aws/dev/ec2-public/terragrunt.hcl — contoh cross-stack dependency:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/aws/ec2"
}
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
vpc_id = "vpc-mock"
public_subnet_ids = ["subnet-mock-1", "subnet-mock-2"]
}
}
dependency "sg" {
config_path = "../sg"
mock_outputs = {
sg_id = "sg-mock"
}
}
inputs = {
name = "ec2-public-dev"
subnet_id = dependency.vpc.outputs.public_subnet_ids[0]
security_group_id = dependency.sg.outputs.sg_id
associate_public_ip = true
}
Perhatikan mock_outputs — ini membuat stack EC2 bisa di-plan meskipun stack VPC belum di-apply. Penting untuk CI/PR flow.
Step 5 — Plan & Apply
Single stack:
cd iac/live/aws/dev/vpc terragrunt validate terragrunt init terragrunt plan terragrunt apply
Semua stack di AWS sekaligus:
cd iac/live/aws terragrunt run-all plan terragrunt run-all apply # hati-hati!
Output dari stack lain:
cd iac/live/aws/dev/vpc terragrunt output
Step 6 — Tambah Env Baru (Misal staging)
Seluruh proses cuma copy + edit minimal:
mkdir -p iac/live/aws/staging/network cp iac/live/aws/dev/vpc/terragrunt.hcl iac/live/aws/staging/network/
Edit 2 hal: # - name = "vpc-staging" # - cidr_block = "10.50.0.0/16" # - tags.Environment = "staging"
Backend, provider, region — semua otomatis dari root.hcl. Zero duplication.
State Management
State Terragrunt tersimpan di .terragrunt-cache/ (lokal) atau di remote backend (S3/GCS).
Untuk production, migrate ke remote:
AWS (S3 + DynamoDB lock):
aws s3api create-bucket --bucket my-tf-state --region ap-southeast-3 \
--create-bucket-configuration LocationConstraint=ap-southeast-3
aws s3api put-bucket-versioning --bucket my-tf-state \
--versioning-configuration Status=Enabled
aws dynamodb create-table --table-name my-tf-locks \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
Lalu uncomment block remote_state di root.hcl.

Admin website igunawan.com, System Administrator, DevOps Engineer