找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4042

积分

0

好友

532

主题
发表于 5 小时前 | 查看: 3| 回复: 0

适用读者:在 K8s 上做应用交付的运维工程师、SRE、DevOps 工程师。本文不讲"Helm 是什么",直接讲一个能上生产的 Chart 仓库要长什么样、模板怎么写才不出事、版本怎么管才不爆炸、怎么和 GitOps 串联。

引子:被 Helm 玩坏的一个下午

去年 Q3,我们做了一次基础组件的批量升级。要做的事很简单:把内部 chart 仓库里 7 个中间件(Kafka、Redis、MySQL、PostgreSQL、MongoDB、ES、RabbitMQ)从 chart version 1.4 升到 2.0,2.0 改了 values 结构和部分模板。

听起来很常规。但下午 4 点开始,告警群炸了:

  1. Kafka:升级后消费者 offset 提交失败,因为 chart 里把 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR 写成了固定 1,但生产集群 3 个 broker 期望 3。
  2. Redis:values 把 auth.password 改成了必填,但我们的运维文档没同步更新,6 套 Redis 集群全部启动失败。
  3. MySQL:依赖 chart 升级时版本号没对齐,主从的 chart 引用了不同的 mysql 版本,复制链路断了。
  4. PostgreSQL:升级后 pg_hba.conf 被模板里写错,外部连接直接拒绝。
  5. ES:chart 把 discovery.type 默认从 single-node 改成了 zen,但生产是 3 节点模式,集群组不起来。
  6. MongoDB:PVC 模板路径变了,老集群的 PVC 没被复用,触发新 PVC 绑定,空目录启动后数据全空。
  7. RabbitMQ:service account 名字改了,但因为我们 namespace 里有 NetworkPolicy 锁了 service account 引用,新 SA 起不来 pod。

复盘结论:不是 Helm 出了问题,是 chart 的设计 + 版本管理 + 升级流程出了问题

具体地说:

  • values 结构破坏性变更没标 version bump
  • chart 依赖没固定版本
  • 没有用 helm template 渲染出 YAML 跟 diff 工具对比
  • 没有 lint / unit test 流程
  • 升级是手动 helm upgrade,没有 GitOps 拉起来
  • 没有分环境灰度

这之后我们重写了整个 chart 仓库的设计规范、CI 流程、版本策略。这篇文章就是把那套规范系统化讲出来。

一、问题背景

Helm 在 K8s 应用交付里几乎是事实标准。但「会用 helm install」和「会做企业级 Helm Chart 仓库」是两个完全不同的事。常见痛点:

痛点 表现 根因
升级爆炸 chart 一升,应用起不来 values 破坏性变更没标,依赖没固定
多环境差异 dev / staging / prod 配置混乱 values 散落多处,模板里硬编码
测试困难 改一个 values 不知道影响哪些环境 没有 unit test / integration test
仓库管理 chart push 上去就是"新版本" 没有版本策略、签名、审计
回滚困难 升级错了回不去 没有 GitOps、版本记录不全
重复劳动 多个 chart 重复实现同样模板 缺少 common chart、缺少子 chart
安全审计 谁改了什么、什么时候、什么版本不可追 没有 chart 签名、没有 changelog
Helm 自身限制 模板语法错、whitespace 难调 没理解 _helpers.tpl 命名约定

对应的解决方向:

  • 工程化 Chart 设计:固定目录结构、命名约定、values 规范
  • 测试体系:lint、unit test、integration test、render diff
  • 仓库管理:ChartMuseum / Harbor / OCI Registry,版本策略,签名
  • GitOps 串联:ArgoCD / Flux + Helm + 版本回滚
  • 升级流程:分环境灰度、暂停窗口、自动回滚

二、适用场景

不是所有团队都需要自建 Chart 仓库。按规模和场景选型:

团队现状 推荐方案 原因
几个人 + 几个 chart + 单一 K8s 集群 GitHub Pages + helm-s3 plugin / OCI Registry 零运维,够用
中等规模,10+ chart,3-5 个环境 ChartMuseum + S3/MinIO + Helm CLI 轻量,标准协议
大规模,50+ chart,多团队协作 Harbor(OCI)+ GitLab CI + ArgoCD 企业级,审计、签名、RBAC
强合规,金融/政企 OCI Registry + cosign 签名 + Sigstore 满足审计要求
不允许公网访问 自建 ChartMuseum + 内网 DNS 完全内网

不推荐自建 Chart 仓库的场景:

  • 内部只有 1-2 个 chart → 用 GitOps repo 里的本地 chart 即可
  • 没有 K8s 集群 → 用 docker compose 替代
  • 只有单个 namespace 的几个 deployment → 用 Kustomize 更轻

三、核心知识点

3.1 Helm 3 与 Helm 2 的差异

Helm 3(2019 年发布)已经去除了 Tiller,所有 release 信息存在 K8s secret / configmap 里。如果还在用 Helm 2,必须升级:

维度 Helm 2 Helm 3
架构 Client + Tiller(集群内) Client-only(kubeconfig 鉴权)
Release 存储 Tiller 维护 etcd K8s secret(命名空间下)
默认驱动 ConfigMaps + Secrets Secrets(默认)
Chart 名 chart-name chart-name(一样)
依赖 requirements.yaml + requirements.lock Chart.yamldependencies + Chart.lock
三方支持 全是 chart 形式 chart 形式 + OCI artifact
helm install --name 支持 移除(--generate-name 替代)
回滚 helm rollback
安全模型 Tiller 集群权限大 kubeconfig 鉴权,最小权限

重点:本文章所有内容基于 Helm 3。Helm 2 已 EOL,不要再使用。

3.2 Chart 的核心结构

mychart/
├── Chart.yaml              # Chart 元信息(必填)
├── Chart.lock              # 依赖锁定文件(自动生成)
├── values.yaml             # 默认 values(必填)
├── values.schema.json      # values 校验(可选)
├── charts/                 # 依赖 chart 目录(自动生成)
│   ├── mysql-9.10.0.tgz
│   └── redis-17.0.0.tgz
├── templates/              # 模板目录
│   ├── _helpers.tpl        # 命名模板
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── configmap.yaml
│   ├── secret.yaml
│   ├── pvc.yaml
│   ├── serviceaccount.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── NOTES.txt           # post-install 提示
│   └── tests/              # helm test
│       └── test-connection.yaml
├── ci/                     # CI 配置
│   ├── lint.yaml
│   ├── unittest.yaml
│   └── ct.yaml
├── .helmignore             # 类似 .gitignore
├── README.md               # Chart 说明
└── CHANGELOG.md            # 变更日志

关键文件作用:

  • Chart.yaml:name、version、appVersion、dependencies、keywords、maintainers
  • values.yaml:默认配置,所有可配置项必须出现
  • _helpers.tpl:命名模板(define),避免重复
  • templates/:K8s 资源模板
  • Chart.lock:依赖锁定(必须 commit 进 Git)

3.3 Chart 版本号与 appVersion

Chart 有两个版本字段,含义不同:

字段 含义 变化条件
version Chart 本身的版本 Chart 模板、values 结构、依赖变化时改
appVersion Chart 打包的应用版本 应用版本变化时改(如 mysql 8.0.32 → 8.0.33)

version 必须遵循 SemVer:MAJOR.MINOR.PATCH

  • MAJOR:破坏性变更(values 结构、模板输出、依赖主版本)
  • MINOR:新功能(新增 values 选项、新的模板文件)
  • PATCH:bug 修复(默认 values 调整、注释修正)

示例:

apiVersion: v2
name: mychart
description: A production-grade application chart
type: application
version: 1.4.2  # Chart 1.4.2
appVersion: "1.21.0"  # 应用 1.21.0

升级原则:

  • Chart 改 template → 升 PATCH 或 MINOR
  • Chart 改 values 必填项 / 删字段 → 升 MAJOR
  • 应用版本升级(appVersion 改)→ 至少升 PATCH

3.4 依赖管理

Chart 可以依赖其他 chart。在 Chart.yaml 中声明:

dependencies:
- name: mysql
  version: "9.10.0"
  repository: "https://charts.bitnami.com/bitnami"
  condition: mysql.enabled
