阶段二 · GitOps 持续交付

Helm 与 Kustomize

一句话总结

同一个应用要部署到 dev/staging/prod,90% 的 YAML 一样,10% 不同;
Helm 用模板 + 变量解决差异,Kustomize 用基线 + 叠加(overlay) 解决差异--前者像"填空的模具",后者像"打补丁",二者常常配合使用.

前置回顾

第 05 篇的 ApplicationSet 里出现过一行 path: overlays/{{env}}--每个环境读各自的配置目录.当时按下没表:这些"各环境的配置"到底怎么组织,才能既复用公共部分,又表达环境差异?本篇就是回答这个问题.它是 GitOps 配置仓库的"内容物"该如何编排.

问题:多环境的 YAML 该怎么管

假设一个应用要上三个环境,差异其实很少:

dev          staging       prod
副本数 1 2 6
镜像 tag :dev-latest :sha :v1.2.0
内存上限 256Mi 512Mi 2Gi
域名 dev.x.com stg.x.com x.com
其余 ~90% ────────── 完全相同 ──────────

最朴素的做法:复制三份完整 YAML,各改各的.后果你大概能猜到:

  • 给所有环境的 Deployment 加一个 label? -- 要在 3 个文件里改 3 遍,漏一个就不一致
  • 新增一个环境 pre? -- 又复制一整套,继续手动维护
  • 某次只想改 prod 的副本数,却手滑动了公共部分? -- 三个环境一起遭殃

根因和第 02 篇可复用 Workflow,第 05 篇 ApplicationSet 完全一样:重复的东西散落多处,改一处要改 N 遍,迟早不一致.Helm 和 Kustomize 是两种消除这种重复的思路.

Helm:模板化的思路

Helm 把一套 K8s 资源打包成一个 Chart.核心动作是模板渲染:YAML 里挖空,变量从 values.yaml 注入.

# templates/deployment.yaml -- 模板,该变的地方挖成占位符
apiVersion: apps/v1
kind: Deployment
spec:
replicas: {{ .Values.replicaCount }} # ← 从 values 取
template:
spec:
containers:
- name: app
image: "{{ .Values.image.repo }}:{{ .Values.image.tag }}"
resources:
limits:
memory: {{ .Values.resources.memory }}
# values.yaml -- 默认值
replicaCount: 1
image:
repo: ghcr.io/org/myapp
tag: dev-latest
resources:
memory: 256Mi

渲染时(helm templatehelm install),Helm 把模板里的 {{ }} 用 values 填上,生成最终 YAML.多环境怎么办?每个环境一个 values 文件,覆盖默认值:

# values-prod.yaml -- 只写和默认值不同的部分
replicaCount: 6
image:
tag: v1.2.0
resources:
memory: 2Gi
$ helm install myapp ./mychart -f values-prod.yaml
# 公共结构来自模板,prod 的差异来自 values-prod.yaml
# 后面的 -f 文件覆盖前面的默认值 -- 这就是 values 的分层覆盖

Helm 像简历模板的填空:模板固定了排版和栏目(姓名:,工作经历:),每个人填自己的内容(values).换个人就换一份填空内容,版式不用重做.

Helm 不止是模板:它还是个包管理器

Helm 的定位其实是"Kubernetes 的 apt/npm".除了模板化,它还带来:

  • 分发与复用:Chart 可以打包推到仓库,别人 helm install 一条命令就装好一整套(如 helm install redis bitnami/redis)--这也是你部署第三方中间件最常用的方式
  • 版本与发布(Release):每次安装/升级是一个有版本号的 Release,helm rollback 可回退
  • 条件与循环:模板里能写 if/range,表达复杂逻辑(如"prod 才创建 HPA")

Helm 模板本质是对文本做替换,它并不"理解" YAML 结构.后果是:缩进错一格,变量没引号导致类型错乱,这类问题在渲染前不易察觉;模板逻辑一复杂(嵌套 if + range + 缩进控制),可读性会迅速下降.功能强大,但容易写得很"绕".

Kustomize:叠加的思路

Kustomize 走完全相反的路:没有模板,没有占位符,全是合法的纯 YAML.它的模型是 base(基线)+ overlay(叠加层).

base 放公共的,完整的,能直接 apply 的 YAML:

# base/deployment.yaml -- 一份完整,合法的 YAML(replicas 先写个基准值)
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 1
# ... 其余公共配置

每个环境是一个 overlay,只声明"我要在 base 上打什么补丁":

# overlays/prod/kustomization.yaml
resources:
- ../../base # ① 引用 base
patches: # ② 在 base 上打补丁,只写要改的字段
- patch: |-
- op: replace
path: /spec/replicas
value: 6
target:
kind: Deployment
name: myapp
images: # ③ Kustomize 内置的镜像替换能力
- name: ghcr.io/org/myapp
newTag: v1.2.0
$ kubectl kustomize overlays/prod    # 渲染出 prod 的最终 YAML
# (kubectl apply -k overlays/prod 可直接应用)

Kustomize 像给照片加滤镜:原图(base)不动,每个场景套不同的滤镜层(overlay)--这张调亮一点,那张裁个边.原图始终是原图,叠加是非侵入的.

Kustomize 的最大优点:全程都是真 YAML

因为没有 {{ }} 占位符,base 和 overlay 都是合法 YAML--可以直接 kubectl apply,能被编辑器校验,能被 IDE 高亮和补全.而且 kubectl内置 Kustomize(-k 参数),无需额外安装工具.简单场景下,它的心智负担明显比 Helm 模板低.

