iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Kubernetes

從零到一:使用 K8S + GitOps 打造異構技術棧的資料分析平台系列 第 20

[Day 20] 部署策略:滾動更新、回滾與金絲雀發布 —— 從 Deployment 內建的滾動更新,到 Argo Rollouts 的金絲雀,以及出事時到底該怎麼退回去。

  • 分享至 

  • xImage
  •  

Day 20: 部署策略:滾動更新、回滾與金絲雀發布

從 Deployment 內建的滾動更新,到 Argo Rollouts 的金絲雀——以及出事時到底該怎麼退回去。

1. 為什麼不能直接切換?

Wafer BI 的使用者正在看報表,你突然把服務關掉換新版,他的畫面直接報錯——這種「維護中」的體驗在 2026 年是不被原諒的。要做到零停機更新 (Zero Downtime),需要聰明的部署策略。

2. K8S 內建款:滾動更新 (Rolling Update)

Deployment 預設就是滾動更新:起一個新 Pod → 等 Readiness Probe 通過(Day 14 的伏筆回收)→ 導流量 → 關一個舊 Pod → 重複到換完。搭配好好寫的探針,基本的零停機就有了。

控制節奏的是兩個旋鈕,寫在 helm/wafer-bi/templates/api-gateway.yaml

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # 換版期間最多可以「多」出幾個 Pod
      maxUnavailable: 0    # 換版期間最多可以「少」幾個可用 Pod

以 4 個 replica 為例,三種組合的差別很有感:

設定 換版期間的 Pod 數 特性
maxSurge: 1 / maxUnavailable: 0(我們用的) 4~5 個,可用數永遠 ≥ 4 最安全,但必須有多一個 Pod 的資源餘量
maxSurge: 0 / maxUnavailable: 1 3~4 個,可用數可能掉到 3 不吃額外資源,代價是換版期間容量少 25%
25% / 25%(K8S 預設值) 4~5 個,可用數可能掉到 3 最快,但尖峰時段換版可能被打爆

兩個容易吃虧的細節:

  • maxUnavailable: 0 的隱藏前提是叢集排得出那顆新 Pod。節點資源不夠時,新 Pod 卡在 Pending、舊 Pod 又不准關,整個更新就永遠停在半路,而且服務看起來一切正常(因為舊版還在跑),你不主動看沒人會告訴你。
  • 這時候救場的是 progressDeadlineSeconds(預設 600 秒):超時後 Deployment 會被標記 ProgressDeadlineExceeded。但請注意——它只是把狀態標紅,不會自動回滾,這是很多人的誤解。真的要退,得靠下一節。

滾動更新還有個天生限制:換就是全換。新版本有 bug 的話,滾動到最後所有使用者都會中獎,你只能眼睜睜看著錯誤率一路爬升。

3. 出事了:rollout 的三個指令

滾動更新的完整工具組其實只有三個動詞,平常沒事不會用到,出事的時候手要夠快:

# 盯著這次更新有沒有走完(CI 裡當閘門很好用,卡住會非 0 退出)
kubectl rollout status deployment/api-gateway -n k8sdemo --timeout=120s

# 看歷史版本(每個 revision 對應一個 ReplicaSet)
kubectl rollout history deployment/api-gateway -n k8sdemo

# 退回上一版 / 退回指定版本
kubectl rollout undo deployment/api-gateway -n k8sdemo
kubectl rollout undo deployment/api-gateway -n k8sdemo --to-revision=3

能退幾步由 revisionHistoryLimit 決定(預設 10)——K8S 是靠「保留舊的 ReplicaSet(replica 數為 0)」來記住歷史的,你在 kubectl get rs 看到一堆 DESIRED 為 0 的舊 RS 不是垃圾,那就是你的後悔藥。設成 0 等於自廢武功。

實測:開一個 4 replica 的 demo Deployment(跑的是我們自己的 api-gateway image,maxSurge: 1 / maxUnavailable: 0progressDeadlineSeconds 縮短成 60 秒方便觀察),然後故意把 image 換成一個拉不到的 tag:

https://ithelp.ithome.com.tw/upload/images/20260822/201825491mZTpRAWZY.png

▲ 壞版本卡住的完整現場:rollout status 非 0 退出、舊版 4 個 Pod 一個都沒關、ProgressDeadlineExceeded 但不自動回滾、undo 之後舊 ReplicaSet 原地復活

