起因:老闆想省錢

「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 自動備份,讓這次踩坑能在一小時內修復完成。