AI时代的Kubernetes教程36招-第22招-Helm和Kustomize到底该怎么组织多环境部署?
Helm 与 Kustomize 在数字人平台上的交付分工
一个 Chart 打天下,还是一层 overlay 分环境?冲突在这里。
[读者] 正在把数字人平台交付到多套 K8s 环境的工程师
[痛点] Helm values 越堆越多,环境差异与产品差异纠缠在一起
[现在读] 客户数与环境数同时增长,改一处配置要动十几个文件
[读完] 能划分 Helm 与 Kustomize 的职责边界,并跑通一次双工具交付
[旧方案] 单个 Helm Chart + 每环境一份 values
|
v
[新需求] 产品要复用,环境要独立,差异要可审阅
|
v
[冲突] 值参数化无法表达结构差异,分支与组合爆炸
|
v
[后果] 渲染结果不可预期,回滚粒度粗,审阅成本高
我是老李,在一家做数字人平台的团队负责交付。平台本身不复杂:一组无状态 API(会话与资产管理)、一个实时音视频信令与推理网关、一个离线渲染 worker,再加一个模型配置同步任务,全部跑在 K8s 上。早期只有一套集群、一个客户,一个 Chart 加一份 values.yaml 就收工了。
后来客户变成三个,环境从一套变成六套(每个客户 staging + prod)。镜像 tag、副本数、GPU 节点选择、Ingress 域名、会话超时、模型网关地址全都不一样。values 文件从一个变成六个,Chart 模板里的 if 分支从两处变成二十余处。
更拧巴的是产品定位:我们既想把它当产品包卖出去——可复用、可版本化、可升级;又想让每个客户的环境保留自己的差异——可独立演进、可随时回滚。一个工具能不能同时扛下这两件事?
到底是该把环境差异也写成 values,还是该换一种完全不靠变量的表达方式?
如果只能选一个工具,是选模板化更强的那个,还是选更贴近原生 YAML 的那个?
选错了会怎样——是配置越来越乱,还是产品升级时把客户环境一起覆盖掉?
01、故事
最早那版目录长这样:只有一个 Chart,values 按环境拆文件。
deploy/
└── helm/
└── digital-human/
├── Chart.yaml
├── values.yaml
├── values-staging.yaml
├── values-prod.yaml
└── templates/
第一次出事是在客户 B 上线的时候。客户 B 要求推理网关固定调度到 workload=digital-human 的节点池,客户 A 又要求网关和 API 分处不同命名空间。运营希望 staging 的会话超时短一点方便压测,prod 要长一点。
当时的做法非常自然:在 values 里加开关。gateway.nodeSelector、splitNamespace、sessionTimeoutSeconds,模板里写 {{- if .Values.splitNamespace }}。一次两次没问题,六次之后模板就成了一张网:读一个 Deployment 要看三个 if 才能确定它最终会不会被渲染出来。
任务是:在不动产品代码的前提下,让同一份 Chart 同时服务六套环境,并且新增客户时只增加「差异描述」,不再修改任何模板。
一图铺开时
值与人同增
客户渐成林
分支乱如麻
02、问题
旧方案的失效是渐进的,但每一条都能指认。
新增一个客户,要复制一份 values-prod.yaml,然后逐字段核对——因为没人记得清哪些字段是产品默认值、哪些是上一个客户的环境特例。产品发版升级镜像 tag 时,要改六份 values,漏改一份就出现环境间版本漂移。有一次误改,把客户 prod 的会话超时改成了 staging 的值,线上表现是长会话被提前断开,排查了半天才定位到一份 yaml。
技术表现也很典型:helm template -f values-prod-b.yaml 的输出和预期不符,但看不出是哪一层把它覆盖掉的;模板里的 if 分支产生了无法枚举的组合路径,helm diff 的结果没人敢直接点 approve;回滚只能整包回滚 Release,没法只回滚某个环境的一条差异。
要把这件事做完,需要满足四个可验证的标准:
deploy/helm/digital-human/templates里不再出现任何环境判断(能 grep 到.Values.env之类的分支即视为不达标);- 任一环境都能独立渲染出完整清单,
kustomize build的结果可单独审阅; - 新增一个环境,只新增一个 overlay 目录,Chart 目录零改动;
- 升级产品版本,只改 Chart 的版本与默认镜像,overlay 不动。
旧法已乏力
新域自成墙
差异无处安
标准须先立
03、原理
Helm 有两个身份,别混用。 一是渲染器:把 Chart 加 values 渲染成最终 YAML(helm template)。二是发布管理器:维护 Release、revision、回滚,安装记录默认存在集群里以 sh.helm.release.v1.* 命名的 Secret 中(具体存储方式以当前官方文档为准)。前一个身份是纯函数,后一个身份会改集群。
values 是数据,不是结构。 模板能表达的差异,上限就是模板语言能拼出来的差异。当差异是「多一个 sidecar」「多一条 NetworkPolicy」「换一个 IngressClass」,用值去表达就只能退化成布尔开关加 if 森林。这是 values 爆炸的数学原因,不是团队纪律问题。
Kustomize 是无模板的结构合并。 它不理解变量,不做字符串替换,只做 YAML 层面的合并与替换:base 提供基准,overlay 用 patch 描述差异,再叠加 namespace、images、replicas 这类 transformer。
反直觉判断:patch 不报错,不等于 patch 生效。strategic merge 在 target 匹配不上、或路径对不上时,可能静默跳过,kustomize build 照样返回 0。所以 Kustomize 的安全性从来不来自「语义校验」,而来自「渲染产物可以 diff 给人看」。
Helm values 是一条覆盖链。 Chart 默认值 → 父 Chart 的 values → -f 传入的文件(后者覆盖前者)→ --set。同一个值可能在四层被改过,最终态只有 helm get values --all 能说清。这条链一旦和环境差异混在一起,可读性就没了。
模板非万能
合并非覆写
值穷则形变
形变需补丁
04、架构
[输入] 产品版本号 + 目标环境名
|
v
[模块] deploy/helm/digital-human(Chart:能力与默认值)
|
v
[数据/状态] values.yaml / Chart 版本 / Release 记录
|
v
[处理] helm template 渲染 → Kustomize base → overlay patch
|
v
[输出] 环境专属清单 → kubectl apply → 集群实际状态
边界:Helm 侧只回答「这个产品长什么样」——包含哪些容器、需要哪些默认配置、安装后要跑什么 hook。Kustomize 侧只回答「这次部署和基准差在哪」——副本数、镜像 tag、域名、超时、差异化环境变量。任何一方越界,职责就开始重叠:Chart 里出现环境名,或者 overlay 里试图拼装产品结构。
收益:产品能力只有一处定义,环境差异只有一处定义。审阅一个客户环境,看 overlay 目录就够了;审阅产品本身,看 Chart 就够了。
代价:渲染变成两段,出问题时要在两个渲染器之间定位;工具版本需要对齐,kubectl 内置的 kustomize 与独立二进制不是一回事。
适用条件:环境之间的差异以「结构差异 + 少量数值差异」为主,且团队能接受一个统一的渲染入口(CI 或 GitOps)。如果差异主要集中在少量数值上,单一 Helm 反而更简单。
上有产品包
下有环境层
权责若分明
边界即稳定
05、实战一次
环境与依赖:一个可用的 Kubernetes 集群;本机安装 helm 3 与 kustomize 独立二进制;kubectl 版本与集群匹配。三者请按当前官方文档核验最新稳定版,并在 CI 中锁定同一套版本。
目标目录结构:
deploy/
├── helm/
│ └── digital-human/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── service.yaml
│ └── hooks/
│ └── migrate-job.yaml
└── overlays/
├── staging/
│ ├── kustomization.yaml
│ └── patch-config.yaml
└── prod/
├── kustomization.yaml
└── patch-config.yaml
Chart 元信息,只声明产品身份:
apiVersion: v2
name: digital-human
description: 数字人平台可复用交付包
type: application
version: 0.1.0
appVersion: 0.1.0
values 只保留产品语义的默认值,一个环境开关都不放:
deploymentName: api
replicaCount: 1
image:
repository: registry.example.com/digital-human/api
tag: 0.1.0
pullPolicy: IfNotPresent
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: '1'
memory: 1Gi
service:
port: 8080
config:
sessionTimeoutSeconds: 300
modelEndpoint: http://model-gateway:9000
命名模板单独抽出来,避免在每一处重复拼接:
{{- define "digital-human.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "digital-human.fullname" -}}
{{- printf "%s-%s" .Release.Name (include "digital-human.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
工作负载模板里不出现任何环境判断:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "digital-human.fullname" . }}
labels:
app.kubernetes.io/name: {{ include "digital-human.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app.kubernetes.io/name: {{ include "digital-human.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "digital-human.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
containers:
- name: api
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
env:
- name: SESSION_TIMEOUT_SECONDS
value: '{{ .Values.config.sessionTimeoutSeconds }}'
- name: MODEL_ENDPOINT
value: {{ .Values.config.modelEndpoint }}
resources:
{{ toYaml .Values.resources | indent 12 }}
Service 与 Deployment 共享同一套标签:
apiVersion: v1
kind: Service
metadata:
name: {{ include "digital-human.fullname" . }}
labels:
app.kubernetes.io/name: {{ include "digital-human.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
selector:
app.kubernetes.io/name: {{ include "digital-human.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
Hook 放在产品包内部,因为「升级前先做一次配置迁移」属于产品能力,不属于环境差异:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "digital-human.fullname" . }}-migrate
annotations:
'helm.sh/hook': pre-install,pre-upgrade
'helm.sh/hook-weight': '-5'
'helm.sh/hook-delete-policy': before-hook-creation,hook-succeeded
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
command:
- /app/dh-api
- migrate
helm.sh/hook-weight 控制同一次 hook 生命周期内的执行顺序,helm.sh/hook-delete-policy 控制这个 Job 何时被清理,两者语义以官方文档为准。
接下来是 staging overlay。它把本地 Chart 目录当 chart 源,把少量数值通过 valuesInline 传进去,其余差异全部用 patch 表达:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: digital-human-staging
helmGlobals:
chartHome: ../../helm
helmCharts:
- name: digital-human
releaseName: dh
namespace: digital-human-staging
valuesInline:
replicaCount: 1
config:
sessionTimeoutSeconds: 300
images:
- name: registry.example.com/digital-human/api
newTag: 0.1.0-rc1
patches:
- path: patch-config.yaml
patch 用显式 target 指明它要作用于哪个对象,而不是靠猜:
apiVersion: apps/v1
kind: Deployment
metadata:
name: dh-digital-human
spec:
template:
spec:
containers:
- name: api
env:
- name: MODEL_ENDPOINT
value: http://model-gateway.staging.svc.cluster.local:9000
prod overlay 结构相同,只换 namespace、镜像 tag 和资源规格,patch 额外压上真实资源请求。
启动与验证命令:
helm lint deploy/helm/digital-human
helm template dh deploy/helm/digital-human \
--namespace digital-human-staging > /tmp/rendered.yaml
kustomize build --enable-helm deploy/overlays/staging | head -40
kustomize build --enable-helm deploy/overlays/staging \
| kubectl apply --dry-run=server -f -
注意 --enable-helm 是 kustomize 独立二进制渲染 helmCharts 字段所必需的开关,kubectl apply -k 不会替你传这个参数——这一点在后面会成为一个真实的坑。
验证方式与预期结果:
$ kustomize build --enable-helm deploy/overlays/staging | grep -nE 'namespace:|replicas:|image:|MODEL_ENDPOINT'
4: namespace: digital-human-staging
18: replicas: 1
31: image: registry.example.com/digital-human/api:0.1.0-rc1
41: value: http://model-gateway.staging.svc.cluster.local:9000
未在当前环境实测,以上命令与输出为预期结果,示例输出仅用于说明应当校验哪些行,请以自己集群的实际渲染结果为准。到这里 V1 已经可用:Chart 里没有任何环境分支,环境差异全部落在 overlay 目录。
一装可复现
一渲见真形
先通最小路
再谈大格局
06、排查
V1 跑通之后,问题从「配置怎么写」变成「配置到底生效了没有」。下面三条都真实发生过。
现象一:kustomize build 成功,但 prod 的 MODEL_ENDPOINT 还是 Chart 默认值。
怀疑:patch 写错了。
检查:把渲染结果单独导出,看 Deployment 的实际名字。
kustomize build --enable-helm deploy/overlays/prod \
| grep -n 'kind: Deployment' -A 3
证据:渲染后的对象名是 dh-prod-digital-human,而 patch 里的 metadata.name 还是 dh-digital-human。原因是 prod overlay 的 releaseName 改成了 dh-prod,而 Chart 里的 fullname 模板把 release 名拼进了对象名。
根因:patch 的 target 与资源实际名字不一致,strategic merge 找不到匹配对象,静默跳过并返回成功。
修复:patch 的复用对象名不要手写。把 patches 改成带 target 的写法,target 只声明 kind,或者从渲染结果里取真实名字再写死。
错误尝试: 一开始的做法是给 patch 加 metadata.namespace,想通过 namespace 匹配到对象。这个做法是错的——对象名不匹配时,补 namespace 只会让它更不匹配;而且 kustomize 的 namespace transformer 在 patch 之后才统一施加,patch 阶段的资源未必带 namespace。加字段治不了匹配失败这个病。
现象二:本地跑得好好的 kustomize build --enable-helm,在同事机器上换成 kubectl apply -k deploy/overlays/staging 直接失败,报的是 helmCharts 字段无法识别或缺少 helm 相关开关。
怀疑:kustomization.yaml 写错了。
检查:分别确认两个命令背后的 kustomize 版本。
kustomize version
kubectl version --client
证据:独立二进制版本较新,支持 helmCharts 并接受 --enable-helm;kubectl 内置的 kustomize 是另一条发布线,版本往往更旧,既不认这个字段,也没有这个开关。
根因:同一个 kustomization.yaml 由两个不同的渲染器解释,二者支持的字段集与默认行为不完全一致。
修复:统一渲染入口。CI 与本地一律使用固定版本的独立 kustomize build,产物通过管道交给 kubectl。
kustomize build --enable-helm deploy/overlays/staging \
| kubectl apply -f -
错误尝试: 为了让 kubectl apply -k 能跑,把新字段改回旧写法,结果在本地新版本上又出现了行为差异与弃用警告。这个方向是错的——问题不在字段新旧,而在于有两个渲染器同时解释同一份文件。降级写法只是把版本冲突从「报错」变成「静默不一致」。
现象三:kustomize 交付正常,某天运维同事顺手执行了一次 helm upgrade,第二天发现 prod 的副本数和环境变量又回到了 Chart 默认值。
怀疑:kustomize 没生效。
检查:查集群里 Deployment 的 app.kubernetes.io/managed-by 与最近一次变更来源,同时看是否有同名 Release。
kubectl get deploy dh-prod-digital-human -n digital-human-prod -o yaml \
| grep -nE 'managed-by|replicas:|MODEL_ENDPOINT'
证据:对象上带着 Helm Release 的注解,而 overlay 施加的字段被覆盖回了 Chart 默认值。
根因:同一个对象被两条渲染路径写入——一条是 helm upgrade --install,一条是 kustomize build | kubectl apply。后写的赢,差异丢在了没人看的地方。
修复:确立单一写入路径。既然环境差异由 overlay 负责,就不再对同一个 Release 执行 helm upgrade;Helm 在这个架构里只承担渲染职责。集群状态改由统一的 apply 入口管理。
现象莫轻信
怀疑须求证
路径若失配
补丁静无声
07、优化
三条证据指向同一个根因:配置生效与否,取决于渲染路径是否唯一、可观测。V2 的改动全部围绕这一点,而不是围绕语法。
修改一:把 Chart 模板里的环境分支清零。 原因:分支是组合爆炸的来源,且让渲染结果无法静态推断。新行为:模板只依赖产品语义的 values,任何环境名都不出现在 templates 目录下。验证方式:
grep -rnE '\.Values\.(env|stage|namespace|domain)' deploy/helm/digital-human/templates
预期无输出。有输出即说明职责越界。
修改二:patch 全部改为显式 target,并在 CI 中断言生效。 原因:静默跳过是本次最危险的失败模式。新行为:每个 patch 至少声明 kind 与目标对象名,并增加一条断言——渲染结果中必须出现 patch 里写的那个字段值,否则 CI 失败。验证方式:
kustomize build --enable-helm deploy/overlays/prod \
| grep -c 'model-gateway.prod.svc.cluster.local'
预期输出为 1。输出为 0 表示 patch 未命中。
修改三:统一渲染入口,禁止双写。 原因:现象三的根因是两条写入路径。新行为:交付流程只保留一个入口,helm 只用于 helm lint 与 helm template,安装与更新一律走渲染产物。
kustomize build --enable-helm deploy/overlays/prod \
| kubectl diff -f -
预期无差异输出(即幂等)。有差异时说明集群状态与仓库描述不一致,先查明来源再 apply。
修改四:锁定工具版本。 原因:现象二证明版本漂移会直接改变渲染语义。新行为:CI 镜像里固定 helm 与 kustomize 的版本,开发者本地通过同一镜像执行渲染命令。
这些改动的验证都属于「可观察的方法」,本文不给出任何提升百分比或耗时数据,请在自己的环境中按上面的命令实测。
根因既已明
改法不贪多
一处归一责
整体自成章
08、演进
[同一输入] 产品版本 + 目标环境
|
+--[V1] 单 Chart + 每环境一份 values / 代价:分支爆炸、覆盖链不可见
|
+--[V2] Chart 出产品包 + overlay 出环境差异 / 代价:两段渲染、版本需对齐
|
[Trade-off]
得到:新增环境只加目录,产品升级不动 overlay
失去:一份文件看全所有配置的直觉
适用边界:环境差异以结构差异加少量数值差异为主,且有统一渲染入口
正确性:V1 的正确性依赖人不犯错——六份 values 与二十余处分支,正确性靠核对。V2 把产品语义与环境语义物理隔离,正确性靠目录结构约束,新增环境时不需要理解既有环境。
稳定性:V1 的失败模式是静默的字段漂移;V2 的失败模式变成渲染失败或 diff 非空,两类都是可见的。但 V2 引入了新的静默失败——patch 未命中,必须靠 CI 断言兜住。
复杂度:V1 单工具,概念少,但配置数量随客户数线性增长且互相耦合。V2 双工具,概念多一层,但配置数量按职责分片,增长的是目录而不是条件分支。
成本:V2 增加了工具版本对齐、渲染产物校验、单一写入路径这三项持续的维护成本。这些成本必须由流程固定下来,否则会退化回双写。
适用范围:环境差异体现在结构、调度、命名空间、资源规格上的场景,V2 明显更合适。如果差异只是几个数值,且环境数量不超过两三个,单 Chart 加两份 values 的收益更高。
遗留问题:patch 的断言是手写的,覆盖不全;命名空间与标签的一致性仍靠命名约定;密钥与配额这类横切能力还没有归属,下一步要么放进 Chart 作为产品能力,要么拆成 Kustomize Component 复用。
旧路有新价
新路有旧债
取舍非对错
只在边界中
09、洞见
9.1 分工线要画在差异的性质上,而不是工具的强弱上
选择 Helm 还是 Kustomize,通常被讨论成能力对比:谁更强大、谁更灵活。但真正决定分工的是差异的性质。数值差异(副本数、超时、标签)适合参数化;结构差异(多一个容器、多一条网络策略、换一个调度约束)适合 patch。把结构差异硬塞进 values,就会长出 if 森林;把数值差异硬塞进 patch,就会得到一堆只差一行的 overlay 文件。
9.2 反直觉判断:patch 不报错,不等于 patch 生效
大多数配置工具的失败是显式的——语法错、字段错、类型错,都会中断流程。Kustomize 的 strategic merge 不是:target 匹配不上时它可能安静跳过,渲染成功、退出码为零、apply 也成功。这意味着「命令成功」在这个架构里不具备证明力。必须额外构造一个断言,去证明「这个字段确实来自那个 patch」。把渲染产物当契约来测,比信任工具返回值更可靠。
9.3 反直觉判断:两个工具并不必然比一个更难维护
直觉上,多引入一个工具就多一份成本。但在多环境场景里,单个工具被迫同时承担「产品定义」和「环境定义」两个正交职责,成本会以组合数增长——三个客户两个环境就是六份 values,每个新增客户都在增加耦合。拆成两个工具之后,成本从「组合增长」变成「固定的一层对齐成本」。前提是这三个条件成立:职责边界清晰、渲染入口唯一、产物可 diff。缺任何一条,双工具就会退化成双写。
9.4 产品包的版本只应由 Chart 版本决定
如果客户的 prod 环境需要通过修改 overlay 来适配,那 overlay 就是环境的一部分,不该影响产品版本号。反过来,产品升级时若必须修改 overlay,说明 Chart 里还残留着环境假设。这条规则很好用:它能反过来告诉你边界画错了没有。
工具无高下
职责有分野
判断须留痕
演化自成规
10、系统落地
原来有什么:一个 Chart 加六份 values,模板里带环境分支,集群状态由 helm upgrade 写入。配置的正确性依赖人工核对。
本篇新增什么:deploy/overlays/ 目录承担环境差异;Chart 收敛为纯产品包;渲染入口统一为 kustomize build --enable-helm;CI 增加 patch 生效断言与 kubectl diff 幂等校验;工具版本在 CI 中锁定。
现在能做什么:同一份 Chart 服务多客户多环境,新增环境只新增一个 overlay 目录;产品升级只改 Chart 版本与默认镜像;任一环境的最终清单可以单独导出审阅,也可以单独 diff。
还缺什么:密钥与外部依赖的连接信息没有归属;NetworkPolicy、ResourceQuota 这类横切策略没有统一表达;patch 断言的覆盖率不足;多集群分发还没有版本化的通道。
下一步如何演进:把横切能力抽成 Kustomize Component,让多个 overlay 可以按需组合;密钥接入外部密钥管理系统而不是写在 values 里;把渲染产物归档成发布物,配合 GitOps 做「仓库即唯一真相」的收敛。这些都属于新的工程任务,不在本篇范围内。
原有三分地
今增一段墙
能者可复制
缺者待补全
11、小结
Q1 → 不是二选一,而是分层:Helm 管产品能力,Kustomize 管环境差异
Q2 → 数值差异进 values,结构差异进 patch,边界按差异性质划分
Q3 → 统一渲染入口 + patch 生效断言 + kubectl diff 幂等校验
状态 → 同一份 Chart 服务多环境,新增客户只新增 overlay 目录
一问定职责
二问定边界
三问定校验
状态即可交
12、作业
12.1 理解题:为什么环境差异用 values 表达会爆炸,用 patch 表达却能收敛?
参考答案:values 只能表达数值与布尔开关,模板要据此产生不同结构,就必须写条件分支,分支数量随差异维度的组合增长。patch 描述的是「相对于基准的最终样子」,每个 overlay 只关心自己那几处差异,不需要知道其他环境的存在,因此文件数量随环境数线性增长,而不是随维度组合增长。
12.2 实战题:在本地用 helm template 渲染一份 Chart,再让 Kustomize 渲染同一份 Chart,验证两种方式产出的清单是否一致,并解释差异来源。
参考答案:先用 helm template dh deploy/helm/digital-human 导出清单,再用一个 overlay 通过 helmGlobals.chartHome 指向同一个 Chart 目录,执行 kustomize build --enable-helm。差异通常来自三处:release 名与 namespace 参数不同导致对象名不同;overlay 中的 images、replicas、namespace transformer 做了二次修改;如果 overlay 用了 configMapGenerator,还会多出名称后缀哈希。逐条对齐这三处,两份清单应当一致。
12.3 排障题:kustomize build 成功退出,但目标环境的副本数没有变化,列出你的排查顺序。
参考答案:第一步确认渲染产物里 replicas 到底是多少,而不是先怀疑集群;第二步确认副本数的来源——是 Helm values、overlay 的 replicas 字段,还是 patch,三个来源同时存在时以最后生效者为准;第三步确认 patch 的 target 是否与渲染出的对象名、kind 完全匹配,这是最常见的静默跳过原因;第四步确认这类字段是否受其他 transformer 影响;最后用 kubectl diff 确认集群当前状态与期望状态的差别,同时排查是否存在第二条写入路径(例如有人执行过 helm upgrade)。
12.4 架构判断题:有同事提议彻底放弃 Helm,改用纯 Kustomize 组织所有环境。你的判断是什么?
参考答案:关键看是否需要「可复用、可版本化、可升级的产品包」。如果只是本团队内部的少量环境,纯 Kustomize 的 base/overlay 已经够用,少一层模板工具更简单。但如果要把产品交付给多个客户、需要 Go template 的循环与函数来生成重复结构、需要 hook 语义来管理安装前后的动作、需要 Release 记录与回滚,那么 Helm 提供的是 Kustomize 不打算提供的能力。反过来,去掉 Helm 也意味着要自行实现版本化打包与安装生命周期,成本往往被低估。判断依据是产品交付模型,不是工具偏好。
理解须成句
实战须成行
排障须成链
判断须成规
13、思考
这场冲突的本质,从来不是 Helm 和 Kustomize 谁更强,而是一个工具被要求同时承担两个正交职责:定义产品,和定义环境。当这两个职责挤在同一个参数体系里,差异就只能靠条件分支表达,分支的组合数就是维护成本的形状。
把职责拆开之后,判断标准也变得可操作了:凡是描述「这个产品是什么」的,放进 Chart;凡是描述「这次部署与基准差在哪」的,放进 overlay。凡是越界的写法,都会在某一处露出破绽——Chart 里出现环境名,或者 overlay 里试图拼装产品结构。
更值得带走的一点是:在这个架构里,「命令成功」不等于「配置生效」。patch 的静默跳过、双写路径的相互覆盖,都不会报错。所以必须把渲染产物当成契约来断言,把 diff 当成证据来看。工程判断的可靠性,最终来自可验证的产物,而不是来自工具的承诺。
一图分两界
一渲定真形
一补知取舍
一役见边界

浙公网安备 33010602011771号