這張圖有以下四個地方可以看:

  1. rollout status 用 exit code 1 收場——這就是它在 CI 裡的價值,kubectl apply 永遠是成功的(宣告成功不等於部署成功),只有 rollout status 會告訴你「這版沒上去」
  2. 壞版本卡在 ErrImagePull,但 AVAILABLE 還是 4——maxUnavailable: 0 說到做到,新 Pod 沒 Ready 就一個舊的都不准關。服務完全沒有受影響,而且也因此沒有任何人會發現出事了
  3. ProgressDeadlineExceeded 出現了,然後……就沒有然後了——Available=True 依舊,K8S 只是把這次更新標記成失敗,它不會替你退回去。這件事必須你自己(或 CI)動手
  4. undo 之後看 ReplicaSet:壞的 revision 2 被縮到 0(留著當後悔藥),revision 3 直接沿用原本那個好的 RS——回滾不是重新部署一次舊版,是把舊的 ReplicaSet 叫醒,所以它才會這麼快

順帶一提我們自己埋的雷:Helm 模板裡有一行 deployed-at: {{ now | date ... }} 的 Pod annotation。它讓每次渲染出來的 Pod template 都不一樣,好處是強制重新部署,壞處是 rollout history 會被灌滿意義不明的 revision,而且在 ArgoCD 眼中永遠 OutOfSync。方便和乾淨,這裡只能挑一個。

4. 回滾三層樓:GitOps 之下,前兩層都是幻覺

這是我覺得整個系列最反直覺的一段。上面那個 kubectl rollout undo 打下去,在我們這套架構裡是沒有用的

原因在 k8s/argocd-app.yaml 裡,我們自己寫的那行註解:

  syncPolicy:
    automated:
      prune: true
      # 如果有人手動改了 K8s(例如 kubectl edit),自動把它改回來,維持與 Git 一致
      selfHeal: true

kubectl rollout undo 改的是叢集裡的 live 物件,而 Git 裡的 values.yaml 還寫著那個壞掉的 image SHA。對 ArgoCD 來說,你這叫「手動改了 K8S」——於是它盡忠職守地把壞版本蓋了回來,而且不會有任何錯誤訊息。

這件事我實際跑了一次:把我們 repo 裡的一份 manifest(k8s/base/observability/jaeger.yaml)交給一個 selfHeal: true 的 ArgoCD Application 管理,假設 Git 裡的 1.60.0 就是那個壞版本,然後用 kubectl rollout undo 退回 1.57.0

https://ithelp.ithome.com.tw/upload/images/20260822/20182549LHmfUBU1W3.png

▲ GitOps 底下的 rollout undo:undo 成功了 2 秒,就被 ArgoCD 的自動 Sync 蓋回壞版本,全程沒有任何錯誤訊息

兩秒。不是三十秒,是兩秒——undo 印出 rolled back、image 欄位確實變成 1.57.0,我還來不及切視窗,它就變回 1.60.0 了。而且證據鏈非常完整:

  • kubectl get events 裡那行 Initiated automated sync to '6bdc539...'——這串 SHA 就是我 repo 的 main HEAD,它是拿著 Git 的版本來壓你的
  • initiatedBy: {"automated":true}——沒有任何人按過任何按鈕,這是控制器自己的意志
  • 從頭到尾 phase=Succeeded。對 ArgoCD 來說,這就是一次正常的同步

三層樓一次講清楚:

層級 指令 在我們這套 GitOps 下的下場
K8S 物件 kubectl rollout undo ⚠️ 2 秒後被 selfHeal 蓋回壞版本,且靜默無聲
Helm release helm rollback wafer-bi 1 ❌ 沒有東西可以退——叢集裡根本沒有 Helm release
Git commit git revert <bump commit> ✅ 唯一真正有效的回滾

中間那格要補充一下:Day 10 教的 helm rollback 不是騙人的,它在「你自己 helm upgrade --install」的環境完全有效。但 ArgoCD 是把 Chart 用 helm template 渲染成純 YAML 再 apply,Helm 在這裡只剩下「模板引擎」的身分。Helm 的版本歷史其實是存在 namespace 裡、type 為 helm.sh/release.v1 的 Secret,而在我們的叢集查下去是這樣的:

$ kubectl get secret -n k8sdemo -l owner=helm
No resources found in k8sdemo namespace.

一筆都沒有(截圖最後一段)。沒有 release 就沒有 revision,helm rollback 連要退什麼都不知道。同一個指令在不同架構下的有效性不同,這種事光看文件是看不出來的。

所以正確的緊急回滾長這樣:

# 1. 找到 CI 那筆自動 commit(Day 19 的 bump image tags)
git log --oneline --grep="deploy: bump image tags"

# 2. 把它 revert 掉——values.yaml 的 tag 退回上一個 SHA
git revert <bad-commit-sha>
git push

推上去,ArgoCD 偵測到 Git 變了,自動 sync 回舊版,滾動更新再跑一次。回滾在 GitOps 裡不是一個特殊操作,它就是一次普通的部署,只是方向朝後。

