← 返回 Atlas
Kubernetes

Kubernetes CRD

要理解 CRD,别从 API 对象开始,从一次真实需求开始:你想在 Kubernetes 里跑一个 PostgreSQL 集群,官方词表里只有 Deployment/Service/Pod。两条路:要么手动建一堆 Deployment + StatefulSet,盯着它们,坏了手动救;要么发明一个新名词『PostgreSQLCluster』,让集群认识它,再由一个程序守着它——前者是运维,后者就是 CRD + controller,俗称 Operator。本文一路从业务用户怎么用,讲到亲手造一个 CRD,再到让它真正活起来的 controller,最后到生产级的三件套。

k8s · CRD · operator

先看业务用户怎么用:apply 一个自定义对象就行

真实场景看 cert-manager:它向集群注册了一个叫 Certificate 的自定义资源。业务用户要 HTTPS 证书,不写任何签发逻辑,只 apply 一段声明:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-tls
spec:
  secretName: my-tls
  dnsNames: [api.example.com]
  issuerRef: { name: letsencrypt, kind: ClusterIssuer }
几秒后 controller 自动向 Let's Encrypt 申请、做 ACME 校验、把证书写进 my-tls Secret。业务用户从头到尾没碰 ACME 协议——这就是 CRD 的价值:把『一个领域的操作』变成『声明式对象』,再交给 controller 履约。你也完全可以为自己业务发明一个名词,重复这个模式。

亲手造一个 CRD:一份完整 YAML 拆解

造一个叫 Website 的 CRD,只有三部分:names(怎么称呼它)、scope(集群级还是命名空间级)、schema(它长什么样)。
01apiVersion: apiextensions.k8s.io/v1
02kind: CustomResourceDefinition
03metadata:
04  name: websites.example.com
05spec:
06  group: example.com
07  names: { plural: websites, singular: website, kind: Website }
08  scope: Namespaced
09  versions:
10    - name: v1
11      served: true
12      storage: true
13      schema:
14        openAPIV3Schema:
15          type: object
16          properties:
17            spec:
18              type: object
19              properties:
20                image: { type: string }
21                replicas: { type: integer, minimum: 1 }
kubectl apply -f 之后,集群就认识 websites 这个名词了:kubectl get websites 是空列表;再 kubectl apply -f 一个 website.yaml(里面写 spec.image、spec.replicas),就创建出一条自定义对象,和创建 Pod 一样能 get/describe/delete。注意:到这里还没有任何 Pod 会跑起来——CRD 只定义了『名词和它长什么样』,真正干活的人(盯着它 reconcile 的 controller)还没出场。

让 CRD 活起来:controller 与 Reconcile

Controller 是盯着 Website 对象、把现实拨向 spec 的程序。用 kubebuilder 起脚手架:
kubebuilder init --domain example.com
kubebuilder create api --group web --version v1 --kind Website
生成的骨架里,Reconcile 函数是这么一段循环(Website 增删改、或它管理的 Deployment 变化,都会触发一次):
func (r *WebsiteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  var w webv1.Website
  if err := r.Get(ctx, req.NamespacedName, &w); err != nil {
    return ctrl.Result{}, client.IgnoreNotFound(err)
  }
  // 确保 spec.image + spec.replicas 对应的 Deployment 存在:没有就建,规格不对就改
  // 把观察结果写回 status,比如 Ready: True
  return ctrl.Result{}, r.Status().Update(ctx, &w)
}
make install 把 CRD 装进集群,make run 在本地跑 controller。然后 apply 一个 Website:controller 侦测到新对象,生成对应 Deployment;删除 Website,它再回收资源。这就是『契约是 CRD,履约是 controller』的全部含义。

生产级三件套:status 条件、finalizer、webhook

光有 Reconcile 还不够生产用。status 条件:把健康状态写成 conditions 数组(像 Pod 的 Ready 那样),kubectl get website 和 UI 才能读到『现在到底咋样』,否则别人只能猜。finalizer:在对象删除前先做清理(比如回收云盘),在 metadata.finalizers 里挂一个名字,Reconcile 里做完清理再移除它——finalizer 没移除,对象永远卡在 Terminating。admission webhook:复杂校验和默认值注入用 validating/mutating webhook,能跨字段校验、能自动填默认值,比内联 schema 强得多;而 CEL 校验(1.25+)把一部分规则直接写进 schema,连 webhook 服务都不用起。

版本演进:为什么 v1alpha1 不是稳定的

