Helm实战:从Kubernetes YAML管理混乱到自定义模板一键部署
发布时间:2026/10/16 5:44:12 锦皓数字建站

开头先说实话如果你的Kubernetes集群还停留在“复制粘贴一堆YAML然后kubectl apply”的阶段那这篇文章是为你准备的。我自己的项目在做到第3套环境的时候光是Deployment和Service清单就改了5遍每次换环境都要核对镜像版本、命名空间、副本数改完还要担心有没有漏掉某个Label。后来实在受不了才把Helm捡起来。这篇文章不绕弯子重点就两个一是把Helm部署起来二是从零写一套自定义模板让同一份YAML在不同环境里自动长成不同的样子。Helm这东西圈内叫它“Kubernetes的包管理器”这话对一半。它确实像apt、yum那样能装能卸但它真正的价值在于“模板化”。你把YAML写成一个模板把变化的部分抽成参数部署时传不同的参数同一套模板就能适配开发、测试、生产三套环境。这才是Helm让我真正折服的地方。1. 从YAML管理混乱说起Helm解决的不只是“安装”1.1 一个真实项目的YAML管理痛点以前我维护一个内部服务清单文件长这样deployment.yaml、service.yaml、ingress.yaml每个环境一套。开发环境、测试环境、生产环境三个目录每份文件90%的内容是一样的只有镜像版本、副本数、域名、资源配置不一样。问题很快就来了。某天要升级版本开发环境改了镜像tag测试环境忘了同步。过两天生产环境要增加一个环境变量我拿着开发环境的文件复制过去改结果把开发环境的Namespace也带过去了。更麻烦的是回滚的时候根本不知道上一版是什么参数只能翻git历史运气好能找到运气不好只能凭记忆恢复。这种状态持续了大概两个月我意识到真正的问题不是“YAML写错了”而是我的工作方式逼着人在做“手工查找替换”。只要有一个环节没同步线上和线下就分叉了。Helm的思路恰好相反模板里写“这里放镜像版本”部署时通过参数指定“生产环境用v1.2.0”模板本身只有一套参数跟着环境走。1.2 Helm的核心抽象Chart、Release、RepositoryHelm里有三个概念必须先搞明白否则后面很容易绕晕。**Chart图表包**是Helm的应用打包格式。一个Chart就是一组文件的集合里面包含描述应用信息的Chart.yaml、部署用的模板文件templates/、以及给模板提供默认参数的values.yaml。你可以理解为Chart就是“安装了应用的安装包”只不过安装包里的脚本依赖参数才能生成最终结果。**Release发布实例**是Chart部署到Kubernetes集群后形成的运行实例。同一个Chart部署三次就会产生三个Release分别对应开发、测试、生产。每次部署时Helm会记录Release的状态和参数所以升级、回滚都是在这个Release维度上做的。**Repository仓库**是Chart的存放地点类似Docker镜像仓库。Helm可以从仓库拉取别人打包好的Chart来部署也可以把自己写的Chart推到仓库里分享。搞懂这三个概念之后Helm的整个使用逻辑就清楚了从仓库拉Chart或者本地写一个用values参数渲染出YAML然后apply到集群形成一个Release。之后所有操作都是围绕这个Release进行。2. 安装Helm直接上v3不要回头看2.1 为什么不用v2如果你在网上搜过Helm的安装教程大概率会看到一堆关于Tiller的内容。那是Helm v2时代的东西——v2需要在集群里装一个叫Tiller的服务端组件它拥有集群的较高权限负责执行安装操作。这种做法在安全层面争议很大后来社区也承认这个设计有问题。v3把Tiller彻底拿掉了Helm变成了一个纯客户端工具。它直接在本地调用kubeconfig连接API Server把渲染好的YAML交给Kubernetes执行。相比v2v3的优势很明显没有服务端常驻组件权限模型和kubectl完全一致也不用担心Tiller本身被攻击。基于这个背景我强烈建议新项目直接用v3没必要去接触老版本。2.2 安装步骤与版本验证安装Helm最省事的方式是用官方提供的脚本curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh脚本执行完它会自动把Helm二进制放到/usr/local/bin/helm。验证安装helm version # 输出类似 # version.BuildInfo{Version:v3.14.4, GitCommit:81c902a6ceb0fc9a5c4e9d1b2f70e4c5b0e83a11, GitTreeState:clean, GoVersion:go1.21.5}提示如果服务器无法访问外网可以换第二种方式——从官方GitHub Releases页面下载对应架构的压缩包解压后把helm二进制放到PATH目录即可。注意选对架构amd64和arm64别搞混。如果你的机器上有包管理器也可以直接装# Debian/Ubuntu apt-get install helm # macOS brew install helm但这种方式装的版本可能不是最新的我在某次测试环境里就遇到过apt源的Helm还是3.12的老版本而本地Chart要求3.13以上导致render结果有细微差异。所以还是推荐脚本安装或者下载二进制版本可控出问题也容易排查。2.3 配置Repository源安装好Helm后默认是没有配置任何Chart仓库的需要手动添加。我常用的是官方仓库和国内镜像仓库根据实际情况选择helm repo add bitnami https://charts.bitnami.com/bitnami helm repo add aliyun https://apphub.aliyuncs.com helm repo update配置完成后可以用helm search repo来搜索可用的Charthelm search repo nginx # NAME CHART VERSION APP VERSION DESCRIPTION # bitnami/nginx 15.2.2 1.25.3 Chart for nginx这里有个小坑要提醒一下有些仓库在helm repo update时会报“x509: certificate signed by unknown authority”之类的证书错误。我遇到过一次原因是仓库使用的HTTPS证书链不完整。解决办法要么换一个源要么在可信的前提下用helm repo add --insecure-skip-tls-verify参数跳过验证——但是生产环境不要这么干纯粹是为了本地测试方便。3. 自定义模板的骨架Chart目录到底该怎么搭3.1 从零开始创建ChartHelm提供了helm create命令可以一键生成标准Chart骨架helm create mychart tree mychart # mychart/ # ├── Chart.yaml # ├── charts/ # ├── templates/ # │ ├── NOTES.txt # │ ├── _helpers.tpl # │ ├── deployment.yaml # │ ├── hpa.yaml # │ ├── ingress.yaml # │ ├── service.yaml # │ ├── serviceaccount.yaml # │ └── tests/ # │ └── test-connection.yaml # └── values.yaml这个骨架是Helm官方准备的标准模板里面包含了Deployment、Service、Ingress、ServiceAccount、HPA等常用资源。但实际使用中我通常会把hpa.yaml、serviceaccount.yaml先删掉因为很多内部服务用不上留着反而干扰阅读。除了自动生成也可以手动创建。目录结构需要满足以下最小约定mychart/ ├── Chart.yaml # Chart的元数据必填 ├── values.yaml # 默认参数必填 └── templates/ # 模板文件目录可以没有但一般都会有3.2 Chart.yaml和values.yaml的分工Chart.yaml描述Chart自己的信息内容不多但很重要apiVersion: v2 name: mychart description: 一个用于演示的自定义Chart type: application version: 0.1.0 appVersion: 1.16.0这里的version是Chart的版本每改一次打包版本号要递增。appVersion是应用本身的版本它只作为元数据显示不会直接影响模板渲染。values.yaml是模板和真实参数之间的纽带。它定义了一组默认值模板通过这组值进行渲染。当用户部署时传入自定义参数默认值会被覆盖。replicaCount: 2 image: repository: nginx tag: 1.25.3 pullPolicy: IfNotPresent service: type: ClusterIP port: 80 resources: limits: cpu: 500m memory: 512Mi3.3 Chart.yaml中apiVersion v2和v1的区别现在helm create默认生成apiVersion: v2这是Helm 3时代的格式。v1是Helm 2时代的老格式主要区别在于v2要求必须有Chart.yaml中的type字段而且对依赖管理的方式不同。如果你从网上拿到一个老ChartapiVersion是v1在Helm 3下也能用但会有兼容性警告。建议手头的新Chart统一用v2。4. 模板语法详解让YAML“活”起来的核心机制模板的本质就是“Go语言模板YAML”。理解了这一点你就能明白Helm模板的绝大多数行为。4.1 使用{{ }}插入动态内容模板里最基础的语法是双大括号包裹表达式apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-{{ .Chart.Name }}这里的.Release.Name是Helm内置对象代表当前Release的名字。.Chart.Name则对应Chart.yaml里的name字段。渲染后如果Release名是“myapp”Chart名是“mychart”那么name字段会变成“myapp-mychart”。这就回答了一个很多新手会问的问题“为什么模板里到处都是点开头的东西”因为Go模板通过.来引用当前上下文.Release.Name的意思就是“从根上下文找到Release对象再拿到它的Name属性”。4.2 内置对象一览Helm模板中常用对象有这些对象含义示例.Release当前Release信息.Release.Name, .Release.Namespace.ChartChart.yaml中的元数据.Chart.Name, .Chart.Version.Valuesvalues.yaml中的参数.Values.image.tag.Files模板目录内的非模板文件内容.Files.Get config.toml.Capabilities集群能力信息.Capabilities.KubeVersion.Version.Template当前模板的信息.Template.Name.Values是最常用的它对应values.yaml的文件内容。.Values.image.tag就取到上面values.yaml中的tag值。4.3 变量作用域与with/if的细节模板中的变量用$开头。例如{{- $imageTag : .Values.image.tag | default latest -}} image: {{ $imageTag }}with可以改变当前上下文作用域{{- with .Values.service }} spec: ports: - port: {{ .port }} {{- end }}进入with块之后.就变成了.Values.service所以.port等价于.Values.service.port。这个写法能少打几个字但容易让人迷惑我个人的建议是三行以内的代码可以用with复杂场景直接写完整路径更清晰。if的用法很直观{{- if .Values.autoscaling.enabled }} replicas: {{ .Values.autoscaling.minReplicas }} {{- end }}注意if判断的是布尔值或是否存在。如果对应的键不存在表达式的值就是nil空if判断为假不会进入分支。4.4 管道、default和quote的配合Go模板的管道|可以链式处理数据这是模板里最灵活的部分。最常用的组合是default和quoteimage: {{ .Values.image.repository }}:{{ .Values.image.tag | default latest }}default的作用是如果管道前的值是空值nil、空字符串、false、0就返回默认值。这在处理“用户可能没传入某个参数”的场景非常实用。quote会把结果用双引号包裹起来强制转换为字符串{{- if .Values.service.type }} type: {{ .Values.service.type | quote }} {{- end }}有些值在YAML里如果不加引号会被解析成布尔或数字。比如true和false如果直接渲染YAML解析器会当成布尔类型用quote强制成字符串就能避免意外的类型转换。这个坑我踩过不止一次后来凡是字符串类型的环境变量值我一律用quote包住。4.5 range循环迭代range用于遍历列表和字典最常见的场景是遍历环境变量或端口列表env: {{- range .Values.envVars }} - name: {{ .name }} value: {{ .value | quote }} {{- end }}此时在range块内部.代表当前遍历到的元素。如果遍历的是字典还可以同时拿到key和value{{- range $key, $val : .Values.labels }} {{ $key }}: {{ $val }} {{- end }}range还有一个细节range进入空集合时不会执行循环体这往往会让渲染结果里残留多余的空行。为了消掉空行我在每个range结束处都会加上{{- end }}开头也尽量用{{- range让空白字符被吃掉。模板渲染后的空行虽然不影响Kubernetes解析但多了会让人看不下去排查问题时也会干扰视线。5. 实战编写一套属于自己的完整Chart模板前面把基础语法过了一遍现在进入正题——写一套完整的、能直接部署的自定义模板。这套Chart我命名为“webapp”模拟一个标准的Web应用一个Deployment、一个Service可选Ingress。5.1 定义values.yaml参数好的模板一定是从好的values.yaml开始的。参数设计决定了模板后续的扩展性。我的values.yaml长这样replicaCount: 1 image: repository: your-registry/webapp tag: 1.0.0 pullPolicy: IfNotPresent imagePullSecrets: - name: regcred nameOverride: fullnameOverride: service: type: ClusterIP port: 80 targetPort: 8080 ingress: enabled: false className: nginx hosts: - host: app.example.com paths: - path: / pathType: Prefix resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi envVars: - name: APP_ENV value: production - name: LOG_LEVEL value: info设计时我坚持几个原则尽量保留默认值让用户不传参数也能跑起来凡是环境相关的参数全部放到values里模板里不写死字符串值默认加上quote避免类型转换问题。5.2 编写Deployment模板apiVersion: apps/v1 kind: Deployment metadata: name: {{ include webapp.fullname . }} labels: {{- include webapp.labels . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include webapp.selectorLabels . | nindent 6 }} template: metadata: labels: {{- include webapp.selectorLabels . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} containers: - name: {{ .Chart.Name }} image: {{ .Values.image.repository }}:{{ .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - containerPort: {{ .Values.service.targetPort }} env: {{- range .Values.envVars }} - name: {{ .name }} value: {{ .value | quote }} {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }}这段模板里有几个值得说的地方。include webapp.fullname .是调用_helpers.tpl里定义的模板函数后面会细说。nindent的作用是在换行后再缩进指定空格数这是让模板渲染结果保持YAML结构正确的关键。我来演示一下nindent的作用如果直接写成labels: {{- include webapp.labels . }}渲染后会变成labels: app.kubernetes.io/name: webapp app.kubernetes.io/instance: release-name第二行到第三行没有缩进YAML会解析失败。用nindent 4则会把include的结果整体缩进4个空格刚好匹配上一级的缩进深度。5.3 使用_helpers.tpl管理公共标签templates/_helpers.tpl是Helm模板的“公共函数库”。它不产生具体的Kubernetes资源但可以定义可复用的模板片段。我写的webapp Chart的_helpers.tpl长这样{{- define webapp.name -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix - }} {{- end }} {{- define webapp.fullname -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix - }} {{- else }} {{- $name : default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix - }} {{- else }} {{- printf %s-%s .Release.Name $name | trunc 63 | trimSuffix - }} {{- end }} {{- end }} {{- end }} {{- define webapp.labels -}} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace _ }} {{- include webapp.selectorLabels . }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} {{- define webapp.selectorLabels -}} app.kubernetes.io/name: {{ include webapp.name . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }}我解释一下这里的逻辑。“webapp.name”用来计算应用短名规则是如果用户通过nameOverride指定了名字就用指定的否则用Chart.yaml里的name。“webapp.fullname”计算完整资源名逻辑是如果用户指定fullnameOverride直接用它否则优先用“Release名Chart名”拼接如果Release名本身已经包含Chart名就不再重复拼接。这段逻辑其实是从Helm官方模板里借鉴过来的很实用。trunc 63是为了保证生成的资源名不超过Kubernetes的63字符上限。trimSuffix -是为了防止拼接后结尾出现多余的连字符。5.4 编写Service和Ingress模板Service模板相对简单apiVersion: v1 kind: Service metadata: name: {{ include webapp.fullname . }} labels: {{- include webapp.labels . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: {{ .Values.service.targetPort }} protocol: TCP selector: {{- include webapp.selectorLabels . | nindent 4 }}Ingress模板要用if判断是否启用{{- if .Values.ingress.enabled }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ include webapp.fullname . }} labels: {{- include webapp.labels . | nindent 4 }} spec: ingressClassName: {{ .Values.ingress.className }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} pathType: {{ .pathType }} backend: service: name: {{ include webapp.fullname $ }} port: number: {{ $.Values.service.port }} {{- end }} {{- end }} {{- end }}这里有个关键细节当用range遍历hosts列表时当前上下文.会变成hosts里的元素所以调用include webapp.fullname .时.已经不对了。这种情况下需要用$来引用根上下文。include webapp.fullname $里的$就是根上下文这样在循环内部也能拿到完整的Chart和Release信息。这个细节很多教程不会讲我在第一次写Ingress模板时就栽在这里渲染出来的service.name始终是空的。5.5 用helm template验证渲染结果写完模板后一定先用helm template在本地渲染一遍确认YAML内容符合预期再丢到集群里。渲染命令helm template webapp ./webapp --debug --dry-run你可以看到完整的YAML输出。用--dry-run配合kubectl可以对渲染结果做最终校验helm template webapp ./webapp | kubectl apply --dry-runclient -f -这一步能提前发现YAML语法或Schema问题避免直接推到集群里报错。我在写模板的过程中渲染命令至少来回执行了几十次每次改一点都跑一遍很快就定位到问题。6. 实际部署从helm lint到升级回滚的一次完整流程6.1 helm lint检查模板正确性部署前先用helm lint检查Chart结构是否合法helm lint ./webapp # Linting ./webapp # 1 chart(s) linted, 0 chart(s) failed如果输出有WARNING但不影响使用可以暂时忽略如果有ERROR那就必须处理。我见过最典型的问题是Chart.yaml里version不符合SemVer规范或者templates目录里存在多余的模板文件导致资源冲突。6.2 安装Release并查看状态一切就绪后用helm install部署helm install prod-webapp ./webapp -n production --create-namespace这里prod-webapp是Release名./webapp是Chart路径-n production指定目标命名空间。不加--create-namespace的话如果命名空间不存在安装会直接失败。安装完成后查看状态helm status prod-webapp -n production用helm list看所有Releasehelm list -n production # NAME NAMESPACE REVISION UPDATED STATUS CHART # prod-webapp production 1 2024-06-15 10:30:22.123456789 0000 UTC deployed webapp-0.1.06.3 升级、回滚与删除同一个环境需要更新镜像版本或调整参数时用helm upgradehelm upgrade prod-webapp ./webapp -n production \ --set image.tag1.1.0 \ --set replicaCount3每次upgrade都会产生一个新的Revision版本号。用helm history可以查看历次版本helm history prod-webapp -n production # REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION # 1 ... superseded webapp-0.1.0 1.0.0 Install complete # 2 ... deployed webapp-0.1.0 1.0.0 Upgrade complete如果升级后发现参数配错了想回到上一个版本helm rollback prod-webapp 1 -n production这个命令会直接使用第1个版本的配置重新部署等于秒级回滚。我实际验证过很多次比手动改YAML再kubectl apply快得多而且不会出现“改漏一处导致前后不一致”的问题。不需要该服务时用helm uninstall删除helm uninstall prod-webapp -n production这会删除Release以及通过Helm创建的所有Kubernetes资源。提示注意区分helm delete是v2的语法v3直接用helm uninstall。如果你还在用helm delete大概率是装了旧版alias建议立即切换到helm uninstall。6.4 常见错误与我的排查套路用Helm半年多我把最常见的报错分成三类。第一类错误与模板渲染有关。典型表现是helm install时输出“Error: failed to render chart: parse error at (webapp/templates/deployment.yaml:20): unexpected {{end}}”。这通常是模板语句没配对或者range/if没有正确闭合。解决办法是打开出错的文件数一下每个{{- if都配了{{- end}}用编辑器的高亮匹配功能很快能定位。第二类错误与Kubernetes集群交互有关。比如“Error: unable to build kubernetes objects from release manifest: resource mapping not found for name”。这种通常是模板里kind写错了或者APIVersion不符合集群版本。我遇到过模板里写了extensions/v1beta1的Ingress但集群版本已经到1.25这个API版本早就被移除了。解决办法是把apiVersion改成networking.k8s.io/v1。第三类错误与values参数类型有关。典型表现是把字符串传成了数字导致渲染报错。比如helm upgrade ... --set resources.limits.cpu500如果模板里写的是{{ .Values.resources.limits.cpu | quote }}那么传进去的值是整数500rather than字符串“500”quote会把500变成“500”长度是3但如果模板里用了atoi之类的函数可能就会出问题。更常见的坑是传布尔值YAML里false会变成字符串“false”导致if判断永远为真。所以我对所有“希望是字符串”的值一律用quote包住对所有“希望是数字或布尔”的值绝不加quote。以上内容是理论结合实战把Helm从安装到自定义模板的完整链路走了一遍。你再回过头去看本质上Helm就干了两件事把YAML变成可参数化的模板把部署动作变成可记录的版本操作。只要理解了这两个核心逻辑其余的语法细节都是纸老虎。最后分享一个我个人的小习惯每次写新模板时先在本地用helm template --debug跑一遍渲染结果再把渲染结果仔细读一遍确认每个字段的缩进和类型都对然后再部署。别嫌麻烦这一步能帮你避开90%的“部署到集群才报错”的尴尬。这个流程我已经跑了几十个Chart稳定得不能再稳了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。