第 8 章:把 Kubernetes 接到雲端 — GKE(managed K8s)

第 8 章:把 Kubernetes 接到雲端 — GKE(managed K8s)

這一章在做什麼? 我們用 Terraform 開一個跑在 GCP 上的 Kubernetes 叢集 GKE(Google Kubernetes Engine),把第 7 章那套 manifest 幾乎原封不動套上去,並用 LoadBalancer 取得一個對外 IP,讓全世界都連得到。做完這章,你會知道「本機 kind 學的東西,怎麼搬到真正的雲端 K8s」。

為什麼需要 managed K8s? Kubernetes 分兩部分:控制平面(叢集的大腦)和工作節點(實際跑你的 Pod 的機器)。自己維護控制平面(高可用、升級、安全)非常吃力。GKE 幫你把這些都管好,你只管部署。其中 Autopilot 模式更進一步連節點都幫你管——你只丟 Pod、按 Pod 用量付費,完全不碰 VM。

前置:第 5 章的 Terraform(已啟用 container.googleapis.com)、GHCR 上的公開 image。

💸 本章會計費! GKE Autopilot 的「叢集管理費」大多被免費額度抵掉,但 Pod 的運算(vCPU/記憶體)、以及 LoadBalancer 都會計費。所以章末第一優先就是 destroy——學完立刻拆。


步驟 1:先搞懂 GKE 與 Autopilot

名詞 白話
Managed K8s 雲商幫你代管 Kubernetes 的控制平面(大腦),你只用、不維護它。
GKE Google 的 managed Kubernetes 服務。
Autopilot 模式 GKE 連「工作節點(VM)」也幫你管。你只部署 Pod、按 Pod 實際請求的資源付費,不必管理機器。對新手最省心。
Standard 模式 你自己管理節點池(VM 數量、型號)。較有彈性但較費心。

我們用 Autopilot

🔁 與第 7 章的關係:kind 是「本機玩具叢集」,GKE 是「雲端正式叢集」。好消息是:K8s manifest 幾乎通用——你在 kind 寫的 Deployment/Service,搬到 GKE 只要改兩個地方(image 來源、Service 對外方式)。這就是 K8s 的可攜性價值。


步驟 2:Terraform — 開一個 GKE Autopilot 叢集

建立 code/terraform/gke.tf

# code/terraform/gke.tf
resource "google_container_cluster" "gke" {
  name     = "nlt-gke"
  location = var.region # 用 region = regional 叢集

  enable_autopilot    = true  # 啟用 Autopilot(連節點都託管)
  deletion_protection = false # 關鍵:讓 terraform destroy 能刪掉叢集
}

逐行解釋:

  • google_container_cluster.gke:宣告一個 GKE 叢集。
  • location = var.region:用 region(如 asia-east1)開「區域型」叢集。
  • enable_autopilot = true:用 Autopilot 模式(自動管理節點、VPC-native 等預設都幫你設好)。
  • deletion_protection = false:又是這個——預設 true 會讓 destroy 失敗,務必設 false。

套用(叢集建立要等 5–10 分鐘,這是正常的):

cd code/terraform
terraform apply    # 輸入 yes

步驟 3:讓 kubectl 指到 GKE

Terraform 建好叢集後,用 gcloud 抓憑證,把 kubectl 的 context 切到 GKE:

# 首次可能要先裝驗證外掛
gcloud components install gke-gcloud-auth-plugin

gcloud container clusters get-credentials nlt-gke \
  --region asia-east1 --project 你的-project-id

kubectl config current-context   # 應變成 gke_你的專案_asia-east1_nlt-gke
kubectl get nodes                # Autopilot 下可能暫時看不到節點,正常

現在 kubectl 講的對象從本機 kind 變成了雲端 GKE。同一個 kubectl,換個 context 就換叢集。


步驟 4:把 manifest 套上去(只改兩個地方)

第 7 章的 k8s/namespace.yamlk8s/postgres.yaml 完全沿用(in-cluster Postgres 直接跑在 GKE 裡)。k8s/app.yaml 只要改兩處:

改① image 改用 GHCR 公開 image(雲端叢集沒有 kind load,要從 registry 拉):

# code/k8s/app.yaml(Deployment 內,把 image 那行改成)
          image: ghcr.io/<你的帳號>/no-logic-trade:latest
          imagePullPolicy: Always

改② Service 改成 LoadBalancer(向雲端要一個對外 IP):

# code/k8s/app.yaml(最後的 Service 改成)
apiVersion: v1
kind: Service
metadata:
  name: nlt-api
  namespace: nlt
spec:
  type: LoadBalancer # ★ 雲端會配一個對外 IP
  selector:
    app: nlt-api
  ports:
    - port: 80
      targetPort: 8080
  • type: LoadBalancer:請 GCP 配一個「對外負載平衡器 + 公開 IP」,把外部流量導進你的 Pod。(kind 用 port-forward,雲端用 LoadBalancer。)

套用:

