Helm 4 在 2025 年 11 月正式 GA,距離現在快滿一年。2026 年 9 月 9 日 Helm 3 已經發出最後一次 feature release,安全補丁排定 2027 年 2 月 10 日截止,中間留給團隊的緩衝期只剩不到五個月。Chart 本身可以無痛帶進 Helm 4,但 CI/CD 腳本、post-renderer、--wait 依賴的 RBAC,全部都有硬性破壞相容的地方。真正麻煩的是這些改動大多不會在 lint 或 dry-run 階段被抓到,往往等到 pipeline 綠燈跑完、實際到叢集才炸。
Helm 4 的整個重寫背後有兩條主軸。一條是把「helm client 端做過多事情」這個十年老問題往 Kubernetes API server 那邊推,主要體現在 server-side apply 變成新裝的預設;另一條是把 plugin subprocess 這種可以直接跑任意執行檔的攻擊面收進 Wasm 沙箱。這兩件事單獨看都是安全升級,合在一起就是升級路徑上會被踩爆的地雷區。
Wasm plugin runtime 把 subprocess 那條後門收掉
Helm 3 的 plugin 系統設計非常寬鬆:一個 plugin.yaml 加一支可執行檔,helm 直接以 subprocess 呼叫,把當下的環境變數、KUBECONFIG、認證 token 全部繼承過去。這條路走了七年,也養出一批 helm-diff、helm-secrets、helm-git 這類重度倚賴 subprocess 的外掛。問題是這個模型跟供應鏈安全完全對立,任何從 registry 拉下來的 plugin 都能讀寫檔案、發網路請求、甚至 exec 別的程式。
Helm 4 把 plugin 收成三種明確類型:cli(新增 helm 子命令)、getter(chart repository 協定,例如 s3、oci、git)、post-renderer(渲染後改寫 manifest)。底層改成用 Extism 的 Wasm runtime 跑,plugin 拿到的只是宣告過的 host function 白名單,沒辦法直接跑任意程序或碰檔案系統。原本的 subprocess 模型還在,向後相容用的:把 plugin.yaml 加上 apiVersion、type、runtime、runtimeConfig 這幾個新欄位,把 usage、description、ignoreFlags 塞進 config 區塊,就能讓舊 plugin 以 subprocess runtime 繼續跑。
真正要提前規劃的是 plugin 本身的作者何時會遷移到 Wasm。目前多數外部 plugin 還沒完成 Wasm 移植,短期內大家會停在 subprocess 相容模式。但一旦某個上游改成純 Wasm,繼承 host 環境變數這件事會直接消失,plugin 內部要靠什麼讀取 kubeconfig 或 secret 得走 Helm 4 定義的宣告式 host function,這是一條沒辦法直接抄 Helm 3 code 的路。內部自寫 plugin 的團隊,建議升到 Helm 4 之後先把 subprocess 模式跑過一輪測試,再決定要不要投資 Wasm 遷移。
–post-renderer 不能再直接指 script 這一刀最傷 CI
Kubernetes 生態圈裡「用 helm 加一段 kustomize overlay」這個組合大概佔了 GitOps 團隊三成的工作量。Helm 3 的做法非常直觀:
1 | helm install myapp ./chart --post-renderer ./scripts/kustomize-overlay.sh |
--post-renderer 直接吃一支可執行檔的路徑,stdout 進、stdin 出。Helm 4 把這條路徹底封起來,--post-renderer 現在只接受已註冊的 plugin 名稱:
1 | helm install myapp ./chart --post-renderer kustomize-overlay |
換句話說,任何跑在 CI 或 ArgoCD runner 上、事先把腳本丟進 workspace 再 pipe 給 helm 的做法,升到 Helm 4 當天就會直接失效。修法是把腳本包成 plugin,plugin.yaml 標 type: post-renderer、runtime: subprocess,用 helm plugin install 或者 mount 進 $HELM_PLUGINS 目錄。這件事本身不困難,但需要在 CI 的 base image、GitOps controller 的 sidecar、開發者本機三個地方同步處理。多數團隊會在升級當週才發現 base image 少了 plugin。
比較穩的順序是:先在 Helm 3 環境把腳本先包成 plugin、加註冊、把 CI 呼叫改成 plugin 名稱,這一步在 Helm 3 下就會通過,因為 3.x 也支援用 plugin 提供的 post-renderer。等這條路走順之後再升 Helm 4,post-renderer 這一塊就不會炸。
kstatus 讓 --wait 需要 watch 權限這件事會把 CI 卡在門口
Helm 3 的 --wait 邏輯很陽春:輪詢 Deployment、StatefulSet、DaemonSet 的 status,看 replica 到位就算好。這個做法對 Job、CRD、自訂 controller 幾乎沒用,也是為什麼 Helm 3 生態裡到處是「install 完 sleep 30 秒」的 workaround。Helm 4 換上 kstatus,這是 kubernetes-sigs/cli-utils 提出來的 readiness detection 標準,會依照 resource 種類套用適配的判斷條件,也支援讀 CRD 的 Ready condition。
換 kstatus 的實務代價寫在一行 RBAC 上:Helm 4 的 --wait 需要對 chart 佈署的所有 resource 種類擁有 watch verb。缺少的話操作不是繼續跑、只是後段等待失敗,而是在還沒對 chart 動任何一個 kubectl apply 之前就直接錯誤退出。多數團隊的 Helm ServiceAccount 只有 get、list、create、update、patch、delete,因為 Helm 3 從來沒用過 watch。這條 RBAC 在升級當下會把 pipeline 全部擋在門口。
修法有兩種。乾淨的做法是掃過所有 chart 使用的 resource kind,把 watch 補到 role 或 clusterrole 裡。不想動 RBAC 的話,Helm 4 保留了 --wait=legacy,會退回 Helm 3 的輪詢邏輯。這個 flag 對混合佈署過渡期很有用,但 kstatus 帶來的可靠性升級(尤其是 CRD readiness 判斷)就等於沒開。長期還是應該補 RBAC。
還有一個容易被忽略的細節:kstatus 對 CRD 的預設判定是「有 Ready: true condition 才算好」。如果 chart 佈署的自訂 resource 沒有實作這條 condition,--wait 會一路 timeout。這裡沒有捷徑,只能請 controller 作者補 status,或者把這些 CR 從 --wait 範圍剔除。
Server-side apply 只對新裝生效、升級預設不動
Helm 3 的三向合併演算法是 client-side apply 的爛攤子集大成:本地端拿 last-applied annotation、新 manifest、線上 live state 三份資料出來手動 diff,跑進去的邏輯藏在 helm 的 Go code 裡。任何 controller 加過的欄位(例如 HPA 改過的 replicas、operator 補上的 volume)都可能被覆蓋、或者悄悄漂移不同步。這條路走了太久,Kubernetes 社群早就用 server-side apply(SSA)補了。
Helm 4 的預設是:新裝的 release 走 server-side apply,既有的 Helm 3 release 在升級時繼續用 client-side apply,除非顯式加 --server-side。這個切法很保守但合理,avoids 一升級就把所有 release 的 field ownership 重新洗一遍。想要主動遷移過去,helm upgrade --server-side 會強制把該 release 轉成 SSA 管理,之後 field ownership 就進 apiserver 的軌道。
實務上要注意兩件事。第一,SSA 對 field ownership 有明確語意,chart 沒宣告的欄位就是別的 controller 或使用者的東西,升級不會動。這對 HPA、Cluster Autoscaler、operator 補欄位的場景是好消息。但如果 chart 之前是靠「反正 client-side apply 會覆蓋」這種行為在維持狀態,遷移到 SSA 之後可能會發現線上 state 沒被拉回 chart 值。第二,SSA 在管 controller 亂動的欄位時容易出現 conflict 錯誤,Helm 4 遇到會直接失敗,需要用 --force-conflicts 顯式接管。這個 flag 用重了會把 field ownership 搶回 helm,等於退回原本沒有 SSA 的世界。建議是留在預設,遇到 conflict 才個案處理。
命令列上那些會突然變成硬錯誤的旗標
除了 plugin 跟 wait 兩塊之外,還有幾個每天都在跑的 flag 被靜悄悄換名。這些改動在 Helm 4.0 給 deprecation warning、在後續小版本才變成硬錯誤,但只要 CI 開 set -e 或者用 --strict,還是會直接紅:
--atomic改成--rollback-on-failure,語意不變,只是名字更精確。--force改成--force-replace,避免跟 SSA 的--force-conflicts混淆。helm registry login拒絕帶https://或http://前綴,只吃 domain。之前的HELM_EXPERIMENTAL_OCI=1環境變數會導致明確錯誤,OCI 從 Helm 4 開始就是穩定介面。- Go SDK 的 import path 從
helm.sh/helm/v3換成helm.sh/helm/v4,任何以 library 形式嵌入 Helm 的內部工具都要跟著改。
這幾個看起來很小,但團隊要升級時常常在 shell script 或 GitHub Action 裡看到十幾個地方要改。比較安全的做法是先在 Helm 3 環境把新名字都跑一次相容測試(Helm 3 後期版本已經接受新旗標),確認沒有腳本相依舊名,再切到 Helm 4。
遷移順序這樣排比較不會出事
面對 Helm 4 這種橫跨 plugin、RBAC、CLI 三面的破壞相容,最痛的做法是「先升版再處理」。合理的順序反而是先把每個能在 Helm 3 就修好的地方先修完,再切版本:
- Plugin 先包好:現有 post-renderer 腳本改成 plugin 形式,在 Helm 3 環境驗證能跑,順便讓開發者本機
helm plugin install完備。 - CLI flag 換名:把
--atomic、--force、helm registry login這幾個常見的先掃 CI script、Makefile、Ansible playbook。 - RBAC 補 watch:對 Helm 使用的 ServiceAccount role 補上
watchverb,涵蓋常見的 workload kind 跟 chart 有用到的 CRD。 - Staging 環境切 Helm 4:先跑 install/upgrade/rollback/uninstall 四種操作,特別注意 kstatus 對 CRD 的 readiness 判斷。
- Server-side apply 逐 release 遷移:新裝的 release 自然會走 SSA,既有的用
helm upgrade --server-side遷移,一次一個 release 觀察 field ownership 的變化。
Helm 3 到 Helm 4 這條路,chart 這一側幾乎沒改動,工程量都在 CI/CD、RBAC 跟 plugin 生態。對已經在 Kubernetes 上跑 GitOps 的團隊,這波遷移可以順便把過去七年累積下來的 hack 清一清:把 sleep 30 換成 kstatus、把 –post-renderer 腳本收成 plugin、把 field ownership 交還 SSA。這些改動長期看下來的回報遠大於升級當下的痛。
在臺灣自架 Kubernetes 叢集或者用 Helm 部署 self-hosted 服務的團隊,如果需要把控制平面跟工作節點跑在低延遲、可自管的環境,NCSE Network 的 VPS 主機(Intel Gold CPU、NVMe SSD、是方電訊機房)能承接 kube-apiserver 到 etcd 這條敏感的路徑,也提供 IP Transit 讓叢集外部流量走臺灣本地骨幹。有興趣可以到 ncse.tw 看細節。