- name: redis
  version: "17.0.0"
  repository: "https://charts.bitnami.com/bitnami"
  condition: redis.enabled
  alias: cache

锁定依赖:

helm dependency update   # 生成 Chart.lock
helm dependency build    # 下载到 charts/ 目录

Chart.lock 完整内容:

dependencies:
- name: mysql
  repository: https://charts.bitnami.com/bitnami
  version: 9.10.0
- name: redis
  repository: https://charts.bitnami.com/bitnami
  version: 17.0.0
  digest: sha256:xxxxxxxxxxxxx
  generated: "2026-01-15T10:00:00.000000000Z"

关键:Chart.lock 必须 commit,否则不同机器 helm dependency update 会拉到不同版本。

3.5 模板语法核心

Helm 模板基于 Go template,扩展了 Sprig 函数库和部分 Helm 专属函数。

3.5.1 基础替换

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.appName }}
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
        - name: {{ .Values.appName }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"

3.5.2 流程控制

{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ include "mychart.fullname" . }}
{{- end }}

3.5.3 范围

env:
  {{- range $key, $value := .Values.env }}
  - name: {{ $key }}
    value: {{ $value | quote }}
  {{- end }}

3.5.4 函数

# 字符串
name: {{ .Values.appName | default "myapp" | quote | lower }}
# 数字
replicas: {{ .Values.replicaCount | int }}
# 列表
args: {{ .Values.args | toYaml }}
# 字典
labels: {{- toYaml .Values.commonLabels | nindent 4 }}
# 编码
secret: {{ .Values.secret | b64enc | quote }}

3.5.5 命名模板(_helpers.tpl

{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "mychart.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 -}}

使用:

metadata:
  name: {{ include "mychart.fullname" . }}

3.5.6 关键函数

函数 用途 例子
quote 加双引号 {{ "abc" \| quote }}"abc"
default 默认值 {{ .Values.x \| default "y" }}
required 必填校验 {{ required "msg" .Values.x }}
toYaml 字典转 YAML {{ toYaml .Values.l \| nindent 4 }}
nindent 换行+缩进 {{- toYaml .Values.l \| nindent 4 }}
tpl 渲染字符串 {{ tpl .Values.templateStr . }}
lookup 查 K8s 资源 {{ (lookup "v1" "ConfigMap" "ns" "name").data }}
b64enc / b64dec base64 {{ "abc" \| b64enc }}
sha256sum 哈希 {{ "abc" \| sha256sum }}
randAlphaNum 随机串 {{ randAlphaNum 10 }}
include 命名模板(可作为函数) {{ include "mychart.labels" . }}
template 命名模板(渲染为字符串) {{ template "mychart.name" . }}

3.5.7 include vs template

维度 include template
输出 渲染后的字符串 渲染后的字符串
能否 pipe 进其他函数 不能
推荐 推荐 几乎弃用

正确示例:

labels:
  app.kubernetes.io/name: {{ include "mychart.name" . }}
  app.kubernetes.io/instance: {{ .Release.Name }}
  app.kubernetes.io/managed-by: {{ .Release.Service }}

3.6 Hooks

Hook 是在 release 生命周期某些节点执行的特殊资源:

Hook 触发时机
pre-install install 之前
post-install install 之后
pre-delete delete 之前
post-delete delete 之后
pre-upgrade upgrade 之前
post-upgrade upgrade 之后
pre-rollback rollback 之前
post-rollback rollback 之后
test helm test 触发

示例:job hook 用于数据库迁移

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "mychart.fullname" . }}-migrate
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["/app/migrate.sh"]

3.7 OCI Registry

Helm 3 支持把 chart 存为 OCI artifact,与容器镜像用同一仓库:

# 登录 OCI registry
helm registry login harbor.example.com -u admin -p Harbor12345

# 推送 chart
helm push mychart-1.0.0.tgz oci://harbor.example.com/charts

# 列出
helm list oci://harbor.example.com/charts

# 拉取
helm pull oci://harbor.example.com/charts/mychart --version 1.0.0

实际生产中,helm registry login 密码建议从 ~/.docker/config.json 读,或通过环境变量 HELM_REGISTRY_CONFIG

3.8 仓库协议

协议 工具 例子
HTTP API(chart 仓库) ChartMuseum https://charts.example.com
OCI artifact Harbor, Gitea, ACR, ECR oci://registry.example.com/charts
本地目录 离线 file://./charts
HTTP 文件 静态文件 https://example.com/index.yaml

四、整体排查或实施思路

按"先建规范,再搭仓库,最后接 GitOps"的顺序:

  1. 第一步:Chart 设计规范(1-2 天)

    • 目录结构、命名约定、values 规范
    • 必填字段、版本号策略
    • 模板中禁止硬编码
  2. 第二步:基础公共库(1 周)

    • 抽取公共命名模板(common chart / library chart)
    • _helpers.tpl_labels.tpl_tplvalues.tpl
    • 抽出公共 chart 包(library chart)
  3. 第三步:单元测试(1 周)

    • helm-unittest 插件
    • 每个 chart 至少 3-5 个核心测试用例
    • CI 中跑
  4. 第四步:CI 集成(3-5 天)

    • helm linthelm templatect lint
    • 渲染后跟 K8s 集群中的实际资源 diff
    • 阻塞 main 分支
  5. 第五步:仓库搭建(2-3 天)

    • 选 ChartMuseum / Harbor / OCI
    • 配置权限、审计、签名
    • 推送流程
  6. 第六步:GitOps 串联(1-2 周)

    • ArgoCD / Flux 监听 chart 仓库
    • 多环境分层
    • 自动同步策略
  7. 第七步:升级流程(持续)

    • 灰度发布
    • 回滚预案
    • 版本审计

五、实战步骤

5.1 创建第一个生产级 Chart

# 1. 创建 chart 骨架
helm create mychart
cd mychart

# 2. 清理默认模板(保留 _helpers.tpl)
rm templates/deployment.yaml
rm templates/service.yaml
rm templates/serviceaccount.yaml
rm templates/hpa.yaml
rm templates/ingress.yaml
rm templates/secret.yaml
rm templates/configmap.yaml
rm templates/pvc.yaml
rm templates/NOTES.txt
rm templates/tests/test-connection.yaml

5.2 写 Chart.yaml

apiVersion: v2
name: mychart
description: |
  Production-grade application chart. Use this as a template for new charts.
type: application

# Chart 版本
version: 0.1.0
# 应用版本
appVersion: "1.0.0"

# 关键字(helm search 可见)
keywords:
  - application
  - microservice
  - production

# 维护者
maintainers:
  - name: Platform Team
    email: platform@example.com
    url: https://wiki.example.com/platform

# 仓库地址
home: https://github.com/example/mychart
sources:
  - https://github.com/example/mychart-src

# 图标
icon: https://example.com/icon.png

# Kubernetes 版本要求
kubeVersion: ">=1.24.0-0"

# 应用分类
annotations:
  category: Application
  licenses: Apache-2.0

# 依赖
dependencies:
  - name: common
    version: "2.x.x"
    repository: "oci://registry.example.com/charts"
    condition: common.enabled

5.3 写 values.yaml

# 应用配置
image:
  repository: myorg/myapp
  tag: ""  # 默认用 .Chart.AppVersion
  pullPolicy: IfNotPresent

imagePullSecrets: []

# 副本
replicaCount: 3

# 服务账户
serviceAccount:
  create: true
  annotations: {}
  name: ""

# Pod 配置
podAnnotations: {}
podLabels: {}
podSecurityContext: {}

# 资源
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

# 探针
livenessProbe:
  httpGet:
    path: /healthz
    port: http
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: http
  initialDelaySeconds: 5
  periodSeconds: 5

# 调度
nodeSelector: {}
tolerations: []
affinity: {}
topologySpreadConstraints: []

# 服务
service:
  type: ClusterIP
  port: 80
  targetPort: 8080

# Ingress
ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts:
    - host: chart-example.local
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls: []

# HPA
autoscaling:
  enabled: false
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

# ConfigMap
config:
  log_level: info
  env: production
  features: {}

# Secret(实际值不放这里)
secret:
  enabled: false
  existingSecret: ""
  data: {}

