4.3. Kustomize 声明式应用管理
同一套 Deployment,在 dev 要 1 个副本、
v1.0镜像,在 prod 要探针、资源限制、TLS 证书和独立命名空间。为每个环境复制一份完整 YAML,迟早会漂移:改一个镜像 tag 要改四处,漏改一处就是事故。Kustomize 把公共部分写成 base,把差异写成 overlay 里的 patch,再用kubectl apply -k一次渲染并提交。
- 层级:
L4 · 实战
- 难度:
进阶
- 预计:
70 分钟
- 前置:
Deployment、Service、ConfigMap 与 Secret
4.3.1. 你将学到
base/overlay 的分层模型,
kustomization.yaml各字段的职责边界。configMapGenerator/secretGenerator的 hash 后缀机制,以及它与滚动更新的关系。strategic merge patch 与 JSON6902 patch 各自适合改什么。
Kustomize 与 Helm 的分工,以及一条能进 CI 的
kubectl -k工作流。
4.3.2. 为什么需要它
环境之间的差异往往只有三五个字段,却逼着人把整份清单复制四遍。Helm 用模板解决复用,代价是引入 Go template,清单不再是合法 YAML;Kustomize 反过来,把「写好的 YAML」当输入,用 patch 做局部覆盖,输出仍是纯 YAML。它从 kubectl 1.14 起内置,kubectl kustomize 与 apply -k 无需额外安装。
做法 |
输入 |
输出 |
适合 |
|---|---|---|---|
复制目录 |
多份完整 YAML |
无 |
一次性实验 |
Helm |
Chart 模板 + values |
渲染结果 |
第三方组件、需要版本与依赖 |
Kustomize |
base + overlay + patch |
渲染结果 |
自研应用、按环境分层 |
4.3.3. 核心概念
4.3.3.1. base 与 overlay
目录结构本身就是分层:base/ 放 Deployment、Service 与公共标签,dev/、prod/ 各有一份 kustomization.yaml,prod 另有 patches/ 与 secrets/。每个含 kustomization.yaml 的目录都是一个 Kustomization,overlay 用 resources 引用 ../base,Kustomize 先渲染 base,再按顺序应用 overlay 的生成器与变换器。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deploy-demoapp.yaml
- service-demoapp.yaml
resources 既能列文件,也能列目录(目录里必须有 kustomization.yaml)。base 的改动会自动流到所有 overlay,这是「单一事实来源」的来源。kubectl kustomize <dir> 只打印渲染结果、不碰集群;kubectl apply -k <dir> 才真正提交,-k 与 -f 不能同时使用。
备注
原书字段与 v1.31 写法
原书 base/kustomization.yaml 写的是 commonLabels。kustomize v4 起把 bases、patchesStrategicMerge、patchesJson6902、commonLabels 标记为废弃,v5 仍能读但会打印 warning:bases 换 resources,两种 patch 字段合并为 patches,commonLabels 换 labels。本章用新写法,实验里给出对照。
4.3.3.2. 生成器:configMapGenerator 与 secretGenerator
手写 ConfigMap 的痛点是「改了内容却忘了改名字」:Deployment 引用 demoapp-conf,内容变了,Pod 不重启,进程还在读旧配置。生成器从文件或字面量生成对象,并在名字后追加内容 hash。
默认产出 demoapp-conf-<hash>、demoapp-ssl-<hash>。Kustomize 的 nameReference 转换器会把引用它们的 configMapKeyRef.name、volumes[].secret.secretName 一并改成新名字,所以带 hash 是安全的;内容一变 hash 就变,Pod 模板随之变化,滚动更新自动触发。generatorOptions.disableNameSuffixHash: true 会关掉这个机制:名字固定,但失去「改配置即滚动」,原书为让外部引用稳定而关闭它,代价是每次改配置要手动 kubectl rollout restart。
备注
字面量里的引号
literals 的格式是 key=value,Kustomize 不会剥掉引号。原书写 host="0.0.0.0",生成出来的值带引号,应用读到的是 "0.0.0.0"。除非确实需要引号,一律写 host=0.0.0.0。
4.3.3.3. 变换器:改名、命名空间与标签
变换器在渲染阶段批量改写资源,避免在 base 里写死环境信息。
字段 |
作用 |
注意 |
|---|---|---|
|
给所有资源名加前后缀 |
引用处会被同步改写 |
|
设置 |
只影响命名空间级资源 |
|
批量加标签 |
|
|
换镜像、覆盖副本数 |
|
commonLabels 会同时写进 Deployment 的 spec.selector.matchLabels,而 selector 在已创建的对象上不可变,直接 apply 会报 field is immutable;labels 默认 includeSelectors: false,只加 metadata 与 Pod 模板标签,所以更安全。
4.3.3.4. patch:strategic merge 与 JSON6902
strategic merge patch 长得像原始资源,只写要改的部分,用 apiVersion/kind/name 定位目标。列表按 merge key 合并——containers 按 name 匹配,所以 patch 里必须写对容器名。
apiVersion: apps/v1
kind: Deployment
metadata:
name: demoapp
spec:
template:
spec:
containers:
- name: demoapp
env:
- name: PORT
valueFrom:
configMapKeyRef: {name: demoapp-conf, key: port}
JSON6902 是 RFC 6902 的操作序列,靠路径索引定位,适合改数组元素、删字段、加一个 base 里不存在的端口。
- op: replace
path: /spec/ports/0/targetPort
value: 8080
- op: add
path: /spec/ports/1
value: {name: https, protocol: TCP, port: 443, targetPort: 8443}
合并语义清晰的用 strategic merge,需要按下标增删改数组的用 JSON6902;replace 只能用于已存在的节点,add 才能新建,路径写错会直接渲染失败。
4.3.3.5. 与 Helm 的分工
二者不冲突,边界按「谁维护这份清单」划:第三方组件用 Helm(版本、依赖、社区 values 都在别人手里),自研应用用 Kustomize(清单在自己仓库,差异只跟环境有关)。
两者可以叠加:用 helm template 渲染第三方 Chart,再用 Kustomize 打补丁;或用 helmCharts 字段声明式渲染。Argo CD、Flux 原生识别 kustomization.yaml,这是它比「脚本 + helm template」更适合声明式交付的原因。
4.3.3.6. kubectl -k 工作流
kubectl kustomize demoapp/prod > /tmp/prod.yaml # 只渲染,便于 review 与存证
kubectl diff -k demoapp/prod # 与集群现状对比,CI 里最有价值的一步
kubectl apply -k demoapp/prod # 提交
顺序是「渲染 → diff → apply」:kubectl diff 显示服务器端将要发生的变化,比 apply --dry-run 更接近真实结果。CI 里把 /tmp/prod.yaml 作为制品存档,线上事故时能立刻回答「当时到底 apply 了什么」。
4.3.4. 动手实验
实验:base 到 dev/prod 两级 overlay
目标:一份 base 支撑 dev 与 prod,prod 额外叠加生成器、探针、TLS 卷与 Service 端口。
前置:Kubernetes v1.31;ikubernetes/demoapp 镜像可拉取;本机有 openssl。
4.3.4.1. 步骤 1 · 建 base 并渲染
按上面的结构放好 base 的三个文件(Deployment 用 ikubernetes/demoapp:v1.0,Service 暴露 80),然后 kubectl kustomize demoapp/base | head -12 确认渲染,再 kubectl create namespace dev。注意 kubectl apply -k demoapp/base -n dev 的 -n 只作用于请求,不会写进渲染结果,容易被误认为「已经分环境」;分环境请用 overlay 的 namespace 字段。
4.3.4.2. 步骤 2 · dev overlay:只改副本与镜像
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../base]
namespace: dev
labels:
- pairs: {environment: dev}
images:
- {name: ikubernetes/demoapp, newTag: v1.1}
replicas:
- {name: demoapp, count: 2}
kubectl apply -k demoapp/dev
kubectl -n dev get deploy demoapp -o jsonpath='{.spec.replicas}{"\n"}{..image}{"\n"}'
预期输出 2 与 ikubernetes/demoapp:v1.1。labels 没写 includeSelectors,selector 仍是 base 里的 app: demoapp,不会触发不可变错误。
4.3.4.3. 步骤 3 · prod overlay:生成器 + patch
先备证书:openssl req -x509 -newkey rsa:2048 -nodes -days 365 -subj "/CN=demoapp" -keyout prod/secrets/tls.key -out prod/secrets/tls.crt。
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: [../base, namespace.yaml]
namespace: prod
labels:
- pairs: {environment: prod}
images:
- {name: ikubernetes/demoapp, newTag: v1.2}
configMapGenerator:
- name: demoapp-conf
literals: [port=8080, host=0.0.0.0]
secretGenerator:
- name: demoapp-ssl
files: [secrets/tls.crt, secrets/tls.key]
type: kubernetes.io/tls
patches:
- path: patches/deploy-demoapp-env.yaml
- path: patches/deploy-demoapp-probes.yaml
- target: {version: v1, kind: Service, name: demoapp}
path: patches/patch-service-targetport.yaml
deploy-demoapp-probes.yaml 给容器加 /livez、/readyz 探针,patch-service-targetport.yaml 就是上面的 JSON6902 片段。原书 prod 的 patch 以 demoapp.port 为键引用 ConfigMap,而生成器产出的键是 port,两者不一致,Pod 会卡在 CreateContainerConfigError;实验里统一成 port。
4.3.4.4. 步骤 4 · 渲染、提交、验证
kubectl kustomize demoapp/prod > /tmp/prod.yaml
kubectl diff -k demoapp/prod | head -40
kubectl apply -k demoapp/prod && kubectl -n prod get cm,secret
名字带 hash(如 demoapp-conf-8m2kt)说明生成器生效,patch 里的 demoapp-conf、demoapp-ssl 已被改写成带 hash 的全名。再改一次 images.newTag 并重新 apply,观察 Pod 是否滚动——这就是「配置即版本」的实际效果。
4.3.4.5. 验证清单
kubectl kustomize demoapp/prod里所有命名空间级资源都是namespace: prod。ConfigMap 与 Secret 名字带 hash,Deployment 里的引用同步更新。
Pod 有
livenessProbe与readinessProbe,容器内/etc/demoapp/certs/tls.crt存在。Service 的
targetPort是 8080,且多出 443→8443。dev 副本数为 2、镜像
v1.1,prod 为v1.2,两者互不影响。
4.3.5. 常见问题与排错
现象 |
可能原因 |
处理方式 |
|---|---|---|
|
|
在 overlay 目录跑 |
Pod |
patch 引用的 ConfigMap 键与生成器不一致 |
|
patch 完全不生效 |
目标的 |
先渲染,确认对象的真实名字 |
|
用了 |
去掉 selector 变更,或删掉 Deployment 重建 |
改了 ConfigMap 但 Pod 没重启 |
|
移除该选项,或手动 |
JSON6902 报 |
数组下标不存在,或 |
先 |
4.3.6. 进阶挑战
把原书那份裸的 sidecar 片段(
deploy-demoapp-add-sidecar.yaml只有- name: proxy一段)改写成合法的 strategic merge patch 接入patches,验证容器顺序与postStart。用
components/抽出「可观测性组件」,给 dev 与 prod 同时注入 Prometheus 注解与 sidecar,只在 prod 打开。在 CI 里跑
kubectl kustomize+kubectl diff -k,把渲染结果与 diff 作为制品;再对比同一应用用 Helm 渲染的差异。
4.3.7. 小结
base 放公共资源,overlay 只写差异;
kubectl kustomize渲染,kubectl apply -k提交。生成器的 hash 后缀让「配置变更」变成「对象变更」,nameReference 会同步改写引用。
strategic merge 按名字合并列表,JSON6902 按下标操作;Kustomize 管按环境分层,Helm 管第三方组件的打包分发。