怎么选:Helm vs Kustomize

维度 Helm Kustomize
核心机制 模板 + 变量注入 base + overlay 打补丁
文件形态 {{ }} 的模板(非合法 YAML) 全程合法纯 YAML
逻辑能力 强(if/range/函数) 弱(刻意不支持逻辑)
分发/包管理 ✅ 能打包,推仓库,装第三方 ❌ 不是包管理器
学习曲线 较陡(模板语法) 平缓(就是 YAML)
最适合 分发给他人的,可配置项多的复杂应用 自家应用的多环境差异管理

一句话经验:要把应用打包分发给别人安装,或配置项极多 → Helm;管自己应用的几个环境差异 → Kustomize.但这不是二选一--

二者结合:Helm 出模板,Kustomize 打补丁

真实世界里最常见的反而是组合使用,各取所长.典型场景:你要用一个别人发布的 Helm Chart(比如第三方中间件),但想对它做一点 Chart 本身没暴露的定制.

Kustomize 可以把"先用 Helm 渲染出 YAML,再对结果打 overlay 补丁"串成一条流水线:

# kustomization.yaml
helmCharts: # ① 先让 Kustomize 调 Helm 渲染 Chart
- name: redis
repo: https://charts.bitnami.com/bitnami
valuesInline:
replica:
replicaCount: 2
patches: # ② 再对渲染结果打补丁(改 Chart 没开放的字段)
- path: extra-annotations.yaml

组合管线:Helm 负责"生成基础",Kustomize 负责"环境微调"

Helm Chart + values  ──渲染──▶  完整 YAML  ──Kustomize overlay──▶  各环境最终 YAML
(复用社区/复杂模板) (打上本地,本环境的补丁)

回到 GitOps:ArgoCD 原生支持两者

这一篇的内容不是孤立的--它直接服务于第 05 篇.ArgoCD 的 Application 原生认识 Helm 和 Kustomize:你在 source.path 指向的目录里放的是 Chart 还是 kustomization,ArgoCD 自动用对应工具渲染后再同步.

# 第 05 篇的 Application,source 指向一个 Kustomize overlay
spec:
source:
repoURL: https://github.com/org/myapp-config
path: overlays/prod # ← 就是本篇的 overlay 目录!
targetRevision: main
# ArgoCD 检测到这是 kustomization,自动 kustomize build 后同步

于是第 05 篇 ApplicationSet 里那行 path: overlays/{{env}} 完全闭合了:ApplicationSet 为每个环境生成一个 Application,各自指向本篇的一个 overlay,ArgoCD 渲染后同步到对应集群.配置的复用(本篇)和部署的自动化(第 05 篇)在这里合成一条完整链路.

快速回顾

  • 要解决的问题:多环境 90% 配置相同,10% 不同,复制多份会导致"改一处要改 N 遍,迟早不一致"
  • Helm = 模板 + 变量:YAML 挖空,各环境用不同 values 文件覆盖;还是个包管理器(打包/分发/装第三方);代价是模板本质是字符串拼接,复杂后难读
  • Kustomize = base + overlay:base 放公共完整 YAML,overlay 只打补丁;全程合法纯 YAML,kubectl 内置,心智负担低;但不是包管理器
  • 选型:分发给他人/配置项多 → Helm;管自家应用的环境差异 → Kustomize
  • 常组合:Helm 渲染基础 + Kustomize 打补丁,各取所长
  • 接 GitOps:ArgoCD 原生支持二者,Application 的 path 指向 Chart 或 overlay 即自动渲染同步--闭合第 05 篇的 overlays/{{env}}

既然 Kustomize 更简单又是 kubectl 内置,为什么大家还大量用 Helm?

因为分发.当你要安装 Redis,Prometheus,Nginx Ingress 这类第三方组件时,社区几乎都以 Helm Chart 形式发布,一条 helm install 就装好,且暴露了大量可配置项.Kustomize 没有"包仓库"生态,做不了这件事.所以常见分工:装别人的东西用 Helm,管自己的环境差异用 Kustomize.

为什么 Kustomize 故意不支持 if/循环这类逻辑?

这是刻意的设计哲学.Kustomize 认为配置就该是"声明你要什么",而不是"写一段会生成配置的程序".一旦引入逻辑,YAML 就退化成模板代码,失去了"所见即所得,能直接 apply,能被工具校验"的优点.它用"能力换简单"--牺牲灵活性,换取可读性和可预测性.理解这个取舍,就明白它和 Helm 不是谁更好,而是两种价值观.

动手练习

  1. Helm Chart 基础:用 helm create mychart 生成一个 Chart,改 values.yaml 的副本数,用 helm template 看渲染结果,再写一个 values-prod.yaml 覆盖它对比差异.
  2. Kustomize overlay:用 Kustomize 写一个 base + dev/prod 两个 overlay,各自只改副本数和镜像 tag,用 kubectl kustomize overlays/prod 看渲染结果.
  3. Helm vs Kustomize 对比:对比两种方式:同样"给所有环境的 Deployment 加一个公共 label",在 Helm 里改哪,在 Kustomize 里改哪?哪个更直观?
  4. (进阶)组合管线:在 Kustomize 里用 helmCharts 渲染一个 bitnami 的 Chart,再打一个 overlay 补丁,体验组合管线.
  5. (串联)GitOps 集成:把你的 overlay 目录推到配置仓库,让 ArgoCD 的 Application 指向它,确认 ArgoCD 自动 kustomize 渲染并同步.