# 数据库
database:
  enabled: false
  type: postgresql
  host: ""
  port: 5432
  name: ""
  user: ""
  existingSecret: ""

# 中间件
redis:
  enabled: false
  host: ""
  port: 6379
  existingSecret: ""

5.4 写 _helpers.tpl

{{/*
Expand the name of the chart.
*/}}
{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "mychart.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 }}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "mychart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
ServiceAccount name
*/}}
{{- define "mychart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "mychart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
Image reference
*/}}
{{- define "mychart.image" -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion }}
{{- printf "%s:%s" .Values.image.repository $tag }}
{{- end }}

{{/*
Validate required values
*/}}
{{- define "mychart.validateValues" -}}
{{- if and .Values.database.enabled (not .Values.database.host) }}
{{- fail "database.host is required when database.enabled is true" }}
{{- end }}
{{- end }}

5.5 写 templates/deployment.yaml

{{- include "mychart.validateValues" . }}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "mychart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "mychart.selectorLabels" . | nindent 8 }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      annotations:
        # 强制 deployment 在配置变化时滚动
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "mychart.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            runAsNonRoot: true
            runAsUser: 65532
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
          image: {{ include "mychart.image" . | quote }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          livenessProbe:
            {{- toYaml .Values.livenessProbe | nindent 12 }}
          readinessProbe:
            {{- toYaml .Values.readinessProbe | nindent 12 }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          envFrom:
            - configMapRef:
                name: {{ include "mychart.fullname" . }}
            {{- if .Values.secret.enabled }}
            - secretRef:
                name: {{ include "mychart.fullname" . }}
            {{- end }}
          {{- if .Values.database.enabled }}
          env:
            - name: DB_HOST
              value: {{ .Values.database.host | quote }}
            - name: DB_PORT
              value: {{ .Values.database.port | default 5432 | quote }}
            - name: DB_NAME
              value: {{ .Values.database.name | quote }}
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.database.existingSecret }}
                  key: username
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: {{ .Values.database.existingSecret }}
                  key: password
            {{- end }}
          volumeMounts:
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: tmp
          emptyDir: {}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.topologySpreadConstraints }}
      topologySpreadConstraints:
        {{- toYaml . | nindent 8 }}
      {{- end }}

5.6 写 templates/configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
data:
  {{- range $key, $value := .Values.config }}
  {{ $key }}: {{ $value | quote }}
  {{- end }}
  # 渲染额外配置文件
  {{- if .Values.config.features }}
  features.yaml: |
    {{- toYaml .Values.config.features | nindent 4 }}
  {{- end }}

5.7 写 templates/secret.yaml

{{- if .Values.secret.enabled }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
type: Opaque
data:
  {{- range $key, $value := .Values.secret.data }}
  {{ $key }}: {{ $value | b64enc | quote }}
  {{- end }}
{{- end }}

5.8 写 templates/_tplvalues.tpl(通用字典渲染)

{{/*
Render a dict with nindent support.
Usage: {{- include "mychart.tplvalues.render" ( dict "value" .Values.path "context" $ ) }}
*/}}
{{- define "mychart.tplvalues.render" -}}
    {{- if typeIs "string" .value }}
        {{- tpl .value .context }}
    {{- else }}
        {{- tpl (.value | toYaml) .context }}
    {{- end }}
{{- end -}}

5.9 校验 values 格式

values.schema.json

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["image", "replicaCount", "service"],
  "properties": {
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": {"type": "string", "minLength": 1},
        "tag": {"type": "string"},
        "pullPolicy": {"enum": ["Always", "IfNotPresent", "Never"]}
      }
    },
    "replicaCount": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100
    },
    "service": {
      "type": "object",
      "required": ["port", "targetPort"],
      "properties": {
        "port": {"type": "integer", "minimum": 1, "maximum": 65535},
        "targetPort": {"type": "integer", "minimum": 1, "maximum": 65535},
        "type": {"enum": ["ClusterIP", "NodePort", "LoadBalancer"]}
      }
    }
  }
}

5.10 本地调试

# 1. Lint
helm lint ./mychart

# 2. 模板渲染并保存
helm template myrelease ./mychart \
  --namespace prod \
  --values values-prod.yaml \
  > rendered.yaml

# 3. 干跑安装
helm install myrelease ./mychart \
  --namespace prod \
  --values values-prod.yaml \
  --dry-run \
  --debug

# 4. 干跑升级
helm upgrade myrelease ./mychart \
  --namespace prod \
  --values values-prod.yaml \
  --dry-run \
  --debug

预期输出(干跑):

NAME: myrelease
LAST DEPLOYED: Tue Jun 17 10:00:00 2026
NAMESPACE: prod
STATUS: pending-install
REVISION: 1
TEST SUITE: None
HOOKS:
MANIFEST:
---
# Source: mychart/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
...

5.11 添加依赖

Chart.yaml 增加:

dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled
    alias: db

更新依赖:

helm dependency update ./mychart
# 生成 Chart.lock + 下载 postgresql-12.x.x.tgz 到 charts/

# 验证
helm dependency list ./mychart
# NAME        VERSION   REPOSITORY                                   CONDITION
# db          12.x.x    https://charts.bitnami.com/bitnami          postgresql.enabled

5.12 安装到集群

# 1. 创建 namespace
kubectl create namespace prod

# 2. 准备生产 values
cat > values-prod.yaml <<EOF
replicaCount: 5
image:
  repository: myorg/myapp
  tag: "1.21.0"
resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 2000m
    memory: 2Gi
database:
  enabled: true
  host: db.prod.svc
  port: 5432
  name: myapp
  user: myapp
  existingSecret: myapp-db-credentials
secret:
  enabled: true
  data:
    apiKey: "your-api-key-here"
EOF

# 3. 模拟跑一次
helm install myapp ./mychart \
  --namespace prod \
  --values values-prod.yaml \
  --dry-run

# 4. 真正安装
helm install myapp ./mychart \
  --namespace prod \
  --values values-prod.yaml \
  --wait \
  --timeout 5m

# 5. 查看
helm list -n prod
kubectl get all -n prod -l app.kubernetes.io/instance=myapp

5.13 升级

# 1. 修改 values
vi values-prod.yaml
# 改 image.tag: 1.21.0 -> 1.22.0

# 2. 干跑升级看 diff
helm diff upgrade myapp ./mychart \
  --namespace prod \
  --values values-prod.yaml
# (需要 helm-diff 插件)

# 3. 安装 diff 插件
helm plugin install https://github.com/databus23/helm-diff

# 4. 真实升级
helm upgrade myapp ./mychart \
  --namespace prod \
  --values values-prod.yaml \
  --wait \
  --timeout 10m

# 5. 查看升级历史
helm history myapp -n prod
# REVISION  UPDATED                   STATUS     CHART           APP VERSION  DESCRIPTION
# 1         Tue Jun 17 10:00:00 2026  superseded mychart-0.1.0   1.0.0        Install complete
# 2         Tue Jun 17 14:00:00 2026  deployed   mychart-0.1.0   1.0.0        Upgrade complete

5.14 回滚

# 1. 查看历史
helm history myapp -n prod

# 2. 回滚到上一版本
helm rollback myapp -n prod
# 或指定 REVISION
helm rollback myapp 1 -n prod

# 3. 验证
helm list -n prod
kubectl get pods -n prod -l app.kubernetes.io/instance=myapp

5.15 卸载

# 1. 模拟跑(先看会删什么)
helm uninstall myapp -n prod --dry-run --debug

# 2. 真实卸载
helm uninstall myapp -n prod

# 3. 验证
kubectl get all -n prod -l app.kubernetes.io/instance=myapp
# 预期:No resources found

# 4. 保留历史(默认是删的)
helm uninstall myapp -n prod --keep-history

5.16 写单元测试

安装 helm-unittest

helm plugin install https://github.com/helm-unittest/helm-unittest

templates/deployment_test.yaml

suite: test deployment
templates:
  - deployment.yaml
release:
  name: myapp
  namespace: prod
values:
  - values.yaml
