Interface Design #

Module yang bagus bukan hanya module yang bekerja dengan benar — tapi module yang mudah dipakai dengan benar dan sulit dipakai dengan salah. Interface module adalah segalanya: variable apa yang diterima, mana yang wajib, mana yang opsional, apa nilai defaultnya, dan seberapa granular kontrol yang diberikan ke pemanggil. Interface yang dirancang buruk memaksa pemanggil memahami detail implementasi yang seharusnya tersembunyi, atau membuat mereka takut mengubah nilai karena tidak yakin apa dampaknya.

Prinsip Interface yang Baik #

INTERFACE MODULE YANG BAIK:
  ✓ Sedikit variable wajib — hanya yang benar-benar tidak bisa di-default
  ✓ Default yang masuk akal untuk semua variable opsional
  ✓ Nama variable yang jelas dan konsisten
  ✓ Deskripsi yang menjelaskan tujuan, bukan mengulang nama
  ✓ Validasi yang menangkap nilai invalid lebih awal
  ✓ Output yang cukup untuk semua use case yang umum

INTERFACE MODULE YANG BURUK:
  ✗ Terlalu banyak variable wajib — pemanggil harus tahu terlalu banyak
  ✗ Variable tanpa default yang sebenarnya bisa punya default masuk akal
  ✗ Nama variable yang ambigu atau tidak konsisten dengan module lain
  ✗ Tidak ada validasi — error muncul saat apply, bukan saat plan
  ✗ Output yang tidak cukup — pemanggil tidak bisa mendapat nilai yang dibutuhkan

Variable Wajib vs Opsional #

Hanya buat variable wajib (tanpa default) jika nilainya benar-benar tidak bisa di-default — karena nilainya unik per deployment dan tidak ada nilai “masuk akal” yang bisa dipilih.

# variables.tf — desain yang baik

# WAJIB: Nilai yang unik per deployment, tidak bisa di-default
variable "name" {
  description = "Nama unik untuk resource — digunakan sebagai prefix di semua resource yang dibuat module ini"
  type        = string
  # Tidak ada default — setiap caller harus tentukan sendiri
}

variable "vpc_id" {
  description = "ID VPC tempat resource akan ditempatkan"
  type        = string
  # Tidak ada default — bergantung pada infrastruktur caller
}

# OPSIONAL: Nilai yang punya "standar industri" yang masuk akal
variable "instance_type" {
  description = "EC2 instance type untuk web server"
  type        = string
  default     = "t3.micro"
  # t3.micro adalah pilihan masuk akal untuk workload ringan
}

variable "enable_deletion_protection" {
  description = "Aktifkan deletion protection pada RDS — sebaiknya true di production"
  type        = bool
  default     = true
  # Default true — lebih baik terlindungi dari yang tidak
}

variable "backup_retention_days" {
  description = "Jumlah hari backup RDS disimpan"
  type        = number
  default     = 7
  # 7 hari adalah standar yang umum
}

Menghindari Over-Parameterization #

Terlalu banyak variable membuat module sulit digunakan. Setiap variable yang ditambahkan adalah beban kognitif untuk pemanggil.

# ANTI-PATTERN: Over-parameterized — terlalu banyak variable detail
variable "subnet_cidr_bits"              { type = number }
variable "subnet_newbits"                { type = number }
variable "subnet_netnum_offset"          { type = number }
variable "nat_gateway_eip_allocation_id" { type = string }
variable "route_table_propagation_vgws"  { type = list(string) }
variable "dhcp_options_domain_name"      { type = string }
variable "dhcp_options_domain_name_servers" { type = list(string) }
# 50+ variable untuk hal-hal yang sebenarnya punya default yang baik

# BENAR: Opinionated defaults, expose hanya yang benar-benar perlu
variable "cidr_block" {
  type = string
  # Satu variable untuk CIDR — module yang hitung subnet-nya sendiri
}

variable "az_count" {
  type    = number
  default = 2
  # Module bisa kalkulasi subnet CIDR secara internal
}

# Jika ada kebutuhan override yang jarang, gunakan optional object:
variable "advanced_config" {
  description = "Konfigurasi lanjutan — gunakan hanya jika default tidak sesuai"
  type = object({
    enable_nat_per_az          = optional(bool, false)
    dhcp_domain_name_servers   = optional(list(string), ["AmazonProvidedDNS"])
  })
  default = {}
}

Pola Pass-Through Variable #

Untuk module yang membungkus resource kompleks, hindari menduplikasi semua argumen resource sebagai variable module. Gunakan pola yang lebih selektif.

