Helm Chart 交付、升级与回滚
凌晨发布窗口里,最让人不安的不是 helm upgrade 报错,而是命令成功后服务仍不可用:有人临时加过 --set,CI 又传入另一份 values;Chart 里有一个升级前 Job 卡住;新版本还顺手更新了 CRD。此时 Git 中的 YAML、终端里渲染出的清单和集群中保存的 release revision 已经不是同一个状态。只盯 Deployment,往往既解释不了“这次到底装了什么”,也无法判断回滚会恢复哪些对象。
Helm 把一组 Kubernetes 资源模板、默认配置和依赖打包成 Chart,再把一次安装或升级保存成有编号的 release revision。它解决的是参数化打包与发布历史,不替代 Kubernetes 控制器,也不自动解决数据库兼容、Secret 托管和流量灰度。把这几层状态拆开,是用好 Helm 的起点。
先确认拿到的是可信客户端
Helm 是本地客户端,拥有 kubeconfig 对应身份的权限;它不需要在集群里安装 Tiller。当前主线为 Helm 4,Helm 3 处于迁移维护期。按Helm 版本支持策略,Helm 4 以编译时 Kubernetes 客户端 minor 为上界,支持该版本与向后三个 minor;例如 4.2.x 对应 Kubernetes 1.33 至 1.36,且不承诺向前兼容 1.37。不要把“命令能连接”当成超出窗口仍受支持。团队应固定 Helm major/minor,并在升级客户端前用真实 Chart 做渲染和安装回归。
Windows 可以从官方 release 下载 zip,macOS 和 Linux 也可以使用官方二进制。包管理器更方便,但信任链变成“Helm 发布方 + 包管理仓库维护者”。对 CI 和受控开发机,稳妥做法是固定版本、下载对应平台压缩包,再核对 release 页面给出的 SHA-256 或签名;不要执行来源不明的 curl | sh。官方脚本也应先下载、审阅并固定版本后执行:
curl -fsSLo get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4
less get_helm.sh
DESIRED_VERSION=v4.2.3 bash get_helm.sh --no-sudo第一条只下载脚本,第二条让操作者检查下载地址、写入位置和提权行为,最后一条才安装指定版本。v4.2.3 是这组命令的可重复示例;执行时应先在Helm 官方 release 确认团队批准的当前补丁版及校验值。Helm 3 基线要改用 scripts/get-helm-3 并指定 v3.x.y,不要让安装脚本在 major 之间漂移。企业代理、内网或离线环境应由制品管理员把官方归档、校验文件和签名一同镜像到内部仓库,客户端只访问受控源。安装后不要只看“有输出”,还要确认二进制路径和环境目录:
command -v helm
helm version --short
helm env预期看到固定版本和可解释的 HELM_CONFIG_HOME、HELM_CACHE_HOME、HELM_DATA_HOME。如果版本与团队基线不同,先处理 PATH 中的多份安装;如果 helm env 指向临时用户或管理员目录,后续 repo 配置、registry 凭证和插件也会落在意料之外的位置。Helm 采用 Apache-2.0 许可证,本体免费;真正的成本来自 Chart 仓库、OCI registry、签名基础设施、集群资源以及维护发布契约的人力。
首次接触共享集群时,先显式确认目标和权限。下面的 namespace 专供实验,真实项目应换成团队分配的开发 namespace:
kubectl config current-context
kubectl cluster-info
kubectl auth can-i create deployments -n helm-lab
kubectl auth can-i create secrets -n helm-lab
kubectl create namespace helm-labcurrent-context 和 API 地址用于阻止误连,两个 can-i 分别验证工作负载与 release 存储所需能力。namespace 尚未创建时,权限检查仍可执行;创建失败若显示 Forbidden,不要换管理员 kubeconfig 绕过,应申请最小 namespace 权限或改用本地集群。
repo、OCI 和 Chart 不是同一个对象
传统 Helm repository 是一个可通过 HTTP 获取的 index.yaml 加若干 .tgz Chart 包。helm repo add 只是把名称和 URL 写入本机配置,helm repo update 才拉取索引缓存;安装 repo/chart 时,客户端根据缓存解析版本并下载包。因此两台机器使用同一个短名称,缓存时间和源配置不同,可能选出不同版本。
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo bitnami/nginx --versions
helm show chart bitnami/nginx --version <固定版本>
helm show values bitnami/nginx --version <固定版本> > nginx-defaults.yaml
helm pull bitnami/nginx --version <固定版本> --destination .helm-cache这些命令依次注册源、刷新索引、查看可选版本、读取 Chart 元数据、导出默认值并把固定版本下载到本地。helm search 有结果只证明本地索引认识它,helm pull 成功才证明包可取得。项目配置必须记录确切 Chart 版本,不能把“索引中的最新版本”当成可重复输入。
OCI registry 不使用 helm repo add 和 index.yaml。Chart 以 OCI artifact 存储,用 oci://registry/path/chart 引用,登录凭证与容器 registry 相似:
printf '%s' "$REGISTRY_TOKEN" | helm registry login registry.example.com \
--username "$REGISTRY_USER" --password-stdin
helm show chart oci://registry.example.com/platform/charts/demo --version 1.4.2
helm pull oci://registry.example.com/platform/charts/demo --version 1.4.2
helm registry logout registry.example.com--password-stdin 避免 token 直接进入命令历史和进程参数,但 CI 仍需屏蔽标准输入来源和调试日志。版本 tag 便于阅读,digest 才把内容身份钉死;高保证交付应在晋级记录中保存 Chart digest、应用镜像 digest 和签名验证结果。公开 Chart 也不能因“来自知名 repo”就省略审查:Chart 模板能创建 RBAC、Webhook、CRD、Job 和任意工作负载,权限影响由安装者身份决定。
本地 Chart、repo Chart 和 OCI Chart 最终都会展开为同一类目录结构。Chart.yaml 描述名称、Chart 版本、应用版本与依赖;values.yaml 提供默认值;templates/ 经过 Go template 渲染;crds/ 中的 CRD 走独立安装路径。appVersion 是展示信息,不决定 Chart 包版本,也不会自动改镜像 tag。
用一个可清理项目看清 values 合并
下面的实验不依赖外部 Chart。新建临时目录并运行 helm create helm-lab-app,然后只保留一个 ConfigMap 模板。PowerShell、Bash 或编辑器都可以创建文件,关键是最终目录如下:
helm-lab-app/
Chart.yaml
values.yaml
templates/
configmap.yaml
values/
dev.yaml
hotfix.yamlChart.yaml:
apiVersion: v2
name: helm-lab-app
description: Reproducible Helm values lab
type: application
version: 0.1.0
appVersion: "1.0.0"values.yaml 放 Chart 作者认可的安全默认值:
replicaCount: 1
image:
repository: nginx
tag: "1.27.5"
service:
port: 80
feature:
mode: safe
legacyHeader: enabledvalues/dev.yaml 放环境差异:
replicaCount: 2
feature:
mode: devvalues/hotfix.yaml 放一次有意覆盖:
feature:
mode: guarded
legacyHeader: nulltemplates/configmap.yaml 把合并结果显式输出,便于观察:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-settings
labels:
app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
app.kubernetes.io/instance: {{ .Release.Name | quote }}
data:
mode: {{ .Values.feature.mode | quote }}
replicas: {{ .Values.replicaCount | quote }}
{{- with .Values.feature.legacyHeader }}
legacyHeader: {{ . | quote }}
{{- end }}现在依次执行语法检查和本地渲染:
helm lint ./helm-lab-app -f values/dev.yaml -f values/hotfix.yaml
helm template demo ./helm-lab-app \
--namespace helm-lab \
-f values/dev.yaml \
-f values/hotfix.yaml \
--set-string feature.mode=emergency \
--debug多个 -f 从左到右覆盖,后面的 hotfix.yaml 胜出;--set-string 再次覆盖,最终 mode 应为 emergency。legacyHeader: null 会从合并树中删除默认键,因此模板的 with 不输出该字段。列表通常整体替换,映射才逐键合并;遇到探针、环境变量数组和 tolerations 时尤其要看最终 YAML,不要凭文件层级猜结果。
helm lint 检查 Chart 结构和部分模板问题,不证明 API server 接受资源。helm template 完全在本地渲染,没有集群 discovery、RBAC、准入策略和 CRD 现实状态。需要集群证据时继续执行:
helm install demo ./helm-lab-app \
--namespace helm-lab \
-f values/dev.yaml \
-f values/hotfix.yaml \
--dry-run=server \
--hide-secret \
--debug--dry-run=server 会连接集群并尝试服务端校验,因而可能暴露 Forbidden、API 缺失或 admission 拒绝;它仍不会持久化 release。--hide-secret 用于避免 dry-run 输出 Secret,但不能替代 CI 日志脱敏。Helm 4 的字符串参数有 none、client、server 三种语义:不写该 flag 时是 none 并会真实执行,只写 --dry-run 时是兼容的客户端模拟。团队脚本应显式写 --dry-run=client 或 --dry-run=server,避免空值 flag 和默认不模拟被混为一件事。
反向实验:让类型错误穿过 lint
把命令行覆盖改为:
helm template demo ./helm-lab-app \
--set replicaCount=not-a-number当前模板对该值只做 quote,渲染仍可能成功,输出 replicas: "not-a-number"。这正好说明 values 没有天然业务类型契约。给 Chart 增加 values.schema.json:
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"replicaCount": { "type": "integer", "minimum": 1, "maximum": 20 },
"feature": {
"type": "object",
"properties": {
"mode": { "type": "string", "enum": ["safe", "dev", "guarded", "emergency"] }
},
"required": ["mode"]
}
},
"required": ["replicaCount", "feature"]
}再次运行同一命令,预期失败并指出 replicaCount 类型不匹配。这个失败发生在资源提交前,是可复用的配置契约证据。Schema 仍不能验证镜像存在、Service 可访问或凭证有效,这些要靠集群和应用级检查。
从 install 走到可解释的 rollback
完成渲染后执行真实安装。Helm 4 把 Helm 3 的 --atomic 更名为 --rollback-on-failure;旧名在 Helm 4 仍能用,但会发出弃用警告,不应继续写入新脚本。安装失败时,该参数会卸载本次安装并把 --wait 默认设为 watcher;--timeout 约束单个 Kubernetes 操作和 Hook Job 的等待时间。Helm 3 执行同一实验时把下面的 --rollback-on-failure 换成 --atomic。实验 Chart 只有 ConfigMap,几乎会立即完成:
helm install demo ./helm-lab-app \
--namespace helm-lab \
-f values/dev.yaml \
--rollback-on-failure \
--timeout 2m
helm status demo -n helm-lab
helm get values demo -n helm-lab --all
helm get manifest demo -n helm-lab
kubectl get configmap demo-settings -n helm-lab -o yaml四份证据分别回答 release 状态、计算后的 values、Helm 保存的渲染清单和集群当前对象。Helm 默认把 release revision 存成目标 namespace 中的 Secret;拥有读取这些 Secret 权限的人可能看到 values 与 manifest,所以“可以读 release 历史”实际上是敏感权限。不要把数据库密码、私钥正文或长期云凭证直接塞进 values。
修改 values/dev.yaml 中的 feature.mode 为 guarded,再升级:
helm upgrade demo ./helm-lab-app \
--namespace helm-lab \
-f values/dev.yaml \
--rollback-on-failure \
--timeout 2m
helm history demo -n helm-lab
kubectl get configmap demo-settings -n helm-lab \
-o jsonpath='{.data.mode}{"\n"}'预期 history 出现 revision 2,ConfigMap 输出 guarded。每次 upgrade 和 rollback 都会产生新 revision,而不是把历史指针倒回旧编号。要恢复 revision 1:
helm rollback demo 1 -n helm-lab --wait --timeout 2m
helm history demo -n helm-lab
kubectl get configmap demo-settings -n helm-lab \
-o jsonpath='{.data.mode}{"\n"}'预期新增 revision 3,内容恢复为 revision 1 的 dev。回滚恢复的是 Helm 当时保存的 Kubernetes 清单与 values,不会逆转外部数据库迁移、对象存储写入、消息消费或 Hook 已产生的副作用。任何有状态变更都要先定义向后兼容窗口和独立恢复方案。
helm upgrade --install 适合幂等流水线,但要特别决定 values 继承语义。默认升级以 Chart 新默认值和本次参数重新计算;--reuse-values 会把上次 release 的值继续带入,可能让已从 Git 删除的临时覆盖长期存活。更可审计的做法是每次传入完整、版本化的环境 values,并用 helm get values --all 与期望渲染做差异检查。
反向实验:制造一次可观察的失败 revision
在 templates/ 增加 bad.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-bad
data:
broken: {{ required "feature.requiredValue is required" .Values.feature.requiredValue | quote }}不提供该值,先运行:
helm upgrade demo ./helm-lab-app -n helm-lab -f values/dev.yaml --rollback-on-failure预期在模板阶段失败,错误包含 feature.requiredValue is required,集群中的 demo-settings 仍保持上一个成功状态。若失败发生在资源提交或等待阶段,--rollback-on-failure 会尝试恢复到前一个成功 release;此时必须检查 helm history、helm status 和 namespace Event,不能只相信终端最后一句。回滚本身也可能被 Hook、不可变字段或运行时故障阻断,参数名不等于成功保证。删除 bad.yaml 后再次渲染,确认故障源已经清除。
Hook 和 CRD 会突破普通 release 直觉
Hook 是带 helm.sh/hook annotation 的模板资源,可在 install、upgrade、rollback 或 delete 的特定阶段运行。常见用途是迁移检查和备份 Job,但它也最容易把“声明式发布”变成带副作用的脚本链。Hook 按 weight、kind、name 排序并等待 Ready;一个永不完成的 pre-upgrade Job 会让整个 upgrade 卡在 timeout。
更隐蔽的是,Hook 创建的资源不作为普通 release 资源跟踪,helm uninstall 不保证删除它们。Job 应至少设置 TTL 或删除策略:
metadata:
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
ttlSecondsAfterFinished: 300排查 Hook 先看 helm get hooks demo -n helm-lab,再看对应 Job、Pod 日志和 Event。不要把生产数据迁移写成“失败就自动回滚”的 Hook 后便认为数据安全:Kubernetes 对象可回滚,已提交的数据事务未必可逆。
CRD 的声明放在 Chart 的 crds/ 目录时,会在模板和普通资源之前安装;Helm 不会升级或删除这些 CRD,也不支持在该目录中使用模板。这样设计是为了避免误删自定义资源导致数据丢失。结果是 Chart 升级成功不代表 CRD schema 已升级,卸载 release 也不会清掉 CRD 和其自定义资源。
平台团队通常把 CRD 与控制器版本做独立兼容矩阵:先检查存量 Custom Resource、conversion webhook 和 storage version,再升级 CRD,最后升级控制器。开发集群做实验时,可用以下命令取证,而不是把 no matches for kind 简化成模板错误:
helm show crds <chart引用> --version <固定版本>
kubectl api-resources | grep <资源名>
kubectl get crd <crd-name> -o yaml
kubectl get events -n helm-lab --sort-by=.lastTimestamp所有权冲突不是加一个参数就能消失
Helm 通过 release 元数据和 Kubernetes 对象 annotation 判断资源是否属于该 release。若同名 Service 已由 kubectl apply、另一 release 或 GitOps 控制器管理,安装常见错误会指出 invalid ownership metadata。Helm 4 又引入了第二层所有权:新 release 默认使用 Server-Side Apply,字段级归属记在 managedFields;升级和回滚默认沿用该 release 上一次的 apply 方式,因此从 Helm 3 迁来的 release 不会在首次 Helm 4 升级时突然切换 SSA。强行接管之前先回答三个问题:对象与字段现在分别由谁拥有、停掉旧管理器后谁继续调和、接管失败如何恢复。
kubectl get service <name> -n helm-lab -o yaml
kubectl get service <name> -n helm-lab \
-o jsonpath='{.metadata.labels.app\.kubernetes\.io/managed-by}{"\n"}'
kubectl get service <name> -n helm-lab --show-managed-fields直接手改 annotation 或加 --take-ownership 让 Helm “认领”资源,只是跳过 Helm 归属检查,不证明现有 spec 与 Chart 兼容,也不会自动停止另一个调和器。更稳的迁移是在低风险环境渲染清单、比较不可变字段和 selector,暂停旧调和器,备份对象,明确短暂中断窗口后再接管。SSA 报字段冲突时应先找出对应 manager,不要直接加 --force-conflicts。Helm 4 已把 Helm 3 的 --force 更名为 --force-replace;它可能通过删除重建解决不可变字段更新,代价是 Service ClusterIP、Pod、Job 或存储关联出现中断,不能作为常规升级开关。
Secret、权限与发布证据
Helm 渲染阶段能接触的所有值,都可能出现在 debug 输出、release Secret、CI artifact 或失败日志中。即使模板最终生成 Kubernetes Secret,base64 也不是加密。更可靠的路径是让 values 只保存外部 Secret 的引用,由 External Secrets、Secrets Store CSI Driver 或平台注入机制在集群侧取值;若必须交付 Secret 清单,至少使用加密 Git 工作流、最小解密权限和日志屏蔽。
安装账号只应拥有目标 namespace 与确需的 cluster-scoped 资源权限。普通应用 Chart 如果声称只部署 Deployment/Service,却要求创建 ClusterRole、CRD 或 MutatingWebhookConfiguration,应触发额外审查。发布前可先枚举渲染对象:
helm template demo <chart引用> -f values/dev.yaml > rendered.yaml
kubectl apply --dry-run=server -f rendered.yaml -n helm-lab
kubectl auth can-i --list -n helm-lab第一步形成可审计清单,第二步让 API server 和 admission 参与校验,第三步查看当前身份权限。rendered.yaml 可能含 Secret,验证结束应安全删除,CI 不应把它作为公开 artifact。部署记录至少保存 Chart name/version/digest、镜像 digest、values 来源提交、目标 cluster/namespace、release revision、验证结果和回滚目标;不要保存明文敏感值。
供应链审查还要处理 Chart 依赖。helm dependency update 会根据版本约束重新解析并改写 Chart.lock,适合维护者主动升级;可重复构建应提交 Chart.lock,在 CI 使用 helm dependency build 按锁文件恢复。对传统 .tgz,Helm provenance 可用 helm verify 或安装时 --verify 检查包与签名;对 OCI,可结合 digest 固定和组织采用的签名方案。签名只证明“由某个密钥签出且内容未变”,不证明 Chart 没有高危权限或恶意 Hook。
容量、成本和长期治理
release 历史会占用目标 namespace 的 etcd 存储,包含大段 manifest 与 values。频繁自动升级、庞大清单和过大的 revision 保留窗口会放大 API server 与备份成本。helm upgrade --history-max 默认保留 10 个 revision,设为 0 才是不限制;团队不应凭默认值做容量承诺,保留多少要由回滚窗口、审计要求和 etcd 预算决定。压到只剩一两个 revision 会让故障恢复失去证据。
Helm 自身不提供渐进流量、自动指标判定或跨集群事务。需要金丝雀与自动回退时,应让专门 rollout 控制器或 GitOps 平台承担调和,Helm 负责打包和生成期望对象。两个系统同时修改同一字段,会造成持续漂移;团队必须明确唯一写入者,并把人工 helm upgrade、GitOps 自动同步和紧急 kubectl edit 的优先级写进变更制度。
长期维护时,把 Chart 版本升级当依赖升级:先读 changelog 和弃用项,渲染旧/新版本,比较资源种类、RBAC、不可变字段、CRD 与 Hook,再在隔离 namespace 安装和回滚。只有 helm lint 通过远远不够;可接受的证据链是模板可解释、服务端 dry-run 可接受、实际 release Ready、应用探针或测试通过、history 可定位、清理后无遗留 Hook 和意外 cluster-scoped 对象。
清理实验并确认没有留下第二套状态
先查看 release 和 Hook,再卸载:
helm list -n helm-lab
helm get hooks demo -n helm-lab
helm uninstall demo -n helm-lab --wait --timeout 2m
kubectl get all,configmap,secret,job -n helm-labuninstall 删除受 release 管理的资源和 release 历史,但带 helm.sh/resource-policy: keep 的对象、Hook 资源、PVC、CRD 与外部系统副作用可能仍在。逐项确认后再删除实验 namespace:
kubectl delete namespace helm-lab
rm -rf helm-lab-app values nginx-defaults.yaml rendered.yaml .helm-cache最后检查 kubectl get namespace helm-lab 返回 NotFound,并确认 registry 已 logout、临时 Chart 与渲染清单已删除。若 namespace 长时间 Terminating,先查 finalizer 和仍存资源,不要粗暴移除 finalizer 掩盖控制器或存储清理失败。
至此,一次 Helm 变更应该能回答得很具体:Chart 从哪里来、内容身份是什么,哪些 values 以什么顺序合并,客户端与服务端各验证了什么,revision 在哪里保存,失败发生在模板、Hook、API、调和还是应用层,以及回滚和卸载究竟能恢复或删除哪些状态。能回答这些问题,Helm 才是可治理的交付工具,而不只是一个更短的 kubectl apply。
