如果在生产环境里已经叫了一整年的 Deployment 和 StatefulSet,某天你突然发现无论如何都表达不了业务想要的“发布策略”,或者运维同学说“能不能让研发直接提一个资源来申请数据库实例”,这时候你基本就走到 Kubernetes 的高级扩展点面前了:CRD。
CRD(CustomResourceDefinition,自定义资源定义)是 Kubernetes 可扩展性的核心机制。它允许你往集群里注册一种全新的 API 资源类型,之后kubectl可以像操作 Deployment 一样去 get、apply、watch 这个新对象,API Server 也会自动帮它做校验、持久化和权限控制。说得更直白一点:内置资源是出厂预装的,而 CRD 是运行当中自己注册的“新车型”。这篇文章会从原理讲到实操,再到企业落地一定会踩的坑,适合刚接触 Operator 概念的开发/运维同学,也想给已经写过 CRD 但没跑通 controller 的人补上最后一块拼图。
1. 打开 Kubernetes 自定义资源的大门
1.1 从内置资源到 CRD:一个可插拔的资源系统
刚开始接触 Kubernetes 的时候,很多人对“资源”的理解约等于 Deployment、Service、ConfigMap。这些内置资源构成了 K8s 对外的主要使用面,但它们远远不是全部。Kubernetes 从来没有把自己绑定死在这十几个资源上,它向下定义了一套 API 约定:只要有一套 Group(API 组)、Version(版本)和 Kind(类型)的三元组,一个对象就能被 API Server 保存、校验、分发,CRD 就是这套约定中面向用户开放的注册入口。
为什么这件事这么重要?因为大部分业务系统不只有“跑一组容器”这种需求,还有“申请一个数据库实例”、“创建一条灰度策略”、“发布一个应用版本”这些更贴近业务领域的对象。传统做法是在 Kubernetes 外面写一张数据库表,再写一堆接口让用户录入,然后脚本去生成 Deployment,整个过程既绕又不好维护。CRD 的方式是直接把业务对象变成 K8s 原生资源,天然继承 etcd 持久化、kubectl 操作、RBAC 权限控制,以及客户端基于 watch 机制实时感知变化的能力。等于把业务建模做进了集群底座里。
另外一个很现实的点:CRD 的复用价值极高。社区里 Prometheus Operator、Argo CD、Karpenter 等大量项目都在用 CRD 暴露自己的配置入口。你学会了 CRD,未来看任何一个现代云原生项目的文档,都不会被各种陌生资源类型吓到。反过来,掌握 CRD 也会让团队内部工具建设走向标准化,因为大家已经熟悉了kubectl apply -f这套心智模型。
1.2 什么场景值得用 CRD,什么场景别硬上
从我的实践看,CRD 的典型场景可以归成五类。第一类是基础设施即代码化,数据库实例、消息队列、缓存集群,用户希望像写 YAML 一样申请一个“存储服务”,典型如 RedisCluster、MySQLOperator 这类。第二类是部署流程抽象,把多个 Deployment、Service、Ingress 打包成一个“应用”交付给研发,用户在界面上只看到业务自己的术语。第三类是策略类资源,网络隔离策略、预算审批策略,CRD 可以把这些配置带进集群并被控制器消费。第四类是设备与调度扩展,比如把 GPU、FPGA 这类设备抽象成资源,controller 负责上报和绑定。第五类是把外部系统拉进集群管理范围,比如通过 CRD 描述一个云厂商负载均衡器,再由 controller 调用 API 创建。
但反过来,不推荐硬上 CRD 的情况也存在。如果只是临时记录一些元数据,或者外部系统需要直接通过数据库查询,那 CRD 很可能只是徒增复杂度。如果一个对象没有任何关联的控制动作,那它更像一张数据表,用 ConfigMap 加文档约定也许就够了。CRD 最大的成本在于配套 controller 的开发和运维,只有当你真的需要“声明式管理 + 自动调谐”这个组合时,它才值回票价。所以判断标准永远不是“这功能听起来高级”,而是“有没有一个控制器能消费这个定义并持续收敛状态”。
2. CRD 是如何被 API Server 接纳的
2.1 API 路径、Group/Version 与 Kind 三件套
在写 CRD 之前,至少要搞明白几个术语:Group、Version、Kind 是 Kubernetes API 资源的身份标识。Group 通常是一个域名,比如apps.example.com,对应 API 路径的一部分/apis/apps.example.com/v1;Version 就是v1、v1beta1这类;Kind 是对象的类型名,比如WebApp。注册完毕之后,客户端会向https://<apiserver>/apis/apps.example.com/v1/namespaces/<ns>/webapps这样的 RESTful 路径发起请求。这种设计本质上就是一套标准的 REST API 规范,kubectl 的能力比如kubectl get webapps,只不过是对这些 HTTP 接口的封装。
CRD 元数据本身是一种资源,它定义在apiextensions.k8s.io/v1下,可以理解为“注册表的注册项”。一个 CRD 支持同时声明多个版本,其中必须有一个版本标记为storage: true,由它负责把数据写到 etcd;其他版本可以有served: true表示对外提供 API,但只作为访问层面的展示版本,API Server 会自动在版本之间转换。在 v1 版本的 CRD 中,新增或修改 schema 会导致字段变化,所以版本策略非常关键,我在第 5 章会单独展开。
这里还想强调一个容易忽略的细节:CRD 的metadata.name必须是<plural>.<group>的格式,比如你定义 group 是apps.example.com,复数形式是webapps,那么 CRD 名字就是webapps.apps.example.com。这是 API Server 识别 CRD 与 API 路径映射关系的固定规则,写错了根本注册不上。类似这种“看起来无所谓但错一个字母就废”的约定,在 K8s 里其实很多,后面会继续提到。
2.2 声明式模型的三要素:Spec、Status 和 Controller
如果你打开任意一个内置资源的 YAML,会发现它的结构有套路:spec表示用户声明的期望状态,status表示系统观察到的当前状态,metadata保存名字、标签、注解等元数据。CRD 诞生的目标之一就是让开发者按照同样的三要素去设计自定义资源,否则 API Server 能存数据,但没人保证数据会被“实现”。
这不是技术洁癖,而是 Kubernetes 控制循环的底层逻辑。内置资源 Deployment 能帮我们拉起 Pod,不是 Deployment 对象本身有魔法,而是 kube-controller-manager 里有一堆 controller 在 watch Deployment 和 Pod,不断比较“期望副本数”和“实际 Pod 数”,再通过 API 创建或删除 Pod。用户自定义的 CRD 如果没有对应的 controller,它就像一个被存档的表格,谁也不会来理它。所以我评价一个 CRD 设计是否合理,通常看三件事:有没有清晰的spec输入、有没有独立的status输出、有没有一个 controller 去消费 spec 并更新 status。三者齐全,CRD 才算真正活起来。
这一章如果只记住一句话,那就是:CRD 只是定义,功能靠 controller。API Server 只负责把它当作普通对象存取,不会因为你定义了一个 WebApp 就自动去创建 Deployment。很多人第一次写完 CRD,创建了 CR 之后等半天看没有反应,就开始怀疑集群出了问题,其实更多的只是 controller 还没诞生。
3. 手写一个可落地的 CRD:从定义到创建实例
3.1 案例设计与字段规划
用一个大家都懂的案例:团队里运维负责对外网站接入,每次上线要创建一个 Deployment、一个 Service、一个 Ingress,还要把公网域名给到申请者。现在把这个流程转换成 CRD,比如叫WebApp。它放在 groupapps.example.com、versionv1、kindWebApp,短名用wa。用户提交的 YAML 大概是这样的预期:
apiVersion: apps.example.com/v1 kind: WebApp metadata: name: my-blog spec: image: nginx:1.24 replicas: 2 domain: blog.example.com env: - name: TZ value: "Asia/Shanghai"字段选择要遵守一个原则:只在 spec 放用户关心的声明,不要把 controller 内部的临时状态塞进去。这里我们保留image、replicas、domain和env这四个字段,已经足够支撑一个初级网站托管场景。如果以后需要暴露健康检查或资源限制,可以再增加,但要记住任何字段一旦对外承诺,后续改起来就需要版本迁移,所以首版宁可少而精。
校验规则也要提前想清楚:image必填;domain必填且必须是合法域名;replicas是整数,范围 1 到 20;env是数组,每个元素是name/value。这些约束会在 API Server 层生效,用户提交不合法配置会直接被拒绝,controller 那边就少了很多脏数据防御工作。
3.2 编写 CRD 的 YAML(附全量可复制配置)
下面这份 CRD 配置是我在测试环境验证过的,可以直接保存为webapp-crd.yaml并应用。为了便于理解,我加了中文注释,实际文件中注释不会影响使用。
apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: webapps.apps.example.com spec: group: apps.example.com names: kind: WebApp singular: webapp plural: webapps shortNames: - wa scope: Namespaced versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object required: - image - domain properties: image: type: string domain: type: string pattern: '^[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]+$' replicas: type: integer minimum: 1 maximum: 20 default: 1 env: type: array items: type: object properties: name: type: string value: type: string required: - name additionalPrinterColumns: - name: Domain type: string jsonPath: .spec.domain - name: Replicas type: integer jsonPath: .spec.replicas - name: Age type: date jsonPath: .metadata.creationTimestamp这段配置的要点有几个。metadata.name必须是<plural>.<group>,也就是webapps.apps.example.com,这是硬性约定。scope可以是Namespaced或Cluster,大多数业务资源用 Namespaced,全局策略才用 Cluster。versions里至少有一个版本要storage: true,这里我们只有一个v1,所以自然它就是存储版本。additionalPrinterColumns负责让kubectl get的结果更好看,它不会影响 API 存储,但对日常巡检帮助非常大。
3.3 安装 CRD 并创建自定义资源实例
保存后执行kubectl apply -f webapp-crd.yaml,过几秒用kubectl get crd | grep webapp就能看到webapps.apps.example.com,说明 API 类型已经注册成功。接着写一个实例文件my-blog.yaml:
apiVersion: apps.example.com/v1 kind: WebApp metadata: name: my-blog spec: image: nginx:1.24 replicas: 2 domain: blog.example.com env: - name: TZ value: "Asia/Shanghai"执行kubectl apply -f my-blog.yaml,然后kubectl get webapps,也可以简写成kubectl get wa。现在你应该能看到 Domain 和 Replicas 两列输出。注意,这时候执行kubectl describe webapp my-blog,会发现只有 spec 内容,没有任何 status 字段。这是正常的,因为我们还没给这个 CRD 配 controller,API Server 不会主动填 status。很多新手在这里以为创建失败或者 controller 没装上,其实这就是“定义已完成,实现未启动”的状态。
3.4 校验机制:让错误配置在入口就拦下
schema 里的约束不是摆设。比如把replicas设为 0 或者 30,kubectl apply会直接报错,信息类似spec.replicas: Invalid value: 30: must be less than or equal to 20。把image字段删掉也会被拒绝,因为required已经声明了。这套校验发生在 API Server 侧,即使绕过 kubectl,通过 curl 或客户端发起请求一样会被拦截。它带来的好处是:你可以把大量配置错误拦截在源头,controller 不需要再写一堆防御性代码去处理非法输入。
实际使用中,建议把kubectl explain用起来。CRD 注册后,kubectl explain webapp.spec会输出 schema 中定义的字段结构,这对研发同学写资源清单非常友好。另外注意,v1 的 CRD 默认情况下会对未知字段做 prune(剔除),如果用户打错了一个字段名,kubectl apply不会报错,但kubectl get -o yaml里看不到错字段,这个行为容易造成困惑。如果想要更严格,可以配合 CEL 或 admission webhook 增加额外验证,这一部分放在第 5 章讲。
4. 让 CRD 真正干活:Controller 与调谐流程
4.1 CRD 只是数据字典,Controller 才是引擎
到了这一步,你应该已经发现,光有一个 CRD 什么也做不了。要让它变成能自动拉起一套网站,必须写一个 controller。Controller 的概念可以理解成一个一直盯着集群看的巡检员:它用 Kubernetes 的 Watch 机制监听webapps资源的变化,拿到一个对象后,对照对象的spec去查看集群里是否已经有了对应的 Deployment、Service、Ingress;有且配置一致就不动作,没有或者配置有偏差就通过 Kubernetes API 去创建、修改或删除。整个过程不断重复,直到实际状态收敛到期望状态。
Controller 的工程实现可以自己写,也可以用现成框架。生产上绝大多数 Operator 都是基于 Go 的 controller-runtime 写的,它把 informer、workqueue、leader-election 都封装好,开发者只需要实现一个Reconcile函数。如果团队是 Python 或 Node,也可以用对应的 client 版本实现一个简化 controller,前提是自己处理事件重试和状态冲突。我的建议是:如果只是快速验证,用 Python 的 kubernetes 客户端写一个 watch 循环完全够用;如果要做成生产级 Operator,认真上 controller-runtime,它避免了很多本地缓存和并发回调的坑。
4.2 一个最简 Controller 的观察、对比与执行
用 Python 演示核心逻辑会更容易让人看懂。下面不是完整可上生产代码,但已经把最重要的流程串起来了:
from kubernetes import client, config, watch def reconcile(webapp): ns = webapp["metadata"]["namespace"] name = webapp["metadata"]["name"] spec = webapp["spec"] dep_name = f"webapp-{name}" apps_v1 = client.AppsV1Api() desired_replicas = spec.get("replicas", 1) try: dep = apps_v1.read_namespaced_deployment(dep_name, ns) if dep.spec.replicas != desired_replicas: dep.spec.replicas = desired_replicas apps_v1.replace_namespaced_deployment(dep_name, ns, dep) except client.exceptions.ApiException as e: if e.status == 404: dep_body = { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": {"name": dep_name, "namespace": ns}, "spec": { "replicas": desired_replicas, "selector": {"matchLabels": {"app": name}}, "template": { "metadata": {"labels": {"app": name}}, "spec": {"containers": [{"name": "main", "image": spec["image"]}]}, }, }, } apps_v1.create_namespaced_deployment(ns, dep_body) else: raise def main(): config.load_kube_config() crd_api = client.CustomObjectsApi() stream = watch.Watch() for event in stream.stream(crd_api.list_cluster_custom_object, group="apps.example.com", version="v1", plural="webapps"): evt_obj = event["object"] if event["type"] in ("ADDED", "MODIFIED"): reconcile(evt_obj) # DELETED 情况记得清理 Deployment,这里省略这段代码有几个关键点。watch会保持长连接推送事件,事件类型有ADDED、MODIFIED、DELETED。reconcile函数需要是幂等的:重复执行多次结果一致,因为不管事件来几次,都是先对比再操作。这里最常被忽略的是处理DELETED事件,当用户删掉一个webapp,controller 应该清理对应的 Deployment、Service、Ingress,否则集群里会留下一堆孤儿资源。实际操作中还要设置 Finalizer,否则用户删 CR 的流程会失控,这个问题放到第 6 章讲。
4.3 别把 Controller 当成事件回调来用
在带团队时,我发现一个非常普遍的误解:很多人以为 CRD controller 是“加一个注解,然后触发一个业务回调”的事件系统。这是对声明式控制最大的误读。Controller 不保证事件一定会被及时处理,它保证的是“当前实际状态向期望状态持续收敛”。比如你的 controller 崩溃了十分钟,这期间所有 CR 变化可能排队也可能被跳过;等 controller 恢复后,它不应该依赖丢失的事件,而应该通过 List 全量扫描,把每个 WebApp 重新 reconcile。这也是为什么 controller-runtime 框架往往采用 workqueue 加周期性的 re-list,而不仅仅是 watch。
如果确实需要一个“CR 变更后立刻执行一次脚本”的需求,CRD 也能做,但要注意把执行结果写回 status,并且 controller 要能处理执行失败的情况,提供重试与退避。换句话说,哪怕业务是一次性任务,也要按“可重入任务”来设计。这一节最后留一个经验:生产 controller 一定要加 leader election,多副本同时跑同一个 controller 会引发竞态,出现 Deployment 被反复重建的诡异现象。我们曾经用四个副本跑过一段时间,最后查出就是没用 leader election 导致,教训很深。
5. 企业落地必用的高级配置
5.1 用 AdditionalPrinterColumns 提升 kubectl 巡检体验
前面 CRD 定义里已经放了一个additionalPrinterColumns示例,这里展开说说它的价值。默认情况下,kubectl get webapp只会显示NAME和AGE两列,对状态了解很有限。通过给 CRD 增加打印列,比如域名、副本数、当前状态、Ready 数量,巡检时一眼就能判断资源是否健康。列定义写在 versions 节点下面,jsonPath 直接指向字段路径,比如.status.readyReplicas。它不会改变存储和 API 结构,只是 kubectl 打印的辅助元数据,所以后续增加一列不会带来版本兼容问题,非常推荐从第一版就加上。
用一手经验说话:我们在平台上线了三十多个 CRD,凡是用了additionalPrinterColumns的资源,大家巡检效率远远高于看完整 YAML 的资源。另外还可以配合kubectl get xxx -o wide,如果列足够多,Debug 时可以少打很多命令。运维同学在 Grafana 接入告警后,平时看板都不用进,直接在终端对着列输出排查,速度提升非常明显。
5.2 Subresource 的权限隔离:status 与 scale
默认 CRD 的所有字段都平等,任何能读写该资源的用户都可能直接篡改 status。这不是企业想要的。通常我们希望“普通用户只能改 spec,controller 才有权写 status”,这就要启用子资源。你只需在版本定义中加一段:
subresources: status: {}加上之后,API Server 会为/status单独生成一个 HTTP 端点。kubectl get webapp my-blog -o yaml仍然能同时看到 spec 和 status,但普通客户端如果尝试把整个对象写回去并修改 status,会被拒绝或忽略;只有 controller 通过 status 子资源更新才生效。这个机制极大保护了控制面数据的可信度,也符合声明式架构中“观察结果由控制器汇报”的原则。
除了status,还有scale子资源。启用后能支持kubectl scale webapp my-blog --replicas=3,以及 HPA 自动扩缩容。YAML 如下:
subresources: status: {} scale: specReplicasPath: .spec.replicas statusReplicasPath: .status.replicas labelSelectorPath: .status.selector不过要注意,如果spec.replicas和status.replicas字段没有定义,API Server 会报错,所以启用scale前要保证 schema 里有对应字段。这个功能的收益在于:你的 CRD 可以接入 K8s 原生的扩缩容生态,而不是自己再造一套轮子去监听外部指标。
5.3 高级 OpenAPI 校验和 CEL 表达式
CRD 的 schema 不只是声明类型和必填,还支持很多约束。最常用的是minimum、maximum、pattern、enum等,这些属于 OpenAPI v3 的基础件。到了 Kubernetes 1.25 之后,CRD 原生支持 CEL(Common Expression Language)校验,这让我们能表达跨字段的逻辑,比如“域名不能等于 namespace 名”、“副本数必须大于 minReadyReplicas”。示例:
spec: type: object properties: spec: type: object properties: replicas: type: integer minReadyReplicas: type: integer x-kubernetes-validations: - rule: "self.spec.replicas >= self.spec.minReadyReplicas" message: "replicas must be greater than or equal to minReadyReplicas."注意 CEL 校验是写在 schema 层级内的,可以放在对象级、数组级或字段级。初期会有学习成本,但比 admission webhook 更轻、更稳定。如果业务校验极其复杂,比如要调外部系统校验配额,才需要自建 webhook。我的经验是:能用 CEL 解决的就别碰 webhook,webhook 一旦故障会直接影响集群写路径,而且证书轮换、超时配置都是额外负担。
5.4 多版本共存与升级策略
CRD 资源上线只是开始,真正麻烦的是后面改字段。你可能会从v1alpha1升到v1beta1,或者给 v1 定义里新增字段。这里最安全的做法是协商好版本兼容策略。一个 CRD 可以同时声明多个 versions,比如有v1alpha1和v1,但只有一个是 storage 版本。API Server 在收到旧版本请求时会把对象自动转换到 storage 版本再存,所以如果没有写转换逻辑,不同版本的字段命名要尽量保持兼容,或者使用无转换但字段仍然兼容的策略。如果两个版本结构差异很大,就需要 conversion webhook,那又是一大坨运维负担。
我的建议很直接:内部工具从第一版就用v1,不要轻易引入v1alpha1;给字段加限制时尽量宽松,因为收窄限制会造成旧对象无法通过新校验,API Server 在查询这些对象时可能直接报错。我见过有团队因此整个 CRD 无法升级,最终只能靠写迁移 Job 人肉处理存量数据。如果真到了结构不兼容那一步,宁可新增资源类型,也不要强行改同一个 CRD。
6. 实战中的坑与排查套路
6.1 资源创建成功但没反应
我们经常收到这种反馈:kubectl apply一个 CR 成功后,等了十分钟什么都没发生。第一步不是去看集群日志,而是确认有没有 controller 在监听这个资源。可以kubectl get deploy看有没有 controller 部署的 Deployment;再kubectl logs -f <controller-pod>看有没有对应的 watch 事件。常见原因是 controller 镜像没有启动、RBAC 没授权它访问这个 CRD,或者 controller 连接的集群上下文不对。还有一种情况是事件到了但 controller 内部 panic 了,这种往往在日志里有 stack trace。从经验来说,超过一半的“没反应”都是权限或运行环境问题,而不是业务逻辑问题。
这里给一个排查命令清单:先kubectl get crd确认 CRD 存在;再看kubectl api-resources | grep webapp确认当前 kubeconfig 能访问;最后看 controller 的kubectl logs。很多团队把 controller 权限授予到所有 namespace 时容易漏掉customresourcedefinitions本身,记得 ClusterRole 里要加上对应 group。也可以单独开一个 terminal 执行kubectl get webapp -w,观察 API Server 是否真的把事件推给了 controller。
| 现象 | 可能原因 | 快速排查动作 |
|---|---|---|
| CR 创建后无任何反应 | controller 没运行 | 查看 controller Deployment 状态和日志 |
| CR 创建后偶尔触发 | RBAC 未授权 watch CRD | 检查 ClusterRole 中 apiGroups/resources |
| controller 崩溃重启 | 代码 panic 或连接断开 | 用kubectl logs --previous看前一轮日志 |
| 事件丢失但最终能自愈 | 依赖全量 re-list | controller 日志看是否周期性 reconcile |
6.2 CRD 升级失败和字段被卡住
CRD 升级时常遇的一个报错是structural schema error或must preserve unknown fields。v1 的 CRD 要求 schema 是“结构化的”,即不允许随意声明 unknown fields。如果你最初把一个字段定义为 object 但没写 properties,后续想往里面塞任意结构,API Server 会拒绝更新。这时除非你显式设置x-kubernetes-preserve-unknown-fields: true,否则没法绕过。这意味着设计 schema 时要给可能变化的 object 提前留出 map 类型,或者直接声明为 preserve unknown fields,否则后面升级基本等于重写。
另外一个坑是你在新版本里给某个字段增加了minimum: 10的约束,结果 etcd 里已经躺着 replicas=5 的旧对象,API Server 在版本校验或查询时可能会直接返回错误,导致资源不可读。升级前一定要做数据兼容检查,必要时写一个迁移 Job 用补丁把存量对象改到符合新 schema,再更新 CRD。这个顺序不能反,否则线上资源会突然全部 get 不出来。
6.3 删除 CRD 之后 Namespace 一直 Terminating
喜欢用 Finalizer 是好事,但如果 controller 没有实现 finalizer 的清理逻辑,删除 CRD 或 namespace 时就会卡在 Terminating。具体表现是:删掉一个 WebApp 对象后,它一直显示 Terminating,因为 controller 收到 DELETE 事件后没有调用移除 finalizer;如果你在整套 controller 已下线的情况下删 CRD,namespace 也会卡住。排查时kubectl get webapp my-blog -o yaml | grep finalizers,看确认有webapp.example.com/finalizer,然后去 controller 日志确认 DELETE reconcile 是否成功。
解决办法其实很简单:controller 在处理 DELETED 时,先做清理操作(比如删除对应 Deployment),再通过 API 把这个 CR 的 finalizers 列表清空或者直接 PUT 不带 finalizer。如果没有 controller,你可以手工 patch 移除 finalizer 应急,但不建议养成习惯,因为可能留下孤儿资源。更根本的做法是在 CRD 设计阶段就想清楚哪些资源生命周期绑定 CR,哪些需要独立保留。
6.4 RBAC 与资源名的坑
最后整理一个高频误区:RBAC 规则里的resources字段写的是复数资源名,也就是webapps,不需要带 group。很多人会把apiGroups写成extensions或者空,或者把resourceNames写错,结果 controller 启动后有权限读取,但 watch 被拒。还有,自定义资源的名字偶发会与内置资源重名,在kubectl get all里展示时很容易混淆,所以 group 尽量使用有辨识度的域名,不要用太通用的example或demo。我见过有人用apps作为 group,结果和内置apps组重叠,权限规则全乱了,排查了一下午才发现是命名冲突。
关于 CRD 本身的运维,我还有一个习惯给到读者:把 CRD 的配置文件提交到 Git,用 ArgoCD 或 FluxCD 做持续交付,这样版本可回滚,环境差异可追踪。生产环境改 CRD 一定要走双人 code review,因为这个文件一旦出错,影响的是整个集群的 API 面。
我个人在实际操作中的体会是:CRD 的入门门槛不高,写一个 CRD YAML 十分钟就能学会,但真正让它健康运行一年,依赖的是你对 Kubernetes 控制循环、RBAC、版本兼容这些底层机制的尊重。我不止一次看到团队因为图省事跳过 Finalizer 或 status 子资源,最后在运维事故里付出更大代价。不要急,先把一个最小可用的 CRD 加上 controller 跑通,再逐步加校验和高级功能。这套能力一旦掌握,它带给你的不仅是“能自定义资源”,更是一种把业务需求翻译成集群能力的架构思维。最后再分享一个小技巧:每新增一个 CRD,先写使用说明和 demo YAML,这些文档的价值会在半年后你忘了细节时彻底体现出来。