Pahami mengapa modul menjadi fondasi IaC di skala enterprise. Pelajari struktur standar modul, membuat VPC module yang reusable, memanggil modul lokal maupun remote, hingga version pinning dan best practices-nya.

Setelah di episode 8 sebelumnya kita membahas Advanced State Manipulation — import, moved, dan keluarga perintah terraform state — pada episode kali ini kita naik satu tingkat abstraksi lebih tinggi: Membuat & Mengelola Reusable Modules.
Mari jujur sejenak. Jika project Terraform kalian dibiarkan tumbuh tanpa aturan, cepat atau lambat main.tf akan berubah menjadi monster berisi ribuan baris yang mengelola VPC, subnet, security group, instance, database, load balancer, sampai bucket S3 — semuanya dalam satu file. Setiap kali ingin menambah instance baru, kalian menyalin puluhan baris lalu mengganti beberapa nama. Ubah di satu tempat, lupa di tempat lain. Ini yang dalam dunia software engineering disebut kode copy-paste, dan kita semua tahu ke mana arahnya: inkonsistensi, drift antar resource, dan kesalahan yang sulit ditelusuri.
Analoginya seperti restoran dengan banyak cabang. Bayangkan jika setiap koki menulis resep dengan formatnya masing-masing — satu di sobekan kertas, satu di buku catatan, satu lagi hafal di kepala. Hasilnya, menu yang sama bisa terasa berbeda tiap cabang. Solusinya adalah standarisasi resep: satu format baku, satu sumber kebenaran, yang bisa dipakai ulang di semua cabang. Di Terraform, "resep baku" itu bernama module.
Di episode ini kalian akan belajar mengapa modul penting, struktur standar sebuah modul, cara membuat modul sendiri, cara memanggil modul lokal maupun remote dari Terraform Registry, hingga cara pinning versi modul agar tim tetap terkendali. Ini adalah keterampilan yang membedakan config "yang berhasil jalan" dari config "yang layak produksi".
Module adalah container untuk satu set resource yang digunakan bersama-sama. Ia berfungsi seperti function dalam bahasa pemrograman: menerima input (variables), melakukan pekerjaan internal (resources), lalu mengembalikan hasil (outputs). Pemanggilnya tidak perlu tahu detail implementasi — cukup tahu "apa yang bisa dikonfigurasi" dan "apa yang didapat".
Dari sini lahir lima keuntungan utama yang membuat modul menjadi standar industri:
Tidak ada aturan mati bahwa sebuah modul harus punya nama file tertentu — Terraform hanya membaca semua file .tf di dalam direktori tersebut. Namun komunitas telah menyepakati konvensi berikut agar modul mudah dipahami siapa pun:
modules/
└── vpc/
├── main.tf # Definisi resource utama
├── variables.tf # Deklarasi input variables
├── outputs.tf # Nilai yang diekspos ke pemanggil
├── versions.tf # Constraints Terraform & provider
└── README.md # Dokumentasi cara pakai| File | Fungsi |
|---|---|
main.tf | Resource utama yang dikelola modul (wajib minimal satu file .tf). |
variables.tf | Deklarasi input — type, description, default, hingga validation. |
outputs.tf | Nilai yang bisa dikonsumsi pemanggil, misal vpc_id, subnet_ids. |
versions.tf | Blok terraform { required_version, required_providers } untuk mengunci versi. |
README.md | Dokumentasi: kapan memakai modul, variabel yang tersedia, contoh penggunaan. |
Note
Nama file hanyalah konvensi — Terraform menggabungkan semua file .tf dalam satu direktori menjadi satu konfigurasi. Yang benar-benar wajib adalah minimal satu resource (atau data/output murni) di dalam modul. Konvensi lima file di atas justru membuat modul konsisten dan mudah di-review, apalagi jika nanti dipublikasikan ke module registry.
Cara terbaik memahami modul adalah membuatnya sendiri. Mari kita buat modul vpc yang reusable: menerima nama, CIDR, daftar CIDR subnet publik, dan availability zones sebagai input; mengelola VPC, subnet, internet gateway, dan route table; lalu mengekspos ID-nya sebagai output.
Pertama, kunci versi di versions.tf:
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}Lalu deklarasi input di variables.tf. Perhatikan bahwa setiap variabel punya description dan type — ini bukan sekadar gaya, tapi dokumentasi hidup bagi pemanggil:
variable "name" {
description = "Nama untuk VPC dan resource di dalamnya"
type = string
}
variable "cidr" {
description = "CIDR block utama VPC"
type = string
default = "10.0.0.0/16"
}
variable "azs" {
description = "Daftar availability zones untuk subnet"
type = list(string)
}
variable "public_subnet_cidrs" {
description = "Daftar CIDR block untuk subnet publik"
type = list(string)
}
variable "tags" {
description = "Tag tambahan yang di-merge ke semua resource"
type = map(string)
default = {}
}Sekarang main.tf — jantung modul. Perhatikan pola merge(...) untuk menggabungkan tag bawaan dengan tag dari pemanggil, dan penggunaan count untuk membuat subnet sebanyak isi list:
resource "aws_vpc" "this" {
cidr_block = var.cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(
{ Name = var.name },
var.tags,
)
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = merge(
{ Name = "${var.name}-public-${count.index + 1}" },
var.tags,
)
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = merge(
{ Name = "${var.name}-igw" },
var.tags,
)
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = merge(
{ Name = "${var.name}-public" },
var.tags,
)
}
resource "aws_route_table_association" "public" {
count = length(var.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}Terakhir, ekspor nilai yang berguna di outputs.tf. Ingat: hanya yang dideklarasikan di sini yang bisa diakses pemanggil:
output "vpc_id" {
description = "ID dari VPC yang dibuat"
value = aws_vpc.this.id
}
output "public_subnet_ids" {
description = "Daftar ID subnet publik"
value = aws_subnet.public[*].id
}
output "internet_gateway_id" {
description = "ID dari Internet Gateway"
value = aws_internet_gateway.this.id
}Tip
Selalu tulis description pada variabel dan output. Ketika modul dipakai banyak tim, description inilah yang muncul saat seseorang menjalankan terraform plan atau membaca dokumentasi — tanpa description, modul kalian akan terasa seperti kotak hitam yang harus dibongkar untuk dipahami.
Modul di atas belum berguna sampai dipanggil. Di direktori root, kita mendeklarasikan modul dengan blok module — sumber (source) dan input-nya:
module "vpc" {
source = "./modules/vpc"
name = "app-vpc"
cidr = "10.0.0.0/16"
azs = ["ap-southeast-1a", "ap-southeast-1b"]
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
tags = {
Environment = "dev"
ManagedBy = "terraform"
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
subnet_id = module.vpc.public_subnet_ids[0]
tags = {
Name = "web-server"
}
}Dua hal penting di atas:
name, cidr, azs, dst. melewati blok variabel modul.module.<nama>.<output> — module.vpc.public_subnet_ids[0] mengambil subnet pertama dari modul. Inilah yang membuat implicit dependency otomatis terbentuk: Terraform tahu bahwa instance bergantung pada modul VPC, tanpa perlu depends_on.Setelah menulis blok module, jalankan terraform init. Modul lokal tidak perlu diunduh, tapi Terraform akan memastikan strukturnya valid:
terraform initInitializing modules...
- vpc in modules/vpc
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching ">= 5.0.0"...
- Installing hashicorp/aws v5.61.0...
- Installed hashicorp/aws v5.61.0 (signed by HashiCorp)
Terraform has been successfully initialized!Note
Perintah terraform get adalah pendahulu historis dari terraform init yang khusus untuk mengunduh modul. Di Terraform modern, terraform init sudah mencakup pengunduhan modul (lokal maupun remote) sekaligus instalasi provider dan inisialisasi backend. Gunakan terraform init sebagai perintah standar; terraform get hanya relevan jika kalian ingin sekadar mendownload modul tanpa menyentuh provider/backend.
Ada dua kategori sumber modul yang akan kalian temui setiap hari:
source = "./modules/vpc". Modul berada dalam repo yang sama, biasanya di folder modules/. Cocok untuk kode internal yang belum mau dibagikan lintas repo. Path relatif harus diawali ./ atau ../ (menulis source = "modules/vpc" tanpa ./ akan dianggap nama registry).module "vpc" {
source = "./modules/vpc"
name = "app-vpc"
cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24"]
azs = ["ap-southeast-1a"]
}Perhatikan detail pada contoh di atas:
<namespace>/<name>/<provider> — di sini terraform-aws-modules/vpc/aws, modul VPC komunitas yang paling banyak dipakai di dunia.git:: diikuti URL, lalu // untuk menunjuk subdirektori di dalam repo, dan ?ref= untuk menentukan tag/branch/commit. Ini adalah pola umum untuk private module repository.Saat terraform init dengan modul remote, output akan menampilkan proses instalasi dari registry:
Initializing modules...
Downloading terraform-aws-modules/vpc/aws 5.8.1 for vpc...
- vpc in .terraform/modules/vpc
Initializing the backend...Sama seperti dependency di aplikasi, modul remote harus di-pin versinya lewat argument version. Tanpa pinning, terraform init akan selalu menarik versi terbaru dan infrastruktur kalian bisa berubah perilaku tanpa disadari. Aturan penulisannya sama seperti constraint versi provider:
| Constraint | Arti |
|---|---|
version = ">= 1.0" | Versi berapa pun mulai 1.0 ke atas (terlalu longgar) |
version = "~> 1.2" | Versi 1.2.x — boleh patch, tidak melewati minor |
version = ">= 1.0, < 2.0" | Rentang antar 1.0 dan 2.0 |
version = "1.2.5" / = 1.2.5 | Hanya persis 1.2.5 (paling ketat) |
Warning
Modul lokal (./modules/...) tidak mendukung argument version — mereka selalu mengikuti kode di direktori tersebut, yang secara alami sudah di-versioning oleh git. Untuk modul remote, wajib mencantumkan version. Untuk sumber Git, gunakan ref yang menunjuk tag rilis (seperti ?ref=v1.2.0), bukan branch seperti main, agar perubahan di branch tidak diam-diam masuk ke infrastruktur produksi.
Menulis modul yang "bisa dipakai" itu mudah; menulis modul yang nyaman dipakai tim lain butuh disiplin. Beberapa praktik yang terbukti di lapangan:
type dan description; beri default hanya jika ada nilai yang masuk akal secara umum.examples/ — ini sekaligus menjadi fixture untuk test (episode 11).required_providers di versions.tf.aws_vpc.this.id alih-alih menebak ID yang sama.README.md dengan tabel variabel dan output — atau gunakan generator seperti terraform-docs agar dokumentasi sinkron dengan kode.1. Lupa menjalankan terraform init setelah menambah modul baru
Error paling klasik saat baru belajar: Error: Module not installed. This module is not yet installed. Run "terraform init" to install all modules required by this configuration. Solusinya selalu sama — jalankan terraform init.
2. Source path lokal tanpa ./ atau ../
source = "modules/vpc" akan diterjemahkan sebagai nama modul registry, bukan path lokal, dan berujung pada error Error: Failed to query available provider packages atau Module not found. Selalu tulis ./modules/vpc.
3. Modul tanpa versions.tf
Tanpa constraint provider, dua modul dalam satu project bisa menuntut versi provider yang berbeda, dan init akan gagal dengan konflik. Selalu deklarasikan required_providers dan required_version di setiap modul.
4. Menempatkan konfigurasi provider di dalam modul
Blok provider "aws" { region = "ap-southeast-1" } di dalam modul tidak portable dan akan memicu peringatan "provider configurations are only allowed in the root module". Urusan region/credential adalah hak pemanggil.
5. Mengonsumsi output yang tidak dideklarasikan
module.vpc.subnet_ids akan error Unsupported attribute jika modul tidak mengekspor subnet_ids (di contoh kita namanya public_subnet_ids). Cek outputs.tf modul sebelum mengonsumsi.
6. Modul monolitik
Satu modul yang mengelola VPC, EC2, RDS, dan IAM sekaligus adalah anti-pattern — ia kembali menjadi "monster", hanya berpindah lokasi. Pecah menjadi modul kecil yang fokus, sesuai kelompok resource yang memang selalu dipakai bersama.
Pada episode 9 ini kalian telah menguasai fondasi modularitas Terraform: memahami mengapa modul penting sebagai standarisasi kode infrastruktur yang reusable dan teruji; mengenal struktur standar modul (main.tf, variables.tf, outputs.tf, versions.tf, README.md); membuat VPC module lengkap dengan input, resource, dan output; memanggil modul lokal maupun remote (Registry & Git); melakukan version pinning; serta best practices dan kesalahan umum yang kerap ditemui tim pemula.
Poin kunci yang perlu dibawa pulang:
./ untuk modul lokal, <namespace>/<name>/<provider> untuk Registry, dan git::...//subdir?ref= untuk Git.Kode yang ter-modularisasi memang memecahkan masalah duplikasi, tapi meninggalkan satu pertanyaan besar: bagaimana mengelola dev, staging, dan prod secara bersamaan tanpa saling menabrak? Inilah topik yang sering menjadi sumber konflik antar tim.
Di episode 10 selanjutnya kita akan membahas Workspaces vs Directory-Based Multi-Environment — memahami konsep terraform workspace, keterbatasannya yang sering menyulap orang, hingga struktur direktori berbasis environment yang menjadi standar enterprise. Pastikan tetap semangat!