自定义资源,就是Kubernetes里一种可插拔的东西。它不像Pod、Service那样内置,而是靠CustomResourceDefinition(CRD)声明出来的新对象。你可以把它理解成手机自带应用之外的“第三方应用”。想用这个第三方应用,得先安装一个“应用商店描述文件”,也就是CRD。CRD装好之后,才能创建对应的“自定义资源实例”,简称CR。CRD是模板,CR才是真正填了内容的表单。

Flux 是一套非常流行的 GitOps 工具,盯住 Git 仓库里的 YAML 变化,自动把配置同步到集群。Kustomize Controller 是 Flux 的插件,专门负责处理 Kustomize 目录。它会把仓库里的多个 YAML 先“揉一揉”,再一次性 apply 到集群。听起来很顺畅,但一旦碰上自定义资源,坑就一个接一个来了。

一、先弄明白几个基础概念

1.1 自定义资源是什么鬼

Kubernetes 内置的对象有 Pod、Service、Deployment,这些就是官方出厂应用。可业务上总有奇奇怪怪的需求,比如“哨兵策略”,用来控制某个应用的 CPU 和内存阈值。Kubernetes 本身不认识这个对象,所以我们需要自己定义。定义方式就是写 CRD。CRD 注册完成以后,我们再去创建 SentinelPolicy 这样的实例,Kubernetes 就愿意接受了。这个过程就像自己画了一个新图标,然后把这个图标装到手机桌面里。

1.2 Flux 和 Kustomize Controller 在干嘛

Flux 会监听 Git 仓库的变化,发现新提交以后把仓库拉到本地,然后找到你配置的 Kustomize 目录,交给 Kustomize Controller 处理。Kustomize Controller 先读取 kustomization.yaml,执行“构建”动作,把分散的资源文件整合成一份 YAML,再调用 Kubernetes API 把这些资源创建或更新到集群里。

1.3 它们之间的适配到底指什么

适配自定义资源,简单说就是让 Kustomize Controller 能正确理解并部署那些不在 Kubernetes 内置类型里的对象。听起来容易,真做起来全是细节。构建顺序、补丁方式、参数类型、依赖关系,每一个点都可能让整个流程卡住。

二、入坑之前,先看看这些坑点

2.1 资源顺序不对,自定义资源像迷路的孩子

如果你把 CRD 和 CR 放在同一个 kustomization.yaml 里,表面看挺合理,但实际运行时会出问题。Kustomize 可以一次构建出两份 YAML,但 apply 的顺序有时候并不是你想要的。更头疼的是,CRD 创建完成后,API Server 还需要一点时间注册新类型。如果 CR 立刻被 apply,API Server 可能还没“认识”这个类型,于是报错说找不到对应的 kind。

技术栈:Kubernetes + Flux + Kustomize + Shell
下面这个目录结构会让不少人翻车:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# 这是一个示例目录结构
kustomization/
├── kustomization.yaml    # 同时引用了 crd 和 sentinel-policy
├── crd.yaml              # 自定义资源定义
└── sentinel-policy.yaml  # 自定义资源实例
# 技术栈:Kubernetes + Flux + Kustomize + Shell
# kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - crd.yaml
  - sentinel-policy.yaml

然后执行:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# 用 kubectl apply 模拟 Flux 的行为
kubectl apply -k kustomization/
# 运气不好时会看到类似错误:
# error: unable to recognize "kustomization/sentinel-policy.yaml": no matches for kind "SentinelPolicy" in version "sentinel.example.io/v1"

这个错误出现的核心原因,是 CRD 和 CR 没有分开处理,也没有等待 CRD 就绪。Flux 在真实环境里也会遇到同样的问题,因为它就是按照 Kubernetes 原生规则去 apply 的。

2.2 构建工具不认自定义资源的“家族关系”

Kustomize 里有一种补丁方式叫 Strategic Merge Patch,它非常聪明,能根据 Kubernetes 内置类型自动合并字段。但这种聪明对自定义资源无效。因为 Kustomize 没有内置 CRD 的 schema,不知道自定义资源内部有哪些字段,更不知道列表字段该怎么合并。于是你用 Strategic Merge Patch 改自定义资源时,有时候什么都不发生,有时候直接报错。

2.3 想给自定义资源打补丁,结果 Kustomize 不买账

举个例子,你有一个 SentinelPolicy 对象,想用 patchesStrategicMerge 把 memory 从 512 改成 1024。Kustomize 看到这个 patch 文档,只知道它是一个普通 YAML,但不确定该如何与目标资源融合。特别是如果你要改列表字段,比如规则列表,它可能把整个列表替换掉,或者完全没反应。这不是你的问题,是工具本身对未知资源不够了解的局限。

2.4 参数替换的坑:值对了但类型不对