tests:
  - it: should render deployment with default values
    asserts:
      - hasDocuments:
          count: 1
      - containsDocument:
          kind: Deployment
          name: myapp
      - equal:
          path: spec.replicas
          value: 3
      - equal:
          path: spec.template.spec.containers[0].image
          value: "myorg/myapp:1.0.0"

  - it: should respect replicaCount override
    set:
      replicaCount: 5
    asserts:
      - equal:
          path: spec.replicas
          value: 5

  - it: should set resource limits
    set:
      resources:
        limits:
          cpu: 1
          memory: 1Gi
    asserts:
      - equal:
          path: spec.template.spec.containers[0].resources.limits.cpu
          value: 1

  - it: should fail when required value missing
    set:
      database:
        enabled: true
        host: ""
    asserts:
      - failedTemplate:
          errorMessage: "database.host is required when database.enabled is true"

  - it: should not render secret when disabled
    set:
      secret:
        enabled: false
    templates:
      - secret.yaml
    asserts:
      - hasDocuments:
          count: 0

  - it: should render secret when enabled
    set:
      secret:
        enabled: true
        data:
          apiKey: "test"
    templates:
      - secret.yaml
    asserts:
      - containsDocument:
          kind: Secret
          apiVersion: v1

跑测试:

helm unittest ./mychart
# 预期:
# PASSED  test deployment
# Tests: 6 passed, 0 failed, 0 skipped

5.17 写 lint 配置

ci/lintconf.yaml

chart:
  - mychart
  - subchart1
  - subchart2
check-charts: true

ci/chart_schema.yaml

chart:
  - mychart
validate-maintainers: false
lint-conf: ci/lintconf.yaml

跑 chart-testing:

# 安装 ct(chart-testing)
brew install chart-testing  # macOS
# 或
curl -sSL https://github.com/helm/chart-testing/releases/download/v3.10.1/chart-testing_3.10.1_linux_amd64.tar.gz | tar xz
sudo mv ct /usr/local/bin/

# 跑 lint
ct lint --config ci/chart_schema.yaml
# 预期:
# Linting chart...
# Linting mychart...
# All charts linted successfully

# 跑 install 演练(需要 K8s 集群)
ct install --config ci/chart_schema.yaml

5.18 搭建 ChartMuseum

5.18.1 Docker 启动

docker run -d \
  --name chartmuseum \
  -p 8080:8080 \
  -e DEBUG=true \
  -e STORAGE=local \
  -e STORAGE_LOCAL_ROOTDIR=/charts \
  -v /opt/chartmuseum:/charts \
  ghcr.io/helm/chartmuseum:v0.16.0

5.18.2 启用 S3 后端

docker run -d \
  --name chartmuseum \
  -p 8080:8080 \
  -e STORAGE=amazon \
  -e STORAGE_AMAZON_BUCKET=my-charts \
  -e STORAGE_AMAZON_PREFIX=charts \
  -e STORAGE_AMAZON_REGION=cn-north-1 \
  -e AWS_ACCESS_KEY_ID=xxx \
  -e AWS_SECRET_ACCESS_KEY=yyy \
  ghcr.io/helm/chartmuseum:v0.16.0

生产环境不推荐用环境变量传 AWS key,建议用 IAM Role。

5.18.3 Helm 客户端配置

# 添加仓库
helm repo add chartmuseum https://charts.example.com
helm repo update

# 推送 chart
helm push ./mychart-0.1.0.tgz chartmuseum

# 列出
helm search repo chartmuseum/mychart

# 拉取
helm pull chartmuseum/mychart --version 0.1.0

5.18.4 通过 API 操作

# 上传
curl -F "chart=@mychart-0.1.0.tgz" https://charts.example.com/api/charts

# 列表
curl https://charts.example.com/api/charts | jq

# 删除(需要开启 ALLOW_OVERWRITE)
curl -X DELETE https://charts.example.com/api/charts/mychart/0.1.0

5.19 搭建 OCI Registry(推荐)

用 Harbor 是最直接的 OCI Registry 方案。

# 1. 在 Harbor 创建项目 charts
# 2. 登录
helm registry login harbor.example.com -u admin

# 3. 推送
helm push ./mychart-0.1.0.tgz oci://harbor.example.com/charts

# 4. 列出
helm list oci://harbor.example.com/charts

# 5. 拉取
helm pull oci://harbor.example.com/charts/mychart --version 0.1.0

5.20 GitOps 集成(ArgoCD)

5.20.1 Application 资源

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-prod
  namespace: argocd
spec:
  project: production
  source:
    repoURL: https://charts.example.com
    targetRevision: 1.4.2
    chart: mychart
    helm:
      releaseName: myapp
      valueFiles:
        - values-prod.yaml
      parameters:
        - name: replicaCount
          value: "5"
        - name: image.tag
          value: "1.22.0"
  destination:
    server: https://kubernetes.default.svc
    namespace: prod
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
      allowEmpty: false
    syncOptions:
      - CreateNamespace=false
      - PrunePropagationPolicy=foreground
      - PruneLast=true
    retry:
      limit: 5
      backoff:
        duration: 10s
        factor: 2
        maxDuration: 5m

5.20.2 多环境分层

argocd/
├── applications/
│   ├── myapp-dev.yaml
│   ├── myapp-staging.yaml
│   └── myapp-prod.yaml
└── projects/
    └── production.yaml

不同环境用不同 targetRevision:

# myapp-dev.yaml
source:
  chart: mychart
  targetRevision: 1.4.2  # 跟 staging 同步
# myapp-staging.yaml
source:
  chart: mychart
  targetRevision: 1.4.2  # 跟 prod 同步
# myapp-prod.yaml
source:
  chart: mychart
  targetRevision: 1.4.2  # 生产固化版本

5.20.3 自动回滚

ArgoCD 内置同步失败时自动回滚:

syncPolicy:
  automated:
    selfHeal: true
  retry:
    limit: 3

ArgoCD ApplicationSet + Progressive Delivery 是更高级的灰度方案:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: myapp
spec:
  generators:
    - list:
        elements:
          - cluster: dev
            url: https://kubernetes.default.svc
            revision: 1.4.3
          - cluster: staging
            url: https://staging.example.com
            revision: 1.4.3
  template:
    metadata:
      name: 'myapp-{{cluster}}'
    spec:
      project: production
      source:
        repoURL: https://charts.example.com
        targetRevision: '{{revision}}'
        chart: mychart
      destination:
        server: '{{url}}'
        namespace: myapp

5.20.4 ArgoCD Image Updater

myapp chart 可以被 ArgoCD Image Updater 自动跟踪镜像 tag:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  annotations:
    argocd-image-updater.argoproj.io/image-list: myapp=myorg/myapp:1.22.0
    argocd-image-updater.argoproj.io/myapp.update-strategy: latest
    argocd-image-updater.argoproj.io/myapp.allow-tags: regexp:^1\.\d+\.\d+$
spec:
  source:
    chart: mychart
    targetRevision: 1.4.2

六、常用命令

6.1 Chart 创建与调试

# 创建
helm create mychart
# Lint
helm lint ./mychart
# 模板渲染
helm template myrelease ./mychart
helm template myrelease ./mychart --values values-prod.yaml
# 干跑
helm install myrelease ./mychart --dry-run --debug
helm upgrade myrelease ./mychart --dry-run --debug
# 渲染并保存
helm template myrelease ./mychart --values values-prod.yaml > rendered.yaml
# 验证渲染结果
kubectl apply --dry-run=client -f rendered.yaml

6.2 依赖管理

# 更新依赖
helm dependency update ./mychart
# 列出依赖
helm dependency list ./mychart
# 构建依赖(下载到 charts/)
helm dependency build ./mychart
# 列出已下载的 chart
ls -la ./mychart/charts/

6.3 安装与升级

# 安装
helm install myrelease ./mychart --namespace prod --values values.yaml
helm install myrelease ./mychart --namespace prod --values values.yaml --wait --timeout 5m
helm install myrelease ./mychart --namespace prod --values values.yaml --create-namespace
# 升级
helm upgrade myrelease ./mychart --namespace prod --values values.yaml
helm upgrade --install myrelease ./mychart --namespace prod --values values.yaml
# 历史
helm history myrelease -n prod
# 状态
helm status myrelease -n prod
# 列出所有 release
helm list -A
helm list -A --filter 'mychart'
# 卸载
helm uninstall myrelease -n prod
helm uninstall myrelease -n prod --keep-history

