Deploy Google Kubernetes Engine (GKE): VPC-native, Private Node, dan Shared ALB

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)?

AspekGKE
NetworkingVPC-native pakai alias IP → pod IP adalah IP GCP beneran, bukan di-NAT. Routing lebih simple, throughput lebih tinggi.
Integrasi GCPWorkload Identity, Cloud Logging, Cloud Monitoring native. Tidak perlu setup Prometheus/Grafana dari nol.
Master di-manage GCPControl-plane di-upgrade & di-patch otomatis — kamu cuma manage node.
HargaCluster 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

ResourceCIDRCatatan
VPC10.40.0.0/16
gke-subnet-public10.40.0.0/24Reserved untuk jump host/ingress
gke-subnet-private10.40.16.0/24GKE 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/28GCP-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 apply lagi. Untuk production, pakai range IP kantor atau VPN (misal 203.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 fe dan app5 (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:

TypeNameTarget
CNAMElearn$ALB
CNAMEnodeapp$ALB
CNAMEmaintenance$ALB
CNAMEfe$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 ke googleusercontent.com lebih 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"

Leave a Reply

Your email address will not be published. Required fields are marked *