在 Kubernetes 上部署一个生产级应用,往往需要几十个甚至上百个 YAML 文件:Deployment、Service、ConfigMap、Ingress、RBAC 规则、HorizontalPodAutoscaler……这些文件之间存在依赖关系,不同环境(开发、预发、生产)需要不同参数,手工管理极易出错。Helm 正是为了解决这个问题而生:把一组相关的 Kubernetes 资源打包成一个可版本化、可参数化、可共享的制品(Chart),并提供安装、升级、回滚、卸载的生命周期管理。

Helm 的本质是"带版本历史的 YAML 打包器"。它不追踪资源的真实运行状态(那是 Operator 的职责),只记录"这次 install/upgrade 渲染出了哪些 manifest,交给 Kubernetes 执行"。Release 状态存储在集群内的 Secret 对象中(key 为 helm.sh/release.v1),每次升级都新增一个版本,rollback 本质上是重新 apply 旧版本的 manifest 集合。

理解 Helm 的价值,需要同时理解它的边界。Helm 擅长初始部署和简单升级,不擅长复杂的 Day-2 运维(数据迁移、主从切换、证书续签)——那类场景更适合 Operator。Helm 和 Operator 不是竞争关系:很多 Operator 本身就是通过 Helm Chart 来安装的。

数据流全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Chart (templates/ + values.yaml + charts/)


helm install / upgrade
├── values merging
│ default values.yaml
│ ← user values.yaml (-f custom.yaml)
│ ← --set key=value (优先级最高)
├── Go template rendering
│ {{ .Values.image.tag }}
│ {{ include "mychart.labels" . }}
│ {{ toYaml .Values.resources | indent 10 }}
└── kubectl apply (rendered manifest set)


Release object (stored in Secret: helm.sh/release.vN)

├── helm rollback N → re-apply manifest set at version N
├── helm upgrade → new Secret helm.sh/release.v(N+1)
└── helm uninstall → delete all resources in release

Chart 结构解析

目录布局

一个典型的 Chart 目录包含:

1
2
3
4
5
6
7
8
9
10
mychart/
├── Chart.yaml # 元数据:name, version, appVersion, description
├── values.yaml # 默认参数值(用户可覆盖)
├── charts/ # 子 Chart(依赖)
├── templates/ # Go template 文件,渲染为 Kubernetes manifest
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── _helpers.tpl # 下划线开头:不渲染为 manifest,只定义命名模板
│ └── NOTES.txt # helm install 后打印的说明文字
└── .helmignore # 打包时忽略的文件(类似 .gitignore)

Chart.yaml 中的 version 是 Chart 版本(打包制品的版本),appVersion 是被打包应用的版本(如 nginx:1.25.0),两者独立演化。dependencies 字段列出依赖的其他 Chart(如 mysql、redis),执行 helm dependency update 后下载到 charts/ 目录。

values.yaml 与参数覆盖

values.yaml 定义参数的默认值和类型结构,是 Chart 的"接口文档"。Helm 的参数合并优先级从低到高:

  1. Chart 内嵌的 values.yaml(默认值)
  2. 父 Chart 的 values.yaml(subchart 覆盖)
  3. 用户提供的 -f custom-values.yaml(可多个,后面的优先)
  4. 命令行 --set key=value(最高优先级)

需要注意的是,Helm 的 values 合并是浅合并(shallow merge),不是深度合并(deep merge)。如果用 --set 覆盖一个嵌套对象中的某个字段,该对象的其他字段不会自动保留——必须显式传递所有需要的字段,或用 -f 传递完整的 values 文件。

Go template + Sprig 函数库

