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 的生成器与变换器。

base/kustomization.yaml
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 里写死环境信息。

字段

作用

注意

namePrefix / nameSuffix

给所有资源名加前后缀

引用处会被同步改写

namespace

设置 metadata.namespace

只影响命名空间级资源

labels

批量加标签

includeSelectors: true 会改 selector

images / replicas

换镜像、覆盖副本数

images.name 必须精确匹配

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 里必须写对容器名。

prod/patches/deploy-demoapp-env.yaml
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 里不存在的端口。

prod/patches/patch-service-targetport.yaml
- 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:只改副本与镜像

dev/kustomization.yaml
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。

prod/kustomization.yaml
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. 常见问题与排错

现象

可能原因

处理方式

accumulating resources: no such file

resources 路径写错,或目录里没有 kustomization.yaml

在 overlay 目录跑 kubectl kustomize . 逐步定位

Pod CreateContainerConfigError

patch 引用的 ConfigMap 键与生成器不一致

kubectl -n prod get cm -o yaml 对照 configMapKeyRef.key

patch 完全不生效

目标的 kind/name 与渲染结果不符

先渲染,确认对象的真实名字

field is immutable

用了 commonLabels 或 includeSelectors: true,改了 selector

去掉 selector 变更,或删掉 Deployment 重建

改了 ConfigMap 但 Pod 没重启

disableNameSuffixHash: true 关掉了 hash

移除该选项,或手动 kubectl rollout restart

JSON6902 报 path not found

数组下标不存在,或 replace 用在空节点

先 add 再 replace,或改用 strategic merge

4.3.6. 进阶挑战

  1. 把原书那份裸的 sidecar 片段(deploy-demoapp-add-sidecar.yaml 只有 - name: proxy 一段)改写成合法的 strategic merge patch 接入 patches,验证容器顺序与 postStart。

  2. 用 components/ 抽出「可观测性组件」,给 dev 与 prod 同时注入 Prometheus 注解与 sidecar,只在 prod 打开。

  3. 在 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 管第三方组件的打包分发。

4.3.8. 延伸阅读