Studi kasus membangun stack infrastruktur multi-tier lengkap dari nol dengan Terraform: VPC multi-AZ, cluster EKS, database PostgreSQL (RDS) multi-AZ, dan object storage terenkripsi dengan lifecycle policy.

Setelah di episode 16 sebelumnya kita membahas Policy as Code dengan OPA dan Sentinel — memasang guard rail otomatis sebelum terraform apply — pada episode kali ini kita akan merangkai semua yang sudah dipelajari dari episode 0 hingga 16 menjadi satu kesatuan utuh: merancang dan mem-provision stack infrastruktur cloud multi-tier yang lengkap dari nol.
Selama ini kalian sudah mengenal resources, variables, state, modules, CI/CD, hingga policy gate secara terpisah-pisah. Tapi di dunia kerja nyata, infrastruktur tidak pernah berdiri sendiri: aplikasi butuh jaringan, jaringan butuh compute, compute butuh database, dan semuanya butuh penyimpanan objek untuk artifact. Sebuah tim produk biasanya mendapat permintaan seperti ini:
"Tolong siapkan environment production: VPC multi-AZ, cluster Kubernetes, database PostgreSQL, dan bucket untuk artifact build — semua privat, terenkripsi, dan sesuai best practice."
Di episode inilah kalian akan belajar menjawab permintaan itu. Kita akan membangun stack bertingkat (multi-tier) di AWS menggunakan modul komunitas dan modul lokal, memahami mengapa setiap layer disusun seperti itu, melihat ringkasan plan-nya, serta membahas pertimbangan biaya, keamanan security group, dan praktik terbaik untuk skala produksi.
Arsitektur kita terdiri dari empat lapisan (tier) yang masing-masing punya tanggung jawab berbeda. Semua komponen berada di dalam satu VPC privat, kecuali load balancer dan NAT yang memang butuh akses publik secara terkendali.
| Tier | Komponen | Fungsi | Placement |
|---|---|---|---|
| Network | VPC, public/private subnets, Internet Gateway, NAT Gateway | Fondasi jaringan multi-AZ yang terisolasi | 3 AZ (ap-southeast-1a/b/c) |
| Compute | Cluster EKS + node groups | Menjalankan workload containerized (API, worker, cron) | Private subnets |
| Data | RDS PostgreSQL multi-AZ | Menyimpan data transaksional aplikasi | Private subnets, terisolasi |
| Storage | S3 bucket terenkripsi KMS + lifecycle | Menyimpan artifact build & backup | Regional, privat |
Alur datanya: traffic masuk lewat Internet Gateway → load balancer publik → service di dalam cluster EKS → aplikasi membaca/menulis ke RDS (di private subnet) dan S3. Komunikasi ke luar (misal pull image dari registry) keluar melalui NAT Gateway. Tidak ada resource compute maupun database yang punya IP publik.
Note
Ini adalah contoh arsitektur yang disederhanakan namun realistis. Di produksi, kalian biasanya menambahkan layer seperti WAF, private API gateway, jump host/bastion, monitoring (Prometheus/Grafana), dan observability — tetapi fondasi empat tier di atas tetap menjadi kerangka utama yang paling sering dipakai.
Kita gunakan pola directory-based multi-environment dari episode 10 — satu direktori per environment. Struktur lengkapnya:
infrastructure/
├── environments/
│ └── production/
│ ├── main.tf # Root module: orchestration semua layer
│ ├── backend.tf # S3 + DynamoDB (dari episode 5)
│ ├── providers.tf # Provider AWS + default_tags
│ ├── variables.tf
│ ├── terraform.tfvars # Nilai environment (di-gitignore, lihat ep. 15)
│ ├── outputs.tf
│ └── modules/
│ └── rds/ # Modul lokal untuk PostgreSQL
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tfVPC dan EKS kita ambil dari modul komunitas Terraform Registry (terraform-aws-modules/vpc/aws dan terraform-aws-modules/eks/aws) karena keduanya sudah teruji ribuan kali di produksi. RDS kita bungkus sebagai modul lokal untuk menunjukkan cara membuat komponen reusable sendiri. S3 kita tulis langsung di root karena cukup sederhana.
VPC adalah fondasi segalanya. Kita perlu tiga Availability Zone (AZ) untuk high availability, masing-masing punya satu subnet publik dan satu subnet privat. Subnet publik menampung Internet Gateway route, NAT Gateway, dan load balancer; subnet privat menampung semua workload internal.
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${var.project}-vpc"
cidr = "10.0.0.0/16"
azs = local.azs
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true
single_nat_gateway = false # HA: satu NAT per AZ
enable_vpn_gateway = false
enable_dns_hostnames = true
enable_dns_support = true
map_public_ip_on_launch = true
tags = {
Tier = "Network"
}
}Beberapa keputusan penting di sini dan alasannya:
cidr = "10.0.0.0/16" — ruang alamat privat yang cukup besar, dengan subnet /24 per AZ. Jangan pernah pakai CIDR yang bentrok dengan jaringan on-premise atau VPC peering lain.single_nat_gateway = false — dengan satu NAT gateway, seluruh AZ berbagi jalur keluar; jika gateway itu down, AZ privat yang bergantung padanya kehilangan akses internet. Untuk production, pasang NAT per AZ (NAT per AZ, egress per AZ).Warning
NAT Gateway adalah komponen termahal per unit di stack ini (sekitar $32–45/bulan per gateway di AWS, plus data transfer). Dengan single_nat_gateway = true, biaya turun drastis, tapi kalian kehilangan high availability. Aturan praktis: untuk environment production gunakan NAT per AZ; untuk dev/staging cukup satu NAT gateway untuk menghemat biaya.
Layer compute menggunakan Amazon EKS — Kubernetes terkelola. Modul komunitas EKS menyiapkan control plane, node groups, IAM roles, dan security groups secara otomatis. Cluster kita sengaja dibuat privat (endpoint publik dimatikan) karena semua akses dilakukan dari dalam VPC.
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "${var.project}-eks"
cluster_version = "1.31"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
cluster_endpoint_public_access = false
cluster_endpoint_private_access = true
eks_managed_node_groups = {
general = {
desired_size = 3
min_size = 3
max_size = 10
instance_types = ["t3.large"]
capacity_type = "ON_DEMAND"
subnet_ids = module.vpc.private_subnets
}
spot = {
desired_size = 2
min_size = 1
max_size = 8
instance_types = ["m5.large", "m5a.large"]
capacity_type = "SPOT"
subnet_ids = module.vpc.private_subnets
}
}
tags = {
Tier = "Compute"
}
}Pola node group di atas adalah pola produksi yang umum:
general (On-Demand) — menampung workload stateful/kritis yang tidak boleh terganggu (misal controller, database tools).spot — menampung workload stateless dan batch yang bisa diganggu, dengan harga hingga ~60-90% lebih murah.Tip
module.eks.node_security_group_id adalah output penting: nanti kita pakai sebagai sumber yang diizinkan masuk ke RDS. Dengan begitu, hanya pod yang jalan di node EKS yang bisa mengakses database — bukan seluruh subnet.
Database adalah komponen paling "sakral" — kehilangan data tidak bisa dimaafkan. Karena itu kita bungkus RDS sebagai modul lokal yang reusable (modules/rds) sehingga environment lain (staging, dev) bisa memakainya dengan input berbeda.
variable "identifier" {
type = string
}
variable "instance_class" {
type = string
default = "db.r6g.large"
}
variable "vpc_id" {
type = string
}
variable "subnet_ids" {
type = list(string)
}
variable "allowed_security_group_ids" {
type = list(string)
default = []
}
variable "db_name" {
type = string
}
variable "username" {
type = string
sensitive = true
}
variable "password" {
type = string
sensitive = true
}resource "aws_db_subnet_group" "this" {
name = "${var.identifier}-subnet"
subnet_ids = var.subnet_ids
}
resource "aws_security_group" "this" {
name = "${var.identifier}-sg"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.allowed_security_group_ids
content {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [ingress.value]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_db_instance" "this" {
identifier = var.identifier
engine = "postgres"
engine_version = "16.3"
instance_class = var.instance_class
db_name = var.db_name
username = var.username
password = var.password
multi_az = true
db_subnet_group_name = aws_db_subnet_group.this.name
vpc_security_group_ids = [aws_security_group.this.id]
storage_encrypted = true
storage_type = "gp3"
backup_retention_period = 30
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.identifier}-final"
performance_insights_enabled = true
tags = {
Tier = "Data"
}
}output "endpoint" {
value = aws_db_instance.this.endpoint
}
output "security_group_id" {
value = aws_security_group.this.id
}Perhatikan blok dynamic "ingress": ia membuat satu aturan ingress per security group yang diizinkan — jadi kita cukup menyebutkan [module.eks.node_security_group_id] dari root, dan hanya node EKS yang bisa menghubungi port 5432. Ini jauh lebih aman daripada membuka CIDR subnet.
Sekarang panggil modul dari root:
module "postgres" {
source = "./modules/rds"
identifier = "${var.project}-postgres"
instance_class = "db.r6g.large"
db_name = var.db_name
username = var.db_username
password = var.db_password
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
allowed_security_group_ids = [module.eks.node_security_group_id]
}Caution
Jangan pernah mematikan deletion_protection pada database produksi tanpa diskusi tim. Dengan deletion_protection = true, terraform destroy pada RDS akan gagal — ini adalah pengaman terakhir untuk mencegah data hilang karena kesalahan manusia. Saat benar-benar mau menghapus, kalian harus mengubah nilainya dulu secara sadar (dan idealnya melalui change request).
Layer terakhir adalah S3 bucket untuk artifact build dan backup. Kita buat privat total, dengan enkripsi KMS (key rotasi otomatis), versioning, dan lifecycle policy untuk menekan biaya penyimpanan seiring waktu.
resource "aws_s3_bucket" "artifacts" {
bucket = "${var.project}-artifacts"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.artifacts.arn
sse_algorithm = "aws:kms"
}
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_lifecycle_configuration" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
rule {
id = "archive-and-expire"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 365
}
}
}
resource "aws_s3_bucket_public_access_block" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_kms_key" "artifacts" {
description = "KMS key untuk bucket artifacts"
enable_key_rotation = true
deletion_window_in_days = 30
}Logika lifecycle-nya sederhana namun hemat biaya: file disimpan di STANDARD selama 30 hari pertama, pindah ke STANDARD_IA (infrequent access) setelah 30 hari, ke GLACIER setelah 90 hari, dan otomatis dihapus setelah 365 hari. Karena versioning aktif, setiap versi file ikut mengikuti aturan yang sama.
Important
lifecycle { prevent_destroy = true } pada bucket berarti Terraform menolak menghapus bucket ini — baik saat terraform destroy maupun saat kalian menghapus blok-nya. Ini melindungi data artifact dari kehancuran tidak sengaja. Untuk menonaktifkannya, kalian harus mengubah kode secara eksplisit, apply, baru bucket bisa dihapus.
File root merangkai semuanya, menetapkan backend remote (dari episode 5), serta default tags agar seluruh resource otomatis terlabeli:
terraform {
required_version = ">= 1.6.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.60"
}
}
backend "s3" {
bucket = "myapp-tfstate-bucket"
key = "production/fullstack/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "myapp-tfstate-lock"
}
}
provider "aws" {
region = var.region
default_tags {
tags = {
Environment = "production"
Project = var.project
ManagedBy = "terraform"
}
}
}
locals {
azs = ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"]
}Output untuk dikonsumsi aplikasi atau stack lain:
output "vpc_id" {
value = module.vpc.vpc_id
}
output "cluster_name" {
value = module.eks.cluster_name
}
output "cluster_endpoint" {
value = module.eks.cluster_endpoint
}
output "database_endpoint" {
value = module.postgres.endpoint
sensitive = true
}
output "artifacts_bucket" {
value = aws_s3_bucket.artifacts.id
}Dengan semua file sudah tersusun, saatnya menjalankan workflow standar:
terraform init # Unduh provider + modul komunitas
terraform fmt -recursive # Rapikan seluruh kode
terraform validate # Validasi sintaks & referensi
terraform plan -out=tfplan # Buat rencana
terraform apply tfplan # EksekusiOutput dari terraform plan untuk stack seperti ini akan sangat besar — puluhan resource dari modul VPC dan EKS. Ringkasannya kira-kira seperti ini:
Note
Jumlah resource 70-80+ untuk stack seperti ini adalah hal yang wajar — bukan berarti kode kita "boros", melainkan modul komunitas membangun banyak resource pendukung (route tables, route table associations, IAM roles, security groups, dan lain-lain) secara otomatis. Inilah salah satu alasan modul eksis: kalian menulis sedikit kode, tapi menghasilkan infrastruktur produksi yang lengkap dan konsisten.
Stack ini berjalan "untuk selamanya" dan akan terus ditagih. Ada tiga dimensi yang harus selalu kalian pertimbangkan.
| Komponen | Estimasi Perkiraan | Catatan Optimasi |
|---|---|---|
| NAT Gateway × 3 | ~$96–135/bulan | Untuk production (HA). Dev cukup 1 gateway |
| EKS Control Plane | ~$73/bulan | Flat per cluster, tidak tergantung ukuran |
| Node groups (on-demand + spot) | Tergantung workload | Manfaatkan spot untuk workload stateless |
RDS db.r6g.large multi-AZ | ~$300–400/bulan | Set deletion_protection, tapi scaling ke bawah di dev |
| S3 | Sangat kecil | Lifecycle policy memindahkan file ke kelas penyimpanan murah |
Warning
Total biaya stack produksi seperti ini dengan mudah menembus $1.000+/bulan sebelum workload berarti. Inilah alasan mengapa directory-based multi-environment (episode 10) sangat penting: environment dev/staging harus jauh lebih kecil (RDS single-AZ, satu NAT, node group minimal) supaya biaya tidak membengkak tanpa alasan.
kubectl lewat private endpoint atau bastion/jump host.block_public_access + KMS; tidak pernah ada policy bucket yang membuka akses publik.Environment, Project, ManagedBy) memudahkan audit dan alokasi biaya otomatis.| Praktik | Implementasi di Stack Ini |
|---|---|
| Remote state + locking | Backend S3 + DynamoDB (episode 5) |
| Secrets dari luar kode | var.db_username/db_password diambil dari Vault/Secrets Manager (episode 15) |
| Guard rail sebelum apply | Policy gate OPA/Sentinel memeriksa plan (episode 16) |
| Deletion protection | deletion_protection RDS + prevent_destroy S3 |
| Enkripsi di mana-mana | KMS untuk state, RDS, dan S3 |
| Modular & reusable | Modul komunitas (VPC, EKS) + modul lokal (rds) |
| Lifecycle hemat biaya | Transisi STANDARD_IA → GLACIER → expiration |
| Kesalahan | Dampak | Solusi |
|---|---|---|
| Apply semua layer sekaligus | Debugging sulit saat satu layer gagal | Terapkan bertahap: network dulu, lalu eks, rds, storage |
| NAT gateway tunggal di production | SPOF: semua AZ kehilangan egress | single_nat_gateway = false |
RDS tanpa deletion_protection | Database terhapus tidak sengaja | Aktifkan + final_snapshot_identifier |
| Database terbuka ke CIDR subnet | Seluruh VM di subnet bisa akses DB | Restrict ke security group node EKS |
| Cluster EKS publik | Siapa pun dengan IAM bisa akses API | cluster_endpoint_public_access = false |
| S3 tanpa lifecycle | Biaya penyimpanan membengkak tak terkendali | Tambah transition + expiration |
| CIDR bentrok antar VPC | Peering/VPN gagal routing | Rencanakan IP plan sebelum mulai |
| Lupa default_tags | Resource tak terlabel, biaya tak ter-alokasi | Gunakan default_tags di provider |
Memakai -target berlebihan | State/plan tidak konsisten | Batasi -target hanya untuk recovery darurat |
Pada episode 17 ini kita telah membangun stack infrastruktur multi-tier lengkap dari nol di AWS: VPC multi-AZ dengan public/private subnets, Internet Gateway, dan NAT Gateway per AZ; cluster EKS dengan node groups on-demand + spot; database PostgreSQL (RDS) multi-AZ yang hanya bisa diakses dari node EKS; serta S3 bucket terenkripsi KMS dengan versioning dan lifecycle policy. Kita juga menyusun struktur direktori berbasis environment, memanggil modul komunitas dan modul lokal, melihat ringkasan plan, serta membahas pertimbangan biaya, keamanan security group, dan best practices production-grade.
Poin kunci yang perlu kalian bawa pulang:
Semua contoh di atas bisa kalian tiru di AWS account sendiri (gunakan region bebas biaya sesuai free tier saat berlatih) atau adaptasi ke GCP dengan modul analog: GKE menggantikan EKS, Cloud SQL menggantikan RDS, dan GCS dengan lifecycle management menggantikan S3.
Sekarang infrastruktur sudah berdiri kokoh. Tapi ada satu masalah yang diam-diam mengintai: apa yang terjadi ketika seseorang mengubah infrastruktur secara manual di console cloud? State tidak lagi sesuai dengan kenyataan, dan suatu hari terraform plan akan menunjukkan perubahan yang tidak pernah kalian rencanakan.
Di episode 18 selanjutnya kita akan membahas Infrastructure Drift Detection & Refactoring — mendeteksi perbedaan antara kondisi cloud nyata vs state, mengotomatiskan deteksi drift dengan scheduled pipeline, dan merestrukturisasi kode tanpa downtime. Pastikan tetap semangat!