第 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.yaml 與 k8s/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-apiService 拿到 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砍叢集、卻忘了先刪LoadBalancerService,可能留下一條仍計費的轉發規則。所以先kubectl delete namespace nlt、再 destroy,並用上面的forwarding-rules list複查。
🧩 進階:改連 Cloud SQL(呼應第 6 章的選擇)
本章主線為了聚焦在 GKE,沿用了 in-cluster Postgres。若你想像第 6 章那樣連雲端 Cloud SQL,正式做法是:
- 用第 6 章的 Terraform 建一個 Cloud SQL 實例。
- 在 app 的 Pod 裡加一個 Cloud SQL Auth Proxy sidecar 容器,app 改連
127.0.0.1:5432(proxy 幫你安全地轉到 Cloud SQL)。 - 用 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 |
設 false 後 apply 再 destroy |
| 帳單還在跑(已砍叢集) | LoadBalancer 轉發規則殘留 | 先刪 Service 再砍叢集;gcloud compute forwarding-rules list 複查並手動刪殘留 |
curl $IP 不通但 Pod 正常 |
安全群組/防火牆、或還沒 ready | 等 readinessProbe 通過;GKE 預設會開 LB 對應防火牆,稍等即可 |
下一步 → 第 9 章:監控與日誌 — Prometheus + Grafana
服務上雲了,但你「看得見它的健康嗎」?我們會讓 app 吐出指標(/metrics),用 Helm 在叢集裝 Prometheus 收集、Grafana 做儀表板,看見請求量、延遲、錯誤率——出事能即時發現,而不是等使用者來罵。