Terraform tanpa modul adalah infrastruktur copy-paste. Anda mendefinisikan VPC yang sama, instance komputasi yang sama, dan konfigurasi database yang sama di setiap environment — lalu menghabiskan berjam-jam merekonsiliasi drift ketika Anda mengubah sesuatu di satu environment tetapi lupa yang lain. Saya belajar hal ini dengan cara yang keras di Commsult Indonesia, di mana kami memiliki tiga environment GCP yang hampir identik (dev, staging, prod) dengan konfigurasi yang disinkronkan secara manual. Setelah insiden ketiga di mana staging dan prod mengalami drift, saya menulis ulang semuanya sebagai modul Terraform yang dapat dikomposisikan. Panduan ini menunjukkan pola persis yang saya gunakan.
Modul Terraform adalah wadah untuk beberapa resource yang digunakan bersama. Kata kuncinya adalah 'bersama' — modul harus merepresentasikan komponen infrastruktur yang koheren, bukan jenis resource tunggal atau seluruh stack aplikasi. Modul untuk VPC masuk akal. Modul untuk 'database dengan monitoring, alerting, dan backup yang dikonfigurasi' masuk akal. Modul yang hanya merupakan pembungkus tipis di sekitar google_compute_instance tidak — Anda telah menambahkan abstraksi tanpa menambahkan nilai. Struktur modul standar dari HashiCorp jelas: main.tf untuk definisi resource, variables.tf untuk input, outputs.tf untuk nilai yang dibutuhkan pemanggil, dan README.md yang membuat modul dapat dikonsumsi secara eksternal.
Prinsip Don't Repeat Yourself dari rekayasa perangkat lunak berlaku sama untuk kode infrastruktur. Ketika Anda mendefinisikan instance GCP Cloud SQL di main.tf environment dev dan secara terpisah di staging dan prod, Anda memiliki tiga sumber kebenaran — dan mereka akan menyimpang. Sebuah modul menyatukan ketiga definisi tersebut menjadi satu, dengan nilai spesifik environment yang dilewatkan sebagai variabel. Modul memberlakukan batasan — environment prod hanya dapat menggunakan db-custom-4-8192 atau lebih besar, diberlakukan oleh blok validasi di variables.tf. Tidak ada individu yang dapat secara tidak sengaja menyediakan database yang terlalu kecil di produksi.
Kesalahan paling umum dengan modul Terraform adalah scope creep — membuat modul terlalu luas. Modul 'production environment' yang menyediakan VPC, database, instance komputasi, load balancer, dan record DNS dalam satu blok sulit diuji, sulit di-versioning, dan sulit digunakan ulang. Cakupan yang tepat: modul harus mengelola resource yang harus selalu disediakan bersama dan memiliki coupling siklus hidup yang kuat. VPC GCP dan subnetnya sangat terkait erat — sediakan bersama. Instance GCP Cloud SQL dan binding IAM-nya terkait — sediakan bersama. GCP Load Balancer dan layanan backend yang dirutekannya — mungkin modul terpisah yang dikomposisikan di level environment.
Dari pengalaman saya: selalu keluarkan setiap ID dan nama resource dari modul Anda, meskipun Anda tidak menggunakannya segera. Dalam praktiknya, output modul menjadi perekat antar modul — Anda melewatkan VPC ID dari modul networking ke modul compute, dan connection string database dari modul database ke modul aplikasi. Jika Anda tidak mengeluarkannya awalnya, Anda harus memodifikasi modul nanti dan memperbarui semua pemanggil. Over-output dari hari pertama.
Repository modul Terraform yang terstruktur dengan baik memisahkan modul root (definisi environment yang memanggil modul anak) dari modul anak (blok bangunan yang dapat digunakan ulang). Konvensi yang saya gunakan di Commsult Indonesia menempatkan modul anak di bawah modules/ dan konfigurasi root environment di bawah environments/dev, environments/staging, environments/prod. Setiap modul anak memiliki README.md sendiri, variables.tf dengan deskripsi dan blok validasi, outputs.tf, dan main.tf. Modul anak tidak pernah berisi konfigurasi provider — mereka mewarisi dari root. Ini membuat modul agnostik terhadap environment dan benar-benar dapat digunakan ulang.
# modules/cloud-sql/variables.tf
variable "instance_name" {
description = "Cloud SQL instance name"
type = string
}
variable "machine_type" {
description = "Cloud SQL machine type"
type = string
default = "db-custom-2-8192"
validation {
condition = contains(["db-custom-2-8192", "db-custom-4-16384", "db-custom-8-32768"], var.machine_type)
error_message = "Machine type must be one of the approved sizes."
}
}
variable "environment" {
description = "Deployment environment"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
# modules/cloud-sql/outputs.tf
output "instance_connection_name" {
description = "Cloud SQL instance connection name"
value = google_sql_database_instance.this.connection_name
}
output "private_ip_address" {
description = "Cloud SQL private IP address"
value = google_sql_database_instance.this.private_ip_address
}
# environments/prod/main.tf
module "database" {
source = "../../modules/cloud-sql"
instance_name = "prod-db-01"
machine_type = "db-custom-4-16384"
environment = "prod"
}Blok validasi Terraform memungkinkan Anda memberlakukan batasan pada waktu plan daripada waktu apply. Gunakan secara agresif di modul Anda untuk menangkap miskonfigurasi sebelum resource apa pun disentuh. Batasan tipe di variables.tf mendefinisikan apa yang dapat dilewatkan oleh pemanggil — tipe objek dengan kunci yang diperlukan mencegah pemanggil menghilangkan konfigurasi kritis. Gabungkan dengan nilai default hanya untuk pengaturan yang benar-benar opsional — pengaturan keamanan yang diperlukan (enkripsi, retensi backup) tidak boleh memiliki default untuk memaksa keputusan eksplisit per environment.
Di awal adopsi Terraform saya, saya mereferensikan modul internal langsung dari branch Git main: source = 'git::https://github.com/myorg/tf-modules.git//modules/vpc'. Ini tampak nyaman hingga anggota tim melakukan merge perubahan breaking ke main yang langsung merusak apply staging kami. Solusinya: tandai rilis modul (v1.0.0, v1.1.0) dan referensikan versi tertentu: source = 'git::...?ref=v1.1.0'. Perubahan breaking mendapatkan bump versi mayor. Semua environment dikunci ke versi tertentu dan diperbarui secara deliberat, bukan otomatis.
Modul Terraform yang tidak diuji secara diam-diam mengakumulasi bug. Pendekatan pengujian minimum adalah menjalankan terraform validate dan terraform plan terhadap environment nyata (tetapi sementara) untuk setiap pull request. Untuk pengujian yang lebih komprehensif, Terratest (perpustakaan pengujian berbasis Go) memungkinkan Anda menulis tes yang menyediakan infrastruktur nyata, melakukan assertion pada state dan output, dan menghancurkan semuanya setelah pengujian. Untuk modul yang mengelola resource mahal, gunakan localstack atau emulator GCP untuk tes level unit, dan cadangkan Terratest untuk tes integrasi yang berjalan saat merge ke main.
┌─────────────────────────────────────────────────┐
│ Terraform Module Repository Layout │
├─────────────────────────────────────────────────┤
│ tf-modules/ │
│ ├── modules/ (child modules) │
│ │ ├── vpc/ │
│ │ │ ├── main.tf │
│ │ │ ├── variables.tf │
│ │ │ └── outputs.tf │
│ │ ├── cloud-sql/ │
│ │ └── cloud-run/ │
│ └── environments/ (root modules) │
│ ├── dev/ │
│ ├── staging/ │
│ └── prod/ │
└─────────────────────────────────────────────────┘Modul tanpa manajemen state yang tepat menyebabkan race condition dan korupsi. Setiap modul root environment harus menyimpan state dari jarak jauh dengan locking yang diaktifkan. Di GCP, gunakan bucket GCS dengan versioning yang diaktifkan sebagai backend, dan Terraform secara otomatis menangani state locking melalui mekanisme object lock GCS. Setiap environment mendapatkan file state-nya sendiri — state dev, staging, dan prod sepenuhnya terisolasi. Referensi state lintas environment menggunakan sumber data terraform_remote_state (gunakan dengan hemat — ini menciptakan coupling ketat antar environment).
Terraform Registry memiliki ratusan modul komunitas untuk AWS, GCP, dan Azure. Saya menggunakannya untuk pola umum di mana saya tidak memiliki persyaratan khusus — modul Google Network dan modul Google Cloud SQL terawat dengan baik dan menghemat waktu pengembangan yang signifikan. Saya menulis modul internal khusus hanya ketika modul komunitas tidak sesuai dengan persyaratan spesifik kami (biasanya seputar kebijakan IAM, konvensi penamaan, atau konfigurasi multi-region spesifik untuk region Indonesia/Singapura). Aturannya: gunakan modul komunitas sebagai titik awal, fork dan kustomisasi hanya bila perlu, dan kontribusikan peningkatan ke upstream bila memungkinkan.
Sumber & Bacaan Lanjutan