目录
-
- [一、为什么需要 Helm](#一、为什么需要 Helm)
- 二、核心概念模型
-
- [2.1 一张关系图](#2.1 一张关系图)
- [2.2 概念对照表](#2.2 概念对照表)
- [2.3 几个最容易混淆的点](#2.3 几个最容易混淆的点)
- 三、Chart:包的结构
-
- [3.1 标准目录](#3.1 标准目录)
- [3.2 Chart.yaml 字段](#3.2 Chart.yaml 字段)
- [3.3 Package 与 Repository](#3.3 Package 与 Repository)
- 四、values:参数的来源与优先级
-
- [4.1 优先级(从低到高)](#4.1 优先级(从低到高))
- [4.2 使用说明与坑](#4.2 使用说明与坑)
- 五、templates:渲染机制
-
- [5.1 模板长什么样](#5.1 模板长什么样)
- [5.2 把逻辑收敛到 `_helpers.tpl`](#5.2 把逻辑收敛到
_helpers.tpl) - [5.3 条件渲染与 NOTES.txt](#5.3 条件渲染与 NOTES.txt)
- [5.4 渲染流程](#5.4 渲染流程)
- 六、命令速查(按使用阶段)
-
- [6.1 环境与仓库](#6.1 环境与仓库)
- [6.2 本地开发与校验(无需连集群)](#6.2 本地开发与校验(无需连集群))
- [6.3 安装与升级](#6.3 安装与升级)
- [6.4 查看、回滚与卸载](#6.4 查看、回滚与卸载)
- [七、实战:从零发布一个普通 Web 应用](#七、实战:从零发布一个普通 Web 应用)
-
- [7.1 前置条件](#7.1 前置条件)
- [7.2 创建并定制 Chart](#7.2 创建并定制 Chart)
- [7.3 拆分环境 values(只写差异)](#7.3 拆分环境 values(只写差异))
- [7.4 四步发布法](#7.4 四步发布法)
- [7.5 发布后验证](#7.5 发布后验证)
- [7.6 升级、回滚与卸载](#7.6 升级、回滚与卸载)
- [7.7 打包分发给别的集群](#7.7 打包分发给别的集群)
- 八、进阶话题
-
- [8.1 敏感信息不要进 ConfigMap](#8.1 敏感信息不要进 ConfigMap)
- [8.2 Subchart 与 global](#8.2 Subchart 与 global)
- [8.3 按集群版本选择 apiVersion](#8.3 按集群版本选择 apiVersion)
- [8.4 CI/CD 建议](#8.4 CI/CD 建议)
- 九、常见问题排查
- 十、小结
适用版本:Helm 3.x、Kubernetes 1.19+
全文以一个虚构的普通 Web 服务
myapp为贯穿示例:镜像registry.example.com/team/myapp,容器端口8080,部署到app-dev/app-prod两个命名空间。约定:命令均在项目根目录执行,Chart 位于
./myapp;示例为 Bash,Windows 下可用 Git Bash 或 WSL。
一、为什么需要 Helm
一个 Web 服务上 K8s,往往要维护 Deployment、Service、ConfigMap、ServiceAccount、Ingress、HPA 等七八个 YAML。真实环境里还要面对:
- 多环境:开发/测试/生产的镜像 tag、副本数、资源配额、域名都不同,复制粘贴几套 YAML 后很快失控;
- 配置散落:应用参数一部分写在环境变量,一部分写在配置文件,改一处漏一处;
- 发布无状态 :
kubectl apply -f之后没有「上一次发布是什么」的记录,回滚只能靠 Git 翻历史。
Helm 是 Kubernetes 的包管理器 (类比 apt / npm)。它把一组 YAML 模板化、参数化、版本化:参数集中在一个文件里,模板按参数渲染,每次发布在集群里留下一个可查询、可回滚的快照。
以 myapp 为例,同一份 Chart 部署到两个环境,只需换一个 values 文件:
bash
helm upgrade --install myapp ./myapp -n app-dev -f values-dev.yaml
helm upgrade --install myapp ./myapp -n app-prod -f values-prod.yaml
镜像 tag、副本数、资源配额、是否启用 Ingress 全部由参数决定,模板只写一遍。
Helm 3 是纯客户端工具,集群内不再需要 Tiller,直接复用本地
kubeconfig与集群 RBAC。
二、核心概念模型
2.1 一张关系图
#mermaid-svg-PQMqkybVjbTJ5JM9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PQMqkybVjbTJ5JM9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PQMqkybVjbTJ5JM9 .error-icon{fill:#552222;}#mermaid-svg-PQMqkybVjbTJ5JM9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PQMqkybVjbTJ5JM9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PQMqkybVjbTJ5JM9 .marker.cross{stroke:#333333;}#mermaid-svg-PQMqkybVjbTJ5JM9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PQMqkybVjbTJ5JM9 p{margin:0;}#mermaid-svg-PQMqkybVjbTJ5JM9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PQMqkybVjbTJ5JM9 .cluster-label text{fill:#333;}#mermaid-svg-PQMqkybVjbTJ5JM9 .cluster-label span{color:#333;}#mermaid-svg-PQMqkybVjbTJ5JM9 .cluster-label span p{background-color:transparent;}#mermaid-svg-PQMqkybVjbTJ5JM9 .label text,#mermaid-svg-PQMqkybVjbTJ5JM9 span{fill:#333;color:#333;}#mermaid-svg-PQMqkybVjbTJ5JM9 .node rect,#mermaid-svg-PQMqkybVjbTJ5JM9 .node circle,#mermaid-svg-PQMqkybVjbTJ5JM9 .node ellipse,#mermaid-svg-PQMqkybVjbTJ5JM9 .node polygon,#mermaid-svg-PQMqkybVjbTJ5JM9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PQMqkybVjbTJ5JM9 .rough-node .label text,#mermaid-svg-PQMqkybVjbTJ5JM9 .node .label text,#mermaid-svg-PQMqkybVjbTJ5JM9 .image-shape .label,#mermaid-svg-PQMqkybVjbTJ5JM9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PQMqkybVjbTJ5JM9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PQMqkybVjbTJ5JM9 .rough-node .label,#mermaid-svg-PQMqkybVjbTJ5JM9 .node .label,#mermaid-svg-PQMqkybVjbTJ5JM9 .image-shape .label,#mermaid-svg-PQMqkybVjbTJ5JM9 .icon-shape .label{text-align:center;}#mermaid-svg-PQMqkybVjbTJ5JM9 .node.clickable{cursor:pointer;}#mermaid-svg-PQMqkybVjbTJ5JM9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PQMqkybVjbTJ5JM9 .arrowheadPath{fill:#333333;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PQMqkybVjbTJ5JM9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PQMqkybVjbTJ5JM9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PQMqkybVjbTJ5JM9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PQMqkybVjbTJ5JM9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PQMqkybVjbTJ5JM9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PQMqkybVjbTJ5JM9 .cluster text{fill:#333;}#mermaid-svg-PQMqkybVjbTJ5JM9 .cluster span{color:#333;}#mermaid-svg-PQMqkybVjbTJ5JM9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PQMqkybVjbTJ5JM9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PQMqkybVjbTJ5JM9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PQMqkybVjbTJ5JM9 .icon-shape,#mermaid-svg-PQMqkybVjbTJ5JM9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PQMqkybVjbTJ5JM9 .icon-shape p,#mermaid-svg-PQMqkybVjbTJ5JM9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PQMqkybVjbTJ5JM9 .icon-shape .label rect,#mermaid-svg-PQMqkybVjbTJ5JM9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PQMqkybVjbTJ5JM9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PQMqkybVjbTJ5JM9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PQMqkybVjbTJ5JM9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} helm package
helm push
提交集群
每次变更
Chart 目录
Chart.yaml + values.yaml + templates/
Package
name-version.tgz
Repository / OCI Registry
index.yaml
values 输入
values.yaml + -f + --set
helm install / upgrade
模板渲染
K8s manifests
Release
集群资源 + 发布记录
Revision 1..N
用编程类比:Chart 是类,values 是构造参数,templates 是方法体,Release 是对象实例,Revision 是实例的历史快照,Package 是编译产物。
2.2 概念对照表
| 概念 | 说明 | 存在形态 |
|---|---|---|
| Chart | 一个应用包,含元数据、默认值、模板 | 目录 |
| Package | Chart 的分发形态,本质是带 Chart.yaml 的压缩包 |
.tgz 文件 |
| Repository | 存放并索引 Chart 的 HTTP 服务(index.yaml + 若干 .tgz) |
远程服务 + 本地缓存 |
| Release | Chart 在某命名空间的一次运行实例,<名称> + <命名空间> 唯一 |
集群资源 + 一条记录 |
| Revision | 每次 install/upgrade/rollback 产生的递增版本号 | Secret 中的发布记录 |
| values | 渲染时的输入参数,多层合并的结果 | YAML / 命令行 |
| templates | Go template + Sprig,消费 values 生成清单 | templates/*.yaml |
| Hook | 带 helm.sh/hook 注解的资源,在发布生命周期特定时点执行 |
Job / ConfigMap 等 |
2.3 几个最容易混淆的点
| 易混淆 | 结论 |
|---|---|
| Chart vs Package | 同一东西的「源码目录」与「制品文件」,helm install 两者都能接 |
version vs appVersion |
前者是 Chart 版本(决定 .tgz 文件名),后者是应用版本标记,只有模板显式引用它时才会影响镜像 |
| Release vs Revision | 实例 vs 该实例的历史版本;rollback 会新增 Revision,不是把编号退回去 |
values.yaml vs .Values |
默认参数文件 vs 多层合并后的最终对象 |
helm template vs --dry-run |
前者纯本地渲染不连集群;后者仍与 API Server 交互做校验 |
三、Chart:包的结构
3.1 标准目录
执行 helm create myapp 会生成如下脚手架,这也是社区 Chart 的通用结构:
myapp/
├── Chart.yaml # 必需:元数据
├── values.yaml # 默认参数
├── values.schema.json # 可选:JSON Schema 校验 values
├── Chart.lock # 可选:锁定依赖版本(有 dependencies 时生成)
├── charts/ # 可选:子 Chart(subchart)
├── crds/ # 可选:CRD,不参与模板渲染,最先安装
├── .helmignore # 可选:打包时排除文件
├── README.md
└── templates/
├── _helpers.tpl # 命名模板,不生成资源
├── deployment.yaml
├── service.yaml
├── ingress.yaml
├── hpa.yaml
├── serviceaccount.yaml
├── configmap.yaml
├── NOTES.txt # 安装成功后打印的提示
└── tests/
└── test-connection.yaml # helm test 用
约定:templates/ 下以 _ 开头的文件不渲染成资源 ;crds/ 下的资源 Helm 不做模板渲染、也不纳入卸载管理。
3.2 Chart.yaml 字段
yaml
apiVersion: v2 # v2 = Helm 3 格式;v1 是 Helm 2 遗留
name: myapp
description: A generic web application chart
type: application # application(可部署)| library(只提供命名模板)
version: 0.1.0 # Chart 自身版本,打包文件名用它
appVersion: "1.0.0" # 应用版本标记,需模板显式使用才生效
keywords:
- web
maintainers:
- name: platform-team
dependencies: # 可选:依赖的子 Chart
- name: redis
version: "18.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
apiVersion: v2 相对 v1 的变化:依赖声明从独立的 requirements.yaml 移入 Chart.yaml 的 dependencies,新增 type 字段,并引入 Chart.lock 锁定依赖版本。
appVersion 的常见用法是在模板里兜底镜像 tag:
gotemplate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
这样 image.tag 留空时自动跟随 appVersion,显式指定时以 --set image.tag=1.0.1 优先。
3.3 Package 与 Repository
bash
# 打包:产出 ./myapp-0.1.0.tgz
helm package ./myapp --destination ./dist
# 打包时覆盖版本号(CI 中按构建号生成版本)
helm package ./myapp --version 0.2.0 --app-version 1.0.1 --destination ./dist
# 直接从打好的包安装,效果与目录完全一致
helm upgrade --install myapp ./dist/myapp-0.1.0.tgz -n app-prod -f values-prod.yaml
# 查看包内信息(无需解压)
helm show chart ./myapp # Chart.yaml
helm show values ./myapp # 默认 values
helm show readme ./myapp # README
helm show all ./myapp # 全部
分发渠道有两类:
bash
# 1) 传统 Chart Repository:HTTP 服务 + index.yaml
helm repo add bitnami https://charts.bitnami.com/bitnami # 添加仓库
helm repo list # 已配置仓库
helm repo update # 刷新本地索引缓存
helm search repo nginx # 搜已添加仓库
helm search hub wordpress # 搜 Artifact Hub
helm pull bitnami/nginx --untar --version 18.1.0 # 下载并解包到当前目录
helm repo remove bitnami
# 2) OCI Registry(Helm 3.8+ 原生,可复用现有镜像仓库)
helm registry login registry.example.com
helm push ./dist/myapp-0.1.0.tgz oci://registry.example.com/charts
helm pull oci://registry.example.com/charts/myapp --version 0.1.0
helm install myapp oci://registry.example.com/charts/myapp --version 0.1.0 -n app-prod
注意:往传统 Chart Repo 推送需要
helm-push(cm-push)插件;helm push ... oci://是原生能力,两者不要混用。打包版本应当不可变:同一个
name-version发布后改内容,会让仓库缓存和校验混乱,任何改动都要升version。
四、values:参数的来源与优先级
values 是渲染时的唯一输入数据。一个普通 Web 应用的默认参数通常长这样:
yaml
# values.yaml(Chart 自带默认值)
replicaCount: 2
image:
repository: registry.example.com/team/myapp
pullPolicy: IfNotPresent
tag: "" # 留空则取 .Chart.AppVersion
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
automount: true
name: ""
service:
type: ClusterIP
port: 80
containerPort: 8080
ingress:
enabled: false
className: nginx
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
tls: []
config:
message: "Hello from Helm"
logLevel: info
env: []
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 80
4.1 优先级(从低到高)
| 层级 | 来源 | 说明 |
|---|---|---|
| 1 | Chart 内 values.yaml |
默认值,最低 |
| 2 | 父 Chart 对子 Chart 的覆盖 | 子 Chart 只读自己那棵子树 + global.* |
| 3 | -f/--values file |
多个文件按命令行从左到右合并,右侧优先 |
| 4 | --set / --set-string / --set-file / --set-json |
最高;多个按序后者胜 |
bash
# 只用默认值渲染
helm template myapp ./myapp -n app-prod
# -f 覆盖(推荐:只写差异,不复制整份 values)
helm template myapp ./myapp -n app-prod -f values-prod.yaml
# 多个 -f,右侧优先;--set 又高于所有 -f
helm template myapp ./myapp -n app-prod \
-f values-base.yaml -f values-prod.yaml \
--set replicaCount=5 \
--set config.logLevel=debug
# 类型敏感 / 大文本 / 结构化值的专用参数(--set-json 需 Helm 3.10+)
helm upgrade --install myapp ./myapp -n app-prod \
--set-string service.port=8080 \
--set-file config.message=./message.txt \
--set-json ingress.tls='[{"secretName":"myapp-tls","hosts":["myapp.example.com"]}]'
--set-file只是从文件读取值,若模板把它写进 ConfigMap,值依然是明文;密码不要用这种方式,见 8.1 的 Secret 方案。
4.2 使用说明与坑
- 合并规则 :map 递归合并,标量和列表整体替换 。
--set config.logLevel=debug只改叶子;--set env[0].name=FOO会重建整个列表而非追加。想给env加一项,推荐写进-f文件。 --set key=null可删除默认值里的键;--set-string强制字符串,避免"8080"被解析成数字导致类型校验失败。upgrade默认丢弃上次的 values ,按本次输入 + Chart 默认值重新计算;--reuse-values才会沿用上次(CI 中容易踩坑,不建议),--reset-values显式重置。- 用
values.schema.json在渲染前做类型与必填校验,团队共享 Chart 时尤其有用:
json
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["image", "replicaCount"],
"properties": {
"replicaCount": { "type": "integer", "minimum": 1 },
"image": {
"type": "object",
"required": ["repository"],
"properties": { "repository": { "type": "string" } }
}
}
}
- 查看某次发布实际用了什么:
bash
helm get values myapp -n app-prod # 仅用户显式提供的部分
helm get values myapp -n app-prod --all # 含默认值(-a)
helm show values ./myapp # Chart 的默认 values
五、templates:渲染机制
templates/ 下是 Go template + Sprig 函数库,Helm 注入了一批内置对象:
| 对象 | 常用字段 | 典型用途 |
|---|---|---|
.Values |
合并后的参数树 | 读配置 |
.Chart |
Name Version AppVersion |
标签、版本、镜像兜底 |
.Release |
Name Namespace Service IsInstall IsUpgrade Revision |
命名、条件分支 |
.Capabilities |
KubeVersion APIVersions |
按集群版本选 apiVersion、判断 CRD 是否存在 |
.Files |
Get Glob |
读取打包内的非模板文件 |
.Template |
Name BasePath |
调试当前模板、拼模板路径 |
5.1 模板长什么样
deployment.yaml 开头:
gotemplate
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
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 }}
ports:
- name: http
containerPort: {{ .Values.containerPort }}
env:
{{- toYaml .Values.env | nindent 12 }}
{``{-/-}}是空白控制,吃掉多余空行和缩进,否则渲染出的 YAML 可能非法。- 常用函数:
quote、default、toYaml、nindent(缩进对齐)、include、required、fail、sha256sum、tpl、semverCompare,以及控制结构if/with/range。 checksum/config是社区通用技巧:把 ConfigMap 渲染结果做sha256sum写进 Pod 注解,配置一变注解就变,自动触发滚动更新,否则改了 ConfigMap 而 Pod 不会重启。
5.2 把逻辑收敛到 _helpers.tpl
命名模板用 define 定义、include 调用(. 是传入的根对象)。典型内容:
gotemplate
{{/* 生成资源全名:优先 fullnameOverride,否则 <release>-<chart> */}}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{/* 通用标签 */}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/* 用 fail 主动校验,避免渲染出错误资源 */}}
{{- define "myapp.validateIngress" -}}
{{- if .Values.ingress.enabled -}}
{{- if not .Values.ingress.hosts -}}
{{- fail "ingress.enabled=true 时必须提供 ingress.hosts" -}}
{{- end -}}
{{- end -}}
{{- end -}}
模板里也可用 required 对缺失值直接报错:
gotemplate
{{- $host := required "必须配置 ingress.hosts[0].host" (first .Values.ingress.hosts).host -}}
5.3 条件渲染与 NOTES.txt
ingress.yaml 只在开关打开时产生资源,因此渲染结果里可能根本没有这个文件:
gotemplate
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
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 "myapp.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
NOTES.txt也参与模板渲染,但不作为资源提交 ,只在安装/升级成功后打印,可用helm get notes再次查看。常用它输出访问方式:
gotemplate
应用 {{ .Release.Name }} 已部署到 {{ .Release.Namespace }}。
{{- if .Values.ingress.enabled }}
访问地址:http://{{ (first .Values.ingress.hosts).host }}
{{- else }}
本地访问:
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "myapp.fullname" . }} 8080:{{ .Values.service.port }}
{{- end }}
- Hooks :带
helm.sh/hook: pre-install|post-upgrade|pre-delete|test|...注解的模板在发布生命周期特定时点执行,常用于数据库迁移 Job 或helm test的连通性检查;hook-weight控制顺序,hook-delete-policy控制回收。
5.4 渲染流程
加载 Chart 及 subchart → 合并 values(含 schema 校验)→ 渲染全部模板
→ 按 Kind 的既定安装顺序排序(不是文件名字母序)→ 与集群校验
→ 提交资源 → 生成新 Revision
Helm 3 把每次发布的清单、values、NOTES、状态编码后存进集群 Secret(sh.helm.release.v1.<name>.v<N>),所以 helm get manifest 和 helm rollback 不需要本地留 YAML。可用 HELM_DRIVER=configmap|secret|sql|memory 更换存储驱动。
六、命令速查(按使用阶段)
6.1 环境与仓库
bash
helm version # 客户端版本
helm env # HELM_* 环境变量与目录(缓存、配置、插件路径)
helm repo add <name> <url> # 添加 Chart 仓库
helm repo update # 刷新索引
helm repo list / remove <name>
helm search repo <kw> # 搜本地已索引仓库
helm search hub <kw> # 搜 Artifact Hub
helm show chart|values|readme|all <chart>
helm pull <chart> [--untar] # 下载
helm plugin list|install|uninstall
6.2 本地开发与校验(无需连集群)
bash
helm create myapp # 生成脚手架
helm lint ./myapp # 静态检查
helm lint ./myapp --strict # 把 warning 也当失败(CI 用)
helm lint ./myapp -f values-prod.yaml # 用生产参数检查
helm template myapp ./myapp -n app-prod # 纯本地渲染全部清单
helm template myapp ./myapp -n app-prod \
-f values-prod.yaml --set replicaCount=5 # 带参数的渲染
helm template myapp ./myapp \
--show-only templates/deployment.yaml # 只看某一个模板(-s)
helm template myapp ./myapp --debug # 渲染失败时输出上下文
helm dependency build|update ./myapp # 处理 dependencies
helm package ./myapp --destination ./dist # 打成 .tgz
6.3 安装与升级
| 参数 | 作用 |
|---|---|
-n, --namespace |
目标命名空间 |
--create-namespace |
命名空间不存在时自动创建 |
-f, --values |
追加 values 文件,可多次 |
--set 家族 |
命令行覆盖,优先级最高 |
--wait --timeout 5m |
等 Pod Ready 才返回成功 |
--dry-run |
走完整流程但不提交(仍需连集群校验) |
--atomic |
失败自动回滚到上一 Revision |
--cleanup-on-fail |
失败时清理本次新建资源 |
--history-max N |
限制保留的 Revision 数量 |
--kubeconfig / --kube-context |
切换集群 |
bash
# 首次安装(同名 release 已存在会报错)
helm install myapp ./myapp -n app-prod -f values-prod.yaml
# 幂等安装/升级:CI 一律用这个
helm upgrade --install myapp ./myapp \
-n app-prod --create-namespace \
-f values-prod.yaml \
--wait --timeout 5m
# 发一个新镜像版本
helm upgrade myapp ./myapp -n app-prod --reuse-values \
--set image.tag=1.0.1
# 更稳妥的写法:把版本也写进 values 文件,避免依赖上次状态
helm upgrade --install myapp ./myapp -n app-prod \
-f values-prod.yaml --set image.tag=1.0.1 --atomic --history-max 10
# 让 Helm 随机生成 release 名(临时验证用)
helm install ./myapp --generate-name -n app-dev
6.4 查看、回滚与卸载
bash
helm list -n app-prod # 当前命名空间
helm list -A # 全部命名空间
helm status myapp -n app-prod
helm history myapp -n app-prod # 回滚依据
helm get manifest myapp -n app-prod # 实际提交的清单
helm get values myapp -n app-prod -a # 含默认值的完整参数
helm get notes myapp -n app-prod # 安装时打印的 NOTES
helm get all myapp -n app-prod
helm test myapp -n app-prod # 跑 templates/tests/ 下的测试 Pod
helm rollback myapp 3 -n app-prod
helm uninstall myapp -n app-prod
helm uninstall myapp -n app-prod --keep-history # 保留记录供审计
七、实战:从零发布一个普通 Web 应用
7.1 前置条件
| 工具 | 版本 | 用途 |
|---|---|---|
| Helm | 3.x | Chart 渲染与发布 |
| kubectl | 与集群匹配 | 集群访问、验证 |
| K8s 集群 | 1.19+ | 需能拉取 image.repository 指向的镜像 |
bash
helm version # 确认是 3.x
kubectl config current-context # 确认连的是目标集群,别发错环境
kubectl create namespace app-dev
kubectl create namespace app-prod
# 若镜像仓库需要认证,先创建拉取凭证(values 里用 imagePullSecrets 引用)
kubectl create secret docker-registry registry-cred \
--docker-server=registry.example.com \
--docker-username=<用户名> --docker-password=<密码> \
-n app-prod
7.2 创建并定制 Chart
bash
helm create myapp
cd myapp
随后编辑三个文件:
Chart.yaml:name: myapp、version: 0.1.0、appVersion: "1.0.0";values.yaml:把image.repository改成registry.example.com/team/myapp,containerPort设为8080,其余按第四节示例填写;templates/deployment.yaml:确认image使用{``{ .Values.image.tag | default .Chart.AppVersion }},容器端口读.Values.containerPort。
构建并推送应用镜像,tag 与 appVersion 对齐:
bash
docker build -t registry.example.com/team/myapp:1.0.0 .
docker push registry.example.com/team/myapp:1.0.0
7.3 拆分环境 values(只写差异)
values-dev.yaml:
yaml
replicaCount: 1
image:
tag: "1.0.0-rc1"
pullPolicy: Always
service:
type: NodePort
config:
logLevel: debug
env:
- name: LOG_LEVEL
value: debug
values-prod.yaml:
yaml
replicaCount: 3
image:
tag: "1.0.0"
pullPolicy: IfNotPresent
imagePullSecrets:
- name: registry-cred
resources:
limits:
cpu: "1"
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
ingress:
enabled: true
className: nginx
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
两个文件都只写与默认值不同的项。
env是列表,覆盖了就是整体替换,不会与默认值合并。
7.4 四步发布法
bash
# 1) 静态检查:模板语法、values 结构、schema 校验
helm lint ./myapp --strict
helm lint ./myapp -f values-prod.yaml --strict
# 2) 看渲染结果:确认镜像 tag、副本数、端口、Ingress 是否符合预期
helm template myapp ./myapp -n app-prod -f values-prod.yaml > /tmp/prod.yaml
grep -E 'kind:|image:|replicas:|containerPort:|checksum/config' /tmp/prod.yaml
# 只看 Deployment,排查缩进问题
helm template myapp ./myapp -n app-prod -f values-prod.yaml \
--show-only templates/deployment.yaml
# 3) 预演:走完整流程但不提交
helm upgrade --install myapp ./myapp -n app-prod \
-f values-prod.yaml --dry-run --debug
# 4) 正式发布:等就绪 + 失败自动回滚
helm upgrade --install myapp ./myapp -n app-prod \
--create-namespace -f values-prod.yaml \
--atomic --wait --timeout 5m --history-max 10
开发环境同理,只换命名空间与 values 文件:
bash
helm upgrade --install myapp ./myapp -n app-dev -f values-dev.yaml --wait --timeout 5m
7.5 发布后验证
bash
helm list -A
helm status myapp -n app-prod
helm get values myapp -n app-prod -a
helm get manifest myapp -n app-prod | head -40
kubectl -n app-prod get deploy,po,svc,ing
kubectl -n app-prod get cm -l app.kubernetes.io/instance=myapp -o yaml
kubectl -n app-prod describe pod -l app.kubernetes.io/instance=myapp
kubectl -n app-prod logs -f deploy/myapp
kubectl -n app-prod port-forward svc/myapp 8080:80
# 若 Chart 带 tests/test-connection.yaml
helm test myapp -n app-prod
浏览器访问 http://localhost:8080(port-forward)或 http://myapp.example.com(Ingress)。
7.6 升级、回滚与卸载
bash
# 升级:发布新镜像
docker push registry.example.com/team/myapp:1.0.1
helm upgrade --install myapp ./myapp -n app-prod \
-f values-prod.yaml --set image.tag=1.0.1 --atomic --wait --timeout 5m
# 查看历史,确认每次升级的 STATUS / CHART / APP VERSION
helm history myapp -n app-prod
# 回滚到第 2 个 Revision(会生成一个新的 Revision)
helm rollback myapp 2 -n app-prod --wait
# 卸载(CRD、PVC、命名空间不会被删除,需单独处理)
helm uninstall myapp -n app-prod
镜像 tag 变化会触发 Deployment 滚动更新;ConfigMap 内容变化则依赖 checksum/config 注解触发,两者缺一不可。
7.7 打包分发给别的集群
bash
helm lint ./myapp --strict
helm package ./myapp --destination ./dist # dist/myapp-0.1.0.tgz
helm show values ./dist/myapp-0.1.0.tgz
helm registry login registry.example.com
helm push ./dist/myapp-0.1.0.tgz oci://registry.example.com/charts
# 另一集群直接安装(无需源码目录)
helm upgrade --install myapp oci://registry.example.com/charts/myapp \
--version 0.1.0 -n app-prod --create-namespace -f values-prod.yaml
八、进阶话题
8.1 敏感信息不要进 ConfigMap
values.yaml 里的密码若被模板写进 ConfigMap,就是明文且可被任意有读权限的人看到。生产环境应改用 Secret + 环境变量引用:
bash
kubectl create secret generic myapp-secret \
--from-literal=db-password='<密码>' -n app-prod
yaml
# values-prod.yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secret
key: db-password
配合 deployment.yaml 中的 {``{- toYaml .Values.env | nindent 12 }} 即可注入。更完整的方案是 External Secrets Operator、Sealed Secrets 或 Vault Agent;至少也要避免把含密钥的 values 文件提交到 Git。
8.2 Subchart 与 global
父 Chart 通过 Chart.yaml 的 dependencies 引入子 Chart(如 Redis),helm dependency build 依据 Chart.lock 拉取到 charts/。子 Chart 只能读自己名下的 values 子树,唯一例外是 global.*:
yaml
# 父 Chart values.yaml
global:
imageRegistry: registry.example.com
redis: # 子 Chart 名
enabled: false # 对应 dependencies 里的 condition: redis.enabled
auth:
enabled: true
调试某个子 Chart 的渲染结果:
bash
helm template myapp ./myapp --show-only charts/redis/templates/deployment.yaml
8.3 按集群版本选择 apiVersion
gotemplate
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.Version }}
apiVersion: networking.k8s.io/v1
{{- else }}
apiVersion: networking.k8s.io/v1beta1
{{- end }}
.Capabilities.APIVersions.Has "batch/v1" 还能判断某个 API / CRD 是否可用,避免在依赖不满足的集群里渲染失败。helm template --kube-version 1.20.0 可以在本地模拟不同集群版本。
8.4 CI/CD 建议
- 流水线固定使用
helm upgrade --install,配合--atomic --wait --timeout 5m --history-max 10; - 提交前跑
helm lint --strict,并把helm template输出做快照对比(渲染 diff 进 PR); - values 按环境拆文件,密钥走 Secret 而非
-f; - Chart 版本随模板变更递增,产物
.tgz归档到 OCI Registry;应用发版则改appVersion/image.tag; - 用
--kube-context显式指定目标集群,防止误发。
九、常见问题排查
| 现象 | 排查方向 |
|---|---|
ImagePullBackOff |
镜像 tag 是否推送成功;imagePullSecrets 引用的 Secret 是否在同一命名空间 ;pullPolicy 是否为 Always 但仓库不可达 |
CrashLoopBackOff |
kubectl logs 与 kubectl describe pod 看启动异常、探针失败、端口不一致 |
Error: cannot re-use a name |
helm install 遇同名 release 报错,改用 upgrade --install |
模板报 nil pointer evaluating |
values 缺键,用 default 兜底或 required 显式报错;helm template --debug 定位 |
| 渲染出的 YAML 缩进错乱 | 检查 {``{- 空白控制与 nindent / indent 的层数,用 --show-only 单独看该模板 |
| 配置改了但 Pod 没重启 | 检查 Deployment 是否有 checksum/config 注解,及其值是否变化 |
| values 覆盖没生效 | 检查 --set 与 -f 的优先级、多个 -f 的左右顺序、upgrade 是否用了 --reuse-values |
升级报 field is immutable |
Service / Job 的部分字段不可变,需改名或先删除重建;避免用 --set 改 selector |
--dry-run 也报集群错误 |
Helm 3 的 dry-run 仍需连 API Server 做校验;纯本地看结果用 helm template |
helm rollback 后编号没退回 |
符合预期:回滚会新增一个 Revision |
helm test 失败 |
查看测试 Pod 日志;templates/tests/ 下的资源只在 helm test 时创建 |
| 卸载后资源仍在 | helm uninstall 不删 CRD、PVC、命名空间与 Hook 残留,需单独清理 |
十、小结
Helm 的价值不在「少写几行 YAML」,而在于建立了一套可追溯的发布模型:
- Chart / Package 让应用定义可复用、可版本化、可分发;
- values 让环境差异集中成参数,并有明确的覆盖优先级;
- templates 让资源命名、镜像拼接、条件渲染、配置校验和只写一遍;
- Release / Revision 让每次发布成为可查询、可对比、可回滚的快照。
回到 myapp:同一份 Chart + 两个 values 文件就能覆盖开发/生产,一次 --set image.tag=1.0.1 完成升级,而 helm history / rollback 让这次升级随时可撤回------这正是 Helm 在真实项目里最核心的收益。
延伸阅读 :helm help 与各子命令 --help、Helm 官方文档的 Chart 最佳实践与 Chart Template Guide、helm create 生成的脚手架注释。