cd code
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/postgres.yaml
kubectl apply -f k8s/app.yaml

Autopilot 小提醒:Pod 一開始可能卡 Pending 幾分鐘——因為 Autopilot 正在臨時配置節點來跑你的 Pod。這是正常的,等就好。


✅ 如何驗證這一章成功

驗證 1:Pod 都跑起來

kubectl get pods -n nlt
# 預期(可能等幾分鐘):postgres 與 nlt-api 都 Running 1/1

驗證 2:拿到對外 IP 並打它

kubectl get svc -n nlt nlt-api -w
# 等 EXTERNAL-IP 從 <pending> 變成一個真的 IP(要等 1-3 分鐘)

拿到 IP 後(Ctrl+C 離開 watch):

IP=$(kubectl get svc -n nlt nlt-api -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl http://$IP/health
# 預期:{"status":"ok"}

跑一輪流程(打到雲端 GKE):

curl -s -X POST http://$IP/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"gke@example.com","password":"secret123"}'
  • [ ] kubectl get nodes/pods 顯示這是雲端 GKE 叢集且 Pod Running。
  • [ ] nlt-api Service 拿到 EXTERNAL-IP。
  • [ ] curl http://$IP/health{"status":"ok"}

💸 用完即刪(第一優先!)

GKE 叢集 + LoadBalancer 持續計費。正確的拆除順序:先刪 K8s 裡的 LoadBalancer(避免殘留雲端轉發規則),再 destroy 叢集。

# 1) 先刪掉 namespace(連同 LoadBalancer Service 一起移除 → 釋放對外 IP/轉發規則)
kubectl delete namespace nlt

# 2) 再用 Terraform 刪掉 GKE 叢集
cd code/terraform
terraform destroy   # 輸入 yes(刪叢集要等幾分鐘)

確認都清乾淨:

gcloud container clusters list --project 你的-project-id      # 預期:空
gcloud compute forwarding-rules list --project 你的-project-id # 預期:沒有殘留的 LB 轉發規則

最後到 Console → 帳單 → 報表確認花費停止累積。

⚠️ 最常見的漏網之魚:直接 terraform destroy 砍叢集、卻忘了先刪 LoadBalancer Service,可能留下一條仍計費的轉發規則。所以kubectl delete namespace nlt、再 destroy,並用上面的 forwarding-rules list 複查。


🧩 進階:改連 Cloud SQL(呼應第 6 章的選擇)

本章主線為了聚焦在 GKE,沿用了 in-cluster Postgres。若你想像第 6 章那樣連雲端 Cloud SQL,正式做法是:

  1. 用第 6 章的 Terraform 建一個 Cloud SQL 實例。
  2. 在 app 的 Pod 裡加一個 Cloud SQL Auth Proxy sidecar 容器,app 改連 127.0.0.1:5432(proxy 幫你安全地轉到 Cloud SQL)。
  3. Workload Identity 把 K8s 的服務帳號綁到有 cloudsql.client 的 Google 服務帳號(讓 proxy 免金鑰驗證)。

這牽涉較多設定(sidecar + Workload Identity 綁定),屬進階;新手先用 in-cluster Postgres 把 GKE 跑通即可。記得:用 Cloud SQL 時一樣要在章末 destroy。


🧯 常見錯誤 / 踩雷點

症狀 原因 解法
get-credentials 後 kubectl 報 gke-gcloud-auth-plugin not found 缺驗證外掛 gcloud components install gke-gcloud-auth-plugin(或用 brew 版 gcloud 的對應套件)
Pod 長時間 Pending Autopilot 正在配節點 / 資源請求過大 等幾分鐘;kubectl describe pod 看事件;Autopilot 會自動加節點
EXTERNAL-IP 一直 <pending> LoadBalancer 配置中,或配額不足 等 1-3 分鐘;kubectl describe svc -n nlt nlt-api 看事件;檢查專案配額
app ImagePullBackOff GHCR image 不是公開、或 image 名打錯 第 4 章把 package 設 Public;確認 image: 路徑正確
app CrashLoopBackOff 連不上 in-cluster Postgres kubectl logs;確認 DB_HOST=postgres、密碼一致、postgres Pod Ready
terraform destroy 刪不掉叢集 deletion_protection = true falseapplydestroy
帳單還在跑(已砍叢集) LoadBalancer 轉發規則殘留 先刪 Service 再砍叢集;gcloud compute forwarding-rules list 複查並手動刪殘留
curl $IP 不通但 Pod 正常 安全群組/防火牆、或還沒 ready 等 readinessProbe 通過;GKE 預設會開 LB 對應防火牆,稍等即可

下一步 → 第 9 章:監控與日誌 — Prometheus + Grafana 服務上雲了,但你「看得見它的健康嗎」?我們會讓 app 吐出指標(/metrics),用 Helm 在叢集裝 Prometheus 收集、Grafana 做儀表板,看見請求量、延遲、錯誤率——出事能即時發現,而不是等使用者來罵。