Helm Chart 开发实战——从使用 Helm 到开发自己的 Chart
Helm Chart 开发实战——从使用 Helm 到开发自己的 Chart
适读人群:用 Helm 管理 K8s 应用的工程师,想从"用别人的 Chart"进阶到"写自己的 Chart" | 阅读时长:约 17 分钟 | 核心价值:完整掌握 Helm Chart 的开发流程和最佳实践
我第一次正经写 Helm Chart 是在接手公司的平台建设工作之后。
当时每次部署新应用,都要手写一堆 YAML,deployment、service、configmap、ingress……每个应用写一套,改参数全靠 sed 替换,出错率极高。某次上线,同事把 staging 的数据库 URL 配置到了 production 环境,好在是只读操作没出大事,但这让我下决心把所有应用都迁到 Helm Chart。
整个迁移花了两周,之后部署效率提升了一个数量级。这篇文章把我写 Chart 的思路和踩坑经验都写出来。
Helm 基础使用回顾
在写 Chart 之前,先把常用命令整理一遍:
# 添加 repo
helm repo add stable https://charts.helm.sh/stable
helm repo update
# 搜索 chart
helm search repo nginx
helm search hub prometheus
# 安装(不存在则创建,存在则升级)
helm upgrade --install myapp ./myapp-chart \
--namespace production \
--create-namespace \
-f values-prod.yaml \
--set image.tag=v2.3.1
# 查看已安装的 release
helm list -n production
helm list -A # 所有命名空间
# 查看 release 历史
helm history myapp -n production
# 回滚到上一个版本
helm rollback myapp -n production
helm rollback myapp 3 -n production # 回滚到第 3 个版本
# 卸载(默认保留历史)
helm uninstall myapp -n production
# 渲染模板(不安装,看最终生成的 YAML)
helm template myapp ./myapp-chart -f values-prod.yamlChart 目录结构
myapp-chart/
├── Chart.yaml # Chart 元数据
├── values.yaml # 默认配置值
├── values-dev.yaml # 开发环境覆盖值(通常不放进 Chart,单独管理)
├── templates/ # K8s 资源模板
│ ├── _helpers.tpl # 模板函数定义
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── hpa.yaml
│ ├── serviceaccount.yaml
│ └── NOTES.txt # 安装完成后显示的提示信息
└── charts/ # 子 Chart(依赖)实战:从零开发一个 Web 应用 Chart
第一步:Chart.yaml
apiVersion: v2 # Helm 3 用 v2
name: myapp
description: A Helm chart for myapp web service
type: application
version: 1.3.2 # Chart 版本(语义化版本)
appVersion: "2.3.1" # 应用版本
keywords:
- web
- api
maintainers:
- name: Laozhang
email: laozhang@example.com
# 依赖
dependencies:
- name: postgresql
version: "12.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled # 根据 values 决定是否安装第二步:values.yaml(核心配置文件)
values.yaml 是 Chart 的公共接口,设计要考虑通用性和扩展性:
# values.yaml
# 镜像配置
image:
repository: registry.example.com/myapp
tag: "latest" # 建议覆盖成具体版本
pullPolicy: IfNotPresent
pullSecrets: []
# 副本配置
replicaCount: 2
# 资源限制
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 512Mi
# CPU limit 不设置(防止节流),可在具体环境覆盖
# 服务配置
service:
type: ClusterIP
port: 80
targetPort: 8080
# Ingress 配置
ingress:
enabled: false # 默认不开,需要时覆盖
className: "nginx"
host: ""
tls:
enabled: false
secretName: ""
annotations: {}
# 环境变量
env: {}
# LOG_LEVEL: INFO
# DB_HOST: postgres
# 额外的环境变量(从 Secret 注入)
envFrom: []
# - secretRef:
# name: myapp-secret
# HPA 配置
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 60
# 健康检查
probes:
liveness:
enabled: true
path: /actuator/health/liveness
initialDelaySeconds: 60
periodSeconds: 10
readiness:
enabled: true
path: /actuator/health/readiness
initialDelaySeconds: 20
periodSeconds: 5
# 节点亲和性(高级配置,默认空)
nodeSelector: {}
tolerations: []
affinity: {}
# Pod 安全上下文
podSecurityContext:
runAsNonRoot: true
runAsUser: 1001
fsGroup: 1001
# 日志配置
logging:
driver: json-file
maxSize: "50m"
maxFile: "5"
# PostgreSQL 依赖(可选)
postgresql:
enabled: false
auth:
username: myapp
database: myapp第三步:_helpers.tpl(公共模板函数)
{{/*
Generate a full name for the chart.
Usage: {{ include "myapp.fullname" . }}
*/}}
{{- define "myapp.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 }}
{{/*
Common labels for all resources.
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ .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 }}
{{/*
Selector labels (used by Deployment and Service).
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Image string with tag.
*/}}
{{- define "myapp.image" -}}
{{- printf "%s:%s" .Values.image.repository .Values.image.tag }}
{{- end }}第四步:Deployment 模板
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
# 配置变更时触发滚动更新
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
{{- include "myapp.labels" . | nindent 8 }}
spec:
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.image.pullSecrets }}
imagePullSecrets:
{{- toYaml .Values.image.pullSecrets | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: {{ include "myapp.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.targetPort }}
protocol: TCP
{{- if .Values.env }}
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- if .Values.envFrom }}
envFrom:
{{- toYaml .Values.envFrom | nindent 10 }}
{{- end }}
{{- if .Values.probes.liveness.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: {{ .Values.service.targetPort }}
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
failureThreshold: 3
{{- end }}
{{- if .Values.probes.readiness.enabled }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: {{ .Values.service.targetPort }}
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
failureThreshold: 3
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
terminationGracePeriodSeconds: 60踩坑实录一:values 文件里的密码被 git 提交
现象:为了方便,在 values-prod.yaml 里写了数据库密码,这个文件被 push 到了 Git 仓库。
解法:把密钥从 values 里移出,用 --set 或者外部 Secret 注入:
# 通过命令行传密钥(CI/CD 从密钥管理器获取)
helm upgrade --install myapp ./myapp-chart \
-n production \
-f values-prod.yaml \
--set secrets.dbPassword="${DB_PASSWORD}" \
--set secrets.redisPassword="${REDIS_PASSWORD}"或者在 Chart 里引用已存在的 K8s Secret(不由 Helm 管理):
# values.yaml
existingSecret:
name: myapp-db-secret # 已存在的 Secret 名称
passwordKey: password
# templates/deployment.yaml
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.existingSecret.name }}
key: {{ .Values.existingSecret.passwordKey }}踩坑实录二:Chart 升级后资源没有删除
现象:第一版 Chart 里有一个 ConfigMap,第二版把它删掉了,升级后老的 ConfigMap 还在集群里。
原因:Helm 只管理它自己创建的资源,不会自动删除手动创建的资源。但如果 Chart 里去掉了某个模板文件,helm upgrade 并不会删除旧版本里包含这个模板创建的资源。
实际上这个行为是设计如此——Helm 认为"去掉模板 = 我不再管这个资源",而不是"去掉模板 = 删除这个资源"。
解法:用 Helm hook 或者 helm uninstall + install;或者手动删除不需要的资源。
踩坑实录三:模板渲染结果和预期不符
现象:部署后某个环境变量没有注入,但 values 文件里明明有。
调试方法:
# 先渲染,看看实际生成的 YAML
helm template myapp ./myapp-chart -f values-prod.yaml > /tmp/rendered.yaml
cat /tmp/rendered.yaml | grep -A5 "env:"
# 检查 values 是否正确解析
helm get values myapp -n production
# 测试特定 values
helm template myapp ./myapp-chart --set env.LOG_LEVEL=DEBUG写模板时要注意缩进(nindent/indent 很关键),YAML 的缩进错误会导致静默失败。
Chart 打包和分发
# 更新依赖
helm dependency update ./myapp-chart
# 打包成 .tgz
helm package ./myapp-chart --version 1.3.2
# 上传到 Harbor
helm push myapp-1.3.2.tgz oci://registry.example.com/charts
# 或者使用 Helm 仓库服务器(chartmuseum)
helm cm-push myapp-1.3.2.tgz my-chartmuseum-repoHelm Chart 是 K8s 应用部署的标准化工具,值得投入时间认真学。一个好的 Chart 可以让所有环境的部署变成一行命令,大幅降低配置漂移的风险。