# ANTI-PATTERN: Menduplikasi semua argumen RDS sebagai variable module
variable "db_engine"                    { type = string }
variable "db_engine_version"            { type = string }
variable "db_instance_class"            { type = string }
variable "db_allocated_storage"         { type = number }
variable "db_max_allocated_storage"     { type = number }
variable "db_storage_type"              { type = string }
variable "db_storage_encrypted"         { type = bool }
variable "db_kms_key_id"               { type = string }
variable "db_username"                  { type = string }
variable "db_port"                      { type = number }
# ... 30 variable lagi

# BENAR: Opinionated defaults + escape hatch untuk override lanjutan
variable "instance_class" {
  type    = string
  default = "db.t3.medium"
}

variable "allocated_storage" {
  type    = number
  default = 100
}

# Escape hatch: object untuk override yang tidak umum
variable "db_overrides" {
  description = "Override konfigurasi RDS yang tidak di-cover variable utama. Lihat dokumentasi aws_db_instance untuk field yang tersedia."
  type        = any
  default     = {}
}

resource "aws_db_instance" "this" {
  instance_class    = var.instance_class
  allocated_storage = var.allocated_storage

  # Merge override ke dalam konfigurasi dasar
  # (hanya jika menggunakan dynamic blocks atau provider yang mendukung)
}

Tags sebagai Interface Pattern #

Tags adalah contoh yang baik untuk interface yang fleksibel — biarkan pemanggil menentukan tags mereka sendiri dan module menambahkan tags yang diperlukan.

# Pattern yang direkomendasikan untuk tags
variable "tags" {
  description = "Map of tags yang diterapkan ke semua resource yang dibuat module ini"
  type        = map(string)
  default     = {}
}

# Di dalam module, merge tags dari caller dengan tags wajib dari module
locals {
  common_tags = merge(
    var.tags,
    {
      # Tags yang selalu ada, tidak bisa di-override caller
      ManagedBy = "terraform"
      Module    = "vpc"
    }
  )
}

resource "aws_vpc" "this" {
  cidr_block = var.cidr_block
  tags       = merge(local.common_tags, { Name = "${var.name}-vpc" })
}

resource "aws_subnet" "public" {
  count = var.az_count
  # ...
  tags = merge(local.common_tags, {
    Name = "${var.name}-public-${count.index + 1}"
    Tier = "public"
  })
}

Desain Output yang Lengkap #

Output yang baik memastikan pemanggil tidak perlu mengakses resource internal module secara langsung.

# outputs.tf — desain output yang komprehensif

# Output ID untuk semua resource utama
output "vpc_id" {
  value       = aws_vpc.this.id
  description = "ID VPC yang dibuat"
}

# Output ARN jika resource memiliki ARN
output "vpc_arn" {
  value       = aws_vpc.this.arn
  description = "ARN VPC"
}

# Output list untuk resource yang dibuat multiple
output "public_subnet_ids" {
  value       = aws_subnet.public[*].id
  description = "List ID public subnet, berurutan sesuai AZ"
}

# Output objek untuk kemudahan akses multi-atribut
output "nat_gateways" {
  description = "Informasi NAT Gateway per AZ"
  value = {
    for idx, eip in aws_eip.nat : idx => {
      id        = aws_nat_gateway.this[idx].id
      public_ip = eip.public_ip
    }
  }
}

# ANTI-PATTERN: Output yang mengekspos detail implementasi internal
# yang tidak diperlukan pemanggil
output "route_table_association_ids" {
  value = aws_route_table_association.public[*].id
  # Pemanggil hampir tidak pernah butuh ini — jangan expose jika tidak perlu
}

Ringkasan #

  • Sedikit variable wajib — hanya expose yang benar-benar tidak bisa di-default. Setiap variable wajib adalah hambatan bagi pemanggil.
  • Default yang opinionated lebih baik dari tidak ada default — modul dengan default yang masuk akal langsung bisa digunakan dengan konfigurasi minimal.
  • Hindari over-parameterization — gunakan objek opsional (advanced_config) sebagai escape hatch untuk konfigurasi yang jarang dibutuhkan.
  • Tags via merge(var.tags, {}) — biarkan pemanggil menambahkan tags mereka, module tambahkan tags wajib dengan merge.
  • Output yang komprehensif — pastikan semua atribut yang mungkin dibutuhkan pemanggil tersedia sebagai output, tapi jangan expose detail implementasi internal yang tidak relevan.
  • Mudah dipakai dengan benar, sulit dipakai dengan salah — gunakan validation, defaults masuk akal, dan nama yang jelas untuk mendorong penggunaan yang tepat.

← Sebelumnya: Versioning   Berikutnya: Registry →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact