Apa itu Module? #
Bayangkan kamu perlu membuat infrastruktur yang sama — VPC dengan subnet publik dan privat, NAT gateway, dan routing yang benar — untuk environment dev, staging, dan production. Tanpa module, kamu menulis konfigurasi yang hampir identik tiga kali, lalu berjuang menjaga ketiganya tetap konsisten setiap kali ada perubahan. Module menyelesaikan ini dengan membungkus konfigurasi yang bisa dipakai ulang ke dalam satu unit yang bisa dipanggil berkali-kali dengan parameter yang berbeda, persis seperti memanggil fungsi dalam pemrograman.
flowchart TD
A["Root Module"] -->|"module \"vpc\""| B["Child Module\n: ./modules/vpc"]
B -->|"Outputs: vpc_id,\nsubnet_ids"| A
A -->|"module \"app\""| C["Child Module\n: ./modules/app"]
C -->|"Outputs: lb_dns,\ninstance_ids"| A
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#f59e0b,stroke:#d97706,color:#fffApa yang Dimaksud Module #
Module adalah direktori yang berisi satu atau lebih file konfigurasi Terraform (.tf). Setiap direktori Terraform adalah module — termasuk direktori tempat kamu bekerja sekarang, yang disebut root module. Yang membedakan adalah child module: direktori terpisah yang dipanggil dari module lain dengan blok module.
TANPA MODULE — konfigurasi diulang:
environments/
├── dev/
│ ├── main.tf ← vpc + subnet + igw + nat + routing (duplikat)
│ ├── variables.tf
│ └── outputs.tf
├── staging/
│ ├── main.tf ← vpc + subnet + igw + nat + routing (duplikat)
│ ├── variables.tf
│ └── outputs.tf
└── production/
├── main.tf ← vpc + subnet + igw + nat + routing (duplikat)
├── variables.tf
└── outputs.tf
DENGAN MODULE — konfigurasi dipanggil:
modules/
└── vpc/ ← satu konfigurasi VPC yang benar
├── main.tf
├── variables.tf
└── outputs.tf
environments/
├── dev/
│ └── main.tf ← module "vpc" { source = "../../modules/vpc" }
├── staging/
│ └── main.tf ← module "vpc" { source = "../../modules/vpc" }
└── production/
└── main.tf ← module "vpc" { source = "../../modules/vpc" }
Anatomi Module #
Sebuah module terdiri dari tiga komponen utama yang bekerja bersama.
modules/vpc/
├── main.tf ← Resource yang dibuat module ini
├── variables.tf ← Input yang diterima module (parameter)
└── outputs.tf ← Nilai yang diekspor module ke pemanggilnya
# modules/vpc/variables.tf — "parameter" module
variable "cidr_block" {
description = "CIDR block untuk VPC"
type = string
}
variable "environment" {
description = "Nama environment"
type = string
}
variable "public_subnet_count" {
description = "Jumlah public subnet yang dibuat"
type = number
default = 2
}
variable "private_subnet_count" {
description = "Jumlah private subnet yang dibuat"
type = number
default = 2
}
# modules/vpc/main.tf — implementasi internal module
resource "aws_vpc" "this" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = {
Name = "${var.environment}-igw"
Environment = var.environment
}
}
# ... subnet, routing table, NAT gateway, dll.
# modules/vpc/outputs.tf — nilai yang diekspor ke pemanggil
output "vpc_id" {
description = "ID VPC yang dibuat"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "List ID public subnet"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "List ID private subnet"
value = aws_subnet.private[*].id
}
Memanggil Module #
Module dipanggil dari root module atau module lain menggunakan blok module.
# environments/production/main.tf
module "vpc" {
source = "../../modules/vpc" # Path ke direktori module
# Input variable module — sesuai dengan variables.tf di module
cidr_block = "10.0.0.0/16"
environment = "production"
public_subnet_count = 3
private_subnet_count = 3
}
module "vpc_dev" {
source = "../../modules/vpc" # Module yang SAMA dipanggil dengan nilai berbeda
cidr_block = "10.1.0.0/16"
environment = "dev"
public_subnet_count = 1
private_subnet_count = 1
}
# Mengakses output module: module.<nama>.<output_name>
resource "aws_eks_cluster" "main" {
name = "production-eks"
vpc_config {
subnet_ids = module.vpc.private_subnet_ids # Output dari module vpc
}
}
Module adalah Abstraksi #
Keunggulan terbesar module bukan hanya reusabilitas — tapi abstraksi. Pemangil module tidak perlu tahu bagaimana VPC dibuat, hanya apa yang perlu diberikan dan apa yang akan diterima kembali.
DARI SUDUT PANDANG PEMANGGIL:
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.0.0.0/16" ← Input: yang perlu diberikan
environment = "production"
}
module.vpc.vpc_id ← Output: yang diterima kembali
module.vpc.private_subnet_ids
# Pemanggil tidak perlu tahu:
# - Berapa subnet yang dibuat secara internal
# - Bagaimana routing dikonfigurasi
# - Di AZ mana resource ditempatkan
# - Semua detail implementasi tersembunyi di dalam module
flowchart TD
A["🏗️ Module VPC\nReusable"] --> B["dev\nenvironment"]
A --> C["staging\nenvironment"]
A --> D["production\nenvironment"]
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#f59e0b,stroke:#d97706,color:#fff
style D fill:#8b5cf6,stroke:#6d28d9,color:#fffIni membuat tim bisa bekerja di level abstraksi yang berbeda: tim platform menulis module, tim aplikasi menggunakan module tanpa perlu memahami detail jaringan.
Kapan Menulis Module #
Tidak semua konfigurasi perlu dibungkus dalam module. Ada sinyal yang menunjukkan sudah saatnya membuat module.
SUDAH SAATNYA MEMBUAT MODULE JIKA:
✓ Konfigurasi yang sama dipakai di lebih dari satu tempat
(minimal 2-3 kali penggunaan sebelum abstraksi layak)
✓ Ada sekelompok resource yang selalu dibuat bersama
dan punya makna sebagai satu unit (VPC + subnet + IGW + NAT)
✓ Tim lain butuh menggunakan infrastruktur yang sama
tanpa perlu memahami detail implementasinya
✓ Konfigurasi mengandung logic kompleks yang ingin disembunyikan
BELUM PERLU MODULE JIKA:
✗ Konfigurasi hanya dipakai di satu tempat
✗ Resource tidak ada hubungannya satu sama lain
✗ Hanya ingin "mengorganisir" file — gunakan file terpisah
di direktori yang sama (networking.tf, compute.tf, dll.)
✗ Module akan punya hanya 1-2 resource
Module Registry dan Reusability #
Module bisa dipublikasikan ke registry untuk digunakan oleh tim lain atau komunitas.
# Menggunakan module dari Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-southeast-1a", "ap-southeast-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
}
# Menggunakan module dari private registry
module "app" {
source = "app.terraform.io/my-org/app/aws"
version = "~> 2.0"
}
# Menggunakan module dari Git
module "custom" {
source = "git::ssh://[email protected]/my-org/terraform-modules.git//vpc?ref=v1.2.0"
}
flowchart TD
A["Module
Source"] --> B{"Registry?"}
B -->|"Public"| C["registry.terraform.io
terraform-aws-modules/vpc/aws"]
B -->|"Private"| D["app.terraform.io
my-org/module/aws"]
B -->|"Git"| E["git::github.com
my-org/modules.git//vpc"]
B -->|"Local"| F["./modules/vpc
(relative path)"]
style A fill:#e3f2fd,stroke:#1565c0
style C fill:#e8f5e9,stroke:#2e7d32
style D fill:#fff3e0,stroke:#e65100
style E fill:#f3e5f5,stroke:#6a1b9a
style F fill:#fce4ec,stroke:#c62828Module Testing #
Module harus ditest sebelum digunakan di production.
# Terraform test (built-in sejak v1.6)
# Buat file test: tests/vpc_test.tftest.hcl
# Jalankan test
terraform test
# Output:
# tests/vpc_test.tftest.hcl... pass
# "verify_vpc_cidr"... pass
# "verify_subnet_count"... pass
# Test dengan Terratest (Go)
# go test -v -timeout 30m
# tests/vpc_test.tftest.hcl
run "verify_vpc" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR should be 10.0.0.0/16"
}
assert {
condition = length(aws_subnet.private) == 2
error_message = "Should have exactly 2 private subnets"
}
assert {
condition = aws_vpc.main.tags["Environment"] != ""
error_message = "Environment tag is required"
}
}
Module Input Design #
# GOOD: Module dengan input yang jelas dan bertipe
module "web_server" {
source = "./modules/ec2-instance"
name = "web-server"
instance_type = "t3.micro"
subnet_id = module.networking.public_subnet_ids[0]
ami_id = data.aws_ami.latest.id
tags = {
Environment = var.environment
}
}
# Di dalam module:
variable "name" {
description = "Name tag untuk instance"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "subnet_id" {
description = "Subnet ID untuk launch instance"
type = string
validation {
condition = can(regex("^subnet-", var.subnet_id))
error_message = "Harus berupa valid subnet ID."
}
}
Module Output Design #
# Module output harus:
# 1. Mengembalikan semua nilai yang dibutuhkan consumer
# 2. Menggunakan type annotation
# 3. Tandai sensitive jika perlu
output "instance_id" {
description = "ID dari EC2 instance"
value = aws_instance.web.id
}
output "private_ip" {
description = "Private IP address"
value = aws_instance.web.private_ip
}
output "security_group_id" {
description = "Security group ID"
value = aws_security_group.web.id
}
# Sensitive output
output "password" {
description = "Generated password"
value = random_password.result.result
sensitive = true
}
Module Refactoring #
# Saat module sudah terlalu besar, split menjadi sub-modules
# SEBELUM: satu module besar
module "infrastructure" {
source = "./modules/infrastructure"
# 50+ variables...
}
# SESUDAH: modular structure
module "networking" {
source = "./modules/networking"
vpc_cidr = var.vpc_cidr
azs = var.availability_zones
}
module "compute" {
source = "./modules/compute"
subnet_ids = module.networking.private_subnet_ids
instance_type = var.instance_type
}
module "database" {
source = "./modules/database"
subnet_ids = module.networking.database_subnet_ids
engine = var.db_engine
}
Ringkasan #
- Module adalah direktori berisi file
.tf— setiap konfigurasi Terraform sudah berupa module, yang membedakan adalah apakah ia dipanggil sebagai child module.- Tiga komponen module:
variables.tf(input),main.tf(implementasi),outputs.tf(apa yang diekspor).- Module adalah abstraksi — pemanggil hanya perlu tahu input dan output, bukan detail implementasi di dalamnya.
- Panggil dengan blok
moduledan akses output denganmodule.<nama>.<output>.- Module yang sama bisa dipanggil berkali-kali dengan nilai yang berbeda — ini yang membuat konfigurasi multi-environment efisien.
- Buat module saat ada kebutuhan nyata (minimal 2-3 penggunaan) — jangan abstraksi prematur hanya karena terasa “lebih rapi”.
← Sebelumnya: Anti-Pattern Datasource Berikutnya: Struktur →