CRD 的 schema 一旦成为 storage 版本,就是持久承诺:etcd 里已存的对象按旧 schema 序列化。想改结构,要么加新版本(served + storage 切过去),要么写 conversion webhook 在版本间转换。所以社区惯例:v1alpha1 明示『随时推翻』,敢改 schema;v1 才承诺稳定。升级 CRD 时最痛的坑:改 storage version 后老对象读不出来,或 schema 收紧导致老对象校验失败。多版本共存 + conversion 是唯一正路。

实战示例

端到端走一遍:kubectl apply -f crd.yaml(Website 定义)→ kubectl apply -f my-site.yaml(spec.image: nginx, spec.replicas: 2)→ controller 侦测到新对象 → 生成 deployment/my-site(2 副本)。kubectl get website my-site 的 status 里 Ready: True。然后 kubectl delete website my-site → controller 的 finalizer 先回收 Deployment 再放行删除。全程你只声明了『我要一个 nginx 网站』,剩下全是 controller 干的。

子关键词

CRD 三段式 names / schema / scope
names 定义怎么称呼(kind/plural),schema 定义长什么样(OpenAPI),scope 定义集群级还是命名空间级。三段决定了集群认识它的方式。
CR 自定义资源 词表里的一条具体条目
一个 Website 实例,和 Pod 一样可以 kubectl get/apply/describe/delete。CRD 是类,CR 是对象。
Reconcile 循环 watch → 读 spec → 拨现实 → 写 status
controller 的核心:任何相关事件触发一次 Reconcile,它读对象 spec、确保下属资源符合、把结果写回 status。kubebuilder 的 ctrl.Request 就是这个入口。
kubebuilder CRD + controller 脚手架
init 建项目、create api 建资源和控制器、make install 装 CRD、make run 本地跑。写 Operator 的标准起点,别从零手写 informer。
finalizer 删除前的清理钩子
metadata.finalizers 里挂名字,Reconcile 做完清理再移除。没移除就永远 Terminating——最常见的 operator bug 之一。
status 条件 健康状态的可读化
conditions 数组(每个有 type/status/reason),kubectl get 和 UI 读它判断对象健康,controller 负责更新。
admission webhook validating / mutating
mutating 在对象写入前改它(填默认值),validating 在写入前拦它(跨字段校验)。比内联 schema 强,但要部署 webhook 服务。
CEL 校验 schema 内联规则
1.25+ 把校验规则写进 openAPIV3Schema 的 x-kubernetes-validations,像『磁盘容量只能变大』这种规则不用起 webhook。

衍生角度

设计模式:声明式期望 + reconcile 循环

用户只写想要的状态(spec),controller 把现实拨向 spec 并回写 status。kubebuilder 生成的 Reconcile 就是这个模式的模板,整个云原生生态都长这样。

概念拆解:CRD vs CR vs controller

CRD 是词表(定义名词),CR 是词表里的一条具体条目(一个 Website),controller 是履约人(保证每个 CR 符合 spec)。三者缺一,系统都不完整。

风格架构:Operator 的标准骨架

Operator = CRD + controller + (webhook)。cert-manager、prometheus-operator、ingress-nginx 全是这套骨架——理解它就理解了半个云原生生态。

原理分析:为什么 schema 校验只是入口安检

API server 按 schema 做结构校验并 prune(丢弃未声明字段)。没声明的字段多传不报错、但也不会被存下来——所以『没报错』不等于『存住了』,这是新手最容易误判的点。

实际用法与坑

  • apply CRD 前先看 schema:properties 里 type 漏写,字段会被 prune 静默丢弃,存下来就是空。
  • status 字段要开 subresource:CRD 里标记 status: {...} 子资源,否则客户端和 controller 抢着写会冲突。
  • finalizer 加了就要在 Reconcile 里负责移除:忘了移除,对象永远 Terminating。
  • 别改 storage 版本:要加字段就加新 version + conversion webhook,硬改会读不出老对象。
  • controller 没跑,apply 自定义对象一样成功——只是没人履约,别以为装完 CRD 就完事了。

下一步动手做

用 kubebuilder 从零起一个 Website 项目(init → create api → 写 30 行 Reconcile → make install && make run),apply 一个 Website 看 Deployment 被生成;再给它加 finalizer 和 status 条件,体会删除流程;最后读 cert-manager 的 Certificate CRD 定义,看生产级 schema 和条件怎么组织。

相关书籍

Kubernetes in Action

控制器与自定义资源章节,配合 Kubebuilder 上手,是 CRD/Operator 的实践入口。

关联关键词