起因:老闆想省錢
「Staging 環境平常沒人用,每個月還要燒 $45-60 美金,能不能想辦法省一點?」
Staging 環境的成本來自兩個地方:RDS 資料庫(約 $15-20/月)和 EKS 節點(約 $30-40/月)。既然平常沒在用,我想到了一個方案:不用的時候關掉,需要的時候再打開。
於是我寫了兩個腳本:
staging-start.sh:啟動 RDS、擴充節點、部署應用staging-stop.sh:刪除部署、縮減節點、停止 RDS
# staging-stop.sh 核心邏輯
kubectl delete deployment app-strapi-stg app-web-stg
aws eks update-nodegroup-config \
--cluster-name my-cluster \
--nodegroup-name my-nodegroup \
--scaling-config minSize=0,maxSize=2,desiredSize=1 # 從 2 縮到 1
aws rds stop-db-instance --db-instance-identifier my-stg-rds
看起來很合理,但這裡有個問題:Production 和 Staging 共用同一個 nodegroup。
踩坑:AWS 隨機選擇刪除節點
執行 staging-stop.sh 縮減節點時,AWS 會隨機選擇要終止哪個節點。當時的配置:
- 節點 A:運行 Production pods
- 節點 B:運行 Staging pods
我期望刪除節點 B,但 AWS 選了節點 A。Production pods 被強制遷移,觸發了重新調度。
這本身不是大問題,Kubernetes 會自動重建 pods。但在嘗試修復時,我犯了另一個錯誤:直接 kubectl apply 了一個過時的 YAML 檔案。
檔案中的 image 版本是舊的 0.73,而實際運行的是 1.134。舊版應用程式啟動時執行了 schema migration,清空了部分資料欄位和權限設定。
快速修復:RDS 備份 + Sequence 同步
幸好 RDS 有自動備份,修復過程約一小時:
# 1. 從昨天的 snapshot 建立臨時 RDS
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier my-recovery-rds \
--db-snapshot-identifier rds:my-rds-2026-01-05-21-02
# 2. 匯出需要的資料
pg_dump -h recovery-rds.xxx.rds.amazonaws.com \
-t animations -t probiotics -t user_unlocked_animations \
--data-only > backup_data.sql
# 3. 匯入到 Production
psql -h prod-rds.xxx.rds.amazonaws.com -f backup_data.sql
但匯入後 App 還是異常。原因是 PostgreSQL sequence 沒同步:
-- 資料表 MAX(id) = 1,645,但 sequence 還停在舊值
-- 新資料會嘗試用已存在的 id,導致 duplicate key error
-- 修復:同步 sequence 到 MAX(id)
SELECT setval(
'user_unlocked_animations_id_seq',
(SELECT MAX(id) FROM user_unlocked_animations),
true
);
另外還要恢復 Strapi 的 API 權限表 up_permissions,App 才能正常存取 API。
什麼是 Nodegroup?為什麼要分開?
在解釋解法之前,先釐清 EKS Nodegroup 的概念。
Nodegroup 是 EKS 管理的一組 EC2 節點,定義了:
- 節點的機器規格(instance type)
- 自動擴縮配置(min/max/desired)
- 節點標籤(labels)和污點(taints)
- IAM 角色和安全群組
共用 vs 獨立 Nodegroup
【共用 Nodegroup】 【獨立 Nodegroup】
┌─────────────────────────┐ ┌──────────────┐ ┌──────────────┐
│ my-nodegroup │ │ prod-nodes │ │ stg-nodes │
│ ┌─────┐ ┌─────┐ │ │ ┌─────┐ │ │ ┌─────┐ │
│ │Prod │ │ Stg │ │ │ │Prod │ │ │ │ Stg │ │
│ │Pods │ │Pods │ │ │ │Pods │ │ │ │Pods │ │
│ └─────┘ └─────┘ │ │ └─────┘ │ │ └─────┘ │
└─────────────────────────┘ └──────────────┘ └──────────────┘
縮減時 AWS 隨機選節點 縮減 stg-nodes 不影響 prod-nodes
| 面向 | 共用 Nodegroup | 獨立 Nodegroup |
|---|---|---|
| 成本 | 較低(節點共用) | 稍高(各自節點) |
| 隔離性 | 無隔離,互相影響 | 完全隔離 |
| 擴縮控制 | 無法針對環境操作 | 可獨立擴縮各環境 |
| 風險 | 操作一個影響全部 | 操作範圍受限 |
結論:省下的幾塊錢節點費用,換來的是操作風險。Production 和 Staging 應該用獨立 Nodegroup。
AWS 有什麼工具能避免這種情況?
除了獨立 Nodegroup,AWS/Kubernetes 還提供幾個防護機制:
1. Pod Disruption Budget (PDB)
PDB 限制同時可被中斷的 pods 數量,防止縮減節點時影響服務:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: prod-pdb
spec:
minAvailable: 1 # 至少保留 1 個 pod 運行
selector:
matchLabels:
env: prod
當 AWS 嘗試終止節點時,如果會違反 PDB,操作會被阻擋或延遲。
2. Node Taints + Tolerations
比 nodeSelector 更強的隔離機制。Taint 會「排斥」pods,只有帶對應 Toleration 的 pods 才能調度上去:
# 給 production 節點加上 taint
kubectl taint nodes prod-node-1 env=prod:NoSchedule
# Production pods 加上 toleration 才能調度
spec:
tolerations:
- key: "env"
operator: "Equal"
value: "prod"
effect: "NoSchedule"
3. AWS Node Termination Handler
處理 Spot Instance 中斷和節點維護事件,在節點被終止前安全遷移 pods:
helm install aws-node-termination-handler \
eks/aws-node-termination-handler \
--namespace kube-system
4. Karpenter(進階)
AWS 推出的智慧節點管理工具,比傳統 Cluster Autoscaler 更靈活:
- 自動選擇最適合的 instance type
- 更快的擴縮反應時間
- 支援 Spot Instance 混合使用
這次事件最適合的防護
| 工具 | 能否防止這次問題 | 說明 |
|---|---|---|
| 獨立 Nodegroup | ✅ 完全防止 | 從根本隔離環境 |
| PDB | ⚠️ 部分防止 | 可延遲節點終止,但不能阻止 |
| Taints | ✅ 可防止 | 確保 pods 只在對的節點 |
| Node Termination Handler | ❌ 無法防止 | 主要處理 Spot 中斷 |
最佳實踐:獨立 Nodegroup + NodeSelector + PDB 三層防護。
正確做法:獨立 Nodegroup + NodeSelector
省錢的需求是合理的,但架構要正確。最終方案是建立獨立的 Staging nodegroup:
# 建立獨立的 Staging nodegroup(初始 0 個節點)
aws eks create-nodegroup \
--cluster-name my-cluster \
--nodegroup-name my-cluster-nodes-staging \
--scaling-config minSize=0,maxSize=2,desiredSize=0 \
--labels env=stg
並在 Deployment 加入 nodeSelector 確保 pods 調度到正確節點:
# Production
spec:
template:
spec:
nodeSelector:
env: prod
# Staging
spec:
template:
spec:
nodeSelector:
env: stg
現在 staging-stop.sh 只操作 Staging nodegroup,不影響 Production:
aws eks update-nodegroup-config \
--nodegroup-name my-cluster-nodes-staging \
--scaling-config desiredSize=0
加入團隊規範的鐵則
這次經驗讓我在團隊規範加入一條鐵則:
kubectl apply 前必須確認 image 版本
# 套用前先確認目前運行的版本
kubectl get deployment my-app -o jsonpath='{.spec.template.spec.containers[0].image}'
# 比對 YAML 檔案中的版本
grep "image:" deployment.yaml
# 版本一致才能 apply
總結
| 問題 | 解法 |
|---|---|
| 共用 nodegroup 導致縮減時刪錯節點 | 建立獨立的 Staging nodegroup |
| 沒有環境隔離機制 | 使用 nodeSelector 綁定 pods 到特定節點 |
| YAML 檔案版本過時 | 部署前必須確認 image 版本 |
| 資料恢復後仍異常 | 同步 PostgreSQL sequence + 恢復權限表 |
省錢是好事,但要用正確的架構來實現。獨立 nodegroup + nodeSelector 的組合,既能按需開關 Staging 環境省錢,又不會影響 Production。
最後感謝 RDS 自動備份,讓這次踩坑能在一小時內修復完成。
