Kustomize Base、Overlay 与差异治理
同一个 Deployment 在开发环境跑得好好的,测试环境却少了资源限制;工程师打开两份几乎相同的 YAML 手工比对,最后发现不是 Kubernetes 丢字段,而是某个 overlay 的 patch 已经匹配不到改名后的容器。更麻烦的是,本机 kubectl kustomize 渲染正常,GitOps 控制器使用另一版 Kustomize 后输出不同,仓库没有改动,集群却持续显示 drift。
Kustomize 不使用模板占位符。它先装载完整的 Kubernetes 资源,再按 kustomization.yaml 声明执行生成、改名、patch、replacement 和引用重写,最终输出普通 YAML。base 保存多个环境共享且本身可渲染的资源,overlay 引用 base 并只表达该环境的差异。它的优势是最终对象始终符合 Kubernetes 结构;它的风险也来自结构变换:目标选择器、名称哈希、字段路径和执行器版本只要有一个变化,输出就可能静默漂移。
先固定真正执行渲染的版本
Kustomize 有两个常见入口。kubectl kustomize <目录> 和 kubectl apply -k <目录> 使用 kubectl 内嵌版本;独立 kustomize build <目录> 使用单独安装的版本。两者命令相似,但 feature、字段弃用、OpenAPI 处理和 bug 修复并不自动同步。先把版本写成证据:
kubectl version --client --output=yaml
kustomize versionkubectl 输出中的 kustomizeVersion 才是内嵌版本。若独立命令不存在,不影响 kubectl -k 入门;若本机、CI 和 GitOps 控制器都参与渲染,则三处版本都必须记录。Kustomize 项目采用 Apache-2.0 许可证,本体免费;维护成本来自渲染回归、远程依赖镜像、控制器升级和漂移排查。
安装独立 CLI 时,优先从 kubernetes-sigs/kustomize 官方 release 取得对应平台归档,固定版本并核对 release asset 的校验信息。已具备受控 Go 工具链时也可安装明确版本:
go install sigs.k8s.io/kustomize/kustomize/v5@v5.8.1
kustomize version@v5.8.1 是可重复输入;省略版本会让不同时间的开发机得到不同二进制。执行时要在Kustomize 官方 release 确认团队批准版本,不能只看 major:官方已标记 v5.8.0 存在 namespace 向子 kustomization 传播的回归,v5.8.1 修复了该问题,也补上了 Helm 4 集成兼容。企业内网应把官方归档和 checksum 镜像到内部制品库,并让 CI 从同一来源恢复。包管理器安装虽然省事,仍要检查最终路径,避免独立 CLI 与 kubectl 内嵌版本被混为一谈。
Kustomize v5 的 patches、replacements 和较新的 labels 等能力不能假定旧执行器都支持。升级时先读取 release notes,渲染所有生产 overlay,并把输出规范化后比较。不要笼统写“支持 Kubernetes 某版本”:Kustomize 主要处理 YAML,但内置 OpenAPI schema、已移除 API、字段合并策略和最终服务端校验仍与 Kubernetes 版本相关。客户端构建通过不代表目标 API server 接受清单。
建立一个能独立渲染的 base
下面的项目只创建 Deployment、Service 和 ConfigMap,适合在本地或临时 namespace 完整清理:
kustomize-lab/
base/
deployment.yaml
service.yaml
kustomization.yaml
overlays/
dev/
kustomization.yaml
deployment-patch.yamlbase/deployment.yaml 不写环境前缀,也不硬编码生成器最终 hash:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 1
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: web
image: nginx:1.27.5
ports:
- name: http
containerPort: 80
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
- name: API_HOST
value: replace-mebase/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
app: app
ports:
- name: http
port: 80
targetPort: httpbase/kustomization.yaml 组合资源,并从 literal 生成 ConfigMap:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: app-config
literals:
- LOG_LEVEL=info
- API_HOST=http://api.default.svc先单独构建 base:
kubectl kustomize kustomize-lab/base > base-rendered.yaml
kubectl apply --dry-run=client -f base-rendered.yaml第一条应输出三个对象,ConfigMap 名称类似 app-config-<hash>;Deployment 中 configMapKeyRef.name 会同步改成带 hash 的名称。第二条不写入对象,但 kubectl 的 schema 验证和 API discovery 仍可能连接当前集群;完全离线时以第一条的构建结果为本地证据,服务端接受度留到有目标集群时验证。如果 base 不能独立构建,overlay 的每次失败都会混入共享层错误,很难判断差异来自哪里。
hash 来自生成内容。当 LOG_LEVEL 改变,ConfigMap 名称改变,引用它的 Pod template 也变化,Deployment 因而滚动更新。这比覆盖同名 ConfigMap 更容易让运行中 Pod 与配置版本对应。generatorOptions.disableNameSuffixHash: true 可以关闭 hash,但会失去这个自然 rollout 信号;只有明确由应用热加载且另有配置版本观测时,才值得承担同名对象缓存和 Pod 不重启的代价。
overlay 只表达开发环境的差异
overlays/dev/deployment-patch.yaml 修改副本和资源预算。patch 应小而单一,便于发现冲突:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 2
template:
spec:
containers:
- name: web
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
memory: 64Mioverlays/dev/kustomization.yaml 引用 base、合并生成器并复制字段:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: kustomize-lab
namePrefix: dev-
resources:
- ../../base
labels:
- pairs:
environment: dev
includeSelectors: false
patches:
- path: deployment-patch.yaml
configMapGenerator:
- name: app-config
behavior: merge
literals:
- LOG_LEVEL=debug
- API_HOST=http://dev-api.kustomize-lab.svc
replacements:
- source:
kind: ConfigMap
name: app-config
fieldPath: data.API_HOST
targets:
- select:
kind: Deployment
name: app
fieldPaths:
- spec.template.spec.containers.[name=web].env.[name=API_HOST].value这里有三种不同语义。patches 修改已存在对象的结构;overlay 的 configMapGenerator 通过 behavior: merge 覆盖同名 base generator 的键;replacements 从 ConfigMap 的 data.API_HOST 读取值,再写入 Deployment 的特定字段。replacement 复制的是构建时值,不会让 Pod 在运行时动态追踪 ConfigMap;ConfigMap 后续被集群内人工修改,环境变量不会跟着变。
现在构建并检查关键结果:
kubectl kustomize kustomize-lab/overlays/dev > dev-rendered.yaml
kubectl apply --dry-run=client -f dev-rendered.yaml预期对象名称带 dev-,namespace 为 kustomize-lab,Deployment 副本为 2,LOG_LEVEL 引用带 hash 的 ConfigMap,API_HOST 的直接值为开发地址。不要只检查命令退出码,可以用结构化查询验证不变量:
kubectl kustomize kustomize-lab/overlays/dev \
| kubectl create --dry-run=client -f - -o json \
| jq -r '.items[] | [.kind,.metadata.name,.metadata.namespace] | @tsv'如果环境没有 jq,可以直接审阅 dev-rendered.yaml;团队 CI 则应使用 YAML/JSON parser 断言对象数量、镜像 digest、资源 requests/limits、禁止的 cluster-scoped kind 和 Secret 来源,不能靠 grep 缩进文本。
patch 匹配发生在资源身份上
patches 可承载 strategic merge 风格 YAML,也可用 JSON 6902 操作。target 能按 group、version、kind、name、namespace、labelSelector 或 annotationSelector 选择对象,多个 patch 按声明顺序执行。patch 文件中的 metadata.name: app 匹配的是变换链中的资源身份,最终 namePrefix 会得到 dev-app;因此不要为了“看起来像最终名字”擅自把 patch 改成 dev-app。
做一个反向实验,把 deployment-patch.yaml 中的 metadata.name 改成 missing-app,再构建:
kubectl kustomize kustomize-lab/overlays/dev预期失败,错误中出现无法找到唯一 patch 目标或 no matches。这条证据说明 Kustomize 没有按文件位置随便合并,而是在累计的 ResourceMap 中按 Group/Version/Kind/Name/Namespace 标识资源。恢复 name: app 后重新构建,副本数应再次为 2。
JSON 6902 适合数组下标或明确的 add/remove/replace,但路径对结构变化敏感。例如:
patches:
- target:
group: apps
version: v1
kind: Deployment
name: app
patch: |-
- op: replace
path: /spec/replicas
value: 3如果 replace 指向不存在路径,构建会失败;add 的行为又不同。结构化 patch 对内置 Kubernetes 类型可利用合并键识别 containers[name=web],但自定义资源若缺少 OpenAPI 合并信息,列表可能整体替换。对 CRD 做复杂 patch 时,必须检查完整输出,并在控制器版本升级后回归。
replacement 解决复制,不解决任意计算
过去常见的 vars 已进入弃用路线,新项目优先使用 replacements。replacement 从一个资源字段复制到多个目标字段,适合把生成后的 Service 名称、ConfigMap 值或 namespace 写入其他对象。它不是字符串模板引擎;要拼接 URL、做条件分支或遍历任意文档时,继续堆字段路径会变得脆弱,应重新设计资源模型,或在受控生成阶段使用更合适的工具。
反向验证 replacement 时,把 source 的 fieldPath 改成 data.MISSING。构建应失败并指出 fieldPath 不存在,而不是悄悄填空。另一个更危险的反例是把 target 的 name: app 改成 name: missing-app:当前 v5 执行器可以在零目标时继续构建,结果保留 replace-me。所以 source 错误能靠失败捕获,target 错误还必须由 CI 直接断言最终字段值,避免“渲染成功但 replacement 没生效”。
generator 是配置构建器,不是密钥保险箱
configMapGenerator 与 secretGenerator 可以从 literals、envs 和 files 生成资源。下面的写法能运行,但不适合真实凭证:
secretGenerator:
- name: app-secret
literals:
- username=demo
- credential=EXAMPLE_ONLY生成结果只是把值放进 Kubernetes Secret 的 base64 字段。源码、渲染输出、终端历史、CI 日志和 GitOps controller 缓存都可能拿到明文。真实项目应让 Git 保存加密密文或外部 Secret 引用,由受控身份在部署阶段解密或从密钥系统读取。不要使用 --enable-alpha-plugins 或 exec plugin 随意执行仓库代码来“顺便解密”;插件扩大了代码执行和供应链边界,必须有准入、固定版本、沙箱和审计。
generator 的 hash 还会影响清理。每次配置变化产生新对象,kubectl apply -k 会创建新 ConfigMap,但不会天然删除所有旧 hash 对象。频繁发布的 namespace 可能积累大量孤儿配置。使用 kubectl apply --prune 需要精确 allowlist 和 label selector,误配会删除不属于当前应用的资源;GitOps 控制器的 prune 也要绑定应用 inventory。成本治理应观测对象数量和更新频率,而不是看到单个 ConfigMap 很小就忽略 etcd 与审计日志增长。
build、diff、apply 要形成同一条证据链
真实变更前,先创建实验 namespace,再分别看本地输出、服务端接受度和集群差异:
kubectl config current-context
kubectl create namespace kustomize-lab
kubectl auth can-i create deployments -n kustomize-lab
kubectl kustomize kustomize-lab/overlays/dev > dev-rendered.yaml
kubectl apply --dry-run=server -f dev-rendered.yaml
kubectl diff -k kustomize-lab/overlays/dev
kubectl apply -k kustomize-lab/overlays/devkustomize 只渲染;服务端 dry-run 会经过 API discovery、schema、RBAC 和 admission,但不保存对象;diff 比较实时对象与期望状态,发现差异时通常返回退出码 1,CI 必须把“有差异”和“命令执行错误”分开处理;apply -k 才产生真实变更。首次 diff 因对象不存在会显示整份新增内容,这是预期证据。
应用后观察 rollout 和生成对象:
kubectl get deployment,service,configmap -n kustomize-lab
kubectl rollout status deployment/dev-app -n kustomize-lab --timeout=2m
kubectl get deployment/dev-app -n kustomize-lab -o yaml
kubectl get events -n kustomize-lab --sort-by=.lastTimestamp预期 Deployment Available、副本为 2,Service selector 能选中 Pod,ConfigMap 名称带 hash。若 rollout status 超时,先看 Pod 与 Event:镜像拉取、配额、PodSecurity 和调度失败都发生在 Kustomize 输出之后,继续重跑 build 不会修复运行时问题。
修改 LOG_LEVEL=trace 后再次 build,应看到 ConfigMap hash 和 Deployment 引用一起变化;kubectl diff -k 应显示旧引用被新引用替换。apply 后出现新 ReplicaSet,证明 hash 变更通过 Pod template 触发 rollout。若关闭 hash 后配置变了而 Pod 不重启,这不是 Kustomize “失效”,而是 Pod template 没有变化;应用必须自行热加载,或由额外 checksum annotation 驱动 rollout。
加载限制保护构建根目录
Kustomize 可以引用目录、文件和远程 Git URL。默认 load restrictor 会限制文件加载,阻止一个 kustomization 随意读取构建根之外的本地文件。这个限制既是可重复性约束,也是敏感文件隔离。
做一个稳定的反向实验:在 overlays/dev/kustomization.yaml 的 resources 中加入指向构建根之外的绝对路径或越界相对路径,再运行:
kubectl kustomize kustomize-lab/overlays/dev预期失败,错误会指出文件不在允许目录或安全路径中。某些执行器提供 --load-restrictor LoadRestrictionsNone 绕过限制,但这不应成为修复项目结构的默认参数。关闭后,恶意或误写的 overlay 可能把工作区中的 kubeconfig、密钥文件或其他项目清单带入渲染输出。正确做法是把共享 base 放在明确的仓库目录、发布为固定版本,并让渲染器只读所需路径。
远程 base 例如 Git URL 会引入网络、可用性和可变引用风险。分支名与浮动 tag 可能在不改消费仓库的情况下改变输出;私有仓库还把凭证带进渲染器。应固定不可变 commit,经过内部镜像和准入审查,并在升级 commit 时保存渲染 diff。离线环境应把依赖 vendoring 到受控目录,许可证和来源随代码一起登记。
项目接入时先决定所有权
推荐把共享对象放在 k8s/base,每个真实部署环境放一个 overlay;本地个人差异不要演变成几十个长期 overlay。base 由应用团队维护可移植契约,环境 overlay 由拥有该环境发布责任的团队维护 namespace、资源预算、入口和策略差异。镜像 tag 或 digest 可以用 images 字段修改:
images:
- name: nginx
newName: registry.example.com/platform/nginx
digest: sha256:<审核过的摘要>生产交付优先固定 digest,tag 作为可读元数据。不要让 CI 直接提交每次构建的随机 patch 到多个 overlay;更稳的是由晋级流程更新唯一镜像字段,经过评审后由调和器部署。
同一 Kubernetes 对象只能有清晰的期望状态所有者。若开发者运行 kubectl apply -k,GitOps 控制器又自动同步同一 overlay,紧急手改会很快被覆盖;反过来暂停自动同步后忘记恢复,仓库也失去控制力。项目文档应写清谁渲染、谁 apply、是否 prune、inventory 在哪里、漂移如何告警、紧急变更如何回写 Git。
Kustomize 与 Helm 的选择也应落在状态模型上。Kustomize 擅长对已有资源做结构化环境差异,输出没有模板语法;Helm 擅长把可配置应用打成版本化 Chart,并保存 release 历史。可以让 GitOps 控制器分别原生渲染二者,也可以在受控流水线把 Helm 输出交给 Kustomize,但嵌套越深,版本、所有权和错误定位越难。不要让 Helm release 与 kubectl apply -k 同时管理同一个对象。
GitOps 看到的是渲染器,不只是 Git
GitOps 控制器通常内置特定 Kustomize 版本,并可能限制 remote base、插件、Helm integration 和 load restrictor。开发机 build 一致而控制器 drift 时,先收集三份事实:本机版本与输出、CI 版本与输出、控制器实际版本和渲染错误。再检查控制器是否注入 namespace、labels、images 或额外 patches。
升级 GitOps 控制器也等于升级潜在渲染器。上线前应对全部 overlay 做 golden render 回归:以固定版本构建,解析为资源集合,按 GVK/namespace/name 排序,比较字段级变化。时间戳、随机值或浮动远程依赖会破坏可重复性,应从生成链移除。输出 diff 中若出现 cluster-scoped 资源、RBAC 扩权、selector 改变、PVC 或 Service 不可变字段变化,要进入专项评审。
Kustomize 没有 release history 和 rollback 命令。回滚通常是恢复 Git 中上一版期望状态,再由相同版本渲染器 apply;如果上一版引用的镜像、remote base 或外部 Secret 已不可取,Git commit 本身并不足以恢复。长期治理要保留不可变镜像和依赖、固定渲染器、记录已部署 commit,并定期演练从旧 commit 重建清单。
权限、容量与长期维护
本地 build 不需要集群权限,apply 所需权限由最终资源决定。CI 可把渲染与部署身份拆开:无 kubeconfig 的任务负责 build、schema 与策略检查;短期、namespace 受限的身份负责 server dry-run、diff 和 apply。这样即使第三方 base 或插件有问题,也不会天然获得集群写权限。
ResourceQuota、LimitRange、admission policy 和集群默认值只在服务端出现。overlay 应显式提供关键 requests/limits、Pod 安全上下文和环境标签,但不要复制平台默认值到每个文件制造漂移。容量评审看最终清单的副本数乘单 Pod request、滚动更新峰值、PVC 总量和生成对象增长;开发演示中的 2 副本与 64Mi 只是实验值,生产值由负载基线和 SLO 决定。
弃用治理不能等 build 报错才开始。bases、patchesStrategicMerge、patchesJson6902、commonLabels 和 vars 等旧写法应按目标执行器的迁移说明逐步收敛到 resources、patches、labels 与 replacements。可以先在清洁分支运行 kustomize edit fix,但 vars 转换要显式使用 kustomize edit fix --vars,官方命令本身也会警告转换可能覆盖多个资源文件且不保证输出等价。迁移前后必须用同一版本构建并保存字段级差异;只改字段名而不比较输出,可能改变 patch 顺序、selector 注入或列表合并结果。
团队应把以下事实变成持续检查,而非发布前口头确认:每个 overlay 能用固定版本构建;同一资源 ID 不重复;禁止未固定的 remote ref;Secret 不以明文进入输出 artifact;所有工作负载有镜像摘要和容量预算;cluster-scoped 对象有单独 owner;prune selector 不会跨应用;控制器升级前后 golden render 只有经过解释的差异。
删除实验并验证 hash 对象已清空
先用同一 overlay 删除它声明的对象:
kubectl delete -k kustomize-lab/overlays/dev
kubectl get deployment,service,configmap -n kustomize-lab预期当前 overlay 中的 Deployment、Service 和当前 hash ConfigMap 被删除。若实验期间产生过旧 hash ConfigMap,它们可能不在当前渲染集合中,仍需按实验标签和 owner 逐一确认;不要在共享 namespace 里用宽泛 label 一键删除。
确认 namespace 只含实验对象后再清理:
kubectl delete namespace kustomize-lab
rm -rf kustomize-lab base-rendered.yaml dev-rendered.yaml最后执行 kubectl get namespace kustomize-lab,预期返回 NotFound。若删除卡住,检查 finalizer、PVC 和控制器状态。清理本地渲染文件也很重要,其中可能包含 Secret、内部 registry 地址和安全策略细节。
一条可治理的 Kustomize 链路应能从最终字段反推来源:它来自 base、哪个 overlay、哪一个 patch、哪个 generator 或 replacement;名称为何带 hash,引用为何同步变化;由哪一版执行器渲染;服务端又因何接受或拒绝。做到这一点,环境差异才是可审查的结构变换,而不是散落在多份 YAML 里的运气。
