Kubernetes 基础体系 · 第 54/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Helm 完整指南:Chart、Template、Values、Release、Hook 和回滚

Helm 是 Kubernetes 的客户端发布工具和模板化打包工具。它把一组 Kubernetes 清单、默认配置、依赖关系和生命周期钩子组织成一个 Chart,然后根据输入配置渲染清单,提交给 Kubernetes API Server,并把发布记录保存为 Release。

可以先用一个关系式概括 Helm:

Manifestn=Render(Chart,Valuesn,Releasen,Capabilities)\text{Manifest}_n = \text{Render}(\text{Chart}, \text{Values}_n, \text{Release}_n, \text{Capabilities})

其中:

  • Chart:应用的打包内容,包括模板、默认 Values、依赖和元数据;
  • Values:本次安装或升级使用的配置;
  • Release:某个 Chart 在某个命名空间中的一次安装实例及其历史版本;
  • Capabilities:Helm 根据目标 Kubernetes 集群发现到的 API 能力;
  • Manifest:Helm 最终提交给 Kubernetes 的 YAML 对象集合。

Helm 不等于 Kubernetes 控制器。Helm 负责生成和提交期望对象,并保存发布历史;Deployment、StatefulSet、Job 等资源仍由 Kubernetes 控制器负责实际调度、创建 Pod 和维持运行状态。


一、Helm 解决什么问题

直接使用 Kubernetes YAML 时,一个应用通常需要维护:

  • Deployment;
  • Service;
  • ConfigMap;
  • Secret;
  • Ingress 或 Gateway;
  • ServiceAccount 和 RBAC;
  • HPA;
  • PDB;
  • Job;
  • 监控和网络策略对象。

不同环境又往往只有部分字段不同,例如:

  • 镜像仓库和标签不同;
  • 副本数不同;
  • 域名不同;
  • 资源请求不同;
  • 是否启用 Ingress 不同;
  • Secret 引用名称不同。

Helm 将“资源结构”和“环境参数”分离:

Chart
├── 模板结构:Deployment、Service、Ingress ...
└── 默认参数:镜像、端口、副本数、资源限制 ...

环境输入
├── values-dev.yaml
├── values-staging.yaml
└── values-prod.yaml

Helm 渲染
└── 生成当前环境的 Kubernetes Manifest

但 Helm 不是任意文本替换器。它的渲染结果还依赖:

  1. Go template 语法;
  2. Helm 提供的内置对象和函数;
  3. Values 合并规则;
  4. Kubernetes API 版本和资源校验;
  5. Release 的当前修订版本;
  6. 安装、升级、回滚命令的生命周期。

理解这些边界,才能正确判断“模板错误”“配置错误”“集群拒绝”“应用启动失败”分别发生在哪里。


二、先区分 Chart、Release 和 Kubernetes 资源

这三个概念经常被混淆。

2.1 Chart 是可安装的软件包

Chart 是描述应用如何部署的静态包。一个典型 Chart 目录如下:

demo/
├── Chart.yaml
├── values.yaml
├── values.schema.json
├── templates/
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   └── tests/
│       └── smoke-test.yaml
├── charts/
└── crds/

各文件作用如下:

  • Chart.yaml:Chart 名称、版本、应用版本、依赖等元数据;
  • values.yaml:默认 Values;
  • values.schema.json:可选的 Values 校验规则;
  • templates/:需要渲染的模板;
  • templates/_helpers.tpl:通常放命名、标签等命名模板,不直接生成 Kubernetes 对象;
  • charts/:依赖 Chart 的归档目录;
  • crds/:CRD 文件,安装时具有特殊处理规则。

Chart 本身不等于一个正在运行的应用。可以把它类比为一个软件安装包或部署程序。

2.2 Release 是 Chart 的一次安装实例

例如:

helm install demo ./demo -n demo --create-namespace

这里:

  • ./demo 是 Chart;
  • demo 是 Release 名称;
  • demo 命名空间中的 Deployment、Service 等是 Kubernetes 资源;
  • Helm 保存的这次安装记录是一个 Release。

同一个 Chart 可以安装成多个 Release:

helm install demo-dev ./demo -n dev
helm install demo-prod ./demo -n prod

它们使用同一份 Chart,但分别拥有不同的 Values、资源对象和 Release 历史。

2.3 Release revision 是发布历史序号

Release 会随着安装、升级、回滚形成修订历史:

revision 1: helm install
revision 2: helm upgrade
revision 3: helm upgrade
revision 4: helm rollback 1

回滚到 revision 1 并不是把历史指针简单地改回 1。Helm 通常会创建一个新的 Release revision,使当前发布状态变为:

revision 5: rollback to revision 1

因此,revision 5 的目标内容来自 revision 1,但它仍然是一次新的发布操作。

Helm 3 默认将 Release 信息保存为 Kubernetes Secret,通常位于该 Release 的命名空间中。可以查看:

kubectl get secret -n demo -l owner=helm

这些 Secret 是 Helm 的内部存储格式,不应直接修改。直接编辑可能导致 Helm 历史与集群资源不一致。


三、Chart 的完整结构和生命周期

3.1 创建一个最小 Chart

