Belajar Terraform - Membuat & Mengelola Reusable Modules
Episode 9 of 21

Belajar Terraform - Membuat & Mengelola Reusable Modules

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.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Pendahuluan

Setelah di episode 8 sebelumnya kita membahas Advanced State Manipulationimport, 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".

Pembahasan Utama

Mengapa Modul? Lebih dari Sekadar Memecah File

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:

  1. Reusability (DRY) — infrastruktur yang sama ditulis sekali, dipakai di banyak tempat. VPC module ditulis sekali, digunakan untuk dev, staging, dan prod.
  2. Enkapsulasi — detail resource disembunyikan di dalam modul. Pemakai cukup mensetel variabel input, bukan memahami cara kerja setiap resource di dalamnya.
  3. Konsistensi & standarisasi — semua environment dibuat dengan konfigurasi yang sama persis, menghilangkan "gaya pribadi" tiap engineer.
  4. Kemudahan review — code review jadi ringkas: "kenapa kamu mengubah port di modul vpc?" dibandingkan membaca seribu baris diff.
  5. Testability — modul yang kecil dan fokus bisa diuji satu per satu dengan Terratest (kita bahas di episode 11).

Struktur Standar Modul Terraform

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/ — struktur modul standar
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
FileFungsi
main.tfResource utama yang dikelola modul (wajib minimal satu file .tf).
variables.tfDeklarasi input — type, description, default, hingga validation.
outputs.tfNilai yang bisa dikonsumsi pemanggil, misal vpc_id, subnet_ids.
versions.tfBlok terraform { required_version, required_providers } untuk mengunci versi.
README.mdDokumentasi: 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.

Membuat Modul VPC Pertama Kita

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:

modules/vpc/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:

modules/vpc/variables.tf
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:

modules/vpc/main.tf
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:

modules/vpc/outputs.tf
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.

Memanggil Modul dari Root Configuration

Modul di atas belum berguna sampai dipanggil. Di direktori root, kita mendeklarasikan modul dengan blok module — sumber (source) dan input-nya:

main.tf
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:

  1. Input dilewatkan sebagai argumentname, cidr, azs, dst. melewati blok variabel modul.
  2. Output dikonsumsi dengan 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 init
terraform init
Output terraform init
Initializing 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.

Local vs Remote Module

Ada dua kategori sumber modul yang akan kalian temui setiap hari:

  • Local modulesource = "./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).
  • Remote module — diunduh dari sumber eksternal: Terraform Registry, Git, atau object storage. Cocok untuk komponen yang dipakai lintas tim/project, seperti yang dikembangkan tim platform.
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:

  • Source registry ditulis dalam format <namespace>/<name>/<provider> — di sini terraform-aws-modules/vpc/aws, modul VPC komunitas yang paling banyak dipakai di dunia.
  • Source Git menggunakan 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:

Output init modul remote
Initializing modules...
Downloading terraform-aws-modules/vpc/aws 5.8.1 for vpc...
- vpc in .terraform/modules/vpc
 
Initializing the backend...

Version Pinning Modul

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:

ConstraintArti
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.5Hanya 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.

Best Practices Membangun Modul

Menulis modul yang "bisa dipakai" itu mudah; menulis modul yang nyaman dipakai tim lain butuh disiplin. Beberapa praktik yang terbukti di lapangan:

  1. Mulai dari kebutuhan nyata. Jangan langsung membuat modul super-generik dengan ratusan variabel. Refactor menjadi modul setelah pola yang sama dipakai minimal dua kali.
  2. Gunakan tipe dan default yang tepat. Setiap variabel wajib punya type dan description; beri default hanya jika ada nilai yang masuk akal secara umum.
  3. Ekspor output yang benar-benar dibutuhkan. Output yang berlebihan menambah permukaan API modul dan sulit dirawat.
  4. Sediakan contoh penggunaan di direktori examples/ — ini sekaligus menjadi fixture untuk test (episode 11).
  5. Jangan menaruh provider/credentials di dalam modul. Konfigurasi provider adalah urusan pemanggil, bukan modul. Modul hanya boleh menyimpan required_providers di versions.tf.
  6. Gunakan referensi antar resource, bukan hardcode. Selalu rujuk aws_vpc.this.id alih-alih menebak ID yang sama.
  7. Versioning sejak awal. Walaupun masih lokal, biasakan memikirkan kompatibilitas perubahan — breaking change harus disertai major version.
  8. Sertakan README.md dengan tabel variabel dan output — atau gunakan generator seperti terraform-docs agar dokumentasi sinkron dengan kode.

Kesalahan Umum (Common Pitfalls)

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.

Penutup

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:

  • Modul adalah "function" untuk infrastruktur: input → resource → output.
  • Gunakan ./ untuk modul lokal, <namespace>/<name>/<provider> untuk Registry, dan git::...//subdir?ref= untuk Git.
  • Selalu pin versi modul remote, dan dokumentasikan variabel/output setiap modul.
  • Refactor menjadi modul saat polanya terulang, bukan di awal.

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!