Introduction
Tutorial hari ke-7 dari seri Newbie Terraform. Kita naik level: provisioning GKE cluster dengan Terraform + Terragrunt, plus pattern shared Application Load Balancer untuk multi-app host-based routing. Kalau kamu sudah nyaman dengan VPC dan Service Account impersonation di GCP (lihat tutorial https://blog.igunawan.com/tutorial-end-to-end-vpc-compute-engine-ssh-dengan-terragrunt-di-gcp/, sekarang kita masuk ke orkestrasi container.
Kenapa GKE (bukan EKS/AKS)?
| Aspek | GKE |
|---|---|
| Networking | VPC-native pakai alias IP → pod IP adalah IP GCP beneran, bukan di-NAT. Routing lebih simple, throughput lebih tinggi. |
| Integrasi GCP | Workload Identity, Cloud Logging, Cloud Monitoring native. Tidak perlu setup Prometheus/Grafana dari nol. |
| Master di-manage GCP | Control-plane di-upgrade & di-patch otomatis — kamu cuma manage node. |
| Harga | Cluster gratis (hanya bayar node). Autopilot mode bisa lebih murah lagi. |
Kenapa VPC-native + Private Node?
- Private node (tanpa external IP): node tidak expose ke internet, attack surface kecil. Cocok untuk production.
- VPC-native (alias IP): pod IP adalah IP GCP asli, jadi
kube-proxy/iptables-nya lebih simpel, dan Network Policy lebih reliable. Kekurangannya: subnet perlu secondary ranges (kita setup di bawah). - Master authorized networks: meskipun control-plane endpoint-nya public (default), kubectl cuma bisa connect dari IP yang di-whitelist. Ini sweet spot antara kemudahan (tidak perlu VPN) dan keamanan (tidak open ke seluruh internet).
Kenapa Shared ALB (1 Ingress untuk Semua App)?
Pattern naif: bikin Ingress per-app (tiap Helm chart punya Ingress sendiri). Masalahnya: 1 Ingress = 1 Application Load Balancer di GCP. Jadi kalau kamu deploy 4 app, kamu bayar 4 ALB (~$20/bulan per ALB), plus 4 IP, 4 sertifikat, 4 set health check, dan ada race condition kalau Helm install di timing berbeda.
Solusi: 1 Ingress di-manual (k8s/shared-ingress.yaml) yang berisi banyak rules[] untuk semua host. Hasilnya:
- 1 ALB untuk semua app.
- 1 static IP untuk semua domain.
- Tambah app baru → tambah 1 block
rules[]di Ingress existing, kubectl apply lagi. ALB existing cuma update URL map, tidak ke-recreate (zero downtime).
Trade-off: tambah app baru harus edit 1 file manual. Tapi itu sehat — routing terpusat di 1 tempat yang bisa di-review via Git diff.
📁 Struktur Proyek
days-7-gcp-gke/
├── charts/learn/ # Helm chart generik (siap multi-app)
│ ├── Chart.yaml
│ └── templates/
│ ├── deployment.yaml
│ ├── service.yaml # Ada annotation NEG untuk GCE Ingress
│ ├── ingress.yaml # Template Ingress (di-disable, lihat catatan di bawah)
│ └── _helpers.tpl
├── modules/gcp/ # Terraform modules (reusable)
│ ├── vpc/
│ │ ├── main.tf # VPC + subnets + Cloud Router + NAT + 2 firewall
│ │ ├── variables.tf # Support secondary_ranges untuk GKE
│ │ └── outputs.tf # subnet map (referenced oleh GKE module)
│ ├── gke/
│ │ ├── main.tf # Cluster Standard + node pool terpisah
│ │ ├── variables.tf # node_pools map, master CIDRs, dll
│ │ └── outputs.tf
│ └── lb/
│ ├── main.tf # google_compute_global_address (shared ALB)
│ ├── variables.tf
│ └── outputs.tf
├── values-learn.yaml # Helm values untuk app "learn"
├── values-nodeapp.yaml # Helm values untuk app "nodeapp"
├── values-maintenance.yaml # Helm values untuk app "maintenance"
├── k8s/
│ └── shared-ingress.yaml # 1 Ingress = semua host rules
└── live/gcp/
├── root.hcl # Provider + impersonation (mirror days-6)
├── env.hcl # Region default
├── _envcommon/
│ ├── vpc.hcl # Shared flags VPC (semua env)
│ └── gke.hcl # Shared cluster mode (semua env)
└── dev/
├── vpc/terragrunt.hcl # VPC dev (CIDR plan: 10.40.x.x)
├── gke/terragrunt.hcl # Cluster dev-gke-cluster
└── lb/terragrunt.hcl # Reserve static IP dev-shared-alb-ip
Sebelum ngetik command apapun, pastikan kamu paham gambar di bawah — ini yang akan kamu bangun.
┌────────────────────────────────────────────────────────┐ │ VPC: dev-gke-vpc (10.40.0.0/16) │ │ │ │ ┌─ gke-subnet-public (10.40.0.0/24) ─┐ │ │ │ (kosong, reserved untuk jump host) │ │ │ └─────────────────────────────────────┘ │ │ │ │ ┌─ gke-subnet-private (10.40.16.0/24) ─────────────┐ │ │ │ Secondary ranges: │ │ │ │ └─ pods (10.40.128.0/17) → 32,768 IP │ │ │ │ └─ services (10.40.32.0/20) → 4,096 IP │ │ │ │ │ │ │ │ ┌── GKE Cluster: dev-gke-cluster ───────────┐ │ │ │ │ │ Zonal: asia-southeast2-a │ │ │ │ │ │ Standard mode │ │ │ │ │ │ Private nodes (no external IP) │ │ │ │ │ │ Public control-plane endpoint (IP-walled)| │ │ │ │ │ Workload Identity enabled │ │ │ │ │ │ Calico network policy │ │ │ │ │ │ │ │ │ │ │ │ Node pool: primary (2x e2-medium) │ │ │ │ │ │ ─ autoscaling │ │ │ │ │ │ ─ auto-repair │ │ │ │ │ │ ─ auto-upgrade │ │ │ │ │ └───────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ │ │ Cloud NAT (outbound internet untuk pull image) ▼ Internet

IP Planning
| Resource | CIDR | Catatan |
|---|---|---|
| VPC | 10.40.0.0/16 | |
gke-subnet-public | 10.40.0.0/24 | Reserved untuk jump host/ingress |
gke-subnet-private | 10.40.16.0/24 | GKE node pool di sini |
| └─ pods (secondary) | 10.40.128.0/17 | ~32k pod IPs |
| └─ services (secondary) | 10.40.32.0/20 | ~4k ClusterIP services |
| Control-plane (private) | 172.16.0.0/28 | GCP-managed, /28 = 16 IP |
Kalau anda deploy ulang di project yang sama dan terjadi IP overlap, ganti angka 10.40.x.x ke range lain (misal 10.50.x.x untuk prod). Pola ini saya pakai untuk dev (10.40) dan prod (10.50) supaya tidak konflik saat peering di masa depan.
Step 1 — Export environment variables
Taruh di ~/.zshrc supaya persist, atau export manual di sesi ini:
cd /Users/gunawan/Documents/learn/terraform/newbie-terraform/days-7-gcp-gke
export PROJECT_ID="«PROJECT_ID»"
export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT="learn-platform-engineer@${PROJECT_ID}.iam.gserviceaccount.com"
Verifikasi ADC masih hidup:
gcloud auth application-default print-access-token # Harus return access token tanpa error
Step 2 — Whitelist IP rumah/kantor kamu
GKE control-plane endpoint-nya public tapi IP-walled. Artinya: walaupun endpoint expose ke internet, kubectl cuma bisa connect dari IP yang kamu whitelist di master_authorized_cidr_blocks.
Cek IP publik kamu saat ini:
curl -s https://api.ipify.org # Output: «IP-PUBLIC-KAMU» (contoh)
Edit live/gcp/dev/gke/terragrunt.hcl, cari baris ini:
master_authorized_cidr_blocks = [
{
cidr_block = "«IP-PUBLIC-KAMU»/32" # ← GANTI dengan IP kamu
display_name = "home-ip-dev"
},
]
⚠ Penting: kalau IP rumah kamu dinamis (sebagian besar ISP rumah), IP ini bisa berubah. Setiap kali berubah, edit &
terragrunt applylagi. Untuk production, pakai range IP kantor atau VPN (misal203.0.113.0/24).
Step 3 — Enable Kubernetes Engine API
API ini belum enabled by default di project baru:
gcloud services enable container.googleapis.com --project=$PROJECT_ID
Tunggu sampai command return (biasanya 10-30 detik). Verifikasi:
gcloud services list --project=$PROJECT_ID --enabled | grep container # Harus muncul: container.googleapis.com
Step 4 — Apply VPC stack (wajib dulu — GKE butuh subnet)
cd live/gcp/dev/vpc terragrunt init terragrunt plan # cek: 7 resource terragrunt apply # ketik "yes"
Kamu harusnya lihat output kurang lebih:
google_compute_network.dev-gke-vpc : Created google_compute_subnetwork.gke-subnet-public : Created google_compute_subnetwork.gke-subnet-private : Created (dengan secondary ranges!) google_compute_router.dev-gke-vpc-router : Created google_compute_router_nat.dev-gke-vpc-nat : Created google_compute_firewall.dev-gke-vpc-allow-internal : Created google_compute_firewall.dev-gke-vpc-allow-iap-ssh : Created
Verifikasi subnet punya secondary ranges:
gcloud compute networks subnets describe gke-subnet-private \
--region=asia-southeast2 --project=$PROJECT_ID \
--format="json" | jq '.secondaryIpRanges'
# Harus muncul:
# [
# { "ipCidrRange": "10.40.128.0/17", "rangeName": "pods" },
# { "ipCidrRange": "10.40.32.0/20", "rangeName": "services" }
# ]
Kalau secondaryIpRanges-nya kosong, cek live/gcp/dev/vpc/terragrunt.hcl — kemungkinan CIDR-nya salah format atau typo nama range.
Step 5 — Apply GKE stack
cd ../gke terragrunt init terragrunt plan terragrunt apply # ketik "yes"
Tunggu 3–5 menit — cluster bootstrap lebih lama dari VM biasa. Jangan panik kalau progress stuck di google_container_cluster.dev-gke-cluster. Itu normal.
Kalau selesai:
google_container_cluster.dev-gke-cluster : Creation complete after 4m12s google_container_node_pool.pools["primary"] : Creation complete after 1m30s
Verifikasi dari GCP CLI:
gcloud container clusters describe dev-gke-cluster \ --zone=asia-southeast2-a --project=$PROJECT_ID \ --format='table(name,location,status,privateClusterConfig.enablePrivateNodes)'
Step 6 — Connect kubectl
# Generate kubeconfig — ini akan download kredensial dan simpan di ~/.kube/config gcloud container clusters get-credentials dev-gke-cluster \ --zone asia-southeast2-a \ --project $PROJECT_ID # Verify kubectl get nodes # NAME STATUS ROLES AGE VERSION # gke-dev-gke-cluster-primary-xxxx-aaaa Ready <none> 2m v1.30.x # gke-dev-gke-cluster-primary-xxxx-bbbb Ready <none> 2m v1.30.x kubectl get pods -A # (akan muncul pod system: kube-proxy, metrics-server, dll)
Kalau kubectl get nodes return “connection refused”, kemungkinan:
- IP rumah kamu tidak ada di
master_authorized_cidr_blocks→ edit & apply lagi. - Cluster masih booting → tunggu 1-2 menit.
🌐 Step 7 — Setup Shared ALB
Sekarang cluster jalan, kita expose app ke internet via 1 ALB.
7.1 — Reserve global static IP
cd live/gcp/dev/lb terragrunt init terragrunt apply
Output:
Apply complete! Resources: 1 added, 0 changed, 0 destroyed. Outputs: address = "34.120.208.xxx" name = "dev-shared-alb-ip"
Cek di GCP:
gcloud compute addresses describe dev-shared-alb-ip --global --project=$PROJECT_ID # address: 34.120.208.xxx # status: RESERVED
IP ini yang akan jadi “wajah” semua app kamu. CNAME semua subdomain ke hostname ALB yang resolve ke IP ini.
7.2 — Deploy app via Helm (tanpa Ingress per-app)
Pattern shared Ingress mengharuskan Ingress di-manage terpisah dari Helm chart. Jadi pastikan di setiap values-*.yaml:
ingress: enabled: false # ← HARUS false
File values-learn.yaml, values-nodeapp.yaml, values-maintenance.yaml sudah saya set begitu. Cek sebelum lanjut:
grep -A1 "ingress:" values-learn.yaml # ingress: # enabled: false
Deploy semua app:
# Create namespace sekali (kalau belum ada) kubectl create namespace be # Deploy 4 app (TIDAK ada Ingress yang dibuat oleh Helm) cd /Users/gunawan/Documents/learn/terraform/newbie-terraform/days-7-gcp-gke helm install learn charts/learn/ -f values-learn.yaml -n be helm install maintenance charts/learn/ -f values-maintenance.yaml -n be helm install nodeapp charts/learn/ -f values-nodeapp.yaml -n be
ℹ Untuk
fedanapp5(yang ada di shared-ingress.yaml), kamu perlu tambah values-nya sendiri atau comment rule-nya dulu di Ingress sebelum apply.
Cek pod & service:
kubectl get pods,svc -n be # NAME READY STATUS RESTARTS AGE # pod/learn-xxxx-yyyy 1/1 Running 0 30s # pod/maintenance-xxxx-yyyy 1/1 Running 0 30s # pod/nodeapp-xxxx-yyyy 1/1 Running 0 30s # # NAME TYPE CLUSTER-IP PORT(S) # service/learn ClusterIP 10.40.32.42 80/TCP # service/maintenance ClusterIP 10.40.32.43 3000/TCP # service/nodeapp ClusterIP 10.40.32.44 3000/TCP
Perhatikan: service IP-nya dari range 10.40.32.0/20 (secondary range services). Bukti bahwa VPC-native bekerja.
7.3 — Apply shared Ingress (1 file, semua host)
kubectl apply -f k8s/shared-ingress.yaml
Output:
ingress.networking.k8s.io/shared-alb created
Tunggu ~1-2 menit sampai GCP selesai provision ALB. Cek:
kubectl get ingress -n be # NAME CLASS HOSTS ADDRESS # shared-alb gce learn.igunawan.com,... 34.120.208.xxx
Kalau kolom ADDRESS kosong lebih dari 2 menit, cek event Ingress:
kubectl describe ingress shared-alb -n be # Lihat di section "Events" — biasanya ada pesan error kalau static IP belum ready
Verifikasi juga dari GCP Console: Network Services → Load Balancing — hanya boleh ada 1 Load Balancer untuk semua app. Kalau lebih dari 1, berarti ada Ingress duplikat.
7.4 — Setup DNS
Ambil hostname ALB:
ALB=$(kubectl get ingress shared-alb -n be -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo $ALB
# Contoh: 34.120.208.xxx.lb.PROJECT_NUMBER.googleusercontent.com
Di DNS provider kamu (Cloudflare / Route53 / Cloud DNS), buat CNAME record untuk semua subdomain:
| Type | Name | Target |
|---|---|---|
| CNAME | learn | $ALB |
| CNAME | nodeapp | $ALB |
| CNAME | maintenance | $ALB |
| CNAME | fe | $ALB |
Alternatif: A record. Bisa juga langsung A record ke
34.120.208.(IP statis yang sudah di-reserve). Lebih cepat resolve, tapi kalau IP pindah (misal pindah project) harus update record. CNAME kegoogleusercontent.comlebih portable — GCP yang handle redirect ke IP nearest.
7.5 — Verifikasi
Tanpa DNS propagate (langsung ke IP, pakai Host header):
ALB_IP="34.120.208.xxx" curl -H "Host: learn.igunawan.com" http://$ALB_IP/ curl -H "Host: nodeapp.igunawan.com" http://$ALB_IP/ curl -H "Host: maintenance.igunawan.com" http://$ALB_IP/
Setelah DNS propagate (bisa 1-10 menit tergantung TTL):
dig learn.igunawan.com +short # Harus return 34.120.208.xxx (atau IP lain jika pakai CDN) curl http://learn.igunawan.com/ # Harus return HTML app "learn"


Admin website igunawan.com, System Administrator, DevOps Engineer