Root vs Child Module #
Setiap konfigurasi Terraform yang kamu jalankan dengan terraform apply adalah root module — direktori tempat kamu berada. Root module adalah orkestrator: ia memanggil child module, mengoper nilai ke dalamnya, dan mengambil output darinya. Child module adalah unit kerja yang bisa dipanggil berkali-kali dari tempat berbeda. Memahami pembagian tanggung jawab ini — mana yang seharusnya ada di root, mana yang seharusnya ada di child — adalah kunci arsitektur Terraform yang bersih.
flowchart TD
A["Root Module\n(entry point)"] -->|"module calls"| B["Child Module A"]
A -->|"module calls"| C["Child Module B"]
B -->|"outputs"| A
C -->|"outputs"| A
B -->|"resource refs"| C
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#f59e0b,stroke:#d97706,color:#fffRoot Module: Orkestrator #
Root module adalah titik masuk dari setiap terraform apply. Ia yang membaca variable dari tfvars, memanggil child module, dan mengelem semuanya bersama.
# environments/production/main.tf — ini adalah root module
# Root module memanggil child module dan mengoper konfigurasi
module "vpc" {
source = "../../modules/vpc"
cidr_block = var.vpc_cidr # Nilai dari variable root module
environment = var.environment
}
module "eks" {
source = "../../modules/eks"
cluster_name = "${var.environment}-eks"
vpc_id = module.vpc.vpc_id # Output dari module lain
private_subnet_ids = module.vpc.private_subnet_ids
}
module "rds" {
source = "../../modules/rds"
identifier = "${var.environment}-db"
vpc_id = module.vpc.vpc_id
private_subnet_ids = module.vpc.private_subnet_ids
}
# Root module juga bisa punya resource langsung (bukan di child module)
# untuk resource yang terlalu spesifik untuk di-abstraksi
resource "aws_route53_record" "app" {
zone_id = var.hosted_zone_id
name = "app.${var.domain}"
type = "CNAME"
ttl = 300
records = [module.eks.load_balancer_dns]
}
Child Module: Unit Kerja #
Child module berisi implementasi dari satu “konsep” infrastruktur. Ia tidak tahu di mana dipanggil, dari environment apa, atau bagaimana outputnya akan digunakan — ia hanya melakukan tugasnya berdasarkan input yang diterima.
# modules/rds/main.tf — ini adalah child module
# Child module tidak tahu tentang environment atau project
# Ia hanya menerima variable dan membuat resource
resource "aws_db_subnet_group" "this" {
name = "${var.identifier}-subnet-group"
subnet_ids = var.subnet_ids
tags = merge(var.tags, {
Name = "${var.identifier}-subnet-group"
})
}
resource "aws_security_group" "this" {
name = "${var.identifier}-sg"
vpc_id = var.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = var.allowed_cidr_blocks
}
}
resource "aws_db_instance" "this" {
identifier = var.identifier
engine = var.engine
instance_class = var.instance_class
allocated_storage = var.allocated_storage
db_subnet_group_name = aws_db_subnet_group.this.name
vpc_security_group_ids = [aws_security_group.this.id]
tags = var.tags
}
flowchart TD
subgraph ROOT["Root Module"]
R_CALL["module \"vpc\""]
end
subgraph CHILD["Child Module: RDS"]
C_RES["aws_db_instance\naws_security_group\naws_db_subnet_group"]
C_OUT["outputs: endpoint\naddress, port"]
end
R_CALL -->|"source = ./modules/rds"| C_RES
C_RES --> C_OUT
C_OUT -->|"module.rds.endpoint"| ROOT
style ROOT fill:#e8f5e9,stroke:#2e7d32
style CHILD fill:#e3f2fd,stroke:#1565c0Sumber Child Module: Berbagai Jenis Source #
Child module bisa berasal dari berbagai sumber. Pilihan source mempengaruhi cara versioning dan update module.
# SOURCE 1: Path lokal — untuk module di monorepo yang sama
module "vpc" {
source = "../../modules/vpc"
# Path relatif dari direktori root module saat ini
}
# SOURCE 2: Terraform Registry — module publik dari registry.terraform.io
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
# Format: <namespace>/<module>/<provider>
}
# SOURCE 3: Git repository — module di repo terpisah
module "vpc" {
source = "git::https://github.com/org/terraform-module-vpc.git?ref=v2.1.0"
# ?ref= bisa berupa tag, branch, atau commit hash
}
# SOURCE 4: Git dengan SSH
module "vpc" {
source = "git::ssh://[email protected]/org/terraform-module-vpc.git?ref=v2.1.0"
}
# SOURCE 5: Subdirektori dalam Git repository
module "vpc" {
source = "git::https://github.com/org/terraform-modules.git//modules/vpc?ref=v1.0.0"
# // (double slash) memisahkan repo URL dari path dalam repo
}
Bagaimana State Mengelola Resource dari Child Module #
Resource yang dibuat oleh child module masuk ke state root module — bukan state terpisah. Address-nya mencerminkan hierarki module.
terraform state list
# Output — resource dari child module diberi prefix module.<nama>:
# module.vpc.aws_vpc.this
# module.vpc.aws_subnet.public[0]
# module.vpc.aws_subnet.public[1]
# module.vpc.aws_subnet.private[0]
# module.vpc.aws_internet_gateway.this
# module.eks.aws_eks_cluster.this
# module.eks.aws_eks_node_group.workers
# module.rds.aws_db_instance.this
# module.rds.aws_security_group.this
# aws_route53_record.app ← Resource langsung di root module (tanpa prefix module.)
# Operasi state pada resource di dalam module
terraform state show module.vpc.aws_vpc.this
terraform state rm module.vpc.aws_subnet.public[0]
# Target plan/apply untuk module tertentu
terraform plan -target=module.vpc
terraform apply -target=module.rds
Komposisi Module: Module Memanggil Module #
Child module bisa memanggil child module lain — tapi ini perlu dilakukan dengan hati-hati agar tidak menciptakan hierarki yang terlalu dalam.
# modules/eks/main.tf — child module yang memanggil child module lain
# EKS module memanggil IAM module untuk membuat role yang dibutuhkan
module "cluster_iam" {
source = "../iam-role"
name = "${var.cluster_name}-cluster-role"
assume_role_service = "eks.amazonaws.com"
policy_arns = [
"arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
]
}
resource "aws_eks_cluster" "this" {
name = var.cluster_name
role_arn = module.cluster_iam.role_arn # Output dari sub-module
vpc_config {
subnet_ids = var.subnet_ids
}
}
REKOMENDASI KEDALAMAN HIERARKI:
Root Module
└── Child Module (level 1) ← IDEAL, mudah dipahami
└── Child Module (level 2) ← MASIH OK untuk abstraksi yang jelas
└── Child Module (level 3) ← MULAI KOMPLEKS, pertimbangkan ulang
└── ... ← HINDARI — sulit di-debug, sulit di-test
Perbedaan Tanggung Jawab #
ROOT MODULE BERTANGGUNG JAWAB UNTUK:
✓ Komposisi — memanggil module yang tepat
✓ Konfigurasi environment — nilai yang berbeda per environment
✓ "Glue" antar module — mengoper output satu module ke input module lain
✓ Resource yang sangat spesifik untuk deployment tertentu
✓ Backend configuration dan provider configuration
✓ Output yang mengekspos nilai ke sistem eksternal
CHILD MODULE BERTANGGUNG JAWAB UNTUK:
✓ Implementasi satu "konsep" infrastruktur
✓ Validasi input yang diterima
✓ Mengekspos output yang berguna untuk pemanggil
✓ Defaults yang masuk akal untuk konfigurasi yang tidak wajib
✗ JANGAN: hardcode nilai yang seharusnya bisa dikonfigurasi
✗ JANGAN: asumsikan context deployment (environment, region)
Root Module Special Behavior #
Root module memiliki perilaku khusus yang berbeda dari child module.
# Root module:
# - Backend configuration (hanya di root)
# - Provider configuration (hanya di root)
# - Variable values diberikan dari CLI/tfvars
# - Output ditampilkan di terminal
# Child module:
# - TIDAK bisa punya backend config
# - TIDAK punya provider sendiri (warisan dari root)
# - Variable values diberikan dari root module
# - Output hanya bisa diakses oleh root module
# Root module (.)
terraform {
backend "s3" { ... } # ✓ Hanya di root
required_providers { ... } # ✓ Hanya di root
}
provider "aws" { ... } # ✓ Hanya di root
module "networking" {
source = "./modules/networking"
# Variable dikirim dari root
vpc_cidr = var.vpc_cidr
}
# Child module (./modules/networking)
# TIDAK ADA backend, provider, atau terraform block
variable "vpc_cidr" { ... } # Diterima dari root
resource "aws_vpc" "main" { ... }
output "vpc_id" { ... } # Dikembalikan ke root
Module Composition Patterns #
Pola komposisi yang baik membuat kode lebih mudah dipelihara.
# PATTERN 1: Thin root module
# Root module hanya memanggil child modules
module "networking" {
source = "./modules/networking"
vpc_cidr = var.vpc_cidr
}
module "compute" {
source = "./modules/compute"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
}
module "database" {
source = "./modules/database"
subnet_ids = module.networking.database_subnet_ids
vpc_id = module.networking.vpc_id
}
# PATTERN 2: Orchestration module
# Satu module besar yang memanggil module kecil
module "infrastructure" {
source = "./modules/infrastructure"
environment = var.environment
vpc_cidr = var.vpc_cidr
instance_count = var.instance_count
}
# Module infrastructure memanggil networking, compute, database
flowchart TD
subgraph THIN["Thin Root Module"]
R1["Root"] --> M1["networking"]
R1 --> M2["compute"]
R1 --> M3["database"]
end
subgraph ORCH["Orchestration Module"]
R2["Root"] --> O1["infrastructure"]
O1 --> N1["networking"]
O1 --> N2["compute"]
O1 --> N3["database"]
end
style R1 fill:#e3f2fd,stroke:#1565c0
style R2 fill:#e3f2fd,stroke:#1565c0
style O1 fill:#fff3e0,stroke:#e65100Child Module Best Practices #
# Child module SEHARUSNYA:
# 1. Tidak punya backend configuration
# 2. Tidak punya provider configuration (kecuali alias)
# 3. Menerima semua nilai melalui variable
# 4. Mengembalikan semua nilai yang dibutuhkan melalui output
# modules/networking/main.tf
variable "vpc_cidr" {
type = string
}
variable "environment" {
type = string
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = { Name = "${var.environment}-vpc" }
}
output "vpc_id" {
value = aws_vpc.main.id
}
output "vpc_cidr_block" {
value = aws_vpc.main.cidr_block
}
Ringkasan #
- Root module adalah orkestrator — memanggil child module, mengoper nilai, dan menyambungkan output satu module ke input module lain.
- Child module adalah unit kerja yang tidak tahu di mana ia dipanggil — hanya menerima input dan menghasilkan output.
- Resource dari child module masuk ke state root module dengan address
module.<nama>.<type>.<name>— bukan state terpisah.- Empat jenis source: path lokal (monorepo), Terraform Registry, Git repository, dan subdirektori Git.
- Pin versi module eksternal dengan
versionatau?ref=— jangan biarkan module selalu mengambil versi terbaru tanpa kontrol.- Batasi kedalaman hierarki — module yang memanggil module yang memanggil module membuat debugging jauh lebih sulit.