這裡也正好回收 Day 15 的伏筆——當初堅持給每個 image 打上 commit SHA 而不是只有 latest,就是為了這一刻:revert 之後 values.yaml 裡躺著的是一個明確、不可變、絕對拉得到的舊版本。如果當初寫的是 latest,你 revert 完 Git,叢集拉到的還是同一包壞掉的 image,哭都沒地方哭。

實務上還有以下兩件事情:

  • revert 那筆 tag commit,比 revert 你的程式碼快得多。revert 程式碼要重跑一次 CI build 六個 image(幾分鐘起跳),revert tag commit 只要 ArgoCD 同步(幾十秒),因為舊 image 早就躺在 GHCR 上了。先止血,再修 bug
  • 真的需要在叢集上手動急救(例如 Git 也壞了),先關掉自動同步:argocd app set wafer-bi-platform --sync-policy none。這等於暫時退出 GitOps,記得事後要開回來,不然哪天大家都忘了為什麼部署不動了。

5. 進階三策略速覽

策略 做法 代價
滾動更新 逐步汰換 有 bug 全員中獎
藍綠部署 新舊兩套並存,流量一鍵切換、一鍵切回 資源雙倍
金絲雀發布 先放一小撮流量試水溫,分階段擴大 需要工具支援

金絲雀 (Canary) 這名字來自礦坑裡的金絲雀——讓一小部分請求先進新版本,有毒(有 bug)的話犧牲最小。

6. 實測:Argo Rollouts 金絲雀發布

Argo Rollouts 提供一個 Rollout 資源,幾乎是 Deployment 的替身,但 strategy 換成可編排的發布劇本:

  strategy:
    canary:
      steps:
      - setWeight: 25          # 先放 25% 給新版本
      - pause: {}              # 無限暫停,等人工確認(或自動分析)
      - setWeight: 50
      - pause: { duration: 20s }
      - setWeight: 100

直接在叢集上實際跑一次完整的金絲雀發布——v1 穩定運行 4 個 replica,發布 v2:

https://ithelp.ithome.com.tw/upload/images/20260822/20182549i9BcOdYS4O.png

▲ Argo Rollouts 金絲雀實測:25% 暫停等確認 → promote → 100% 完成,舊版 ScaledDown

這張圖是整個發布的縮時攝影,有以下兩個關鍵時刻:

  1. 暫停在 25%Status: ॥ PausedSetWeight: 25——revision 2 只起了 1 個 canary Pod,另外 3 個還是 revision 1 的 stable。此刻新版本正在接 25% 的真實流量,你可以盯著監控慢慢看,不爽就 abort,流量立刻全部退回 stable 版本——這是第 4 節那三層樓之外的第四種退法,也是唯一能在「還沒全量」的時候就喊停的,受害者只有那 25%。
  2. promote 放行後:走完 50% → 100%,Status: ✔ Healthy,revision 2 變成 stable 4 連發,revision 1 被 ScaledDown 光榮退役。

整個過程服務沒有中斷過一秒。跟 Deployment 最大的差別就是那個 pause——發布從「一鍵到底的賭博」變成「可以隨時喊停的分段決策」

7. 下一步:讓機器自己決定 promote

現在的 pause 還是等人工按 promote,終極型態是接上 AnalysisTemplate:Argo Rollouts 自動查 Prometheus(Day 22 就會裝了),canary 的錯誤率超標就自動 abort 回滾,全程不需要人醒著。把「半夜發版」變成「睡覺發版」。

8. 小結

今天做的其實是同一件事的正反兩面:怎麼安全地往前推(滾動更新的旋鈕、金絲雀的分段放行),以及怎麼確實地退回來(rollout undo、helm rollback、git revert 三層樓)。前者大家都會查,後者要等出事那天才知道自己會不會——而出事那天你不會有時間查文件。

**自動化的部署鏈路有多強,回滾就必須走在同一條鏈路上。**繞過 GitOps 的急救,效期只有兩秒。

從 Day 15 到今天,我們把「push code」到「安全上線」整條路走完了:CI 建置 → GitOps 同步 → 金絲雀護航 → 出事能退。自動化篇還剩最後一塊拼圖——明天來掃描 Image 裡的安全漏洞,讓供應鏈也睡得安穩。


上一篇
[Day 19] 流水線串接:實現代碼推送即部署的自動化閉環 —— 打通從 Commit 到 K8S 生產環境的最後一哩路。
下一篇
[Day 21] 供應鏈安全:鏡像掃描 (Image Scanning) 與漏洞管理 —— 在 Image 進入叢集前,先抓出潛在的安全漏洞。
系列文
從零到一:使用 K8S + GitOps 打造異構技術棧的資料分析平台21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言