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:#fff

Apa 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:#fff

Ini 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:#c62828

Module 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 module dan akses output dengan module.<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 →

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