6.4 回滚

# 回滚到上一版本
helm rollback myrelease -n prod
# 回滚到指定版本
helm rollback myrelease 3 -n prod
# 查看历史
helm history myrelease -n prod

6.5 仓库

# 添加
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add chartmuseum https://charts.example.com
# 更新
helm repo update
# 列出
helm repo list
# 搜索
helm search repo nginx
helm search hub nginx
# 移除
helm repo remove chartmuseum

6.6 OCI

# 登录
helm registry login harbor.example.com -u admin
# 推送
helm push ./mychart-0.1.0.tgz oci://harbor.example.com/charts
# 列出
helm list oci://harbor.example.com/charts
# 拉取
helm pull oci://harbor.example.com/charts/mychart --version 0.1.0

6.7 插件

# 安装 diff
helm plugin install https://github.com/databus23/helm-diff
# 安装 unittest
helm plugin install https://github.com/helm-unittest/helm-unittest
# 列出
helm plugin list
# 升级
helm plugin update diff
# 卸载
helm plugin uninstall diff

6.8 单元测试

# 跑测试
helm unittest ./mychart
# 详细输出
helm unittest ./mychart -v
# 指定文件
helm unittest ./mychart -f templates/deployment_test.yaml

6.9 Chart 打包

# 打包
helm package ./mychart
# 生成 mychart-0.1.0.tgz
# 同时更新 index.yaml
helm repo index ./
# 推送
helm push ./mychart-0.1.0.tgz chartmuseum

6.10 Show

# 显示 chart 信息
helm show chart ./mychart
# 显示 values
helm show values ./mychart
# 显示 README
helm show readme ./mychart
# 显示所有
helm show all ./mychart

6.11 K8s 资源查看

# 看渲染后的 K8s 资源
kubectl get all -n prod -l app.kubernetes.io/instance=myapp
# 看 release 对应的 secret
kubectl get secret -n prod -l owner=helm
# 看 release 状态
kubectl get configmap -n prod -l owner=helm,status=released

七、配置示例汇总

7.1 完整 Chart.yaml

apiVersion: v2
name: production-app
description: |
  Production application chart for MyApp service.
  Includes ConfigMap, Secret, Deployment, Service, Ingress, HPA.
type: application

version: 1.4.2
appVersion: "1.22.0"

kubeVersion: ">=1.24.0-0"

home: https://wiki.example.com/charts/production-app
sources:
  - https://github.com/example/production-app

icon: https://example.com/icon.png

keywords:
  - production
  - application
  - microservice

maintainers:
  - name: Platform Team
    email: platform@example.com

dependencies:
  - name: common
    version: "2.x.x"
    repository: "oci://registry.example.com/charts"
    condition: common.enabled
  - name: postgresql
    version: "12.5.6"
    repository: "https://charts.bitnami.com/bitnami"
    condition: database.postgresql.enabled
    alias: pg
  - name: redis
    version: "17.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: cache.redis.enabled
    alias: cache

annotations:
  category: Application
  licenses: Apache-2.0

7.2 多环境 values 组织

mychart/
├── values.yaml              # 默认
├── values-dev.yaml          # 开发
├── values-staging.yaml      # 预发
├── values-prod.yaml         # 生产
├── values-prod-shanghai.yaml # 生产-上海
└── values-prod-beijing.yaml # 生产-北京

values-prod.yaml:

replicaCount: 5
image:
  repository: myorg/myapp
  tag: "1.22.0"
  pullPolicy: IfNotPresent
resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 2000m
    memory: 2Gi
autoscaling:
  enabled: true
  minReplicas: 5
  maxReplicas: 20
ingress:
  enabled: true
  className: nginx
  hosts:
    - host: app.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls
database:
  enabled: true
  postgresql:
    enabled: false  # 走外部
  host: pg.prod.svc
  port: 5432
  name: myapp
  user: myapp
  existingSecret: myapp-db-creds
cache:
  redis:
    enabled: true
    host: redis.prod.svc
    port: 6379
    existingSecret: myapp-redis-creds

7.3 CI 中跑测试的完整示例

.gitlab-ci.yml

stages:
  - lint
  - test
  - package
  - publish

variables:
  HELM_VERSION: "3.14.4"
  CT_VERSION: "3.10.1"

# 1. Lint
helm-lint:
  stage: lint
  image: alpine/helm:${HELM_VERSION}
  script:
    - helm version
    - helm lint ./charts/mychart
    - helm lint ./charts/mychart --values ./charts/mychart/values-prod.yaml
  only:
    changes:
      - charts/mychart/**/*

# 2. 单元测试
helm-unittest:
  stage: test
  image: alpine/helm:${HELM_VERSION}
  before_script:
    - helm plugin install https://github.com/helm-unittest/helm-unittest
  script:
    - helm unittest ./charts/mychart
  only:
    changes:
      - charts/mychart/**/*

# 3. 渲染 + kubeconform 校验
helm-render:
  stage: test
  image:
    name: alpine/helm:${HELM_VERSION}
    entrypoint: [""]
  before_script:
    - apk add --no-cache curl
    - curl -sSL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar xz
    - mv kubeconform /usr/local/bin/
  script:
    - helm template myapp ./charts/mychart --values ./charts/mychart/values-prod.yaml > rendered.yaml
    - kubeconform -strict -summary rendered.yaml
  only:
    changes:
      - charts/mychart/**/*

