Output Contract #
Ketika modul Terraform dipakai oleh satu tim, mengubah output adalah pekerjaan kecil. Tapi ketika modul dipakai oleh sepuluh tim sekaligus, menghapus atau mengganti nama satu output bisa memecah semua konfigurasi yang bergantung padanya. Output bukan hanya mekanisme teknis — ia adalah kontrak antara penulis modul dan pemanggilnya. Memahami cara merancang kontrak ini dengan baik adalah keterampilan yang membedakan modul yang cepat dipakai lagi dari modul yang cepat ditinggalkan.
flowchart LR
A["📦 Module VPC\nProvider Output"] -->|Contract: vpc_id, subnet_ids| B["📦 Module Compute\nConsumer"]
A -->|Contract: vpc_id, subnet_ids| C["📦 Module Database\nConsumer"]
D["⚠️ Hapus output\nvpc_id"] -.->|BREAKS| B
D -.->|BREAKS| C
style A fill:#3b82f6,stroke:#1e40af,color:#fff
style B fill:#10b981,stroke:#059669,color:#fff
style C fill:#10b981,stroke:#059669,color:#fff
style D fill:#ef4444,stroke:#dc2626,color:#fffOutput sebagai Public API #
Analogi terbaik untuk output modul adalah public API dari sebuah library. Begitu API tersebut dipakai, mengubahnya butuh pertimbangan tentang backward compatibility.
KONTRAK YANG BAIK:
Module "vpc" menjanjikan:
- output vpc_id → selalu ada, tipe string
- output public_subnet_ids → selalu ada, tipe list(string)
- output private_subnet_ids → selalu ada, tipe list(string)
Pemangil bisa bergantung pada kontrak ini:
module.vpc.vpc_id → AMAN digunakan
module.vpc.public_subnet_ids → AMAN digunakan
BREAKING CHANGE YANG HARUS DIHINDARI:
- Menghapus output yang sudah ada
- Mengubah tipe output (string → list)
- Mengubah nama output
- Mengubah struktur output object secara tidak backward-compatible
flowchart TD
subgraph ROOT["Root Module"]
R_MAIN["main.tf"]
end
subgraph CHILD["Child Module: networking"]
C_VPC["resource aws_vpc"]
C_SUB["resource aws_subnet"]
C_OUT_VPC["output vpc_id"]
C_OUT_SUB["output subnet_ids"]
end
C_VPC --> C_OUT_VPC
C_SUB --> C_OUT_SUB
C_OUT_VPC -->|"module.networking.vpc_id"| R_MAIN
C_OUT_SUB -->|"module.networking.subnet_ids"| R_MAIN
style ROOT fill:#e8f5e9,stroke:#2e7d32
style CHILD fill:#e3f2fd,stroke:#1565c0Prinsip Desain Output yang Baik #
# PRINSIP 1: Nama output yang deskriptif dan konsisten
# ANTI-PATTERN: Nama yang ambigu
output "id" {
value = aws_vpc.main.id # ID apa? VPC? Subnet? Instance?
}
output "ip" {
value = aws_instance.web.public_ip # IP publik atau privat?
}
# BENAR: Nama yang eksplisit tentang resource dan atribut
output "vpc_id" {
value = aws_vpc.main.id
}
output "web_instance_public_ip" {
value = aws_instance.web.public_ip
}
# PRINSIP 2: description yang informatif — menjelaskan UNTUK APA output ini dipakai
# ANTI-PATTERN: Description yang mengulang nama
output "vpc_id" {
description = "The VPC ID" # ✗ Tidak menambah informasi
value = aws_vpc.main.id
}
# BENAR: Description yang menjelaskan konteks penggunaan
output "vpc_id" {
description = "ID VPC utama — digunakan oleh modul compute dan database untuk menempatkan resource di network yang benar."
value = aws_vpc.main.id
}
# PRINSIP 3: Konsistensi tipe — jangan campur tipe untuk output yang serupa
# ANTI-PATTERN: Tipe output yang tidak konsisten
output "public_subnet_id" {
value = aws_subnet.public[0].id # String — hanya satu subnet
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id # List — bisa banyak subnet
}
# Pemangil harus tahu kapan pakai string dan kapan pakai list
# BENAR: Konsisten menggunakan list meskipun hanya satu element
output "public_subnet_ids" {
value = aws_subnet.public[*].id # Selalu list
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id # Selalu list
}
Mengelompokkan Output Terkait #
Daripada banyak output terpisah untuk satu resource, output yang terstruktur lebih mudah digunakan dan lebih tahan terhadap breaking change.
# ANTI-PATTERN: Output yang terlalu granular
output "lb_dns_name" {
value = aws_lb.main.dns_name
}
output "lb_arn" {
value = aws_lb.main.arn
}
output "lb_zone_id" {
value = aws_lb.main.zone_id
}
output "lb_security_group_id" {
value = aws_security_group.lb.id
}
# 4 output terpisah untuk satu resource — rentan breaking change jika ada yang ditambah/hapus
# BENAR: Output terstruktur dalam satu object
output "load_balancer" {
description = "Informasi load balancer — DNS, ARN, zone ID, dan security group"
value = {
dns_name = aws_lb.main.dns_name
arn = aws_lb.main.arn
zone_id = aws_lb.main.zone_id
security_group_id = aws_security_group.lb.id
}
}
# Lebih mudah diperluas — tambah field baru tanpa breaking existing usage
# module.app.load_balancer.dns_name → tetap valid
Menambah Output Tanpa Breaking Change #
Menambah output baru selalu aman. Yang berbahaya adalah mengubah atau menghapus output yang sudah ada.
# AMAN: Menambah output baru
# Pemangil lama tidak terpengaruh, pemangil baru bisa memanfaatkan output baru
# Versi 1.0 module:
output "vpc_id" { value = aws_vpc.main.id }
output "private_subnet_ids" { value = aws_subnet.private[*].id }
# Versi 1.1 module (backward compatible — hanya tambah):
output "vpc_id" { value = aws_vpc.main.id } # Tetap ada
output "private_subnet_ids" { value = aws_subnet.private[*].id } # Tetap ada
output "public_subnet_ids" { value = aws_subnet.public[*].id } # BARU — aman
output "nat_gateway_ips" { value = aws_eip.nat[*].public_ip } # BARU — aman
# BERBAHAYA: Mengubah nama atau tipe output yang sudah ada
# Semua pemangil yang menggunakan output ini akan error
# JANGAN lakukan ini tanpa major version bump:
# Sebelum: output "subnet_ids" { value = ... }
# Sesudah: output "private_subnet_ids" { value = ... } ← rename = breaking change
# Jika harus rename, beri masa transisi:
output "subnet_ids" {
value = aws_subnet.private[*].id
description = "DEPRECATED: Gunakan private_subnet_ids. Akan dihapus di v3.0."
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
description = "List ID private subnet"
}
Output untuk Integrasi dengan Sistem Eksternal #
Output tidak hanya untuk sesama konfigurasi Terraform — sering kali nilainya perlu dikonsumsi oleh sistem lain.
# Output yang dirancang untuk dikonsumsi oleh sistem eksternal
# sebaiknya dalam format yang mudah diparse
output "connection_strings" {
description = "Connection string untuk berbagai komponen — digunakan oleh deployment pipeline"
sensitive = true # Mengandung kredensial
value = {
database = "postgresql://${aws_db_instance.main.username}@${aws_db_instance.main.endpoint}/${aws_db_instance.main.db_name}"
redis = "redis://${aws_elasticache_cluster.main.cache_nodes[0].address}:${aws_elasticache_cluster.main.cache_nodes[0].port}"
}
}
output "kubernetes_config" {
description = "Konfigurasi untuk setup kubectl — digunakan oleh CI/CD pipeline"
value = {
cluster_name = aws_eks_cluster.main.name
cluster_endpoint = aws_eks_cluster.main.endpoint
cluster_region = var.aws_region
}
}
# Konsumsi output dari CI/CD pipeline
DB_ENDPOINT=$(terraform output -raw database_endpoint)
CLUSTER_NAME=$(terraform output -json kubernetes_config | jq -r '.cluster_name')
aws eks update-kubeconfig \
--name "$CLUSTER_NAME" \
--region "$(terraform output -json kubernetes_config | jq -r '.cluster_region')"
Versioning Output Contracts #
Ketika modul berevolusi, output contract perlu dikelola dengan hati-hati untuk tidak memecah pemanggil.
# VERSI 1.0: Output awal
output "db_endpoint" {
description = "Database endpoint"
value = aws_db_instance.main.endpoint
}
# VERSI 1.5: Tambah output baru, tandai lama deprecated
output "db_endpoint" {
description = "DEPRECATED: Gunakan database.primary_endpoint"
value = aws_db_instance.main.endpoint
}
output "database" {
description = "Database connection details"
value = {
primary_endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
engine = aws_db_instance.main.engine
}
}
# VERSI 2.0: Hapus output lama
# output "db_endpoint" { ... } # DIHAPUS
# Consumer HARUS migrasi ke output "database"
flowchart LR
A["v1.0
db_endpoint"] --> B["v1.5
db_endpoint (deprecated)
+ database (new)"]
B --> C["v2.0
database only"]
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#fff3e0,stroke:#e65100
style C fill:#e8f5e9,stroke:#2e7d32Output Documentation Standards #
Output yang didokumentasikan dengan baik adalah kunci untuk module yang bisa dipelihara.
# Standar dokumentasi output:
# 1. Selalu berikan description
output "vpc_id" {
description = "ID dari VPC yang dibuat oleh module ini"
value = aws_vpc.main.id
}
# 2. Deskripsikan format untuk output kompleks
output "database_endpoint" {
description = "Endpoint database. Format: host:port"
value = "${aws_db_instance.main.address}:${aws_db_instance.main.port}"
}
# 3. Tandai sensitive jika mengandung rahasia
output "admin_password" {
description = "Password admin database. HARUS disimpan di secret manager."
value = random_password.admin.result
sensitive = true
}
# 4. Gunakan object output untuk grouping
output "networking" {
description = "Semua informasi networking dari module"
value = {
vpc_id = aws_vpc.main.id
public_subnet_ids = aws_subnet.public[*].id
private_subnet_ids = aws_subnet.private[*].id
nat_gateway_ips = aws_nat_gateway.main[*].public_ip
}
}
# Generate documentation dari output
terraform output -json | jq 'to_entries[] | {
name: .key,
type: .value.type,
sensitive: .value.sensitive
}'
---
## Output Migration Strategy
```bash
# Saat mengubah output contract, ikuti langkah ini:
# 1. Tambah output baru (backward-compatible)
# 2. Tandai output lama sebagai deprecated (di description)
# 3. Beri waktu untuk consumer migrasi (1-2 sprint)
# 4. Monitor: siapa yang masih pakai output lama?
# 5. Hapus output lama di major version berikutnya
flowchart TD
A["Output v1.0
db_endpoint"] --> B["v1.5: Tambah output baru
db_endpoint (deprecated)
database (new)"]
B --> C["Consumer migrasi
ke output database"]
C --> D["v2.0: Hapus
db_endpoint"]
style A fill:#e3f2fd,stroke:#1565c0
style B fill:#fff3e0,stroke:#e65100
style C fill:#e3f2fd,stroke:#1565c0
style D fill:#ffebee,stroke:#c62828Ringkasan #
- Output adalah kontrak publik — begitu dipakai orang lain, perubahan butuh pertimbangan backward compatibility.
- Nama output yang eksplisit —
vpc_id,web_instance_public_iplebih baik dariidatauipyang ambigu.- Description yang menjelaskan penggunaan, bukan mengulang nama — “digunakan oleh modul compute untuk…” jauh lebih berguna.
- Output terstruktur sebagai object lebih mudah diperluas tanpa breaking change dibanding banyak output terpisah per atribut.
- Menambah output selalu aman, menghapus atau rename selalu breaking — jika harus, beri masa transisi dengan deprecation notice.
- Tandai
sensitive = trueuntuk output yang mengandung credential atau data sensitif lain.
← Sebelumnya: Apa itu Output? Berikutnya: Sensitive Output →