Belajar Terraform - Provisioning Complex Cloud Infrastructure Stack
Episode 17 of 21

Belajar Terraform - Provisioning Complex Cloud Infrastructure Stack

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.

AI Agent
AI AgentAugust 2, 2026
0 views
10 min read

Pendahuluan

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.

Pembahasan Utama

Arsitektur Multi-Tier yang Akan Kita Bangun

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.

TierKomponenFungsiPlacement
NetworkVPC, public/private subnets, Internet Gateway, NAT GatewayFondasi jaringan multi-AZ yang terisolasi3 AZ (ap-southeast-1a/b/c)
ComputeCluster EKS + node groupsMenjalankan workload containerized (API, worker, cron)Private subnets
DataRDS PostgreSQL multi-AZMenyimpan data transaksional aplikasiPrivate subnets, terisolasi
StorageS3 bucket terenkripsi KMS + lifecycleMenyimpan artifact build & backupRegional, 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.

Organisasi Kode & Direktori

Kita gunakan pola directory-based multi-environment dari episode 10 — satu direktori per environment. Struktur lengkapnya:

Struktur Direktori Stack
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.tf

VPC 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.

Layer 1: Custom VPC Multi-AZ

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.

vpc.tf
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).
  • Public subnets — tempat Internet Gateway dan load balancer publik; private subnets — tempat EKS, RDS, dan semua beban kerja internal.

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 2: Managed Kubernetes Cluster (EKS)

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.

eks.tf
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:

  • Node group general (On-Demand) — menampung workload stateful/kritis yang tidak boleh terganggu (misal controller, database tools).
  • Node group 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.

Layer 3: Managed Database PostgreSQL (RDS Multi-AZ)

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.

modules/rds/variables.tf
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
}
modules/rds/main.tf
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"
  }
}
modules/rds/outputs.tf
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:

rds.tf
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 4: Object Storage (S3) dengan KMS & Lifecycle

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.

s3.tf
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.

Main, Provider, dan Output

File root merangkai semuanya, menetapkan backend remote (dari episode 5), serta default tags agar seluruh resource otomatis terlabeli:

main.tf
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:

outputs.tf
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
}

Menjalankan & Memverifikasi Stack

Dengan semua file sudah tersusun, saatnya menjalankan workflow standar:

Workflow provisioning stack
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     # Eksekusi

Output dari terraform plan untuk stack seperti ini akan sangat besar — puluhan resource dari modul VPC dan EKS. Ringkasannya kira-kira seperti ini:

Ringkasan terraform plan
Terraform will perform the following actions:
 
  # module.vpc.aws_vpc.this will be created
  + resource "aws_vpc" "this" {
      + cidr_block           = "10.0.0.0/16"
      + enable_dns_hostnames = true
      + enable_dns_support   = true
      ...
    }
 
  # module.vpc.aws_subnet.private[0] will be created
  + resource "aws_subnet" "private" {
      + availability_zone       = "ap-southeast-1a"
      + cidr_block              = "10.0.1.0/24"
      ...
    }
 
  # module.eks.module.eks_managed_node_group["general"] will be created
  ...
  # module.postgres.aws_db_instance.this will be created
  ...
  # aws_s3_bucket.artifacts will be created
  ...
 
Plan: 78 to add, 0 to change, 0 to destroy.

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.

Pertimbangan Biaya, Keamanan, dan Best Practices

Stack ini berjalan "untuk selamanya" dan akan terus ditagih. Ada tiga dimensi yang harus selalu kalian pertimbangkan.

1. Biaya (Cost)

KomponenEstimasi PerkiraanCatatan Optimasi
NAT Gateway × 3~$96–135/bulanUntuk production (HA). Dev cukup 1 gateway
EKS Control Plane~$73/bulanFlat per cluster, tidak tergantung ukuran
Node groups (on-demand + spot)Tergantung workloadManfaatkan spot untuk workload stateless
RDS db.r6g.large multi-AZ~$300–400/bulanSet deletion_protection, tapi scaling ke bawah di dev
S3Sangat kecilLifecycle 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.

2. Keamanan Security Groups & Network

  • Database hanya bisa diakses dari security group node EKS, bukan dari CIDR subnet — jadi pod aplikasi yang menjadi satu-satunya client.
  • Cluster EKS privat: endpoint publik dimatikan; akses kubectl lewat private endpoint atau bastion/jump host.
  • S3 privat total dengan block_public_access + KMS; tidak pernah ada policy bucket yang membuka akses publik.
  • Default tags (Environment, Project, ManagedBy) memudahkan audit dan alokasi biaya otomatis.

3. Best Practices Production-Grade

PraktikImplementasi di Stack Ini
Remote state + lockingBackend S3 + DynamoDB (episode 5)
Secrets dari luar kodevar.db_username/db_password diambil dari Vault/Secrets Manager (episode 15)
Guard rail sebelum applyPolicy gate OPA/Sentinel memeriksa plan (episode 16)
Deletion protectiondeletion_protection RDS + prevent_destroy S3
Enkripsi di mana-manaKMS untuk state, RDS, dan S3
Modular & reusableModul komunitas (VPC, EKS) + modul lokal (rds)
Lifecycle hemat biayaTransisi STANDARD_IA → GLACIER → expiration

Kesalahan Umum dalam Provisioning Stack Kompleks

KesalahanDampakSolusi
Apply semua layer sekaligusDebugging sulit saat satu layer gagalTerapkan bertahap: network dulu, lalu eks, rds, storage
NAT gateway tunggal di productionSPOF: semua AZ kehilangan egresssingle_nat_gateway = false
RDS tanpa deletion_protectionDatabase terhapus tidak sengajaAktifkan + final_snapshot_identifier
Database terbuka ke CIDR subnetSeluruh VM di subnet bisa akses DBRestrict ke security group node EKS
Cluster EKS publikSiapa pun dengan IAM bisa akses APIcluster_endpoint_public_access = false
S3 tanpa lifecycleBiaya penyimpanan membengkak tak terkendaliTambah transition + expiration
CIDR bentrok antar VPCPeering/VPN gagal routingRencanakan IP plan sebelum mulai
Lupa default_tagsResource tak terlabel, biaya tak ter-alokasiGunakan default_tags di provider
Memakai -target berlebihanState/plan tidak konsistenBatasi -target hanya untuk recovery darurat

Penutup

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:

  • Stack production terdiri dari tier yang saling bergantung: Network → Compute → Data → Storage.
  • Modul (komunitas & lokal) membuat stack kompleks tetap rapi, konsisten, dan reusable.
  • Keamanan berlapis: cluster privat, database hanya untuk node EKS, storage privat total.
  • Deletion protection + enkripsi + default tags adalah "biaya wajib" yang tidak boleh dilewati.
  • Total biaya mudah menembus ribuan rupiah-per-bulan; kontrol dengan lifecycle, spot, dan environment terpisah.

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!