Helm Chart 是 Kubernetes 上的应用打包标准,它把一组零散的 K8s 资源收拢成一个可参数化、可版本化、可一键回滚的发布单元。一个应用跑起来后,你手里往往已经攒了十几个 YAML:Deployment、Service、Ingress、ConfigMap、Secret、HPA……测试环境改副本数要动一个文件,生产环境改镜像 tag 要动另一个,回滚还得靠 git revert 加人工 apply。本文从 Chart 目录结构讲起,覆盖模板语法、多环境 values 管理、版本号策略、升级与回滚命令,并点出模板缩进、Secret 明文、–atomic 缺失等五个高频雷区。
一、裸 YAML 的三个痛点
直接用 kubectl apply -f 管理应用,在单人单环境时确实够用。一旦进入多环境、多人协作,三个问题会集中爆发。
第一是参数无法抽离。测试环境副本数 1、生产 6,测试用 512Mi 内存、生产用 2Gi,这些差异只能靠复制整套 YAML 再手改,很快就演变成三四份高度相似却各自漂移的目录。某次上线只改了 prod 目录的镜像 tag,忘了同步 staging,结果预发验证的根本不是要上线的版本。
第二是没有发布单元的概念。一个应用的 8 个资源分 8 次 apply,中间任何一步失败,集群就停在半新半旧的中间态。你既不知道”当前线上是哪一版”,也无法原子回退。
第三是分发困难。想把自己的服务给另一个团队复用,只能打包目录发压缩包,对方还得逐个文件改命名空间和域名。而基础组件(Redis、Nginx Ingress)如果每个团队都自己写一套 YAML,维护成本会被重复放大。想先补齐 K8s 基础概念,可以对照Kubernetes 入门实战里的资源模型部分再往下读。
二、Chart 目录结构:一个包长什么样
Helm 的核心概念只有三个:Chart(应用包,静态模板)、Values(参数,注入模板)、Release(一次具体安装,带版本号)。用 helm create 生成的骨架就能看清职责划分:
helm create myapp
myapp/
├── Chart.yaml # 包元信息:名称、版本、appVersion、依赖
├── values.yaml # 默认参数(生产不要直接改这里)
├── charts/ # 子 chart(依赖的其他 Chart)
├── templates/
│ ├── deployment.yaml # 带模板变量的资源清单
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── _helpers.tpl # 可复用的命名/标签片段
│ └── NOTES.txt # 安装后给用户的提示文本
└── .helmignore
关键在于 templates/ 下不再是死的 YAML,而是 Go template。渲染时 Helm 把 values.yaml 与命令行参数合并成一棵值树,注入模板产出最终清单,再提交给 API Server。这意味着同一份 Chart 可以生成任意环境的部署清单,差异全部收敛到 values。
三、模板语法:把差异变成变量
下面是一段生产可用的 Deployment 模板,覆盖了取值、默认值、条件判断、循环、命名复用五类最常用写法:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "myapp.name" . }}
template:
metadata:
annotations:
# 让 ConfigMap 变更自动触发滚动重启
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- range $k, $v := .Values.env }}
- name: {{ $k }}
value: {{ $v | quote }}
{{- end }}
{{- if .Values.nodeSelector }}
nodeSelector:
{{- toYaml .Values.nodeSelector | nindent 8 }}
{{- end }}
几个要点值得记牢。{{- 的减号会吃掉左侧空白,配合 nindent N 才能得到正确缩进——YAML 对缩进零容忍,这是新手最容易翻车的地方。toYaml 用于把 values 里的整段结构(如 resources、nodeSelector)原样铺开,避免逐字段硬写。include 调用 _helpers.tpl 里定义的命名片段,保证所有资源用同一套名称与标签,后续用 k9s 终端管理按 label 筛资源时会省心很多。
那段 checksum/config 注解是实战中最实用的一招:ConfigMap 内容变了但 Deployment spec 没变时,K8s 默认不会重启 Pod,导致新配置不生效。把 ConfigMap 的哈希写进 Pod 注解,内容一变哈希就变,滚动更新自然触发。
四、多环境管理:values 的分层与优先级
正确姿势是:values.yaml 只放通用默认值,每个环境一个覆盖文件,只写差异项。
# values.yaml —— 通用默认
replicaCount: 1
image:
repository: registry.example.com/myapp
pullPolicy: IfNotPresent
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }
# values-prod.yaml —— 只写差异
replicaCount: 6
image:
pullPolicy: Always
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: 2000m, memory: 2Gi }
nodeSelector:
node-role: app
部署时叠加,后者覆盖前者:
# 生产部署:默认值 + prod 覆盖 + CI 注入的镜像 tag
helm upgrade --install myapp ./myapp \
--namespace prod --create-namespace \
-f myapp/values.yaml \
-f myapp/values-prod.yaml \
--set image.tag=1.8.3 \
--atomic --timeout 5m
优先级从低到高是:Chart 内置 values.yaml < 依次传入的 -f 文件(后盖前)< --set / --set-string。所以让 CI 用 --set image.tag 注入构建产物版本是最稳的做法,环境文件里则不写 tag,避免两处冲突。
values 分环境的两条纪律
一是不要把 values-prod.yaml 写成全量副本。一旦全量,Chart 默认值升级后 prod 收不到任何改进,又回到了目录复制的老路。二是敏感信息不进 values。数据库密码、密钥应通过外部 Secret、云厂商密钥管理或 sealed-secrets 注入,values 文件通常进 Git,明文写进去等于泄露。
五、版本管理:两个 version 别搞混
Chart.yaml 里有两个容易混淆的字段,语义完全不同:
| 字段 | 含义 | 什么时候要改 | 约束 |
|---|---|---|---|
| version | Chart 包自身版本 | 模板、默认值、依赖有任何改动 | 必须严格 SemVer,仓库按它索引 |
| appVersion | 被打包应用的版本 | 业务镜像发新版 | 自由字符串,可与镜像 tag 一致 |
实践中常见错误是只改镜像 tag、不动 Chart version,结果 Chart 仓库里同一个版本号对应了多份内容,别人拉到的包和你本地的不一致。规则很简单:模板变了必须升 version;只是业务发版则升 appVersion,同时把 version 的 patch 位加一。
打包与推仓库:
# 校验语法与最佳实践
helm lint ./myapp
# 打成 tgz(产物名由 name + version 决定)
helm package ./myapp # 输出 myapp-0.3.1.tgz
# 推送到 OCI 仓库(Helm 3.8+ 原生支持)
helm push myapp-0.3.1.tgz oci://registry.example.com/charts
# 从 OCI 仓库安装指定版本
helm install myapp oci://registry.example.com/charts/myapp --version 0.3.1
把 Chart 推到 OCI 仓库(与容器镜像共用一套 registry)是目前推荐做法,比维护静态 index.yaml 的传统 HTTP 仓库省事。这一步很适合放进流水线,和GitLab CI/CD 多环境部署或 GitHub Actions 的构建阶段串起来:镜像推完紧接着推 Chart,二者版本对齐。
六、发布与回滚:Release 才是运维抓手
Helm 把每次 install/upgrade 记录成 Release 的一个 revision,存在目标命名空间的 Secret 里。这让”当前线上是哪一版””上一版是什么”变成可查询的事实:
# 查看某 Release 的全部历史
helm history myapp -n prod
# 渲染但不提交,纯本地预检(强烈建议进 CI)
helm template myapp ./myapp -f myapp/values-prod.yaml | kubectl apply --dry-run=server -f -
# 对比即将变更的内容(需 helm-diff 插件)
helm diff upgrade myapp ./myapp -f myapp/values-prod.yaml
# 回滚到上一版本 / 指定 revision
helm rollback myapp -n prod
helm rollback myapp 7 -n prod
# 查看当前生效的完整 values
helm get values myapp -n prod --all
--atomic 是生产必加参数:升级失败时自动回滚到升级前状态,避免集群卡在中间态;它隐含 --wait,所以要配合 --timeout 给足时间,否则大镜像拉取慢会被误判为失败而回滚。helm rollback 的价值在于它回退的是整个发布单元——镜像、配置、副本数、Ingress 规则一起退,而不是像手工 apply 那样只退一个文件。
另外记得给关键工作负载配 readiness 探针,否则 --wait 只看 Pod Running 就认为成功,而实际进程还没就绪。监控侧建议把 Release revision 作为标签打进指标,这样发布与指标波动能对齐排查,做法可参考Prometheus + Grafana 监控体系。
七、五个高频坑与处置
| 坑 | 典型症状 | 处置 |
|---|---|---|
| 模板缩进错乱 | YAML parse error 或字段丢失 | 统一用 nindent,先跑 helm template 看渲染结果 |
| 密码写进 values | Git 历史泄露凭据 | 外部 Secret 注入,values 只放引用名 |
| 升级未加 –atomic | 失败后半新半旧、需人工救 | upgrade 一律带 –atomic –timeout |
| Chart version 不升 | 同版本号内容不一致,缓存拉旧包 | 模板任何改动必升 version |
| helm uninstall 误删 | PVC/数据一并消失 | 关键 PVC 加 helm.sh/resource-policy: keep |
最后一条尤其要警惕:默认情况下卸载 Release 会删掉它管理的全部资源,包含 PVC。给有状态资源加上 helm.sh/resource-policy: keep 注解,Helm 卸载时会跳过它,数据得以保留。
什么时候不该用 Helm
Helm 的模板是字符串拼接,逻辑一复杂就会出现难以调试的嵌套 if 与空白控制。如果你的清单差异极小,Kustomize 的 overlay 打补丁思路更清爽;如果需要强类型和真正的编程能力,可以看 cdk8s 一类方案。而 Chart 与云资源(数据库实例、负载均衡)的协同,仍建议交给 Terraform 基础设施即代码,让 Helm 只负责集群内的应用层。
八、小结
Helm Chart 解决的不是”写 YAML 太累”,而是让 K8s 应用具备包的属性:可参数化、可版本化、可分发、可原子回滚。落地时抓住四件事——values 分层只写差异、模板缩进统一用 nindent、Chart version 与 appVersion 各司其职、生产升级一律 --atomic 配 --timeout。把它接进流水线后,发布就从”apply 一堆文件”变成”发一个带版本号的包”,出问题一条 rollback 就能退回去。镜像层面的优化可以顺带对照Docker 网络与容器实践,从构建到部署把整条链路打通。