Flux 的 Kustomization 支持 postBuild.substitute,可以在 apply 前把 YAML 里的变量替换成具体值。这个功能很便利,但有个隐藏坑:替换结果默认都是字符串。比如自定义资源里有一个字段,schema 要求是整数,你写 value: ${CPU_LIMIT},然后替换成 "1024"。结果 API Server 拒绝接受,因为字段类型不匹配。这种问题在普通 Deployment 里还好,因为很多字段能自动转换;但自定义资源的 schema 往往定义更严格,一个类型错就全部失败。

2.5 集群端与 Git 端状态错位

GitOps 的核心理念是“Git 里的就是集群里的”。自定义资源往往会被自己的控制器不断更新 status 字段,甚至修改一些 spec 字段。Flux 每次同步时,会发现 Git 里的记录和集群里的状态有差异,于是尝试把资源“纠正”回来。频繁纠正不仅制造噪音,还可能导致正在运行的任务被打断。有些自定义资源的 spec 字段会被控制器按需调整,可 Git 里写的是固定值,两边就会一直打架。

三、优化方案:怎么一步一步把坑填平

3.1 把 CRD 和自定义资源拆开部署

不要把鸡蛋全放在一个篮子里。把 CRD 单独放到一个目录,把自定义资源实例放到另一个目录。这样 Flux 就能分两步走:先把 CRD 注册好,确认可用以后,再去创建 CR。虽然多了一个目录,但这顺序就彻底稳了。

3.2 用 Flux 的 dependsOn 控制先后

Flux 的 Kustomization CRD 自带 dependsOn 字段,专门用来声明依赖关系。你可以在创建 CR 的 Kustomization 里写上“先等 CRD 的 Kustomization 成功”。Flux 会严格遵守这个顺序。

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# flux-kustomization-crd.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: crd-base
  namespace: flux-system
spec:
  interval: 5m
  path: ./kustomize/crd
  sourceRef:
    kind: GitRepository
    name: repo
---
# 技术栈:Kubernetes + Flux + Kustomize + Shell
# flux-kustomization-app.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: app
  namespace: flux-system
spec:
  interval: 5m
  path: ./kustomize/app
  sourceRef:
    kind: GitRepository
    name: repo
  dependsOn:
    - name: crd-base

这样设置以后,crd-base 同步失败时,app 就不会开始执行。顺序不再靠运气,而是靠配置保障。

3.3 对自定义资源使用 JSON Patch

既然 Strategic Merge Patch 不擅长处理自定义资源,那就换 JSON Patch。JSON Patch 就像一张“精准施工图”,明确告诉 Kustomize 要改哪个路径、做什么操作。不依赖 Kustomize 对目标类型是否了解,只要字段路径正确,就能改。

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# kustomization.yaml 中使用 patchesJson6902
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - sentinel-policy.yaml
patchesJson6902:
  - target:
      group: sentinel.example.io
      version: v1
      kind: SentinelPolicy
      name: demo
    patch: |
      # 把内存阈值从 512 改成 1024
      - op: replace
        path: /spec/thresholds/memory
        value: 1024

注意 target 里的 group 要写完整的域名,version 要写 v1,kind 和 name 要和 CR 完全一致。路径从 /spec 开始,不能漏掉前面那个斜杠。

3.4 用 postBuild 批处理修正配置

postBuild.substitute 适合处理字符串类型的变量,比如镜像 tag、环境名称。对于数字、布尔值这类字段,尽量避免直接替换。如果非用不可,可以把自定义资源里的字段类型改成字符串,或者用 JSON Patch 配合变量一起处理。再不行就写一个小脚本,在 CI 里先对 YAML 做类型校验。总而言之,postBuild 是一个好工具,但别让它给你埋类型地雷。

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# flux-kustomization-with-substitute.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: app
  namespace: flux-system
spec:
  interval: 5m
  path: ./kustomize/app
  sourceRef:
    kind: GitRepository
    name: repo
  postBuild:
    substitute:
      SENTINEL_THRESHOLD: "1024"

这个文件只做演示。如果 SENTINEL_THRESHOLD 要填到整数字段里,记得在 CRD 里把对应字段类型设置为 string,否则会被 API Server 拒绝。

3.5 加校验工具,让问题在提交前暴露

最好的修复,是不让问题发生。在 CI 里集成 kubeconform,先跑 kustomize build,再把构建结果交给 kubeconform 去校验。kubeconform 会读取 CRD 的 OpenAPI schema,提前检查字段类型、必填项、路径是否正确。

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# 构建 Kustomize 目录,并用 kubeconform 校验
kustomize build ./kustomize/app | kubeconform -strict \
  -kubernetes-version 1.28.0 \
  -schema-location 'https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{.NormalizedKubernetesVersion}}-standalone{{.ResourceKind}}.json'

自定义资源的 schema 需要额外提供。你可以把 CRD 文件转成 JSON Schema,放到 schema-location 指定的路径里,让 kubeconform 能查到。这步虽然要花点时间准备,但回报很高,很多 patch 路径错误和类型错误在 CI 阶段就能被拦截。

四、一个完整的实战示例

4.1 技术栈说明