# 4. 打包
helm-package:
  stage: package
  image: alpine/helm:${HELM_VERSION}
  script:
    - helm package ./charts/mychart --destination ./dist
  artifacts:
    paths:
      - dist/*.tgz
  only:
    - main
    - tags

# 5. 推送到 ChartMuseum
helm-publish:
  stage: publish
  image: alpine/helm:${HELM_VERSION}
  before_script:
    - helm plugin install https://github.com/chartmuseum/helm-push
  script:
    - helm cm-push ./dist/mychart-*.tgz chartmuseum
  only:
    - main
    - tags

7.4 K8s Secret 提供 values

# 1. 创建 K8s secret
kubectl create secret generic myapp-values \
  --from-file=values-prod.yaml=./values-prod.yaml \
  -n argocd

# 2. ArgoCD Application 引用
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  source:
    helm:
      valueFiles:
        - $values/values-prod.yaml

7.5 Hook 完整示例(DB 迁移)

templates/migrate-job.yaml

{{- if .Values.migration.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "mychart.fullname" . }}-migrate
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    metadata:
      name: {{ include "mychart.fullname" . }}-migrate
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: {{ include "mychart.image" . | quote }}
          command: ["/app/migrate.sh"]
          envFrom:
            - secretRef:
                name: {{ include "mychart.fullname" . }}
      backoffLimit: 3
{{- end }}

7.6 NOTES.txt(用户友好提示)

templates/NOTES.txt

应用已成功部署!

访问地址:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
  https://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
  export NODE_PORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mychart.fullname" . }})
  export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
  kubectl get -o jsonpath="{.status.loadBalancer.ingress[0].ip}" services {{ include "mychart.fullname" . }}
{{- else if contains "ClusterIP" .Values.service.type }}
  kubectl port-forward svc/{{ include "mychart.fullname" . }} 8080:{{ .Values.service.port }}
  curl http://127.0.0.1:8080
{{- end }}

查看状态:
  kubectl get all -l app.kubernetes.io/instance={{ .Release.Name }} -n {{ .Release.Namespace }}

7.7 ApplicationSet 多集群

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: myapp
spec:
  goTemplate: true
  goTemplateOptions:
    - missingkey=error
  generators:
    - list:
        elements:
          - cluster: dev
            url: https://dev-k8s.example.com
            revision: "1.4.2"
            env: dev
          - cluster: staging
            url: https://staging-k8s.example.com
            revision: "1.4.2"
            env: staging
          - cluster: prod
            url: https://prod-k8s.example.com
            revision: "1.4.2"
            env: prod
  template:
    metadata:
      name: 'myapp-{{.cluster}}'
    spec:
      project: production
      source:
        repoURL: https://charts.example.com
        targetRevision: '{{.revision}}'
        chart: mychart
        helm:
          valueFiles:
            - 'values-{{.env}}.yaml'
      destination:
        server: '{{.url}}'
        namespace: myapp
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
          - PrunePropagationPolicy=foreground

八、日志或指标观察方法

8.1 Helm 客户端日志

Helm 是客户端工具,本身没有运行日志。常见调试:

# Debug 模式
helm install myapp ./mychart --debug

# 输出包括:
# - 模板渲染过程
# - 生成的 manifest
# - 错误信息

8.2 Helm release 状态

# 1. 列出
helm list -A

# 输出字段:
# NAME    NAMESPACE REVISION  UPDATED                                STATUS    CHART           APP VERSION
# myapp   prod      3         2026-06-17 14:00:00.123456 +0800 CST  deployed  mychart-1.4.2   1.22.0

# 2. 详细状态
helm status myapp -n prod

# 输出包括:
# NAME: myapp
# LAST DEPLOYED: Tue Jun 17 14:00:00 2026
# NAMESPACE: prod
# STATUS: deployed
# REVISION: 3
# TEST SUITE: None
# NOTES: ...

# 3. 历史
helm history myapp -n prod

8.3 K8s 资源日志

Helm release 对应的资源都在 K8s 中,按 label 查:

# 列出 release 所有资源
kubectl get all -l app.kubernetes.io/instance=myapp -n prod

# Pod 日志
kubectl logs -l app.kubernetes.io/instance=myapp -n prod -f

# 特定容器
kubectl logs myapp-xxxx -c main -n prod

8.4 Helm release 状态说明

STATUS 含义 处理
deployed 安装 / 升级成功 正常
uninstalled 卸载成功 正常
superseded 被新版本取代 正常
failed 失败 排查
pending-install 正在 install 等待
pending-upgrade 正在 upgrade 等待
pending-rollback 正在 rollback 等待
uninstalling 正在卸载 等待

8.5 ArgoCD 监控

如果用 GitOps,release 状态在 ArgoCD 中查看:

# CLI
argocd app list
argocd app get myapp-prod
argocd app history myapp-prod

# UI: ArgoCD WebUI
# 状态:
# - Synced: 期望状态与实际一致
# - OutOfSync: 期望与实际不一致
# - Healthy: 资源健康
# - Degraded: 资源异常
# - Progressing: 升级中

8.6 关键指标

# 1. release 数
kubectl get configmap -A -l owner=helm,name=myapp -o name | wc -l

# 2. 失败 release
argocd app list -o json | jq '.[] | select(.status.health.status != "Healthy") | {name: .metadata.name, status: .status.health.status}'

# 3. 同步状态
argocd app list -o json | jq '.[] | select(.status.sync.status != "Synced") | {name: .metadata.name, sync: .status.sync.status}'

8.7 Prometheus 告警示例

groups:
  - name: helm-releases
    rules:
      - alert: HelmReleaseDegraded
        expr: argocd_app_info{health_status="Degraded"} == 1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Helm release {{ $labels.name }} 处于 Degraded 状态"
          description: "请检查 K8s 资源状态"

      - alert: HelmReleaseOutOfSync
        expr: argocd_app_info{sync_status="OutOfSync"} == 1
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "Helm release {{ $labels.name }} 长时间 OutOfSync"

九、排查路径

9.1 模板渲染失败

现象helm template 报错

Error: template: mychart/templates/deployment.yaml:23:5: executing "mychart" at <.Values.something>: nil pointer evaluating interface{}.something

步骤

# 1. 确认错误位置
# mychart/templates/deployment.yaml:23:5
# 第 23 行第 5 列

# 2. 常见原因:
# - .Values.something 是 nil,但模板里没判断
# - values 没传该字段

# 3. 修复:
# 加 default
{{ .Values.something | default "x" }}

# 4. 加 required 校验
{{ required "msg" .Values.something }}

9.2 Lint 报错

现象helm lintfailed to parse

步骤

# 1. 看完整错误
helm lint ./mychart 2>&1 | head -30

# 2. 常见原因:
# - YAML 缩进错
# - Chart.yaml 字段写错
# - values 引用了不存在的字段
# - 模板函数错误

# 3. 验证 YAML 语法
python3 -c "import yaml; yaml.safe_load(open('mychart/Chart.yaml'))"

9.3 依赖拉取失败

现象helm dependency update 报错

Error: failed to fetch https://charts.bitnami.com/bitnami/index.yaml : 404

步骤

# 1. 检查网络
curl -I https://charts.bitnami.com/bitnami/index.yaml

# 2. 检查 Chart.yaml 中仓库地址拼写
# 正确:https://charts.bitnami.com/bitnami
# 错误:https://charts.bitnami.com(少了 /bitnami)

# 3. 离线场景
helm dependency build ./mychart
# charts/ 目录下的 .tgz 不会被重新下载

9.4 升级后 Pod 起不来

现象helm upgrade 成功,但 pod 一直 CrashLoopBackOff

步骤

# 1. 看 pod 状态
kubectl get pods -n prod -l app.kubernetes.io/instance=myapp
# NAME                   READY   STATUS             RESTARTS   AGE
# myapp-xxxxxxxxxx-yyy   0/1     CrashLoopBackOff   3          2m

# 2. 看 pod 事件
kubectl describe pod myapp-xxx -n prod | tail -30

# 3. 看日志
kubectl logs myapp-xxx -n prod --previous

# 4. 常见原因:
# - 镜像拉不到:kubectl describe pod 看 ImagePullBackOff
# - 配置错误:应用启动报 config not found
# - 资源限制:OOMKilled
# - 端口冲突:service port 被占用

# 5. 紧急:回滚
helm rollback myapp -n prod

9.5 ArgoCD 一直 OutOfSync

现象:ArgoCD 一直显示 OutOfSync,不自动同步

步骤

# 1. 看 diff
argocd app diff myapp-prod

# 2. 看同步状态
argocd app get myapp-prod

# 3. 手动同步
argocd app sync myapp-prod

# 4. 强制覆盖(危险)
argocd app sync myapp-prod --force

# 5. 看同步历史
argocd app history myapp-prod

9.6 Helm release 卡在 pending-install

现象helm list 显示 pending-install,超时

步骤

# 1. 看 release 状态
helm status myapp -n prod

# 2. 看 K8s 资源
kubectl get all -l app.kubernetes.io/instance=myapp -n prod

# 3. 常见原因:
# - hook 失败:migration job 一直 pending
# - webhook 失败:admission webhook 拒绝
# - 资源配额:namespace 资源不足

# 4. 手动回滚
helm rollback myapp -n prod

# 5. 清理
kubectl delete job -n prod -l app.kubernetes.io/instance=myapp

9.7 Chart 仓库连不上

现象helm repo update 报 connection refused

步骤

# 1. 测试仓库
curl -I https://charts.example.com/index.yaml

# 2. 错误排查:
# - DNS 解析问题
# - HTTPS 证书问题
# - 代理问题
# - 仓库服务挂了

# 3. 用 helm repo add 加 insecure 仓库
helm repo add test http://charts-internal.example.com --insecure-skip-tls-verify

# 4. 验证
helm repo list
helm search repo mychart

9.8 OCI 推送失败

现象helm push 报 unauthorized

步骤

# 1. 重新登录
helm registry logout harbor.example.com
helm registry login harbor.example.com -u admin

# 2. 检查项目存在
# Harbor UI 中确认 charts 项目存在且有推送权限

# 3. 检查 OCI 协议支持
# Harbor 2.x 默认开启 OCI

# 4. 验证
helm push ./mychart-0.1.0.tgz oci://harbor.example.com/charts

十、风险提醒

10.1 Chart 模板里硬编码

风险metadata.labelsnamespaceport 等写成固定值,多环境用不了。

# 错误
metadata:
  namespace: prod  # 写死了
  name: myapp
spec:
  port: 8080  # 写死了

# 正确
metadata:
  namespace: {{ .Release.Namespace }}
  name: {{ include "mychart.fullname" . }}
spec:
  port: {{ .Values.service.port }}

10.2 values 破坏性变更不标版本

风险:把 auth.password 改成 existingSecret,但 version 还是 1.4.2,老用户升级就炸了。

# 错误:删除了字段但 version 没改
auth:
  # password 字段被删除了,但 version 还是 1.4.2
  existingSecret: ""

# 正确:升 MAJOR
# version: 2.0.0
# CHANGELOG.md 里写:
## [2.0.0] - 2026-06-17
### BREAKING
- auth.password 改为 auth.existingSecret
- 迁移:把 auth.password 移到 secret,引用 existingSecret

10.3 Chart 依赖没锁版本

风险requirements.yaml~1.0,不同机器拉到不同版本。

# 错误
dependencies:
  - name: mysql
    version: "~9.0"  # 模糊匹配

# 正确:固定 + 锁定
# Chart.yaml
dependencies:
  - name: mysql
    version: "9.10.0"  # 固定

# Chart.lock(自动生成,必须 commit)
dependencies:
  - name: mysql
    repository: https://charts.bitnami.com/bitnami
    version: 9.10.0

10.4 Helm 升级时直接覆盖 Secret

风险helm upgrade 时 Secret 内容变化,会直接覆盖。如果 Secret 是手工 kubectl create 创建的,Helm 不管理,升级可能让 Secret 内容丢失。

# 1. 用 existingSecret 引用已存在的 Secret
database:
  existingSecret: my-db-creds  # 不在 Helm 模板中创建

# 2. Secret 单独用 Sealed Secrets / External Secrets Operator 管理

10.5 Hook 失败导致升级卡住

风险pre-upgrade hook 是 Job,如果失败,整个 release 卡 pending-upgrade

# 防御:
# 1. pre-upgrade hook 加 timeout
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
"helm.sh/hook-weight": "-5"

# 2. 失败时手动清理
kubectl delete job myapp-migrate -n prod
helm rollback myapp -n prod

10.6 Helm 自身的安全

风险:Helm 客户端连 K8s cluster 的 kubeconfig 泄漏。

# 防御:
# 1. kubeconfig 权限最小化
# 2. CI 中用 service account token,不存明文
# 3. Helm 3.14+ 的 OCI 支持 cosign 签名验证

10.7 大集群下 chart 渲染慢

风险:values 文件超过 1 MB、循环 1000+ 资源,渲染会卡住。

# 防御:
# 1. values 精简
# 2. 拆 chart
# 3. 模板里避免复杂循环

10.8 跨 chart 共享 values

风险:subchart 的 values 在父 chart 中通过 .Values.subchart.field 访问,但 Helm 3 改了规则。

# 父 chart values.yaml
subchart:
  field: value

# 父 chart 模板访问
{{ .Values.subchart.field }}  # 正确

# 但 subchart 自己的模板中是
{{ .Values.field }}  # subchart 只看到自己命名空间下的 values

10.9 模板里 lookup 慎用

风险lookup 函数会请求 K8s API,模板渲染变慢,且 helm template 时可能失败。

# 不推荐
{{- $existing := (lookup "v1" "ConfigMap" "ns" "name").data }}

# 推荐:用 values 控制
{{- if .Values.config.data }}
data:
  {{- toYaml .Values.config.data | nindent 2 }}
{{- end }}

10.10 Chart 仓库的访问控制

风险:ChartMuseum 默认无认证,任何人能 push。

# 防御:
# 1. 启用 Basic Auth
-e BASIC_AUTH_USER=admin
-e BASIC_AUTH_PASS=xxx

# 2. 用 Harbor / Cloud Registry 的 RBAC

# 3. 启用 cosign 签名

十一、验证方式

11.1 Lint 通过

helm lint ./mychart
# 预期:[INFO] Chart.yaml: icon is recommended
# 或:no failures

# 用 chart-testing
ct lint --config ci/chart_schema.yaml

11.2 模板渲染与 K8s 校验

# 渲染
helm template myapp ./mychart --values values-prod.yaml > rendered.yaml

# 用 kubeconform 校验 K8s schema
kubeconform -strict -summary rendered.yaml
# 预期:Summary: ... Summary of all resources ... 100% valid

# 用 kubectl --dry-run 验证
kubectl apply --dry-run=client -f rendered.yaml
# 预期:configured (dry-run)

11.3 单元测试通过

helm unittest ./mychart
# 预期:
# PASSED  test deployment
# Tests: 6 passed, 0 failed, 0 skipped

11.4 Dry-run 安装

helm install myapp ./mychart --values values-prod.yaml --dry-run
# 预期:STATUS: pending-install,无错误

11.5 K8s 集群演练

# 1. 准备临时 namespace
kubectl create namespace helm-test

# 2. 演练
helm install myapp ./mychart --namespace helm-test --values values-test.yaml

# 3. 验证资源
kubectl get all -n helm-test

# 4. 演练升级
helm upgrade myapp ./mychart --namespace helm-test --values values-test-v2.yaml

# 5. 演练回滚
helm rollback myapp --namespace helm-test

# 6. 清理
helm uninstall myapp -n helm-test
kubectl delete namespace helm-test

11.6 与 GitOps 集成验证

# 1. 提交后看 ArgoCD 状态
argocd app get myapp-prod
# 状态:Synced / Healthy

# 2. 模拟代码变更
# 改 values 后提交,看 ArgoCD 是否自动 sync

# 3. 模拟故障
# 把镜像改成不存在的版本,sync 应失败

11.7 Chart 仓库连通性

# 添加仓库
helm repo add chartmuseum https://charts.example.com
# 列出
helm search repo chartmuseum/mychart
# 拉取
helm pull chartmuseum/mychart --version 1.4.2

11.8 OCI 推送拉取

# 推送
helm push ./mychart-1.4.2.tgz oci://harbor.example.com/charts
# 列出
helm list oci://harbor.example.com/charts
# 拉取
helm pull oci://harbor.example.com/charts/mychart --version 1.4.2

11.9 升级 diff 验证

# 装 diff 插件
helm plugin install https://github.com/databus23/helm-diff

# 看 diff
helm diff upgrade myapp ./mychart --values values-new.yaml
# 预期:列出修改、新增、删除的资源

11.10 Hook 验证

# 1. 装带 hook 的 chart
helm install myapp ./mychart --dry-run --debug | grep -A 30 "HOOKS"

# 2. 验证 hook Job 是否跑成功
kubectl get jobs -n prod
kubectl logs job/myapp-migrate -n prod

十二、回滚方案

12.1 Helm release 回滚

# 1. 看历史
helm history myapp -n prod

# 2. 回滚到上一版本
helm rollback myapp -n prod

# 3. 回滚到指定版本
helm rollback myapp 5 -n prod

# 4. 验证
kubectl get pods -n prod -l app.kubernetes.io/instance=myapp
helm status myapp -n prod

回滚的限制:

  • 回滚的是 Helm 管理的资源
  • 回滚手动 kubectl create 创建的资源
  • 回滚 Helm 之外修改的资源(需先 reconcile)

12.2 ArgoCD 回滚

# 1. 看历史
argocd app history myapp-prod

# 2. 回滚到指定版本
argocd app rollback myapp-prod

# 3. 回滚到指定 ID
argocd app rollback myapp-prod --id 5

# 4. 验证
argocd app get myapp-prod

UI 操作:ArgoCD WebUI → Application → History → Rollback。

12.3 数据库迁移回滚

如果 chart 里包含数据库迁移,需要单独处理:

# 1. 找出对应的 migration tool
# 例如 Flyway、Liquibase、Prisma Migrate

# 2. 手动回滚
kubectl exec -it myapp-xxx -n prod -- /app/migrate.sh rollback

# 3. 验证
psql -h pg.prod.svc -U myapp -c "SELECT * FROM schema_migrations ORDER BY installed_rank DESC LIMIT 5"

12.4 数据丢失应急

如果升级导致数据问题:

# 1. 立刻停掉应用(不让写入扩散)
kubectl scale deployment myapp -n prod --replicas=0

# 2. 恢复 PVC(如果有 snapshot)
kubectl get pvc -n prod
# 通过 Velero / 云厂商 snapshot 恢复

# 3. 回滚 Helm
helm rollback myapp -n prod

# 4. 验证数据
kubectl exec -it myapp-xxx -n prod -- /app/db-check.sh

# 5. 恢复副本
kubectl scale deployment myapp -n prod --replicas=5

12.5 整 cluster 灾难恢复

如果整个 K8s 集群挂了:

# 1. 通过 Velero 恢复
velero restore create --from-backup backup-2026-06-17

# 2. 重新拉起 ArgoCD
kubectl apply -f argocd-install.yaml

# 3. ArgoCD 自动同步所有 Application

# 4. 验证
argocd app list

12.6 Chart 仓库回滚

如果 push 了错误版本到 ChartMuseum:

# 1. 删除(需要 ALLOW_OVERWRITE)
curl -X DELETE https://charts.example.com/api/charts/mychart/1.4.3

# 2. 重新 push 正确版本
helm push ./mychart-1.4.3.tgz chartmuseum

# 3. 通知用户不要升级

OCI 仓库同样:

# Harbor UI 中删除 tag,或用 API
curl -X DELETE -u admin:Harbor12345 \
  https://harbor.example.com/api/v2.0/charts/mychart/1.4.3

12.7 配置文件回滚

GitOps 中,values 文件在 Git 里,revert 即可:

# 1. 看变更
git log -p values-prod.yaml

# 2. 回滚
git revert HEAD
git push

# 3. ArgoCD 自动 sync
argocd app get myapp-prod

12.8 GitLab CI 中回滚 chart 版本

如果 CI 自动 bump 了 chart version,提交一个 revert commit:

# 1. 找错版本 commit
git log --oneline | head -20

# 2. revert
git revert <commit-hash>
git push

# 3. CI 重新跑

十三、生产环境注意事项

13.1 资源规划

组件 推荐配置 说明
ArgoCD 2 CPU / 4 GB / 20 GB 存储 Application 数 100+ 需要更多
ChartMuseum 1 CPU / 1 GB / 50 GB 存储 存 chart tgz
Harbor(OCI) 4 CPU / 8 GB / 100 GB 存储 同时存镜像和 chart
Helm client 100 MB 磁盘 客户端工具
ETCD(K8s) 取决于集群规模 存 Helm release 状态

13.2 网络规划

  • Chart 仓库必须内网访问
  • Chart 仓库与 K8s API server 互通
  • ArgoCD 需要访问 Git 仓库 + Chart 仓库 + K8s API
  • 出方向:仅 HTTPS(443)出去
  • 入方向:仅 ArgoCD UI / API 开放

13.3 权限与 RBAC

  • 平台运维:仓库写、ArgoCD 写
  • 业务方:仓库读、ArgoCD Application 写自己 namespace
  • 安全审计:仓库只读
  • 普通用户:仓库只读

Harbor 项目权限:

# Harbor project "charts-prod" 角色
- name: admin
  users: [platform-team]
- name: developer
  users: [release-manager]
- name: guest
  users: [app-team]

ArgoCD Project 权限:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: production
spec:
  sourceRepos:
    - https://charts.example.com
  destinations:
    - server: '*'
      namespace: 'prod-*'
  clusterResourceWhitelist:
    - group: '*'
      kind: '*'
  namespaceResourceWhitelist:
    - group: '*'
      kind: '*'

13.4 灾备

  • Velero 备份:每天备份 K8s 资源(含 Helm release 状态)
  • Git 备份:GitLab 本身要备份
  • Chart 仓库备份:独立备份,跨 region
  • ArgoCD 自备份:用 App-of-Apps 模式

13.5 性能

  • 渲染慢:拆 chart、用 --show-only、Kustomize 替代
  • 推送慢:本地先 lint,CI 走并行
  • ArgoCD 同步慢:分集群部署 ArgoCD,减少单集群 Application 数

13.6 多环境策略

环境 同步频率 阻断方式
dev 自动
staging 自动 lint + unit test
prod 手动 lint + unit test + peer review

GitOps 多环境:

argocd/
├── apps/
│   ├── myapp-dev.yaml       # 自动同步
│   ├── myapp-staging.yaml   # 自动同步
│   └── myapp-prod.yaml      # 手动同步
├── projects/
│   ├── dev.yaml
│   ├── staging.yaml
│   └── production.yaml
└── repos/
    └── chart-repos.yaml

13.7 升级窗口

  • 生产环境:工作日 9:00-11:00、14:00-17:00(业务低峰)
  • 数据库相关:避开交易高峰期
  • 金融业务:避开结算期
  • 跨 region:先单 region,验证后再全推

13.8 监控告警

  • Helm release 状态异常(Degraded)
  • ArgoCD Application 长时间 OutOfSync
  • Chart 仓库不可达
  • 渲染任务失败
  • Hook job 失败

13.9 灰度发布

用 Argo Rollouts 做蓝绿 / 金丝雀:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: myapp
spec:
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: {duration: 5m}
        - setWeight: 30
        - pause: {duration: 10m}
        - setWeight: 60
        - pause: {duration: 10m}
        - setWeight: 100

13.10 文档

每个 chart 必须有 README:

# mychart

生产级应用 chart。

## 维护者

Platform Team <platform@example.com>

## 依赖

- PostgreSQL 12.x
- Redis 17.x

## 升级

从 1.3.x 升级到 1.4.x:见 [CHANGELOG.md](./CHANGELOG.md)

## 紧急回滚

见 [RUNBOOK.md](./RUNBOOK.md)

13.11 升级测试

升级前必须做的事:

  1. 静态检查helm linthelm templatect lintkubeconform
  2. 单元测试helm unittest
  3. 环境演练:dev / staging 升级,验证业务
  4. 生产回滚预演:演练 helm rollback
  5. 监控指标基线:升级前抓 CPU / 内存 / QPS 基线,升级后对比

13.12 Chart 治理

  • 命名空间内每个 chart 至少有:README、CHANGELOG、values schema、unittest
  • 每月 review 一次依赖更新
  • 每季度清理 deprecated 字段
  • 弃用 chart 走 archive 流程

13.13 镜像签名

Helm 3.8+ 支持 cosign 签名:

# 签名
cosign sign harbor.example.com/charts/mychart:1.4.2

# 验证
cosign verify harbor.example.com/charts/mychart:1.4.2 --certificate-identity-regexp '.*' --certificate-oidc-issuer 'https://accounts.google.com'

ArgoCD 中可配置 helm.verify: true。

13.14 字段演进

新增字段必须向后兼容:

# 旧字段
auth:
  password: xxx

# 新增字段(可选)
auth:
  password: xxx
  existingSecret: ""
# 老配置还能用

# 等所有用户迁移后再删除 password 字段
# 同时升 MAJOR 版本

十四、总结

Helm Chart 的工程化落地分三块:

  1. Chart 设计:固定目录、命名约定、values 规范、依赖锁定
  2. CI 流程:lint、unittest、kubeconform 校验、chart-testing
  3. GitOps 串联:ArgoCD / Flux + ChartMuseum / OCI Registry

最容易踩的坑:

  • values 破坏性变更没标 MAJOR → 升级爆炸
  • Chart.lock 不 commit → 不同机器拉不同版本
  • CI 跳过 unit test → 模板错误流到生产
  • 没用 helm-diff 看 diff → 升级时改了不期望改的资源
  • 直接 helm upgrade 不分环境 → 一次升级影响所有环境
  • Hook 失败卡住 release → 整个集群 release 状态异常

生产环境重点:

  • Chart 仓库独立备份,跨 region
  • 升级必须分 dev / staging / prod 三步
  • 监控 Helm release 状态 + ArgoCD Application 状态
  • 每月 review 依赖、清理 deprecated
  • 关键 chart 走 cosign 签名
  • 灰度用 Argo Rollouts

下一步可考虑:

  • 接入 OPA / Conftest 做更严格的策略校验
  • 接入 KubeVela 做更上层的抽象
  • 接入 OpenCost / kubecost 做成本监控
  • 接入 Sigstore 做全链路供应链证明
  • 接入 Backstage 做 chart catalog

对 Kubernetes 应用交付和云原生生态感兴趣的工程师,不妨去 云栈社区 看看,上面有大量关于 Helm、GitOps 和容器化技术的实战避坑指南,遇到问题也能找到同路人一起交流。

参考资料(按需查阅,以官方文档为准):




上一篇:从AlphaGo到沙盘:AI决策推演的跃迁时刻
下一篇:文章配图不再愁:ian-xiaohei-illustrations自动生成手绘插画
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-21 06:14 , Processed in 0.614161 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表