如果本机安装了 Helm,可以运行:

helm create demo

这会生成一个示例 Chart,但生产使用前应删除不需要的模板,避免默认示例内容影响部署。

一个最小 Chart.yaml 可以是:

apiVersion: v2
name: demo
description: A minimal demo application chart
type: application
version: 0.1.0
appVersion: "1.0.0"

这里有两个容易混淆的版本:

  • version:Chart 版本,遵循 Chart 包的版本管理;
  • appVersion:应用版本,仅是元数据,Helm 不会自动用它替换镜像标签。

例如:

version: 0.2.0
appVersion: "2025.03.01"

这不代表镜像一定会使用 2025.03.01。镜像标签仍由模板中的 Values 决定。

apiVersion: v2 表示 Helm 3 使用的 Chart API 格式。它不是 Kubernetes 对象的 apiVersion,两者属于不同层次。

3.2 Chart 的处理过程

一次典型安装大致经过以下阶段:

sequenceDiagram
    participant U as 用户或 CI
    participant H as Helm
    participant C as Chart
    participant K as Kubernetes API Server
    participant R as Kubernetes 控制器

    U->>H: helm install/upgrade
    H->>C: 加载 Chart、依赖和 Values
    H->>H: 合并 Values、执行模板渲染
    H->>H: 校验 YAML 和 Kubernetes API 能力
    H->>K: 创建或更新资源
    K->>R: 通知 Deployment、Service 等控制器
    R->>K: 创建 Pod、更新状态
    K-->>H: 返回对象和状态
    H->>K: 保存 Release revision

需要注意,Helm 成功提交对象,不代表应用已经可用。除非使用了合适的 --wait--wait-for-jobs 和超时设置,否则 Helm 可能在 Kubernetes 接受对象后就结束,而 Pod 之后才会拉取镜像、挂载卷并启动。


四、Template:Helm 如何生成 Kubernetes YAML

4.1 Template 的本质

Helm 模板使用 Go text/template 语法,并提供大量 Helm 和 Sprig 函数。

