为什么需要 Helm
裸用 kubectl apply -f 管理 K8s 应用,在规模小时够用,但随着环境增多(dev/staging/prod)和服务增长,问题立刻暴露:
- 配置硬编码:每个环境一份 YAML,镜像 tag、副本数、资源限制全写死,改一个值要改十个文件
- 无版本管理:升级回滚靠手动记录,不知道上次部署了什么版本
- 无法复用:部署 Redis 和部署 MySQL 写两套完全不同的 YAML,无法模板化
Helm 是 K8s 的包管理器,把一组 K8s 资源打包成 Chart,通过 values.yaml 参数化配置,实现一份模板、多环境部署、版本化升级和一键回滚。
本文参考 Helm 官方文档
Helm Chart 目录结构
my-web-app/
├── Chart.yaml # Chart 元信息(名称、版本、描述)
├── values.yaml # 默认配置值
├── values-prod.yaml # 生产环境覆盖配置
├── charts/ # 依赖的子 Chart
├── templates/ # K8s 资源模板
│ ├── _helpers.tpl # 命名模板(可复用的模板片段)
│ ├── deployment.yaml # Deployment
│ ├── service.yaml # Service
│ ├── ingress.yaml # Ingress
│ ├── configmap.yaml # ConfigMap
│ ├── secret.yaml # Secret
│ ├── hpa.yaml # HorizontalPodAutoscaler
│ ├── serviceaccount.yaml # ServiceAccount
│ └── NOTES.txt # 安装后提示信息
└── .helmignore # Helm 打包忽略文件
Chart.yaml 详解
apiVersion: v2 # Helm 3 固定用 v2
name: my-web-app # Chart 名称
description: A production-grade web application Helm chart
type: application # application | library
version: 1.2.0 # Chart 版本(SemVer)
appVersion: "2.4.1" # 应用版本(不强制 SemVer)
keywords:
- web
- api
- microservice
maintainers:
- name: 徐保金
email: xubaojin@example.com
dependencies:
- name: redis
version: 18.x.x
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled # 只有 values 中 redis.enabled=true 时才安装
关键字段:
version是 Chart 自身的版本,每次修改模板或 values 时递增appVersion是打包应用的版本,与 Chart version 相互独立dependencies声明依赖的子 Chart,condition控制是否启用
模板语法
基础表达式
# {{ .Values.xxx }} — 引用 values.yaml 中的值
# {{ .Release.xxx }} — 引用 Helm Release 信息
# {{ .Chart.xxx }} — 引用 Chart.yaml 中的值
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-{{ .Chart.Name }}
labels:
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
常用内置对象:
| 对象 | 说明 | 示例 |
|---|---|---|
.Values | values.yaml 中的值 | .Values.image.repository |
.Release.Name | Release 名称 | my-release |
.Release.Namespace | 目标命名空间 | production |
.Release.IsInstall | 是否首次安装 | true / false |
.Release.IsUpgrade | 是否升级操作 | true / false |
.Chart.Name | Chart 名称 | my-web-app |
.Chart.AppVersion | 应用版本 | 2.4.1 |
.Files | Chart 内文件访问 | .Files.Get "config/nginx.conf" |
管道与函数
Helm 模板支持管道(pipe)和 Sprig 函数库:
# 管道:将前一个函数的输出作为后一个的输入
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
# 常用函数
{{ .Values.replicaCount | int }} # 转整数
{{ .Values.image.tag | quote }} # 加引号
{{ .Values.hostname | upper }} # 转大写
{{ .Values.host | replace "." "-" }} # 替换字符串
{{ include "my-app.fullname" . }} # 调用命名模板
{{ .Values.port | toString }} # 转字符串
# default 函数:值为空时使用默认值
imagePullPolicy: {{ .Values.image.pullPolicy | default "IfNotPresent" }}
# nindent 函数:缩进 N 个空格(注意是 nindent 不是 indent,前者有前导换行)
{{- include "my-app.labels" . | nindent 4 }}
range 循环
# values.yaml
service:
ports:
- name: http
port: 80
targetPort: 8080
- name: metrics
port: 9090
targetPort: 9090
# templates/service.yaml
spec:
type: {{ .Values.service.type }}
ports:
{{- range .Values.service.ports }}
- name: {{ .name }}
port: {{ .port }}
targetPort: {{ .targetPort }}
protocol: TCP
{{- end }}
if 条件
# values.yaml
ingress:
enabled: true
className: nginx
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
# templates/ingress.yaml
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "my-app.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 "my-app.fullname" $ }}
port:
number: 80
{{- end }}
{{- end }}
{{- end }}
注意模板中的 {{- }} 语法——横杠表示去除前导/尾部空白,避免生成 YAML 中出现多余空行导致格式错误。
with 作用域
# values.yaml
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 10
# templates/deployment.yaml
# with 改变 . 的作用域,避免重复写长路径
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 2 }}
{{- end }}
在 with 块内,. 指向 .Values.livenessProbe。如果需要在 with 块内访问根上下文,使用 $(根上下文的别名)。
values.yaml 层级覆盖机制
Helm 的配置覆盖有一套清晰的优先级,从高到低:
--set 参数(最高优先级)
↓ 覆盖
-f / --values 指定的文件
↓ 覆盖
父 Chart 的 values.yaml(最低)
↓ 被覆盖
子 Chart 的 values.yaml(最低中的最低)
–set 传参
# 设置单个值
helm install my-app ./my-web-app \
--set image.tag=v2.4.1
# 设置嵌套值
helm install my-app ./my-web-app \
--set image.tag=v2.4.1 \
--set replicaCount=3
# 设置数组元素
helm install my-app ./my-web-app \
--set ingress.hosts[0].host=api.example.com
# 设置布尔值
helm install my-app ./my-web-app \
--set ingress.enabled=true
-f 多环境配置文件
# values.yaml(默认值)
replicaCount: 2
image:
repository: my-web-app
tag: latest
resources:
requests:
cpu: 100m
memory: 128Mi
ingress:
enabled: false
# values-prod.yaml(生产覆盖)
replicaCount: 5
image:
tag: v2.4.1
resources:
requests:
cpu: 500m
memory: 512Mi
ingress:
enabled: true
className: nginx
hosts:
- host: api.example.com
paths:
- path: /
pathType: Prefix
# 生产部署:默认值被 values-prod.yaml 覆盖
helm upgrade --install my-app ./my-web-app \
-f values.yaml \
-f values-prod.yaml \
-n production \
--create-namespace
父子 Chart 覆盖
# 项目结构
parent-chart/
├── Chart.yaml
├── values.yaml # 父 Chart 的值可以覆盖子 Chart
├── charts/
│ └── child-chart/
│ ├── Chart.yaml
│ └── values.yaml # 子 Chart 自身默认值
└── templates/
# parent/values.yaml
# 以子 Chart 名称为顶级 key,覆盖子 Chart 的值
child-chart:
replicaCount: 3
image:
tag: v2.4.1
父 Chart 的 values.yaml 中以子 Chart 名称为 key 的部分,会覆盖子 Chart 的 values.yaml。但子 Chart 模板中只能访问自己的 .Values,看不到父 Chart 的全局值。
命名模板与钩子
_helpers.tpl
命名模板(named template)是可复用的模板片段,定义在 _helpers.tpl 中。以 _ 开头的文件不会被渲染为 K8s 资源,只供其他模板引用。
{{/* templates/_helpers.tpl */}}
{{/*
生成完整的资源名称:release-name-chart-name
*/}}
{{- define "my-app.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 "my-app.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
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 }}
{{/*
资源选择器标签:Deployment/Service 的 selector 必须一致
*/}}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
在模板中使用:
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
Helm Hooks
Hooks 允许在 Release 生命周期的特定时机执行操作,如安装前初始化数据库、升级后运行迁移脚本。
# templates/post-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-app.fullname" . }}-migrate
labels:
{{- include "my-app.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install,post-upgrade # 时机
"helm.sh/hook-weight": "-5" # 权重(数字越小越先执行)
"helm.sh/hook-delete-policy": hook-succeeded # 成功后删除 Job
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./app", "migrate"]
常用 Hook 时机:
| Hook | 触发时机 | 典型用途 |
|---|---|---|
pre-install | 模板渲染后、资源创建前 | 创建外部依赖(如数据库) |
post-install | 所有资源创建后 | 数据库迁移、初始化数据 |
pre-upgrade | 升级渲染后、替换前 | 备份数据 |
post-upgrade | 升级完成后 | 缓存预热、健康检查 |
pre-delete | 删除前 | 清理外部资源 |
post-delete | 删除后 | 清理确认 |
私有仓库管理:Harbor
Harbor 部署
Harbor 是 CNCF 毕业项目,同时支持容器镜像仓库和 Helm Chart 仓库(2.x 版本后 Chartmuseum 已内置集成)。
# 下载 Harbor 安装包
wget https://github.com/goharbor/harbor/releases/download/v2.9.0/harbor-offline-installer-v2.9.0.tgz
tar xzf harbor-offline-installer-v2.9.0.tgz
cd harbor
# 生成自签名证书(生产环境用正式证书)
mkdir -p /data/cert
openssl genrsa -out /data/cert/ca.key 4096
openssl req -x509 -new -nodes -sha512 -days 3650 \
-subj "/CN=harbor.example.com" \
-key /data/cert/ca.key \
-out /data/cert/ca.crt
# 配置 harbor.yml
cp harbor.yml.tmpl harbor.yml
# harbor.yml 关键配置
hostname: harbor.example.com
http:
port: 80
https:
port: 443
certificate: /data/cert/ca.crt
private_key: /data/cert/ca.key
harbor_admin_password: YourStrongPassword
data_volume: /data/harbor
# 启用 Chart 仓库服务
chart:
absolute_url: false
# 安装(含 Chart 服务)
./install.sh --with-chartmuseum
# 验证
docker ps | grep harbor
# harbor-core, harbor-db, harbor-jobservice, chartmuseum, registry, ...
Chart 推送到 Harbor
# 1. 添加 Harbor Helm 仓库(需认证)
helm repo add my-harbor https://harbor.example.com/chartrepo/library \
--username admin --password YourStrongPassword
# 2. 打包 Chart
helm package ./my-web-app --version 1.2.0
# Successfully packaged chart and saved it to: my-web-app-1.2.0.tgz
# 3. 推送到 Harbor
helm push my-web-app-1.2.0.tgz my-harbor
# 4. 搜索验证
helm search repo my-harbor/my-web-app
# NAME CHART VERSION APP VERSION
# my-harbor/my-web-app 1.2.0 2.4.1
# 从 Harbor 安装
helm install my-app my-harbor/my-web-app \
--version 1.2.0 \
-f values-prod.yaml \
-n production
推送 Helm 插件安装
Helm 3.8+ 内置了 OCI 支持,可以直接用 helm push 推送 Chart 到 OCI 兼容仓库(包括 Harbor)。
# Helm 3.8+ OCI 方式推送
helm registry login harbor.example.com \
--username admin --password YourStrongPassword
helm package ./my-web-app --version 1.2.0
helm push my-web-app-1.2.0.tgz oci://harbor.example.com/charts
# 拉取安装
helm install my-app oci://harbor.example.com/charts/my-web-app \
--version 1.2.0 \
-n production
CI/CD 集成:GitHub Actions
# .github/workflows/helm-release.yml
name: Helm Chart Release
on:
push:
tags:
- 'chart-v*' # tag 格式: chart-v1.2.0
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Helm
uses: azure/setup-helm@v4
with:
version: v3.14.0
- name: Extract version
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/chart-v}" >> $GITHUB_OUTPUT
- name: Lint Chart
run: |
helm lint ./charts/my-web-app
- name: Update Chart.yaml version
run: |
sed -i "s/^version:.*/version: ${{ steps.version.outputs.VERSION }}/" \
./charts/my-web-app/Chart.yaml
- name: Package Chart
run: |
mkdir -p packaged
helm package ./charts/my-web-app \
--version ${{ steps.version.outputs.VERSION }} \
--destination packaged
- name: Login to Harbor
run: |
echo "${{ secrets.HARBOR_PASSWORD }}" | \
helm registry login harbor.example.com \
--username ${{ secrets.HARBOR_USERNAME }} --password-stdin
- name: Push Chart to Harbor
run: |
helm push packaged/my-web-app-${{ steps.version.outputs.VERSION }}.tgz \
oci://harbor.example.com/charts
- name: Generate Release Notes
run: |
echo "## Helm Chart v${{ steps.version.outputs.VERSION }}" > release-notes.md
echo "" >> release-notes.md
echo "### Install" >> release-notes.md
echo '```bash' >> release-notes.md
echo "helm install my-app \\" >> release-notes.md
echo " oci://harbor.example.com/charts/my-web-app \\" >> release-notes.md
echo " --version ${{ steps.version.outputs.VERSION }} \\" >> release-notes.md
echo " -n production --create-namespace" >> release-notes.md
echo '```' >> release-notes.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: packaged/*.tgz
body_path: release-notes.md
token: ${{ secrets.GITHUB_TOKEN }}
实战:完整的 Web 应用 Helm Chart
values.yaml
# values.yaml — 默认配置
replicaCount: 2
image:
repository: my-web-app
tag: ""
pullPolicy: IfNotPresent
pullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
fsGroup: 1000
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: false
className: ""
annotations: {}
hosts: []
tls: []
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
config:
LOG_LEVEL: info
APP_ENV: production
nodeSelector: {}
tolerations: []
affinity: {}
templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.image.pullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "my-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "my-app.fullname" . }}-config
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "my-app.fullname" . }}-config
labels:
{{- include "my-app.labels" . | nindent 4 }}
data:
{{- range $key, $val := .Values.config }}
{{ $key }}: {{ $val | quote }}
{{- end }}
templates/hpa.yaml
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "my-app.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
部署验证
# 渲染模板预览(不实际部署)
helm template my-app ./my-web-app -f values-prod.yaml
# Lint 检查
helm lint ./my-web-app -f values-prod.yaml
# ==> Linting ./my-web-app
# [INFO] Chart.yaml: icon is recommended
# 1 chart(s) linted, 0 chart(s) failed
# Dry-run 部署
helm install my-app ./my-web-app \
-f values-prod.yaml \
-n production \
--create-namespace \
--dry-run
# 正式部署
helm upgrade --install my-app ./my-web-app \
-f values-prod.yaml \
-n production \
--create-namespace
# 查看部署状态
helm list -n production
helm status my-app -n production
# 查看历史版本
helm history my-app -n production
# 回滚到上一版本
helm rollback my-app 1 -n production
总结
Helm Chart 是 K8s 应用管理的标准方式。核心要点:
- 目录结构清晰——
Chart.yaml管元信息、values.yaml管配置、templates/管模板,各司其职 - 模板语法强大——
{{ }}表达式 + Sprig 函数 +range/if/with控制结构,满足 99% 的配置需求 - values 分层覆盖——
--set>-f> 默认values.yaml,多环境配置通过文件分离,不修改模板 - 命名模板复用——
_helpers.tpl中定义标签、名称等公共片段,所有模板统一引用,保持一致性 - Hooks 灵活扩展——
pre-install/post-upgrade等钩子支持在生命周期关键节点执行初始化或迁移 - 私有仓库规范管理——Harbor 统一托管镜像和 Chart,OCI 协议让
helm push更简单 - CI/CD 自动化——GitHub Actions 中
helm lint → package → push全自动,打 tag 即发版
一份好的 Helm Chart,应该做到:开发者改 values.yaml 就能适配新环境,不需要碰模板文件;模板经过 helm lint 和 helm template 验证,不会部署出格式错误的 YAML。这才是 GitOps 的基础。
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- Helm 官方文档 — Helm 项目,参考了Helm 官方文档相关内容