1. 为什么最终选择Helm管理应用:手工YAML到模板化的不归路
先说一个很常见的场景:团队里最开始部署 Kubernetes 应用,基本靠 git 仓库里堆一大堆 YAML。大家心照不宣地把deployment.yaml、service.yaml、configmap.yaml一个个kubectl apply -f丢上去,一个应用也就是三五份文件,看着没什么问题。但等应用多起来,你就会发现这套做法开始不对劲了。
首先是环境差异。同一个应用要部署到 dev、staging、prod,唯一的区别可能就是镜像 tag、副本数、资源配置,但这样就要复制三份 YAML。一旦改了一次环境变量或者加了一个探针,你就要同步改三个文件,漏一个就是事故。其次是升级和回滚。手工kubectl apply没有版本之间的概念,改错了只能靠眼睛找问题,再想办法改回去。再往后,如果要做应用的多实例部署,比如每个租户一套独立环境,手写 YAML 的做法基本等于宣告结束。
Helm 解决的就是这个问题。它把你那一堆 Kubernetes 清单文件打包成一个整体——也就是 Chart,机制上叫模板化渲染:把可变的部分变成变量(Values),不变的部分写成模板。部署的时候,Helm 根据你提供的 values 动态生成一份完整的 YAML,再提交给 Kubernetes。这相当于从"每次手工改文件"进化到"把文件变成可配置的工程"。
除此之外,Helm 自带release 版本追踪。每次helm install、helm upgrade都会生成一个 revision 记录,你可以随时回滚到任意历史版本。这一点我在后面专门讲,因为它是整个工具最值得依赖的特性。
所以这篇文章我打算从零开始,用 Helm 3.10 跑一遍完整的流程:安装、部署 Nginx、改配置、升级、回滚,再到排查常见问题。整篇内容偏实战,都是我自己在项目里用过的命令和踩过的坑,适合正在从纯 kubectl 转向 Helm 的运维和开发同学参考。
2. 环境准备:Helm 3.10安装、集群权限和chart仓库配置
2.1 安装Helm 3.10:别再用旧版本的习惯操作
先强调一个前提:现在的 Helm 3 和 Helm 2 是完全不同的形态。Helm 2 需要在集群里安装 Tiller,这玩意有严重的权限问题,所以 Helm 3 直接去掉了 Tiller。你现在搜"helm 3.10下载",建议直接去 GitHub releases 页面找对应的二进制包,或者用包管理器装。下面是我在 Linux 服务器上的安装方式:
# 下载 Helm 3.10.x 二进制包,假设是 amd64 Linux wget https://get.helm.sh/helm-v3.10.3-linux-amd64.tar.gz tar -zxvf helm-v3.10.3-linux-amd64.tar.gz mv linux-amd64/helm /usr/local/bin/helm helm version如果你在 Mac 上开发,那直接brew install helm就行。Windows 用户用 chocolatey 或者 scoop 也可以。装完验证一下:
helm version输出类似version.BuildInfo{Version:"v3.10.3", ...}就说明装好了。这个版本比较稳定,兼容 Kubernetes 1.25 以下的集群没什么太大问题。如果你集群版本比较高,建议用更新的 Helm 3.x。
安装本身没什么难度,真正容易踩坑的是权限。Helm 3 里所有的操作都靠你在~/.kube/config里提供的 kubeconfig 凭证。也就是说,你在 kubectl 里能做什么,Helm 就能做什么。所以装完以后先检查集群连通性:
kubectl cluster-info如果能正常输出集群地址和版本信息,再执行helm env看看 Helm 的配置目录。这里我遇到过一个尴尬的情况:用户明明有集群权限,但kubectl能读到的 context 不对,导致 Helm 找不到集群。排查方法是检查当前 context:
kubectl config current-context确认这个 context 指向的是你想操作的环境。不要在混乱的 kubeconfig 里同时开着多个 context,否则你很容易把应用装到错误的集群上。
2.2 添加并搜索Chart仓库:找到需要的Nginx Chart
Helm 的应用包目录叫Chart 仓库,类似 apt 或 yum 的软件源。你可以用官方维护的 Artifact HUB 搜索,也可以直接添加常见的 chart 仓库。先从 Bitnami 仓库开始,它维护了一堆常用应用的 Chart,质量比较高:
helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update更新之后,可以搜索一下 nginx 相关的 Chart:
helm search repo nginx这个命令会列出仓库里所有名字包含 nginx 的图表。你可以注意到,光是 Bitnami 仓库里就有nginx和nginx-ingress-controller,它们完全是不同的角色。前者是一个常规的 Nginx Web Server,后者是 Ingress Controller,千万别选错。我一开始为了测试,就直接装了一个nginx普通应用,用来验证安装流程。
这里也顺便说一句:生产环境建议先确认 chart 的版本和你的 Kubernetes 版本兼容性。你可以在搜索结果的APP VERSION列看到对应应用版本,在CHART VERSION看到 Helm Chart 自身版本。始终有个习惯,安装前先查阅它的 values.yaml,了解默认配置和可调参数,后面我们升级的时候才能不出错。
2.3 理解 Helm 的 Releases 和命名空间绑定
在安装之前,建议把 Helm 的几个核心概念理顺,不然直接上手会迷路:
- Chart:一个打包好的应用描述,里面包含了模板、默认 values 和元信息。
- Release:Chart 的一次实际部署实例。同一个 Chart 可以装上多个 Release,比如
nginx-web和nginx-api,它们是独立管理的。 - 版本(Revision):Release 的每一次升级都会生成新的版本号,用来回滚和追踪变更。
Helm 3 默认把 release 的状态信息存储在一个同名的 Secret 对象里。所以你会发现,用helm list -A时,命名空间下会有额外的 Secret,这是正常的。这也解释了一个限制:release 和它的命名空间是绑定的。你安装时用-n指定命名空间,后续的升级、回滚、查询都必须带上同样的命名空间,否则 Helm 会找不到 release。
helm list -n dev如果你忘了 release 装在哪里,可以用helm list -A查看所有命名空间的 release。我以前在测试机装了好几个 demo,最后全忘了,就是靠-A才找回来的。
3. 部署一个真实应用:Nginx从零开始的完整流程
3.1 安装Nginx:一行命令背后的实际改动
现在开始实战。我们用 Bitnami 的 nginx chart 部署一个简单的 Web 服务:
helm install nginx-demo bitnami/nginx -n default如果一切正常,它会输出说明,包括 release 名字、命名空间,以及如何访问应用。这时候你可能会好奇:这一行命令到底做了什么?
过程拆解下来大概是这样的:
- Helm 从本地缓存中读取 chart 包(刚才
repo update已经把缓存下载到~/.cache/helm/repository/)。 - 将 chart 里的模板文件结合默认的 values.yaml 渲染成最终的 Kubernetes 资源清单。
- 调用 Kubernetes API 创建对应的 Deployment、Service、ConfigMap 等对象。
- 在 default 命名空间创建 release 相关的 Secret 信息,记录当前 release 的状态和配置。
你可以验证一下资源是否都创建出来了:
kubectl get pods -n default kubectl get svc nginx-demo -n default你会发现生成的 Deployment 名称带了随机后缀,这是 Bitnami chart 的命名规则。如果用helm list看到 release 状态是deployed,说明安装成功。
3.2 查看Nginx对外访问地址:修改Service类型和暴露端口
默认的 Bitnami nginx chart 创建的是 ClusterIP 类型的 Service,只能在集群内部访问。如果你要测试外部访问,可以把它改成 NodePort:
kubectl edit svc nginx-demo把type: ClusterIP改成type: NodePort,然后看节点端口:
kubectl get svc nginx-demo实际上,更合理的做法是安装的时候直接传参。Helm 支持通过--set参数生效,例如:
helm upgrade nginx-demo bitnami/nginx --set service.type=NodePort看到没?这里我直接用了upgrade而不是install。Helm 里update也可以用于修改现有 release 的配置。其实安装一个已经存在的 release,install会报错,upgrade可以复用这个名字。后面升级还会详细讲。
如果你只是临时想看看 Nginx 的模样,使用kubectl port-forward更简单:
kubectl port-forward svc/nginx-demo 8080:80然后在浏览器打开http://localhost:8080。这个方式不修改任何 Service 类型,最适合本地调试。
3.3 把配置写成values.yaml:比--set更好维护的方式
刚开始你可能会图省事,想把所有参数都用--set传,比如:
helm install nginx-demo bitnami/nginx --set replicaCount=3 --set service.type=NodePort但这样做的问题是:配置项一多,命令行根本没法看历史,也没法 review。我更推荐维护一个自定义的 values 文件,例如在项目目录下建一个my-values.yaml:
replicaCount: 3 service: type: NodePort port: 80 resources: limits: cpu: 250m memory: 256Mi requests: cpu: 100m memory: 128Mi然后执行:
helm install nginx-demo bitnami/nginx -f my-values.yaml -n default这样变更记录全都在 Git 里,代码评审的时候可以清晰地看到改了哪些配置。后来我把这套实践扩展成"每个环境一个 values 文件":dev-values.yaml、prod-values.yaml,它们之间有少量覆盖,再配合 Helm 的层级特性,就能满足多环境部署。
4. 升级、回滚和版本管理:Helm的revision机制
4.1 升级的本质:修改Values后的重新渲染
部署完一个应用不等于结束,后续一定会改镜像 tag、改副本数、改环境变量。这时候 Helm 的核心优势就体现出来了。我仍然用刚才的 nginx-demo 示例,现在想把副本数从默认值改成 3,同时更新镜像版本(假设该 chart 的 appVersion 支持,你可以通过--set image.tag调整),执行:
helm upgrade nginx-demo bitnami/nginx -f my-values.yaml --set replicaCount=3这里我用了--set覆盖 values 文件里的值。注意执行完 upgrade 后,Helm 会把这次变更记录为新的 revision。你可以查看 release 的所有版本记录:
helm history nginx-demo输出会列出 REVISION、UPDATE TIME、STATUS、CHART、APP VERSION、DESCRIPTION。你会看到1和2两条状态,第二条的 STATUS 是deployed,第一条则变为superseded,表示已经被新版本取代。这个机制特别强大,它意味着你可以回到应用历史上任意一个状态。
升级之前,建议先用helm diff upgrade(需要安装 helm-diff 插件)或者helm template来预览将要渲染出来的 YAML。我常用的是:
helm template nginx-demo bitnami/nginx -f my-values.yaml > preview.yaml渲染后 diff 一下,看看会不会引入意外的资源变化。不过helm template不会真的提交到集群,只输出到文件,所以很安全。也可以先 dry-run:
helm upgrade nginx-demo bitnami/nginx -f my-values.yaml --dry-run --debug这个命令会比helm template多提供一个信息:Helm 会告诉你"这个 release 将执行哪些操作"。实测下来,生产环境的升级前我至少会跑一次--dry-run,不然改坏了只能重新回滚,容易产生中间抖动。
4.2 回滚:从revision 2回到revision 1的完整操作
回滚是 Helm 最值得用的功能之一。比如刚才那次升级把配置改错了,导致 Pod 启动不了。你会先确认新版确实有问题:
kubectl get pods -n default如果 Pod 状态不是Running,那赶紧回滚:
helm rollback nginx-demo 1这个命令的含义是:把 releasenginx-demo回滚到revision 1。执行完再helm history nginx-demo,你会发现多了一个 revision 3,状态是deployed,它的内容等同于 revision 1。注意:Helm 的回滚不是真的删除当前版本,而是基于历史版本重新渲染并提交一个新版本。所以历史记录仍然存在,这才是正确的设计思路。
回滚唯一需要注意的点是:如果你升级之后跑了一段时间,集群里有些资源被人工kubectl edit改过,回滚可能把这些人工改动覆盖掉。所以团队协作时要明确:release 命名空间内的 Kubernetes 资源,不要用 kubectl 直接篡改,一切通过 Helm 管理。一旦绕过 Helm 直接改资源,Helm 根本不知道,下次升级会把你的手工改动全部冲掉。
4.3 自定义values文件的版本迁移策略
等到 chart 版本升级时,values 文件的字段可能发生变化。比如旧的自定义 values 里写了service.nodePort,新 chart 可能改成了service.extraFields。升级 chart 版本时,如果直接执行helm upgrade -f old-values.yaml,很可能因为字段不匹配报错或者被忽略。
这时候一定要看升级前后的 release manifest。常用的做法是先helm get values nginx-demo看看当前的用户自定义值,再对比新 chart 的默认 values:
helm show values bitnami/nginx --version <新的chart版本>把项目 values 文件同步成新结构,再执行升级。另一个技巧是:升级比较大的版本前,通常 chart 作者会提供 migration guide,多看 release notes。Bitnami 的 chart 一般在升级说明里列出 breaking changes,这可能包括存储路径变化、Service 端口变化等。
5. 排查实战:我最常遇到的五个Helm问题
5.1 错误的release已经存在:install变成upgrade
一个常见的报错:
Error: release: nginx-demo already exists原因很简单:之前安装过同名 release,但你忘了。解决方案有两个:如果你想彻底删掉重装,先卸载:
helm uninstall nginx-demo如果只是想重新覆盖配置,那用helm upgrade而不是install。对于 CI/CD 流水线,我会写成一个幂等逻辑:先判断是否存在 release,存在就 upgrade,不存在就 install。具体可以这样:
helm upgrade nginx-demo bitnami/nginx -f my-values.yaml --install --force--install参数会让upgrade在 release 不存在时自动执行安装,很便捷。--force会在集群版本变化导致仅代码变化时强制替换资源,但要注意--force实际上通过删除并重建资源的方式实现,可能引起短暂不可用,慎用。
5.2 找不到chart仓库或仓库未更新
报错manifest not found或者chart download failed时,绝大多数是仓库索引过期了。先执行:
helm repo update如果还是找不到,检查 repo 是否添加:
helm repo list有时候你手动改了本地/etc/hosts或者公司网络限速,会导致仓库下载超时。可以把仓库里的 chart 先拉到本地:
helm pull bitnami/nginx --version <chart version> --untarpull 到本地后,后续使用helm install nginx-demo ./nginx安装本地目录,就不依赖仓库网络了。
5.3 升级卡死或hang住:查看pending状态
有时你执行helm upgrade后,它卡在半路,或者最终状态显示:
STATUS: pending-upgrade这通常意味着上一次 upgrade 还没有完成,可能是集群资源创建超时、webhook 卡住、或者你执行命令的客户端断开了。此时不能继续upgrade,因为会报另一个错误。先检查集群实际资源状态:
kubectl get deploy,pods -n default如果资源正常,可以尝试强制完成这次升级:
helm upgrade nginx-demo bitnami/nginx -f my-values.yaml --history-max 10但这可能因为当前 release 状态 locked 而失败。更稳妥的办法是直接查询 release 的秘密对象,了解里面记录的状态:
kubectl get secret -n default -l owner=helm kubectl describe secret sh.helm.release.v1.nginx-demo.v2 -n default如果确定没有实际资源变更,或者集群资源已存在,可以直接修复状态。有一种相对干净的手段是把 release 的 status 字段改回failed或deployed,但我不建议在生产上乱改 secret。多数情况下,问题出在 chart 中的资源创建卡在某处,排查对应资源即可。
5.4 values文件里的值是正常类型,但渲染报错
有个很自然的错误:在 values.yaml 里写了端口数字,但模板里期望的是字符串。比如:
service: port: 80而模板中使用了{{ .Values.service.port | quote }},Helm 会尝试把整数 80 转换成字符串 "80";如果不加quote,就可能在某些字段(如容器端口)渲染成不带引号的数字,Kubernetes API 通常可以接受数字,但遇到 template 里拼接字符串的场景就出问题。
解决办法是遵循模板里对值类型的期望。如果你不确定,打开 chart 的templates目录看看相关模板,或者用helm template看渲染结果。多跑几次渲染,对比输出,很快就能定位。
5.5 升级后Pod没有真正更新
你升级了镜像 tag,但是 Pod 仍然使用旧镜像。常见原因是:Helm 只更新你提供的 values 里的值,如果 deployment 模板中的镜像 tag 是根据某个固定值渲染的,默认值可能没变。很多 chart 的 image.tag 默认使用AppVersion,你需要显式指定:
helm upgrade nginx-demo bitnami/nginx --set image.tag=latest还有一种是 Deployment 的strategy是Recreate或滚动策略设置过大,导致 Pod 长时间不重建。确认是否更新可以看:
kubectl rollout status deploy/nginx-demo -n default如果显示等待,等它完成即可。另一个原因是你在helm get manifest nginx-demo里看到了镜像更新,但kubectl get deployment -o yaml没有变化,那八成是 chart 的模板里有条件判断,你设的值没走通那个分支。这种时候就老老实实去读 chart 的模板代码。
6. 进阶思路:自写chart模板和应用发布流程
6.1 从零开始写一个最小Chart
当你要发布自己的应用时,使用公共 chart 可能并不合适。自己写一个 Chart 其实没那么复杂。首先创建骨架:
helm create myapp这个命令会生成一个标准结构的 Chart:
myapp/ ├── Chart.yaml ├── charts/ # 存放依赖的子 chart ├── templates/ # 所有 Kubernetes 资源模板 │ ├── deployment.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ ├── _helpers.tpl │ └── ... └── values.yaml # 默认配置Chart.yaml是元信息文件,至少填写apiVersion,name,version,appVersion。对于 Helm 3,apiVersion 为v2。下面是一个最简单的示例:
apiVersion: v2 name: myapp description: A simple Helm chart for my app type: application version: 0.1.0 appVersion: "1.16.0"删除helm create生成的过多示例文件,保留一个 deployment.yaml 和一个 service.yaml,然后自定义。在templates/deployment.yaml里,你可以用{{ .Values.replicaCount }}的方式替换副本数,用{{ .Values.image.repository }}:{{ .Values.image.tag }}组合镜像。
6.2 理解模板渲染:_helpers.tpl和include的作用
写 Chart 的入门难点之一是_helpers.tpl。很多初学者不理解为什么 Helm 要把 name 和 labels 抽成一个 define 块,而不是直接在模板里写死变量。
原因有两个:一是 K8s 资源名称要求字母、数字等有限字符,如果 release name 或 chart name 包含.或者长度超限,直接用会报错;二是组件直接要共享一致的标签,用于 Service 选择器和 Deployment 匹配。_helpers.tpl里的 define 块相当于一个通用函数,比如:
{{- define "mychart.fullname" -}} {{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }} {{- end -}}这样你在 deployment.yaml 和 service.yaml 里都可以通过:
name: {{ include "mychart.fullname" . }}引用同一个名称,避免两处手写不一致。刚开始你可能觉得多此一举,但一旦你在多个文件里用到同样的标签,就会明白这个抽象的价值。
6.3 编写values阶层的多环境配置
为了让一套 chart 适配 dev/prod 环境,除了每个环境独立 values 文件,还可以使用 Chart 的values.yaml中的层级结构和global字段。比如全局配置里定义:
global: imageRegistry: "registry.example.com" image: repository: ""然后模板里引用:
image: {{ .Values.global.imageRegistry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}每个环境的 values 文件只需覆盖image.repository和image.tag,仓库域名统一由global控制。这个设计在多个子 chart 或微服务场景下特别有用,避免重复写仓库地址。
6.4 使用CI流水线做发布实战:Chart版本号与自动回滚
当你开始用 Helm 做应用发布,必须解决好两个问题:Chart 版本号怎么管理?失败时怎么自动回滚?
我目前的做法是在 CI 流程里,每次更改代码后构建新的镜像 tag,然后用这个 tag 作为 Chart 的 appVersion 或 image.tag 传入 Helm 升级命令。同时让 CI 在升级前把当前 release 的 revision 记录下来,如果升级后健康检查失败,就执行回滚:
PREVIOUS_REVISION=$(helm history myapp -n production | awk 'NR==2{print $1}') helm upgrade myapp ./chart -n production --atomic --set image.tag=$CI_PIPELINE_IID--atomic是个关键参数,它会在升级失败时自动回滚到上一个 revision,但要注意--atomic依赖超时设置,默认 5 分钟。它只会在超时及失败时回滚,所以通常配合--timeout 2m使用:
helm upgrade myapp ./chart -n production --atomic --timeout 2m --set image.tag=$CI_PIPELINE_IID我实测遇到过一次问题:--atomic回滚时因为某资源删除卡住,最终 rollback 也失败。所以更保险的做法是升级脚本里先记录 revision,失败后手动回滚,并提前检查是否有 incoming webhook 等卡点。后来我在流水线里把回滚步骤放到了专门的异常处理阶段,不再依赖--atomic到底执行到哪一步。
总的来说,Helm 这套体系,核心价值不是帮你省写 YAML 的几行字,而是把 Kubernetes 应用交付变成了"可配置、可记录、可回滚、可自动化"的工程化管理。你越早把应用交给 Helm 管理,后面版本升级和发布时越省力。我从刚开始的helm install nginx-demo到管理几十个微服务 chart,只花了两周时间适应,但从此再也不想回到纯 kubectl 时代了。建议你现在就装一个 Helm,拉一个公共 chart 试试安装和升级,亲自感受一次回滚的快乐,比读再多的教程都管用。