例如:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "demo.fullname" . }}
  labels:
    {{- include "demo.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}

模板渲染时,. 表示当前上下文。最外层上下文通常包含:

  • .Values:合并后的配置;
  • .Release:当前 Release 信息;
  • .Chart:Chart 元数据;
  • .Capabilities:集群能力;
  • .Files:访问 Chart 内部文件;
  • .Template:当前模板信息。

4.2 常用内置对象

.Values

image:
  repository: nginx
  tag: "1.27"

模板:

image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"

.Release

metadata:
  labels:
    app.kubernetes.io/instance: {{ .Release.Name | quote }}

常用字段包括:

  • .Release.Name:Release 名称;
  • .Release.Namespace:目标命名空间;
  • .Release.IsInstall:是否为安装;
  • .Release.IsUpgrade:是否为升级;
  • .Release.Revision:当前修订版本;
  • .Release.Service:通常为 Helm

.Chart

annotations:
  chart-version: {{ .Chart.Version | quote }}
  app-version: {{ .Chart.AppVersion | quote }}

.Chart.AppVersion 只是元数据,不会自动改变容器镜像。

.Capabilities

根据 Kubernetes API 能力选择不同资源版本:

{{- if .Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress" }}
apiVersion: networking.k8s.io/v1
{{- else }}
apiVersion: networking.k8s.io/v1beta1
{{- end }}

但是,旧 API 是否仍存在必须以目标集群实际能力为准。不要因为模板里保留了 fallback,就认为 Kubernetes 会接受已经废弃的 API。当前稳定 Kubernetes 通常要求使用稳定 API,例如:

  • apps/v1 的 Deployment;
  • batch/v1 的 Job;
  • networking.k8s.io/v1 的 Ingress。

具体版本仍取决于目标集群发行版和版本。

4.3 命名模板、include 和作用域

templates/_helpers.tpl 中常见写法:

{{/*
生成资源全名。
*/}}
{{- define "demo.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{/*
通用标签。
*/}}
{{- define "demo.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

在资源中使用:

metadata:
  name: {{ include "demo.fullname" . }}
  labels:
    {{- include "demo.labels" . | nindent 4 }}

include 会返回字符串,因此可以继续交给 nindentquote 等函数处理。相比之下,template 是动作,组合能力较弱。

模板的作用域会随着 withrange 改变:

{{- with .Values.image }}
image: "{{ .repository }}:{{ .tag }}"
{{- end }}

with 内部,. 已经变成 .Values.image,所以不能再直接写 .Values.image.repository。如果需要访问外层上下文,应保存根对象:

{{- $root := . }}
{{- range .Values.env }}
- name: {{ .name }}
  value: {{ .value | quote }}
  instance: {{ $root.Release.Name | quote }}
{{- end }}

这是模板中非常常见的错误来源。

4.4 YAML 缩进和空白控制

以下模板容易产生错误缩进:

labels:
{{ include "demo.labels" . }}

更安全的写法是:

labels:
  {{- include "demo.labels" . | nindent 2 }}

nindent 2 表示先换行,再缩进两个空格。{{--}} 会去除模板边界的空白,但过度使用可能把相邻 YAML 拼接在一起,导致:

replicas: 2containers:

因此应始终渲染后检查,而不是凭模板源码猜结果。

4.5 条件、循环、默认值和强制校验

条件渲染:

{{- if .Values.service.enabled }}
apiVersion: v1
kind: Service
metadata:
  name: {{ include "demo.fullname" . }}
spec:
  selector:
    app.kubernetes.io/name: {{ .Chart.Name }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
{{- end }}

循环渲染环境变量:

env:
{{- range .Values.env }}
  - name: {{ .name }}
    value: {{ .value | quote }}
{{- end }}

默认值:

replicas: {{ .Values.replicaCount | default 1 }}

default 会把某些空值视为“没有设置”。如果 0 是合法值,就不能简单依赖 default

强制要求字段:

image: "{{ required "image.repository is required" .Values.image.repository }}:{{ required "image.tag is required" .Values.image.tag }}"

条件失败会使渲染直接失败,而不是生成不完整的 Kubernetes 对象。

也可以用 fail

{{- if and .Values.ingress.enabled (not .Values.ingress.host) }}
{{- fail "ingress.host must be set when ingress is enabled" }}
{{- end }}

4.6 模板渲染与 Kubernetes 校验必须分开诊断

先只渲染,不访问集群:

helm template demo ./demo \
  -n demo \
  -f values-prod.yaml \
  --debug

检查最终 YAML:

helm lint ./demo -f values-prod.yaml

再让 Helm 调用 Kubernetes API 做 dry-run:

helm upgrade --install demo ./demo \
  -n demo \
  --create-namespace \
  -f values-prod.yaml \
  --dry-run=server \
  --debug

区别是:

  • helm template 主要检查模板渲染;
  • helm lint 检查 Chart 结构和常见问题;
  • --dry-run=server 会请求 API Server,可发现 API 版本、Schema、权限和对象校验问题;
  • 真正安装还可能遇到调度、镜像拉取、PVC、Webhook 和应用启动问题。

--dry-run 生成的输出可能包含 Secret 内容。不要把包含凭据的 dry-run 输出直接写入公共 CI 日志。


五、Values:配置如何进入模板

5.1 默认 Values

values.yaml

replicaCount: 2

image:
  repository: nginx
  tag: "1.27.4"
  pullPolicy: IfNotPresent

service:
  enabled: true
  type: ClusterIP
  port: 8080

resources: {}

env:
  - name: LOG_LEVEL
    value: info

模板:

spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}

5.2 Values 的优先级

常见优先级从低到高大致是:

Chart 默认 values.yaml
< 依赖 Chart 的父级 Values
< 用户提供的 -f 文件,后写文件覆盖先写文件
< --set / --set-string / --set-file / --set-json

例如:

values.yaml

replicaCount: 1
image:
  tag: "1.0"

values-prod.yaml

replicaCount: 3
image:
  tag: "2.0"

命令:

helm upgrade --install demo ./demo \
  -f values-prod.yaml \
  --set replicaCount=5

最终结果:

replicaCount: 5
image:
  tag: "2.0"

通常 Map 会递归合并,标量被高优先级值替换;列表应按“整体替换”理解,而不是假设 Helm 会按索引或键智能合并。列表合并行为是很多环境配置错误的来源。

5.3 --set 的类型陷阱

helm install demo ./demo \
  --set replicaCount=3 \
  --set image.tag=1.27

Helm 会尝试推断类型。镜像标签虽然看起来像数字,实际应当是字符串。更明确的写法是:

helm install demo ./demo \
  --set replicaCount=3 \
  --set-string image.tag=1.27

常用参数:

  • --set key=value:命令行设置值;
  • --set-string key=value:强制字符串;
  • --set-file key=path:把文件内容作为字符串值;
  • --set-json key='{"a":1}':传入 JSON 结构。

例如把证书文件作为 Values:

helm upgrade --install demo ./demo \
  --set-file tls.ca=ca.pem

这并不会自动创建 Secret,也不会自动加密文件内容。命令行、进程参数和 CI 日志可能暴露敏感信息。生产环境通常应使用外部 Secret 管理、Sealed Secrets、SOPS 或云厂商 Secret 集成,而不是把长期凭据直接写进普通 Values 文件。

5.4 Values Schema

values.schema.json 可以在 Chart 层面校验输入:

{
  "$schema": "http://json-schema.org/schema#",
  "type": "object",
  "required": ["image"],
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1
    },
    "image": {
      "type": "object",
      "required": ["repository", "tag"],
      "properties": {
        "repository": { "type": "string", "minLength": 1 },
        "tag": { "type": "string", "minLength": 1 }
      }
    }
  }
}

这样可以在渲染前拒绝:

replicaCount: "three"

Schema 只能校验 Values 形状和部分值约束,不能证明:

  • 镜像一定存在;
  • PVC 一定有可用 StorageClass;
  • 应用一定能连接数据库;
  • IngressClass 一定已安装;
  • Pod 一定能调度。

它减少的是配置输入错误,不是运行时不确定性。


六、一个完整可运行的 Chart 示例

下面构建一个简单的 Nginx Chart。

6.1 values.yaml

replicaCount: 2

image:
  repository: nginx
  tag: "1.27.4"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

resources:
  requests:
    cpu: 10m
    memory: 16Mi
  limits:
    cpu: 100m
    memory: 64Mi

podAnnotations: {}

6.2 templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "demo.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "demo.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Chart.Name }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Chart.Name }}
        app.kubernetes.io/instance: {{ .Release.Name }}
      {{- with .Values.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          readinessProbe:
            httpGet:
              path: /
              port: http
          livenessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Deployment.spec.selector 必须与 Pod 模板标签匹配。这个字段在创建后通常不可随意修改,因此模板生成的名称和 selector 设计需要稳定。

6.3 templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ include "demo.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "demo.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  selector:
    app.kubernetes.io/name: {{ .Chart.Name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: http
      port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP

Service 的 selector 必须能选中 Deployment 创建的 Pod。否则 Service 对象本身创建成功,但不会产生有效 Endpoints。

6.4 安装、观察和验证

helm lint ./demo

helm template demo ./demo -n demo

helm upgrade --install demo ./demo \
  --namespace demo \
  --create-namespace \
  --wait \
  --timeout 5m

预期过程:

  1. Helm 渲染 Deployment 和 Service;
  2. API Server 接受对象;
  3. Deployment 控制器创建 ReplicaSet;
  4. ReplicaSet 创建 Pod;
  5. Pod 拉取 Nginx 镜像并通过探针;
  6. --wait 等待 Helm 关注的资源达到就绪条件;
  7. 命令成功退出。

检查资源:

helm status demo -n demo
helm get values demo -n demo
helm get manifest demo -n demo
kubectl get deploy,rs,pod,svc -n demo
kubectl rollout status deployment/demo-demo -n demo

如果 Pod 处于 ImagePullBackOff

kubectl describe pod -n demo -l app.kubernetes.io/instance=demo
kubectl logs -n demo -l app.kubernetes.io/instance=demo

这时 Helm 模板可能完全正确,失败原因是镜像仓库、凭据、网络或节点运行时问题。


七、依赖 Chart 和模板边界

Chart 可以依赖其他 Chart。例如应用依赖 Redis:

# Chart.yaml
dependencies:
  - name: redis
    version: 20.6.0
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled

然后下载依赖:

helm dependency update ./demo

condition 通常读取顶层 Values:

redis:
  enabled: false

依赖 Chart 的 Values 可以通过嵌套键传入:

redis:
  architecture: standalone
  auth:
    enabled: true

依赖管理的关键边界是:

  • 父 Chart 可以覆盖子 Chart 的 Values;
  • 子 Chart 通常不能直接读取父 Chart 任意私有字段;
  • 全局配置可以放在 global,供多个子 Chart 约定使用;
  • 依赖版本应锁定,避免构建时无意获得不兼容版本。

Chart.lock 用于锁定依赖解析结果。生产构建应尽量基于锁文件和固定仓库来源,而不是每次重新解析浮动依赖。


八、Helm 如何更新资源:不要把它当作纯文本替换

执行:

helm upgrade demo ./demo -n demo

Helm 不只是删除旧 YAML 再创建新 YAML。它会根据历史 Release、当前模板结果和 Kubernetes API 的对象更新语义,对已有对象执行更新操作。最终具体行为还受到 Kubernetes 对象类型约束影响:

  • Deployment 的 Pod 模板变化通常触发滚动更新;
  • Service 的部分字段不可变;
  • StatefulSet 的某些字段不可变;
  • PVC 的容量扩容受 StorageClass 和底层存储支持限制;
  • CRD 的升级有特殊规则;
  • Secret 和 ConfigMap 内容变化不会自动重启 Pod,除非 Deployment 模板也发生变化。

常见做法是把配置内容的哈希加入 Pod 模板注解:

spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}

当 ConfigMap 模板内容变化时,哈希变化,Deployment 的 Pod 模板变化,Kubernetes 才会创建新 ReplicaSet。

这不是 Helm 自动行为,而是模板作者显式建立的因果链:

ConfigMap 内容变化
→ checksum 变化
→ Pod template annotation 变化
→ Deployment revision 变化
→ 新 Pod 创建

如果直接在模板中给 ConfigMap 加时间戳,则每次 Helm 操作都会触发重启,通常会破坏幂等性。


九、Release 状态、并发和失败路径

9.1 常见状态

helm list -A
helm status demo -n demo
helm history demo -n demo

常见状态包括:

  • deployed:当前 Release 已部署;
  • pending-install:安装进行中或异常中断;
  • pending-upgrade:升级进行中或异常中断;
  • pending-rollback:回滚进行中或异常中断;
  • failed:上一次操作失败;
  • superseded:旧 revision 已被后续 revision 替代;
  • uninstalled:已卸载但历史可能保留。

Helm 会对同一个 Release 加操作锁。两个 CI 任务同时执行:

helm upgrade demo ...
helm rollback demo ...

可能出现:

another operation (install/upgrade/rollback) is in progress

这不是 Kubernetes Pod 并发问题,而是 Helm Release 操作级别的并发冲突。应由发布系统保证同一个 Release 串行执行,而不是盲目重试多个写操作。

9.2 --atomic 的含义

helm upgrade --install demo ./demo \
  -n demo \
  --wait \
  --timeout 10m \
  --atomic

--atomic 通常意味着:

  1. 等待资源就绪;
  2. 如果安装或升级失败,自动执行清理或回滚;
  3. 命令最终返回失败。

它不能撤销所有副作用。例如:

  • 已执行的数据库迁移不会自动逆向;
  • 已发送的消息不会撤回;
  • 外部云资源创建不会必然删除;
  • PVC 中的数据不会因为回滚自动恢复;
  • Job 写入外部系统的结果不会被 Kubernetes 还原。

--atomic 是发布失败处理策略,不是事务系统。


十、Hook:把动作插入 Release 生命周期

10.1 Hook 是什么

Hook 是带有特殊注解的 Kubernetes 资源。Helm 在特定生命周期阶段渲染并执行它们,例如:

  • pre-install:安装资源前;
  • post-install:安装资源后;
  • pre-upgrade:升级资源前;
  • post-upgrade:升级资源后;
  • pre-delete:卸载资源前;
  • post-delete:卸载资源后;
  • pre-rollback:回滚资源前;
  • post-rollback:回滚资源后;
  • test:通过 helm test 执行。

一个升级前数据库迁移 Job:

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "demo.fullname" . }}-migration-{{ .Release.Revision }}
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "-10"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  backoffLimit: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migration
          image: "{{ .Values.migration.image.repository }}:{{ .Values.migration.image.tag }}"
          command: ["/app/migrate"]

这里使用 .Release.Revision 让每次 revision 生成不同的 Job 名称。否则同名 Job 在下一次运行时可能因为对象已存在而无法创建。

10.2 Hook 的排序和等待

多个 Hook 同一阶段执行时,Helm 会按以下概念排序:

  1. helm.sh/hook-weight,数值越小越早;
  2. 资源类型;
  3. 资源名称。

因此可以让迁移前置检查使用较小权重:

"helm.sh/hook-weight": "-20"

Hook 资源通常不是普通 Release 资源管理方式的一部分。Helm 会记录 Hook 的 manifest 和执行事件,但不会像普通 Deployment 那样持续负责其生命周期。因此必须设计删除策略:

"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded

常用策略:

  • before-hook-creation:创建同一 Hook 前删除旧 Hook;
  • hook-succeeded:成功后删除;
  • hook-failed:失败后删除。

如果保留失败 Job,诊断更容易;如果自动删除,环境更干净但证据会丢失。生产中常需在可观测性和清理之间取舍。

10.3 Hook 失败如何影响发布

如果 pre-upgrade Job 失败:

Helm upgrade
→ 执行 pre-upgrade Hook
→ Job 失败
→ 升级通常停止
→ 普通资源可能尚未应用

如果 post-upgrade Hook 失败:

Helm upgrade
→ 普通资源已更新
→ 执行 post-upgrade Hook
→ Hook 失败
→ Helm 命令失败,但部分资源已经是新版本

这说明 Hook 不是数据库事务。尤其是 post-* Hook 失败后,不能假设 Kubernetes 资源自动恢复到升级前状态。

10.4 Hook 的生产风险

数据库迁移是 Hook 的典型用途,但需要满足更强条件:

  • 迁移脚本幂等;
  • 同一 Release 不会并发执行迁移;
  • 迁移失败时应用不会使用不兼容的 Schema;
  • 新旧应用版本在迁移窗口内兼容;
  • 回滚应用代码时,数据库 Schema 是否支持向后兼容;
  • 超时后 Job 是否可能仍在运行;
  • 失败证据是否保留。

如果迁移已经提交了不可逆的数据库变化,Helm 回滚只能恢复 Kubernetes 对象,无法恢复数据库状态。


十一、Rollback:回滚什么,不能回滚什么

11.1 查看历史

helm history demo -n demo

示例:

REVISION  UPDATED                  STATUS       CHART        APP VERSION  DESCRIPTION
1         2025-03-01 10:00:00      superseded   demo-0.1.0   1.0          Install complete
2         2025-03-01 11:00:00      superseded   demo-0.2.0   2.0          Upgrade complete
3         2025-03-01 11:10:00      deployed     demo-0.2.0   2.0          Rollback to 1

11.2 执行回滚

helm rollback demo 1 \
  -n demo \
  --wait \
  --timeout 10m

回滚的核心过程可以表示为:

读取目标 revision 1 的 Chart 和配置
→ 生成目标 revision 1 对应的资源内容
→ 按回滚流程执行 pre-rollback Hook
→ 更新 Kubernetes 资源
→ 等待资源就绪(如果使用 --wait)
→ 执行 post-rollback Hook
→ 写入新的 Release revision

可以先观察计划:

helm get manifest demo -n demo --revision 1
helm get values demo -n demo --revision 1

如果当前 Helm 版本支持并且需要更明确的模拟验证,可使用:

helm rollback demo 1 -n demo --dry-run

具体 dry-run 行为和输出格式应以本机 Helm 版本为准;不要把它当成对 Pod 启动和控制器行为的完整模拟。

11.3 回滚的反例:数据库已经升级

假设:

revision 1:应用 v1,数据库字段 name
revision 2:迁移增加字段 display_name,应用 v2 使用 display_name

执行 revision 2 后:

数据库:已经有 display_name
应用:v2

此时 Helm rollback 到 revision 1:

Kubernetes Deployment:回到 v1
数据库:仍然有 display_name

如果 v1 兼容额外字段,回滚成功;如果迁移删除或重命名了 v1 需要的字段,应用可能启动失败。Helm 没有数据库快照,也不知道如何逆向迁移。

因此,安全回滚依赖的是应用和 Schema 的兼容策略,而不是 Helm 命令本身。

11.4 回滚的其他边界

Helm 回滚不能自动恢复:

  • PVC 里的文件和数据库数据;
  • 外部云负载均衡器的所有历史属性;
  • 已发送的通知、消息和支付请求;
  • 被 Hook 修改的外部系统;
  • 被其他控制器或 GitOps 工具随后改写的字段;
  • 已删除且没有写入 Release manifest 的外部资源。

此外,CRD 具有特殊风险。Chart 中的 crds/ 目录资源不会像普通模板一样参与常规升级和回滚流程;CRD 版本迁移通常应由专门的升级方案管理,而不能假设 helm rollback 会自动把 CRD Schema 变回旧版本。


十二、Release 与 Kubernetes 实际状态可能发生漂移

Helm 保存的是它认为某次发布应包含的对象内容,但 Kubernetes 集群中的实际对象可能被其他因素改变:

Helm Release manifest
        ↓
Kubernetes API 对象
        ↓
控制器、Webhook、管理员、GitOps 工具继续修改

例如:

  • HPA 修改 Deployment 的 spec.replicas
  • MutatingAdmissionWebhook 注入 sidecar;
  • 管理员手工修改镜像;
  • GitOps 控制器持续恢复另一份期望状态;
  • 云控制器给 Service 添加 LoadBalancer 字段;
  • Kubernetes 默认值和控制器自动填充字段。

执行:

helm get manifest demo -n demo
kubectl get deployment demo-demo -n demo -o yaml

两者不完全相同并不必然表示 Helm 出错。需要区分:

  1. Helm 模板生成的字段;
  2. API Server 默认字段;
  3. Admission Webhook 注入字段;
  4. 控制器维护字段;
  5. 人工或其他系统修改字段。

如果 Helm 和 GitOps 工具同时管理同一组资源,就会发生竞争:

Helm upgrade 修改 Deployment
→ GitOps 调谐发现与 Git 不同
→ GitOps 恢复旧值
→ 运维看到“Helm 成功但配置又变回去”

一个资源应尽量只有一个明确的声明式管理者。Helm 可以作为 GitOps 工具的渲染器,但此时通常由 Argo CD、Flux 等系统负责持续调谐,而不是在集群外重复执行 Helm 写操作。


十三、Helm 与 Kustomize 的边界

Helm 和 Kustomize 都能生成 Kubernetes YAML,但抽象重点不同。

13.1 Helm 更适合可复用 Chart 和参数化发布

Helm 典型结构是:

Chart + Values + Template
→ 一次 Release

它适合:

  • 第三方软件分发;
  • 多环境参数化;
  • 依赖 Chart;
  • 发布历史;
  • Hook;
  • 安装、升级和回滚。

13.2 Kustomize 更适合对已有 YAML 做变体组织

Kustomize 典型结构是:

Base
  + Overlay
  + Patch
  + Generator
→ 环境清单

它通常不引入类似 Helm Release 的发布历史,也不使用模板语言。配置通过资源合并、Patch、名称前缀、Generator 等方式变化。

13.3 两者不能互相替代所有能力

如果需求是:

同一个软件包交给多个团队
→ 每个团队提供 Values
→ 需要依赖、Hook、Release 历史

Helm 更自然。

如果需求是:

已有一套明确 YAML
→ 生产环境只修改少数字段
→ 希望避免通用模板逻辑

Kustomize 往往更直接。

也可以组合使用,例如 Helm 生成基础清单,Kustomize 再做环境 Patch,但此时必须明确:

  • 哪一层拥有最终字段;
  • 名称和标签在哪一层生成;
  • Secret 和 ConfigMap 的哈希变化由谁负责;
  • GitOps 工具实际调谐的是哪一层输出。

否则会形成难以诊断的多重渲染和覆盖关系。


十四、常用运维命令和结果解释

14.1 查询 Release

helm list -n demo
helm status demo -n demo
helm history demo -n demo

helm status 主要展示 Helm Release 状态和资源摘要,不等于应用健康检查。应用是否能正确响应,还需要:

kubectl get pods -n demo
kubectl describe pod -n demo <pod-name>
kubectl logs -n demo <pod-name>
kubectl get events -n demo --sort-by=.lastTimestamp

14.2 查看配置和渲染结果

helm get values demo -n demo
helm get values demo -n demo --all
helm get manifest demo -n demo
helm get hooks demo -n demo
helm get notes demo -n demo

区别:

  • helm get values:通常显示用户设置的值;
  • helm get values --all:显示合并后的全部值,具体显示行为依 Helm 版本而定;
  • helm get manifest:查看 Release 记录的普通 manifest;
  • helm get hooks:查看 Hook;
  • helm get notes:查看 Chart 输出的使用说明。

14.3 安装和升级

推荐先检查,再执行:

helm lint ./demo -f values-prod.yaml

helm template demo ./demo \
  -n prod \
  -f values-prod.yaml > rendered.yaml

kubectl apply --dry-run=server -f rendered.yaml

helm upgrade --install demo ./demo \
  -n prod \
  --create-namespace \
  -f values-prod.yaml \
  --wait \
  --timeout 10m \
  --history-max 20

--history-max 可以限制 Release 历史数量,避免历史 Secret 无限增长,但过小会减少可回滚的版本范围。

14.4 卸载

helm uninstall demo -n demo

卸载通常删除 Release manifest 中的普通资源,但以下内容可能不会按预期消失:

  • Hook 创建的资源;
  • helm.sh/resource-policy: keep 标记的资源;
  • 外部系统资源;
  • PVC 数据;
  • CRD 或其他 Chart 特殊处理资源。

卸载前应先检查:

helm get manifest demo -n demo
helm get hooks demo -n demo
kubectl get all -n demo
kubectl get pvc -n demo

不要仅依赖 kubectl get all,因为它不包含所有资源类型。


十五、典型故障的定位路径

15.1 模板解析失败

表现:

parse error
unexpected "{{"
unexpected EOF

定位:

helm lint ./demo
helm template demo ./demo --debug

优先检查:

  • {{}} 是否配对;
  • ifwithrangedefine 是否闭合;
  • YAML 缩进;
  • 字符串引号;
  • 变量作用域。

15.2 Values 缺失或类型错误

表现:

nil pointer evaluating interface
wrong type for value

定位:

helm template demo ./demo \
  -f values-prod.yaml \
  --debug

修复方式:

  • values.yaml 提供完整结构;
  • 使用 required
  • 使用 values.schema.json
  • 检查 --set 是否把字符串变成了数字或布尔值;
  • 检查列表是否被后一个文件整体替换。

15.3 Kubernetes API 拒绝

表现:

no matches for kind
unknown field
forbidden
immutable field

区分原因:

  • no matches for kind:目标集群没有该 API 或 CRD;
  • unknown field:字段不符合目标版本 Schema;
  • forbidden:执行身份没有权限;
  • immutable field:对象创建后字段不可修改。

检查:

kubectl api-resources
kubectl api-versions
kubectl auth can-i create deployment -n demo

当前稳定 Kubernetes API 并不意味着所有云厂商集群、旧版本集群和扩展组件都一致。Chart 应明确声明支持的 Kubernetes 版本范围,并在 CI 中针对实际目标版本渲染和校验。

15.4 Helm 成功,但 Pod 不健康

表现:

STATUS: deployed

同时:

kubectl get pods -n demo

显示 CrashLoopBackOffPendingImagePullBackOff

这是因为 Helm 的“发布记录成功”和应用的“业务健康”不是同一个状态。继续检查:

kubectl describe pod -n demo <pod>
kubectl logs -n demo <pod> --previous
kubectl get events -n demo --sort-by=.lastTimestamp
kubectl get endpoints -n demo

可能原因包括:

  • 镜像拉取失败;
  • 探针路径错误;
  • 资源请求导致无法调度;
  • Secret 或 ConfigMap 不存在;
  • PVC 未绑定;
  • 应用启动命令错误;
  • 网络策略阻止依赖访问。

15.5 Hook 卡住或失败

检查:

helm status demo -n demo
helm get hooks demo -n demo
kubectl get jobs,pods -n demo
kubectl describe job -n demo <job>
kubectl logs -n demo job/<job>

如果 Helm 命令超时,不能立即断定 Hook 已停止。Job 或 Pod 可能仍在集群中运行,尤其是超时、客户端断开或控制器延迟时。应先查看实际对象,再决定是否删除。


十六、安全、权限和供应链风险

Helm Chart 可以创建高权限 RBAC、读取 Secret、挂载主机路径和运行特权容器。因此安装第三方 Chart 前应检查:

helm show all oci://example.com/charts/demo
helm template demo oci://example.com/charts/demo \
  --version 1.2.3 \
  | less

关注:

  • ServiceAccount 是否绑定 cluster-admin
  • 是否使用 hostNetworkhostPIDprivileged
  • 是否挂载 /var/run/docker.sock
  • 是否创建集群级 CRD 和 ClusterRole;
  • 镜像来源和摘要是否固定;
  • Hook 是否执行外部命令或访问敏感系统;
  • 模板是否使用了 lookup 读取集群对象;
  • 是否把 Secret 内容输出到 NOTES 或日志。

Helm 模板中的 lookup 可以查询集群现有对象:

{{- $existing := lookup "v1" "ConfigMap" .Release.Namespace "some-config" }}

它使渲染结果依赖集群当前状态,降低可重复性;离线 helm template 时也无法获得相同结果。除非确实需要读取已存在对象,否则应优先通过 Values 或显式资源依赖传递输入。

tpl 可以把字符串再次当成模板执行:

{{ tpl .Values.extraConfig . }}

它很灵活,但也扩大了配置执行能力。若 Values 来自不可信输入,不能把 tpl 当作普通字符串替换。


十七、生产发布的完整流程

一个较完整的发布流程可以是:

# 1. 更新依赖
helm dependency build ./demo

# 2. 静态检查
helm lint ./demo -f values-prod.yaml

# 3. 本地渲染
helm template demo ./demo \
  -n prod \
  -f values-prod.yaml \
  --include-crds \
  > rendered.yaml

# 4. 服务端 dry-run
helm upgrade --install demo ./demo \
  -n prod \
  -f values-prod.yaml \
  --dry-run=server \
  --debug

# 5. 执行发布
helm upgrade --install demo ./demo \
  -n prod \
  --create-namespace \
  -f values-prod.yaml \
  --wait \
  --wait-for-jobs \
  --timeout 10m \
  --atomic \
  --history-max 20

# 6. 验证
helm status demo -n prod
kubectl rollout status deployment/demo-demo -n prod
kubectl get pods -n prod

每一步解决的问题不同:

  • dependency build:确保依赖内容已准备;
  • lint:发现 Chart 结构和模板常见错误;
  • template:审查最终 YAML;
  • dry-run=server:让 API Server 校验资源和权限;
  • upgrade --install:执行幂等的安装或升级;
  • --wait-for-jobs:等待 Job,包括某些迁移 Hook 的完成;
  • --atomic:发布失败时自动尝试回滚或清理;
  • kubectl rollout status:验证 Kubernetes 控制器是否真正完成滚动更新。

但这仍然不是完整业务验收。还需要根据应用执行健康检查、接口测试、数据迁移验证和指标观察。


十八、常见误解与反例

误解一:appVersion 会自动决定镜像版本

不会。只有模板显式引用它才会影响输出:

image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}"

即便如此,很多 Chart 仍应使用独立的 .Values.image.tag,因为 Chart 版本、应用版本和镜像版本不一定同步。

误解二:Helm 回滚等于完整系统回滚

不会。Helm 主要恢复由 Release 管理的 Kubernetes 对象内容。数据库、对象存储、消息系统、云资源和外部副作用需要各自的恢复机制。

误解三:Release 显示 deployed 就代表服务可用

不一定。没有 --wait 时,Helm 可能只确认 API 请求成功。即使使用 --wait,就绪探针也只能表达 Kubernetes 层面的就绪,不代表业务依赖和用户请求都正常。

误解四:Hook Job 会像 Deployment 一样自动被 Helm 管理

不会。Hook 资源具有特殊生命周期,必须配置删除策略,并单独处理日志、重试和失败清理。

误解五:修改 ConfigMap 后 Pod 会自动重启

通常不会。卷挂载内容可能最终更新,但应用是否重新读取取决于应用;环境变量不会自动更新。使用 checksum 注解或显式 rollout 才能让配置变化触发 Pod 模板变化。

误解六:多个系统同时改资源不会有问题

会。Helm、kubectl、Kustomize、GitOps 控制器和云控制器对同一字段拥有不同期望时,会产生漂移、覆盖和反复修改。必须明确资源和字段的管理边界。


十九、如何判断一次 Helm 回滚是否真正成功

不能只看命令退出码,应按层次验证:

helm status demo -n prod
helm history demo -n prod
kubectl get deployment -n prod demo-demo \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
kubectl rollout status deployment/demo-demo -n prod
kubectl get pods -n prod
kubectl logs -n prod -l app.kubernetes.io/instance=demo --tail=100

验证逻辑是:

  1. Helm 是否创建了新的 rollback revision;
  2. Deployment 的镜像和配置是否回到目标版本;
  3. 新 ReplicaSet 是否完成滚动;
  4. Pod 是否通过探针;
  5. 应用日志是否显示兼容的数据库和外部依赖状态;
  6. 关键业务请求是否恢复。

如果第 1 步成功而第 4 步失败,说明 Release 元数据已更新,但应用运行状态仍未恢复。此时应继续分析 Pod、配置、镜像、存储和依赖,而不是重复执行回滚。


二十、总结:用正确的边界理解 Helm

Helm 的核心关系可以压缩为:

Chart:部署内容和模板
Values:本次渲染输入
Template:把输入生成 Kubernetes Manifest
Release:一次安装实例及其历史
Hook:插入安装、升级、删除、回滚阶段的动作
Rollback:用旧 revision 的发布内容创建一次新的发布

完整因果链是:

Chart + Values
→ Helm Template 渲染
→ Kubernetes API 校验与持久化
→ 控制器创建和维护运行对象
→ Helm 保存 Release revision
→ 后续 upgrade、rollback 或 GitOps 调谐继续改变状态

因此,使用 Helm 时至少要同时观察三个状态:

  1. 渲染状态:模板和 Values 是否生成了预期 YAML;
  2. 发布状态:Helm Release 是否成功提交、升级或回滚;
  3. 运行状态:Kubernetes 控制器和应用是否真正健康。

只有三者都验证通过,才可以把一次 Helm 操作视为完成。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。