【K8S 运维实战】31-Helm包管理

发布时间:2026/8/3 4:30:37
【K8S 运维实战】31-Helm包管理 Helm 包管理:Chart 开发与仓库治理 从一把梭 kubectl apply到可复用、可版本化、可治理的部署体系写在前面你可能已经用 Helm 安过不少 Chart——helm install nginx-ingress,一条命令,啪,起来了。但当你的集群从 5 个应用膨胀到上百个,问题就来了:每个团队的 Chart 目录结构五花八门,values.yaml 覆盖逻辑像意大利面条,私有仓库里同一个 Chart 有 v1.0、v1.0-hotfix、v1.0.1-rc 三个版本谁也搞不清哪个上了生产。这篇文章不教你怎么装 Chart,而是教你怎么开发规范 Chart、治理 Helm 仓库、编排多环境部署,让 Helm 从一个安装工具变成你真正的包管理基础设施。核心问题怎么管理上百个应用的部署——让 Chart 可复用、values 可分层、版本可追溯、多环境可差异化?一、原理剖析1.1 Chart 的本质:一套模板 一组默认值Helm Chart 不是打包好的镜像,它是一组 Go Template 一个 values.yaml 的默认值。渲染过程就是:模板引擎把templates/下每个.yaml文件里的{{ .Values.xxx }}替换成实际值,最终输出一份完整的 Kubernetes 资源清单。Chart 目录结构(标准) mychart/ ├── Chart.yaml # 元数据:名称、版本(appVersion vs version)、依赖 ├── values.yaml # 默认值——所有可配置项的基线 ├── templates/ │ ├── _helpers.tpl # 共用模板片段(命名约定、标签集、公共逻辑) │ ├── deployment.yaml │ ├── service.yaml │ ├── ingress.yaml │ ├── configmap.yaml │ ├── hpa.yaml │ ├── NOTES.txt # 安装后提示信息 │ └── tests/ │ └─ test-connection.yaml ├── templates/partials/ # 可选:拆分大模板的子片段 ├── .helmignore # 打包排除列表 └── crds/ # CRD 定义(安装前自动加载)关键区分:version: Chart 自身的打包版本,遵循 semver,每次改动必须递增。appVersion: 应用(镜像)的版本,是信息性字段,不参与 Helm 的版本计算。1.2 Values 分层:默认值 → 組覆值 → 命令行覆值Helm 的值合并遵循一个优先级链,高优先级覆盖低优先级:优先级(从低到高): 1. Chart 内 values.yaml ← 基线默认值 2. Parent Chart 的 values.yaml ← 如果是子 Chart(subchart) 3. User 的 -f values-prod.yaml ← 環境/自定义覆盖文件 4. User 的 --set keyval ← 命令行覆盖(最高优先级) 5. User 的 --set-string keyval ← 同上,强制字符串类型渲染时的合并逻辑是深度合并(dict 递归合并,list 整体替换):# values.yaml (基线)replicaCount:1resources:requests:cpu:100mmemory:128Mi# values-prod.yaml (覆盖)replicaCount:3resources:requests:cpu:500m# memory 未指定 → 保留基线 128Mi# 渲染结果replicaCount:3resources:requests:cpu:500mmemory:128Mi# 深度合并:未覆盖的 key 保留踩坑预警: list 类型不会逐项合并,而是整体替换。如果你在基线写了tolerations: [key1, key2],覆盖文件写了tolerations: [key3],结果是[key3]而不是[key1, key2, key3]。所以 list 类型的 values 通常在基线设为空[],让覆盖文件完整提供。1.3 Helmfile:多 Chart 编排与环境差异化当你有 10 个 Chart 要部署到 3 个环境(dev/staging/prod),纯 Helm 命令行已经不够用了。Helmfile 是一个声明式编排工具,让你用一个helmfile.yaml管理所有 Chart 的安装顺序、值覆盖和版本锁定。Helmfile 值覆盖优先级(在 Helm 之上再加一层): helmfile.yaml ├── repositories: # 声明 Chart 仓库源 ├── releases: │ ├── name: api-gateway │ │ chart: stable/nginx-ingress │ │ version: 4.11.3 # 锁定版本 │ │ values: # 覆盖文件列表(按顺序合并) │ │ - values/base.yaml │ │ - values/{{ .Environment.Name }}.yaml # 環境差异化 │ │ - values/api-gateway.yaml # 应用差异化 │ │ set: # 等同 --set │ │ - clusterName{{ .Environment.Values.clusterName }}Helmfile 的核心价值:版本锁定: 每个 release 指定version,避免最新版漂移。環境差异化: 用{{ .Environment.Name }}模板变量自动选择覆盖文件。依赖顺序:needs字段控制安装先后,比如先装数据库再装应用。批量操作:helmfile sync一条命令同步所有 release 到目标状态。1.4 Helm vs Kustomize:适用场景对比┌──────────────────────────────────────────────────────┐ │ 对比维度 │ ├──────────┬───────────────────┬───────────────────────┤ │ │ Helm │ Kustomize │ ├──────────┼───────────────────┼───────────────────────┤ │ 模板方式 │ Go Template │ 纯 YAML patch/overlay │ │ 分发方式 │ 打包为 Chart │ 不打包,就地叠加 │ │ 版本管理 │ semver 仓库 │ Git commit 即版本 │ │ 值覆盖 │ values 分层 │ overlay 分层 │ │ 适用场景 │ 第三方/公共 Chart │ 内部应用/微调已有资源 │ │ 学习曲线 │ 较高(Go Template) │ 较低(纯 YAML) │ │ 生态 │ Artifact Hub 丰富 │ 原生 kubectl 支持 │ │ 复杂度 │ 适合完整应用包 │ 适合局部定制/补丁 │ └──────────┴───────────────────┴───────────────────────┘ 选型建议: - 安装第三方组件(Nginx Ingress/Prometheus/Redis Operator) → 用 Helm - 内部微服务部署,只需要差异化覆盖 → 用 Kustomize 或 HelmKustomize 插件 - 多团队共享的应用模板 → 用 Helm Chart(标准化强) - 一个集群里对同一组件多处微调 → 用 Kustomize overlay - 两者可以共存: Helm 产出原始 YAML,Kustomize 再 patch二、实战操作2.1 企业级 Chart 模板开发先创建一个规范的 Chart:helm create myapp# 然后我们对生成的模板做规范化改造Chart.yaml— 元数据规范:apiVersion:v2name:myappdescription:内部业务应用部署模板type:applicationversion:1.0.0# Chart 打包版本,每次发布必须递增appVersion:2.4.1# 应用镜像版本,信息性字段kubeVersion:1.28# 支持的 K8s 版本范围home:https://wiki.internal/myappmaintainers:-name:platform-teamemail:platformcompany.comannotations:category:BusinessApplicationlicense:Apache-2.0templates/_helpers.tpl— 共用模板片段(这是企业级 Chart 的灵魂):{{/* 标准命名约定:release-name-chart-name */}} {{- 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 }} {{/* 标准标签集:所有资源必须携带 */}} {{- define myapp.labels -}} helm.sh/chart: {{ printf %s-%s .Chart.Name .Chart.Version | replace _ | trunc 63 | trimSuffix - }} {{ include myapp.selectorLabels . }} app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/part-of: {{ .Chart.Name }} {{- end }} {{/* 选择器标签:Deployment/Service 的 selector 必须用这组 */}} {{- define myapp.selectorLabels -}} app.kubernetes.io/name: {{ include myapp.fullname . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* 公共注释块 */}} {{- define myapp.annotations -}} {{- if .Values.commonAnnotations }} {{ toYaml .Values.commonAnnotations }} {{- end }} {{- end }}templates/deployment.yaml— 使用 helpers 的 Deployment:apiVersion:apps/v1kind:Deploymentmetadata:name:{{include myapp.fullname .}}labels:{{-include myapp.labels .|nindent 4}}{{-with (include myapp.annotations .)}}annotations:{{-.|nindent 4}}{{-end}}spec:replicas:{{.Values.replicaCount}}selector:matchLabels:{{-include myapp.selectorLabels .|nindent 6}}template:metadata:labels:{{-include myapp.selectorLabels .|nindent 8}}{{-with .Values.podLabels}}{{-toYaml .|nindent 8}}{{-end}}spec:{{-with .Values.imagePullSecrets}}imagePullSecrets:{{-toYaml .|nindent 8}}{{-end}}serviceAccountName:{{include myapp.fullname .}}securityContext:{{-toYaml .Values.podSecurityContext|nindent 8}}containers:-name:{{.Chart.Name}}securityContext:{{-toYaml .Values.securityContext|nindent 12}}image:{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}imagePullPolicy:{{.Values.image.pullPolicy}}env:{{-range $key,$value: .Values.extraEnv}}-name:{{$key}}value:{{$value|quote}}{{-end}}ports:-name:httpcontainerPort:{{.Values.service.port}}protocol:TCPlivenessProbe:{{-toYaml .Values.livenessProbe|nindent 12}}readinessProbe:{{-toYaml .Values.readinessProbe|nindent 12}}resources:{{-toYaml .Values.resources|nindent 12}}{{-with .Values.nodeSelector}}nodeSelector:{{-toYaml .|nindent 8}}{{-end}}{{-with .Values.affinity}}affinity:{{-toYaml .|nindent 8}}{{-end}}{{-with .Values.topologySpreadConstraints}}topologySpreadConstraints:{{-toYaml .|nindent 8}}{{-end}}tolerations:{{-toYaml .Values.tolerations|nindent 8}}2.2 Values 分层实战values.yaml— 基线默认值(保守配置):replicaCount:1image:repository:registry.internal/company/myapppullPolicy:IfNotPresenttag:imagePullSecrets:[]nameOverride:fullnameOverride:serviceAccount:create:trueannotations:{}name:podSecurityContext:runAsNonRoot:truerunAsUser:1000securityContext:allowPrivilegeEscalation:falsecapabilities:drop:[ALL]service:type:ClusterIPport:8080ingress:enabled:falseclassName:annotations:{}hosts:[]tls:[]resources:requests:cpu:100mmemory:128Milimits:cpu:500mmemory:512MilivenessProbe:httpGet:path:/healthzport:httpinitialDelaySeconds:30periodSeconds:10readinessProbe:httpGet:path:/readyport:httpinitialDelaySeconds:5periodSeconds:5autoscaling:enabled:falseminReplicas:1maxReplicas:10targetCPUUtilizationPercentage:80tolerations:[]affinity:{}topologySpreadConstraints:[]extraEnv:{}commonAnnotations:{}podLabels:{}values-prod.yaml— 生产环境覆盖:replicaCount:3image:pullPolicy:Alwaystag:2.4.1service:type:ClusterIPingress:enabled:trueclassName:nginxannotations:cert-manager.io/cluster-issuer:letsencrypt-prodhosts:-host:myapp.company.compaths:-path:/pathType:Prefixtls:-secretName:myapp-tlshosts:[myapp.company.com]resources:requests:cpu:500mmemory:256Milimits:cpu:1memory:1Giautoscaling:enabled:trueminReplicas:3maxReplicas:20targetCPUUtilizationPercentage:70tolerations:-key:dedicatedoperator:Equalvalue:productioneffect:NoScheduleaffinity:podAntiAffinity:preferredDuringSchedulingIgnoredDuringExecution:-weight:100podAffinityTerm:labelSelector:matchExpressions:-key:app.kubernetes.io/nameoperator:Invalues:[myapp]topologyKey:kubernetes.io/hostnametopologySpreadConstraints:-maxSkew:1topologyKey:topology.kubernetes.io/zonewhenUnsatisfiable:DoNotSchedulelabelSelector:matchLabels:app.kubernetes.io/name:myappvalues-staging.yaml— 预发环境覆盖:replicaCount:2image:tag:2.4.1-rc1ingress:enabled:trueclassName:nginxhosts:-host:myapp-staging.company.compaths:-path:/pathType:Prefixresources:requests:cpu:200mmemory:128Milimits:cpu:500mmemory:256Mi安装命令:# 开发环境(只用默认值)helminstallmyapp ./mychart-ndev# 预发环境(覆盖)helminstallmyapp ./mychart-nstaging-fvalues-staging.yaml# 生产环境(覆盖命令行微调)helminstallmyapp ./mychart-nprod\-fvalues-prod.yaml\--setreplicaCount5\--setimage.tag2.4.1-hotfix2.3 Helmfile 编排多 Chart 多环境helmfile.yaml:repositories:-name:bitnamiurl:https://charts.bitnami.com/bitnami-name:internalurl:https://harbor.internal/chartrepo/platformhelmDefaults:timeout:600wait:truecreateNamespace:trueenvironments:dev:values:-environments/dev.yamlstaging:values:-environments/staging.yamlprod:values:-environments/prod.yamlreleases:-name:redischart:bitnami/redisversion:18.2.1namespace:middlewarevalues:-values/redis-base.yaml-values/redis-{{.Environment.Name}}.yamlneeds:-middleware/ns# 先确保 namespace 存在-name:api-gatewaychart:internal/api-gatewayversion:2.1.0namespace:{{.Environment.Values.gatewayNamespace}}values:-values/api-gateway-base.yaml-values/api-gateway-{{.Environment.Name}}.yamlneeds:-middleware/redis-name:myappchart:internal/myappversion:1.0.0namespace:{{.Environment.Values.appNamespace}}values:-mychart/values.yaml-mychart/values-{{.Environment.Name}}.yamlneeds:-{{.Environment.Values.gatewayNamespace}}/api-gatewayset:-name:clusterNamevalue:{{.Environment.Values.clusterName}}environments/prod.yaml:clusterName:prod-cluster-01gatewayNamespace:gatewayappNamespace:productionenvironments/dev.yaml:clusterName:dev-cluster-01gatewayNamespace:dev-gatewayappNamespace:dev使用:# 同步所有 release 到 prod 環境的目标状态helmfile-eprodsync# 只更新 myapphelmfile-eprod-lnamemyappsync# 查看将要做的变更(dry-run)helmfile-eproddiff# 销毁所有 releasehelmfile-eprod destroy2.4 私有仓库:Harbor 作为 Helm Chart 仓库Harbor 从 2.0 开始原生支持 Helm Chart 仓库(不需要 ChartMuseum)。# 添加 Harbor 作为 Helm 仓库helm repoaddinternal https://harbor.internal/chartrepo/platform\--usernameadmin--passwordHarbor12345# 搜索 Charthelm search repo internal/# 推送本地 Chart 到 Harborhelm package mychart/ helm push myapp-1.0.0.tgz oci://harbor.internal/platform/myapp# OCI 方式(推荐,K8s 1.30 时代 Helm 3.14 默认支持 OCI)helm pull oci://harbor.internal/platform/myapp:1.0.0 helminstallmyapp oci://harbor.internal/platform/myapp:1.0.0-nprod2.5 CI/CD 集成:helm diff helm secretshelm diff— 变更预览(上线前必看):# 安装插件helm plugininstallhttps://github.com/databus23/helm-diff# 升级前预览差异helmdiffupgrade myapp ./mychart-nprod-fvalues-prod.yaml# 只看变更的资源(不看未变的)helmdiffupgrade myapp ./mychart-nprod-fvalues-prod.yaml --show-only-changedhelm secrets— 加密敏感 values:# 安装插件helm plugininstallhttps://github.com/jkroepke/helm-secrets# 加密 values 中的敏感字段helm secrets enc values-prod.yaml# 生成 values-prod.yaml.dec(解密文件),values-prod.yaml 中敏感字段被 sops 加密# 安装时自动解密helm secretsinstallmyapp ./mychart-nprod-fvalues-prod.yaml# helmfile 集成# helmfile.yaml 中 values 写法不变,helmfile 自动调用 helm-secrets 解密三、踩坑与排查踩坑 1:values 覆盖后 selector 标签不一致,Deployment 滚动更新卡死现象: 用-f values-prod.yaml覆盖了podLabels,升级后新 Pod 无法 Ready,旧 Pod 一直保留,最终超时。# 查看事件kubectl describe deploy myapp-nprod# 输出:# Warning ReplicaSetCreateFailure deployment controller cant find matching pods原因: Deployment 的selector.matchLabels是不可变字段,创建后不能改。如果你在_helpers.tpl里让 selector 受podLabels影响,覆盖后新旧 selector 不一致,Deployment 控制器匹配失败。解决: selector 标签永远只用selectorLabels(不含用户覆盖的 podLabels),podLabels 只加到template.metadata.labels:spec: selector: matchLabels: {{- include myapp.selectorLabels . | nindent 6 }} # 固定不变 template: metadata: labels: {{- include myapp.selectorLabels . | nindent 8 }} # 必须包含 selector 全部标签 {{- with .Values.podLabels }} # 用户额外标签 {{- toYaml . | nindent 8 }} {{- end }}踩坑 2:Chart 升级后旧 Release 残留资源未清理现象:helm upgrade后,旧的 ConfigMap/Secret 还在,新 Pod 读到了旧配置,行为异常。原因: Helm 只管理templates/里当前版本存在的资源。如果你在 v2.0.0 的 Chart 中删除了某个 ConfigMap 模板,Helm 升级时不会自动删除它——Helm 不会跟踪已删除的模板。解决:# 方法 1:手动清理kubectl delete configmap myapp-old-config-nprod# 方法 2:使用 helm-hooks 在升级前清理# 在 templates/ 里加一个 pre-upgrade hook:apiVersion:v1kind:Jobmetadata:name:{{include myapp.fullname .}}-cleanupannotations:helm.sh/hook:pre-upgradehelm.sh/hook-weight:-5helm.sh/hook-delete-policy:hook-succeededspec:template:spec:containers:-name:cleanupimage:bitnami/kubectl:1.30command:-kubectl-delete-configmap-myapp-old-config---ignore-not-found--n-{{.Release.Namespace}}restartPolicy:Never蹈坑 3:helm list 显示 RELEASE NOT FOUND,但资源还在集群里现象:helm list -n prod看不到 release,但kubectl get all -n prod资源都在。原因: Helm 的 release 信息存储在 Secret 里(helm.v2或helm.v3格式)。如果有人手动删除了这些 Secret, Helm 就失忆了。# 查看 Helm 存储的 release Secretkubectl get secrets-nprod-lownerhelm# 找到被删的 Secret 名称解决:# 方法 1:用 helm --force 重新安装(会重建 release Secret)helminstallmyapp ./mychart-nprod--replace# 方法 2:从集群资源重建(最安全的做法)# 先把现有资源导出,再手动构造 release Secret# 实际生产中建议:永远不要手动删除 Helm 的 release Secret!蹈坑 4:Helmfile sync 时 needs 依赖顺序不生效现象:helmfile sync先装了 myapp,redis 还没 Ready,myapp 启动失败连不上数据库。原因:needs只保证安装顺序,不保证依赖资源已经 Ready。Helmfile 发起 install 后就继续下一个,不等健康检查完成。解决: 在 helmDefaults 中设置wait: true,让每个 release 等到所有资源 Ready 再继续:helmDefaults:wait:truetimeout:600或者在 values 里设置 initContainers 等待依赖:initContainers:-name:wait-for-redisimage:busybox:1.36command:[sh,-c,until nc -z redis.middleware 6379; do echo waiting; sleep 2; done]四、最佳实践Chart 开发规范清单命名规范: 资源名用_helpers.tpl的fullname函数,不超过 63 字符,不含大写和特殊字符标签规范: 所有资源必须携带myapp.labelshelper,selector 必须用selectorLabels(不可变)values 规范: 基线 values.yaml 配置保守(1 replica,小资源),所有可配置项都声明默认值(list 设为[])模板规范:_helpers.tpl必须包含 fullname/labels/selectorLabels,大模板拆到partials/目录CRD 规范: CRD 放在crds/目录,Helm 安装前自动加载,升级时不更新(CRD 不可变)Hook 规范: pre-install/post-install/pre-upgrade/post-upgrade 按需使用,必须设置hook-delete-policy测试规范:templates/tests/下放 Pod 测试,helm test自动运行Helm 仓库治理规范框架治理维度规范工具/机制版本号严格 semver,禁止 -rc/-hotfix 后缀进仓库Chart.yaml version 字段 CI 校验兼容性kubeVersion 字段声明最低版本helm lint --with-subcharts安全Chart 内容扫描(镜像 CVE/rbac 权限)trivy scanner / helm charttesting签名OCI Chart 签名(cosign)helm push --sign审批Chart 上仓库前必须 PR Review lint passGitHub CI helm lint保留仓库只保留最近 N 个版本(防止膨胀)Harbor 保留策略 / 自动清理分类按 team/project 划分 projectHarbor project 权限隔离Values 分层最佳实践基线 values.yaml 只放安全默认值,不放环境特定值每个环境一个覆盖文件:values-{env}.yaml,命名一致命令行--set只用于临时调试,不上生产敏感值用helm-secrets加密,不入 Git覆盖文件也入 Git,但.dec解密文件不入库list 类型 values 在基线设为空[],覆盖文件完整提供用helm template本地渲染验证,不直接helm install五、小结Helm 从安装工具进化到包管理基础设施,关键在三件事:规范的 Chart 模板(helpers/labels/selector 做对)、分层的 values 管理(基线→覆盖→命令行优先级链搞清)、仓库治理(semver 版本审批签名清理)。再加上 Helmfile 编排多环境、helm diff 做变更预览、helm secrets 加密敏感值,你就有了从 dev 到 prod 的完整部署管线。Helm 和 Kustomize 不是互斥的——第三方 Chart 用 Helm 安,内部应用用 Kustomize 微调,两者共存才是生产常态。思考题如果你的团队有 50 个内部微服务,每个只需要镜像 tag 和 replica 数不同,你会选 Helm 还是 Kustomize?为什么?Helm 的 OCI 仓库模式(推到 Harbor OCI registry)相比传统 ChartMuseum HTTP 仓库,有什么优势和风险?一个 Chart 的version和appVersion什么时候应该同时递增?什么时候只递增version?延伸阅读Helm 官方文档 - Chart 最佳实践Helmfile GitHubHelm Diff PluginHelm Secrets PluginKustomize 官方文档Harbor Helm Chart 仓库管理