本示例统一使用:Kubernetes + Flux + Kustomize + Shell。下面的 CRD、CR、Kustomization、kubectl 命令,全都在这个技术栈范围内。

4.2 准备自定义资源

先定义一个 CRD,用于管理“哨兵策略”,控制 CPU 和内存阈值。

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: sentinelpolicies.sentinel.example.io
spec:
  group: sentinel.example.io
  scope: Namespaced
  names:
    kind: SentinelPolicy
    plural: sentinelpolicies
    singular: sentinelpolicy
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        # 这里定义了字段类型,后面 patch 和校验都靠它
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                thresholds:
                  type: object
                  properties:
                    cpu:
                      type: integer
                    memory:
                      type: integer

再写一个自定义资源实例:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# sentinel-policy.yaml
apiVersion: sentinel.example.io/v1
kind: SentinelPolicy
metadata:
  name: demo
spec:
  thresholds:
    cpu: 100
    memory: 512

4.3 初始化 Kustomize 目录

目录结构如下:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# 目录结构
kustomize/
├── crd/
│   ├── kustomization.yaml
│   └── crd.yaml
└── app/
    ├── kustomization.yaml
    └── sentinel-policy.yaml

crd 的 kustomization:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# crd/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - crd.yaml

app 的 kustomization:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# app/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - sentinel-policy.yaml
# 这里如果用 patchesStrategicMerge 会掉坑
# 推荐用 JSON Patch
patchesJson6902:
  - target:
      group: sentinel.example.io
      version: v1
      kind: SentinelPolicy
      name: demo
    patch: |
      # 线上环境把 memory 阈值调到 768
      - op: replace
        path: /spec/thresholds/memory
        value: 768

4.4 用 Flux 跑起来

假设你已经创建好了 GitRepository 对象,仓库地址指向自己 Git 仓库。现在创建两个 Kustomization 对象:一个是 crd-base,一个是 app。app 依赖 crd-base。

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# flux-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: crd-base
  namespace: flux-system
spec:
  interval: 5m
  path: ./kustomize/crd
  sourceRef:
    kind: GitRepository
    name: repo
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: app
  namespace: flux-system
spec:
  interval: 5m
  path: ./kustomize/app
  sourceRef:
    kind: GitRepository
    name: repo
  dependsOn:
    - name: crd-base
  postBuild:
    substitute:
      SENTINEL_THRESHOLD: "768"

保存为 flux-kustomization.yaml 后执行:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# 把 Flux 的 Kustomization 对象提交到集群
kubectl apply -f flux-kustomization.yaml

Flux 会先执行 crd-base,等 CRD 完全就绪,再执行 app。

4.5 遇到问题后如何调整

如果 app 报错说几个字段不匹配,先别慌,在本地跑一遍构建:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# 查看 app 目录实际构建出来的内容
kustomize build ./kustomize/app

如果看到 memory 还是 512,说明 JSON Patch 没生效。检查 target 里的 name 是否正确,patch 路径是否拼写准确。如果报错说找不到路径 /spec/thresholds/memory,说明 CR 和 patch 的字段名不一致。

如果报错说 no matches for kind,就去检查 CRD 是否已经注册成功:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# 查看 CRD 是否已经存在
kubectl get crd sentinelpolicies.sentinel.example.io

如果这个 CRD 还没就绪,就先检查 crd-base 这个 Kustomization 的状态:

# 技术栈:Kubernetes + Flux + Kustomize + Shell
# 查看 Flux 的 Kustomization 状态
kubectl get kustomization -n flux-system crd-base

状态为 Ready 再继续排查 app。状态出错了,就看日志。这整套排查链路其实很顺。

五、优缺点总结

5.1 这种适配方案的优点

把 CRD 和自定义资源拆开部署,职责清晰,依赖明确。Flux 的 dependsOn 能保证先后顺序,不再靠运气。JSON Patch 不依赖 Kustomize 对自定义资源内部结构的理解,只要路径正确就能改。配合 CI 里的校验工具,很多错误在集群外面就被解决。整体上,GitOps 流程更可控,出了问题也好回滚。

5.2 这种适配方案的缺点

拆目录会让仓库结构多一层,维护起来稍微麻烦。dependsOn 让应用之间的依赖链路变长,一个 CRD 出问题,后面所有资源全卡住。JSON Patch 的路径一旦因业务调整而改变,就得跟着改。postBuild 的类型陷阱也需要时时注意。可以说,稳定性和灵活性换来了额外的配置成本,这是不可避免的。

六、文章总结

自定义资源和 Flux 的 Kustomize Controller 适配,本质问题是“Kubernetes 不认识的东西,如何让机器按顺序、按规矩放上去”。核心思路很朴素:拆开、排序、用对补丁、提前校验。把 CRD 单独放置,用 dependsOn 等它就绪,再使用 patchesJson6902 做精准修改,最后在 CI 里跑一轮 kubeconform。四个动作做完,绝大多数坑都能被填平。GitOps 从来不是一锤子买卖,把这些细节打磨好,线上才能睡得着觉。