Helm 使用 Go 的 text/template 引擎,并集成了 Sprig 函数库(提供 100+ 实用函数)。常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 引用 values
image: {{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}

# 条件分支
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
...
{{- end }}

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

# 引用命名模板(定义在 _helpers.tpl)
labels:
{{- include "mychart.labels" . | nindent 4 }}

# YAML 序列化
resources:
{{- toYaml .Values.resources | nindent 10 }}

nindentindent 是处理多行 YAML 缩进的核心函数。quote 确保字符串值被正确引用。{{--}} 中的横线控制空白符裁剪,避免渲染结果产生多余的空行。

Release 生命周期

install → upgrade → rollback → uninstall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装(创建 Release,版本号从 v1 开始)
helm install myrelease ./mychart -f prod-values.yaml

# 升级(Release 版本号递增,旧 manifest 存入历史)
helm upgrade myrelease ./mychart --set image.tag=1.26

# 查看历史版本
helm history myrelease

# 回滚到版本 1
helm rollback myrelease 1

# 卸载(删除所有 Release 中的资源,默认保留历史)
helm uninstall myrelease

Release 存储机制

Helm 3 把 Release 状态存储在 Secret 中(Helm 2 用 Tiller 进程存储,Helm 3 去掉了 Tiller)。每个 Secret 的 label 为 owner=helm,name 格式为 sh.helm.release.v1.<release-name>.v<revision>,data 字段包含 gzip 压缩后的 Release 信息(包括完整的渲染后 manifest)。

这意味着:rollback 不依赖外部状态,只需读取对应版本的 Secret,重新 apply 其中记录的 manifest 集合。同时也意味着:Helm 的 Secret 会随版本积累占用 etcd 空间,需要配置 --history-max 限制保留的历史版本数。

Hooks:在生命周期节点执行 Job

Helm Hooks 允许在 install/upgrade/delete 等操作的前后执行额外的 Kubernetes Job:

  • pre-install:在 manifest 渲染完成后、资源安装前执行(常用于数据库初始化)
  • post-install:资源安装后执行(常用于冒烟测试)
  • pre-upgrade:升级前执行(常用于数据库 schema 迁移)
  • pre-delete:卸载前执行(常用于数据备份)

Hook 资源本身也是 Kubernetes 对象(通常是 Job),通过 annotation helm.sh/hook 标记,Helm 在对应时间点 apply 并等待 Job 完成。

OCI Registry 与 Chart Museum

Helm 3 支持把 Chart 作为 OCI artifact 存储到标准的 OCI 兼容 Registry(如 Docker Hub、ECR、GCR、Harbor):

1
2
3
4
5
6
7
8
# 打包 Chart
helm package ./mychart

# 推送到 OCI Registry
helm push mychart-1.0.0.tgz oci://registry.example.com/charts

# 从 OCI Registry 安装
helm install myrelease oci://registry.example.com/charts/mychart --version 1.0.0

Chart Museum 是专为 Helm Chart 设计的 HTTP 服务器,实现了 Helm repository 协议,适合私有部署场景。OCI registry 是更现代的选择,与镜像仓库基础设施复用,不需要额外维护 Chart Museum。

可运行实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 1. 安装 Helm CLI
brew install helm # macOS

# 2. 添加常用 repo
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# 3. 搜索 Chart
helm search repo bitnami/nginx

# 4. 查看默认 values(了解可配置参数)
helm show values bitnami/nginx | head -60

# 5. 模板渲染预览(不实际安装)
helm template mynginx bitnami/nginx \
--set service.type=ClusterIP \
--set replicaCount=2

# 6. 安装(dry-run:渲染但不 apply)
helm install mynginx bitnami/nginx \
--set service.type=ClusterIP \
--dry-run --debug 2>&1 | head -80

# 7. 真正安装
helm install mynginx bitnami/nginx --set service.type=ClusterIP

# 8. 查看 Release 状态
helm status mynginx
helm history mynginx

# 9. 查看 Release 存储的 Secret
kubectl get secrets -l owner=helm

# 10. 升级
helm upgrade mynginx bitnami/nginx --set replicaCount=3

# 11. 回滚
helm rollback mynginx 1

# 12. 卸载
helm uninstall mynginx

实验结果映射到 K8s 对象

helm history mynginx 中的每一行对应一个 Secret 对象,格式为 sh.helm.release.v1.mynginx.v<N>kubectl get secret sh.helm.release.v1.mynginx.v1 -o jsonpath='{.data.release}' | base64 -d | gunzip | jq . 可以看到完整的 Release 元数据,包括渲染后的所有 manifest。

helm template 的输出就是 Helm 渲染后、kubectl apply 前的 manifest 集合,可以用于 GitOps(把渲染结果提交到 Git,由 Argo CD 或 Flux 负责 apply),规避了 Helm 在集群内运行的权限问题。

helm rollback mynginx 1 实际上是找到 sh.helm.release.v1.mynginx.v1 中记录的 manifest 集合,重新 apply,并创建新的 sh.helm.release.v1.mynginx.v3(版本号继续递增)。rollback 不会"撤销"历史,而是创建新的前进版本。

Chart 设计的工程实践

命名模板与代码复用

_helpers.tpl 文件中定义的命名模板是 Chart 代码复用的核心机制。kubebuilder 生成的 Chart 脚手架通常包含 chart.fullnamechart.labelschart.selectorLabels 等标准命名模板,在所有资源的 metadata 中统一引用:

1
2
3
4
5
6
7
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

这套标准 label 方案(app.kubernetes.io/*)是 Kubernetes 推荐的应用标识规范,kubectl、Helm、Argo CD 等工具都能识别这些 label 做资源关联。

values schema 验证

Helm 3.1+ 支持通过 values.schema.json(JSON Schema 格式)对 values.yaml 做类型和约束验证。用户传入类型错误的参数(如把整数值传给字符串字段)时,helm install 在客户端就报错,不会到达集群。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"$schema": "http://json-schema.org/draft-07/schema",
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1,
"maximum": 100
},
"image": {
"type": "object",
"required": ["repository", "tag"]
}
}
}

values.schema.json 是 Chart 接口文档的机器可读版本,IDE 可以基于它提供自动补全和错误提示。

Subchart 与全局 values

当一个 Chart 依赖多个 subchart(在 charts/ 目录下)时,父 Chart 的 values.yaml 可以通过 subchart 名称作为 key 来覆盖 subchart 的默认值:

1
2
3
4
5
6
7
# 父 Chart 的 values.yaml
mysql: # 与 subchart 名称对应
auth:
rootPassword: "secret"
primary:
persistence:
size: 10Gi

global 是特殊的顶层 key,父 Chart 设置的 global.* 值会自动传递到所有 subchart,无需逐个覆盖。常用于传递镜像仓库前缀、imagePullSecrets、环境标识等跨 chart 共享的配置。

Helm 与 GitOps 的结合

在 GitOps 工作流中,Helm 通常作为模板引擎,而不是直接控制 apply 的工具:

Argo CD 的 Helm 模式:Argo CD 读取 Git 仓库中的 Chart 和 values 文件,在集群侧执行 helm template 渲染,然后把渲染结果与集群实际状态对比(diff),由 Argo CD 负责 apply。Helm 的 Release Secret 不由 Argo CD 管理,也不需要 helm install 命令。

这种模式的优势是:Argo CD 能检测到 Git 中的 values 变更和集群中手动修改之间的 drift,自动或手动 sync 回 Git 定义的状态。Helm 直接操作时,如果有人在 Helm 之外修改了资源,Helm 不会检测到这个 drift。

Helm diff 插件

helm-diff 是社区插件,在执行 helm upgrade 前展示本次升级会产生哪些 Kubernetes 资源变更,类似于 terraform plan

1
2
helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade myrelease ./mychart --set image.tag=1.26

输出格式与 kubectl diff 类似,逐字段显示新旧 manifest 的差异。在生产环境做变更前用 helm diff 预览,可以发现意外的副作用——例如修改了一个 values 参数,却导致多个不相关的资源被重建(常见于 ConfigMap hash 作为 Deployment annotation 的场景)。

Helm 测试与 CI 集成

helm lint 与 helm test

helm lint 在本地检查 Chart 的语法和结构问题:模板渲染错误、必填字段缺失、values.yaml 格式问题。这是 CI 流水线中 Chart 变更后的第一道检查,在 push 到 Registry 前执行。

helm test 运行 Chart 中标注了 helm.sh/hook: test 的 Pod,验证 Release 安装后的基本功能。测试 Pod 运行完毕后,Helm 检查退出码:0 表示通过,非 0 表示失败。这类似于 smoke test,例如 nginx Chart 的 test Pod 发起一次 HTTP 请求验证 nginx 正常响应。测试结果存储在 Pod 对象中,helm test 命令结束后 Pod 保留(用于检查日志),下次运行前需手动清理。

多环境 values 管理

实际项目中,通常为不同环境维护单独的 values 文件:

1
2
3
4
deploy/
├── values-dev.yaml # 开发环境:副本数 1,资源 requests 小
├── values-staging.yaml # 预发环境:副本数 2,resources 接近生产
└── values-prod.yaml # 生产环境:副本数 5,HPA 开启,PDB 配置

通过 -f values-prod.yaml 在安装时选择环境,Chart 本身不包含环境差异。这种模式让 Chart 版本(制品)与部署配置(环境参数)分离,Chart 版本升级不影响环境参数,环境参数变更不产生新的 Chart 版本。

模式提炼

Helm 是"带版本历史的 YAML 打包器",而不是配置管理系统,也不是 Operator。它的能力边界:能做参数化渲染、版本历史、rollback;不能做持续调谐、状态感知、Day-2 运维自动化。

values.yaml 是 Chart 的公开接口,应该只暴露真正需要用户关心的参数,隐藏实现细节。过度暴露参数(把所有 Kubernetes 字段都参数化)会导致 values.yaml 比原始 YAML 更难维护。

helm template + GitOps 是 Helm 在大规模场景的常见用法:Helm 负责渲染,Argo CD/Flux 负责 apply 和状态对比。这种模式把 Helm 的模板能力和 GitOps 的审计能力结合,规避了 Helm 直接 apply 时权限过大的问题。

工程迁移表

传统模式 Helm 等价 关键差异
Maven POM + 依赖管理 Chart.yaml + dependencies Chart 打包的是 K8s manifest,而非代码
npm package + semver Chart version + appVersion 两个独立版本号(制品版本 vs 应用版本)
Ansible playbook Helm Chart + hooks Helm 无 idempotent 保证,Ansible 有
Terraform module Helm Chart Terraform 追踪真实状态,Helm 不追踪
Docker Compose file Helm Chart Helm 面向 K8s 集群,Compose 面向单机

常见误解

误解一:Helm 会跟踪资源的真实状态

Helm 只记录"这次 apply 了哪些 manifest",不监控实际运行状态。如果有人在 Helm 之外直接修改了 Deployment 的副本数,helm status 看到的仍然是 release 的版本信息,不会反映实际状态的偏差。helm upgrade 会用新渲染的 manifest 覆盖手动修改,但中间状态 Helm 并不知晓。这个"drift detection"是 GitOps 工具(Argo CD、Flux)的职责,不是 Helm 的。

误解二:values.yaml 支持深度合并

Helm 的 values 合并是浅合并。如果 values.yaml 中定义了:

1
2
3
4
image:
repository: nginx
tag: latest
pullPolicy: IfNotPresent

然后用 --set image.tag=1.25 覆盖,结果是 image.tag=1.25,其他字段正常保留——这是因为 --set 只修改指定的叶子节点。但如果用 -f custom.yaml 传入:

1
2
image:
tag: 1.25

结果是整个 image 对象被替换为 {tag: 1.25}repositorypullPolicy 丢失。这是 Helm values 合并最常见的踩坑点。

误解三:Helm 3 还需要 Tiller

Helm 2 的架构依赖 Tiller(一个运行在集群内的服务端组件),有严重的安全问题(Tiller 通常拥有 cluster-admin 权限)。Helm 3 于 2019 年彻底移除了 Tiller,改为客户端直接用当前用户的 kubeconfig 权限与 API Server 通信。升级到 Helm 3 后,Tiller 相关的安全顾虑已不存在,权限粒度由 kubeconfig 用户的 RBAC 规则决定。

练习

  1. helm create mychart 初始化一个 Chart 骨架,修改 values.yaml 添加一个自定义参数(如 greeting: hello),在 templates/configmap.yaml 中引用这个参数,用 helm template 验证渲染结果,再用 helm install 安装到本地集群,查看生成的 ConfigMap。

  2. 在练习 1 的基础上,添加一个 pre-install hook(一个输出"database initialized"的 Job),安装时观察 Job 执行情况,理解 Helm 如何等待 hook Job 完成后才继续安装其他资源。

  3. helm install 安装 bitnami/wordpress,然后用 helm get manifest myrelease 查看完整的渲染后 manifest,再用 kubectl get secrets -l owner=helm 找到对应的 Release Secret,用 base64 解码并解压,查看 Release 元数据的完整结构。最后执行 helm uninstall,确认所有资源被清理。

系列导航

参考资料