Kubernetes 的核心设计哲学之一是可扩展性。API Server 内置的资源类型——Pod、Deployment、Service——只能覆盖通用计算模型,但现实中的应用远不止于此:数据库集群有自己的主从切换逻辑,消息队列有 topic 副本的分配策略,机器学习训练任务有 GPU 亲和性和检查点管理。CustomResourceDefinition(CRD)和 Operator 模式正是 Kubernetes 给出的扩展机制:让用户把领域知识编码进集群本身,而不是写在外部脚本或运维手册里。

CRD 的核心能力是向 API Server 注册新的 Group/Version/Kind(GVK)。注册完成后,kubectl get myappkubectl get pod 在 API 层面对等——同样走 REST 接口,同样存储到 etcd,同样支持 watch、label selector 和 RBAC 授权。Operator 在 CRD 之上再加一层:部署一个 Controller,持续监听自定义资源的变化,把用户声明的期望状态翻译成若干内置 Kubernetes 资源的增删改操作,并把执行结果写回 status 字段。这个"声明式 API + 调谐循环"的组合,把以往需要人工执行的 Day-2 运维动作——扩容、故障切换、证书续签——变成可自动化的代码路径。

从工程角度看,CRD + Operator 是 Kubernetes 生态的增殖机制。Prometheus Operator、cert-manager、Argo CD、Strimzi(Kafka on K8s)都走同一条路:先定义领域对象(ServiceMonitor、Certificate、Application、KafkaTopic),再让 Operator 把这些对象的期望状态落地到底层资源。理解这套机制,是读懂任何复杂 Kubernetes 扩展的前提。

数据流全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
用户: kubectl apply -f myapp.yaml  (kind: MyApp)


API Server
├── 对照 CRD 中的 OpenAPI v3 schema 做字段验证
├── 通过准入 webhook(如有)做业务校验
└── 写入 etcd(路径: /registry/mygroup.io/myapps/namespace/name


etcd 存储 MyApp 对象


MyApp Controller(Operator 进程)
├── Informer 监听 MyApp 资源的 ADD/UPDATE/DELETE 事件
├── 事件进入 WorkQueue(去重、限速)
└── Reconcile(namespace/name):
get MyApp spec(用户期望)
list 实际 Deployment / Service / ConfigMap
diff spec vs actual
create / update / delete 子资源
patch MyApp.status(写回 conditions、observedGeneration)


子资源(Deployment、Service 等)被 kubelet 落地到节点

CRD:向 API Server 注册新类型

GVK 三元组

每个 Kubernetes API 对象由 Group、Version、Kind 唯一标识。内置资源用空 Group(core group),自定义资源通常用 DNS 域名作 Group,例如 databases.example.com/v1 下的 PostgresCluster

CRD 本身是一个 Kubernetes 对象,kind 为 CustomResourceDefinition,由 API Server 内置的 CRD controller 处理。一旦 CRD 对象写入 etcd,API Server 就动态注册对应的 REST 端点,无需重启。端点路径格式为 /apis/<group>/<version>/namespaces/<ns>/<plural>,例如 /apis/apps.example.com/v1/namespaces/default/myapps

OpenAPI v3 Schema 验证

CRD 的 spec.versions[].schema.openAPIV3Schema 字段承担字段类型和约束验证。常见配置:

  • type: string / integer / boolean / object / array:基本类型约束
  • minimum / maximum:数值范围
  • pattern:正则约束,例如限制 image tag 只能包含字母数字和连字符
  • required:必填字段列表
  • x-kubernetes-preserve-unknown-fields: true:保留 schema 未定义的字段(兼容旧版本时使用,不推荐滥用)

验证发生在 API Server 写入 etcd 之前,字段不合法直接返回 422,不会到达 Operator。

Subresources:/status 与 /scale

/status 把 spec 和 status 的写权限分离。普通用户只能写 spec,Operator 只能写 status(通过 UpdateStatus API)。这防止用户手动篡改 status,也让 RBAC 可以精细授权,同时避免 spec 更新和 status 更新互相覆盖产生的 resourceVersion 冲突。

/scalekubectl scale 和 HPA 能对自定义资源生效。需要在 CRD 中指定 specReplicasPathstatusReplicasPath,HPA controller 通过 /scale 子资源读写副本数,无需了解自定义资源的内部结构。

Conversion Webhook:跨版本迁移

当 CRD 从 v1alpha1 升级到 v1 时,两个版本同时存储在 etcd(各自有 storage version 标记)。API Server 在读写时调用 Conversion Webhook 做对象转换,Webhook 是标准 HTTPS 服务,实现 ConversionReview 请求-响应协议。

不配置 Conversion Webhook 时,多版本 CRD 只能用 None 策略(要求所有版本 schema 完全相同),实际等于不支持多版本演化。

Operator 模式:把运维知识编码为代码

controller-runtime:脚手架与核心抽象

kubebuilder 和 operator-sdk 都基于 controller-runtime 库,核心抽象三件套:

Manager:单个进程可以托管多个 Controller。Manager 负责选主(leader election,多副本部署时避免多个 Reconcile 同时执行)、共享 cache(所有 Controller 共用同一个 Informer,减少 API Server 压力)、健康检查端点(/healthz 和 /readyz)。

Reconciler:用户实现的接口,只有一个方法 Reconcile(ctx, Request) (Result, error)。Request 只包含 namespace/name,Reconciler 自己从 cache 里 Get 对象。返回 Result{Requeue: true} 或非 nil error 时会重新入队,配合指数退避限速。

Scheme:GVK 到 Go struct 的注册表,controller-runtime 用 Scheme 做序列化和反序列化。自定义资源的 Go struct 要注册到 Scheme,才能被 Client 正确处理。

Spec 与 Status 的分工

这是 Operator 设计的核心约定,也是最容易被忽视的地方:

  • spec:用户写,表达期望状态。Operator 只读不写——Operator 修改 spec 会触发无限调谐循环。
  • status:Operator 写,表达观测到的实际状态。用户只读(通过 RBAC 约束)。

status.conditions 是标准化的状态表达,遵循 Kubernetes API 约定:每个 condition 有 type、status(True/False/Unknown)、reason(机器可读短字符串)、message(人类可读描述)、lastTransitionTime。工具如 kubectl wait --for=condition=Ready 可以基于 conditions 做自动化等待。

典型 Reconcile 逻辑结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 1. 获取对象,处理已删除情况
myapp := &appsv1.MyApp{}
if err := r.Get(ctx, req.NamespacedName, myapp); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

// 2. 检查是否在删除中(Finalizer 逻辑)
if !myapp.DeletionTimestamp.IsZero() {
return r.handleDeletion(ctx, myapp)
}

// 3. 调谐子资源(幂等,使用 CreateOrUpdate)
dep := r.desiredDeployment(myapp)
if err := r.createOrUpdate(ctx, dep); err != nil {
return ctrl.Result{}, err
}

// 4. 更新 status(仅通过 Status().Update(),不影响 spec)
myapp.Status.ReadyReplicas = dep.Status.ReadyReplicas
if err := r.Status().Update(ctx, myapp); err != nil {
return ctrl.Result{}, err
}

return ctrl.Result{}, nil
}

Finalizer 机制解决级联删除问题:Operator 在 myapp.Finalizers 中注册自己,API Server 收到删除请求后只设置 DeletionTimestamp 而不真正删除对象,Operator 完成外部资源清理后移除 Finalizer,触发 API Server 真正删除对象。

CRD 的 API 版本演化策略

CRD 上线后不可避免地需要演化。Kubernetes API 的版本策略是:v1alpha1 表示实验性 API(可能随时删除),v1beta1 表示接近稳定(不会无预警删除),v1 表示稳定 GA API。用户可以通过 CRD 的 servedstorage 字段控制哪些版本被 API Server 提供服务、哪个版本是 etcd 存储格式。

多版本 CRD 的演化模式:

  • 新增字段时,在旧版本 schema 中加入新字段(标为 optional),保持 Conversion Webhook 双向转换,旧对象在读取时自动补充新字段的默认值。
  • 废弃字段时,先在新版本中标记字段为 deprecated(通过 description 说明),等旧版本 served=false 后再从 schema 移除。
  • storage version 切换(如从 v1beta1 切到 v1)需要执行 migration job,把 etcd 中所有旧版本对象重写为新版本格式。

CRD 的 spec.preserveUnknownFields: false(Kubernetes 1.15+ 默认值)会在写入 etcd 前剪裁未知字段,防止用户误传未在 schema 中定义的字段——这是防止"schema 漂移"的安全机制,但要求 schema 必须明确声明所有字段。

真实案例:Prometheus Operator 与 cert-manager

Prometheus Operator 定义了 ServiceMonitor、PodMonitor、PrometheusRule、Alertmanager 等 CRD。用户声明 ServiceMonitor(描述要抓哪些 Service 的 metrics),Operator 把它转化为 Prometheus 配置文件并注入到 Prometheus Pod,无需手动编辑 prometheus.yml。新增监控目标只需 apply 一个 ServiceMonitor 对象,Operator 自动生效。

cert-manager 定义了 Certificate、Issuer、ClusterIssuer、CertificateRequest 等 CRD。用户声明 Certificate(描述要为哪个域名申请证书、用哪个 CA),cert-manager Operator 调用 ACME 协议或内部 CA 签发,把结果存入 Secret,并在证书到期前自动续签。证书续签不再是定时任务,而是 Operator 持续调谐的副产品。

两个案例的共同特征:领域知识从文档和人工操作转移到了代码,变得可测试、可版本化、可审计。

Strimzi(Kafka on Kubernetes)是更复杂的案例:它定义了 Kafka、KafkaTopic、KafkaUser、KafkaConnect 等十余个 CRD,Operator 负责 Kafka 集群的初始化、Broker 滚动重启、Topic 副本平衡、证书轮换,达到 Level 4-5 的 Operator 成熟度。Strimzi 的源码是学习复杂 Operator 设计的优质参考——它的 Reconcile 拆分为多个子 Reconciler(ClusterReconciler、TopicOperator),每个子 Reconciler 负责一类资源的调谐,通过 Manager 的事件路由关联。

可运行实验

需要安装 kubebuilder CLI 和本地 Kubernetes 集群(kind 或 minikube)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 初始化项目
mkdir myapp-operator && cd myapp-operator
kubebuilder init --domain example.com --repo github.com/example/myapp-operator

# 2. 生成 API(CRD + Controller 脚手架)
kubebuilder create api --group apps --version v1 --kind MyApp
# 选择 y 创建 Resource,y 创建 Controller

# 3. 安装 CRD 到集群
make install

# 4. 查看注册的 CRD
kubectl get crd myapps.apps.example.com -o yaml | grep -A 10 openAPIV3Schema

# 5. 查看对象(status 为空,Controller 未运行)
kubectl describe myapp demo

# 6. 本地运行 Controller(另一个终端)
make run

# 7. Controller 调谐后查看 status 和事件
kubectl get myapp demo -o jsonpath='{.status}'
kubectl get events --field-selector involvedObject.name=demo

实验结果映射到 K8s 对象

kubectl get crd 列出的每一行对应一个注册到 API Server 的新 REST 端点。/apis/apps.example.com/v1/namespaces/default/myapps 是该端点完整路径,kubectl proxy 后可直接用 curl 访问,返回标准的 Kubernetes API 响应格式。

kubectl describe myapp demo 中的 Events 字段来自 Controller 调用 r.Recorder.Event() 写入,与 Pod Events 机制完全相同,都是 Event API 对象,在 API Server 中短暂保留,不写入 etcd 的持久存储区。

kubectl get myapp demo -o yaml 中的 status.conditions 由 Controller 通过独立的 status subresource 写入。使用独立 subresource 可避免 spec 和 status 的并发更新产生 resourceVersion 冲突,减少不必要的重试风暴。

模式提炼

Operator 模式的本质是把领域专家知识从 runbook 迁移到代码。传统运维依赖文档和人工判断,Operator 把同样的判断逻辑写成 Go 函数,让集群在无人工介入的情况下执行,且每次执行都有日志、事件和 status 可追溯。

设计 CRD 时,spec 应描述"what"(用户想要什么),而不是"how"(怎么做到)。Operator 负责"how"的翻译。如果 spec 开始包含过多实现细节,通常意味着抽象层次设计有问题,用户在承担本该由 Operator 承担的认知负担。

status.conditions 不是可选的便利特性,而是 Kubernetes 生态的标准接口。kubectl wait、GitOps 工具、监控告警系统都依赖 conditions 做自动化决策。跳过 conditions 的 Operator 会让集成方陷入轮询和猜测。

工程迁移表

传统模式 Kubernetes Operator 等价 关键差异
Spring Boot AutoConfiguration CRD + Operator 自动配置子资源 运行时动态、集群范围生效
数据库 Stored Procedure Operator Reconcile 函数 声明式触发、幂等执行
Chef/Puppet 配方 Operator 调谐循环 持续调谐 vs 一次性收敛
运维 runbook 手动步骤 Operator 代码路径 可测试、可版本化、可审计
Helm post-install hook Operator Finalizer + 生命周期管理 Operator 管理完整 Day-2 生命周期

常见误解

误解一:CRD 只是 YAML 模板的另一种形式

CRD 不是模板,是类型注册。模板(如 Helm chart)是文本替换,生成的对象用的仍然是内置类型。CRD 创建的是新的 API 类型,有自己的 REST 端点、RBAC 规则、watch 机制。没有 Controller 的 CRD,对象存在 etcd 里,集群不会对它做任何事情——这和模板引擎的行为方式完全不同。从 kubectl api-resources 可以看到 CRD 注册的资源出现在输出列表中,这是模板永远做不到的。

误解二:Operator 只适合管理有状态数据库

数据库 Operator(MySQL、PostgreSQL、Cassandra)是最常见的案例,但 Operator 适用于任何需要"知识驱动自动化"的场景:证书管理(cert-manager)、监控配置(Prometheus Operator)、GitOps(Argo CD)、机器学习任务调度(Kubeflow)、网络策略管理(Cilium)。判断标准是这个领域是否有足够复杂的 Day-2 运维知识值得编码,而非是否有持久化存储。无状态应用的蓝绿发布策略同样可以用 Operator 实现。

误解三:CRD 注册后不能修改 schema

schema 可以修改,但有限制。向 schema 添加新的非必填字段是向后兼容的,可以直接更新 CRD 对象,已有实例不受影响。删除字段、修改字段类型、把可选字段改为必填,会影响已存储的旧对象,需要配合 Conversion Webhook 和存储版本迁移。x-kubernetes-list-map-keys 等影响合并策略的字段更改需要格外谨慎,API Server 有专门的 validation 阻止不兼容变更。

CRD 的准入控制与验证规则

CEL 验证规则(Validation Rules)

Kubernetes 1.25+ 支持在 CRD schema 中内嵌 Common Expression Language(CEL)表达式,实现跨字段校验,无需额外部署 Validating Webhook:

1
2
3
4
5
6
7
x-kubernetes-validations:
- rule: "self.minReplicas <= self.replicas"
message: "replicas must be >= minReplicas"
- rule: "self.replicas <= self.maxReplicas"
message: "replicas must be <= maxReplicas"
- rule: "!has(self.deprecated) || self.deprecated == false"
message: "deprecated field must not be set to true"

CEL 表达式在 API Server 侧执行,无额外网络调用,延迟比 Webhook 低得多。对于简单的字段间约束,CEL 是比 Validating Webhook 更轻量的选择。复杂业务逻辑(需要查询其他资源、调用外部 API)仍然需要 Webhook。

Status Conditions 的规范写法

Kubernetes API Conventions 对 status.conditions 的写法有详细规定:

  • condition type 使用 PascalCase,表示一种状态维度(Ready、Synced、Degraded)
  • condition status 只能是 True、False、Unknown 三种值
  • reason 是 CamelCase 的单词,机器可读,用于 alert routing 和自动化脚本判断
  • message 是人类可读的完整句子,可以包含详细的错误信息
  • observedGeneration 应等于 .metadata.generation,表示 Controller 已处理了最新版本的 spec
1
2
3
4
5
6
7
meta.SetStatusCondition(&myapp.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
ObservedGeneration: myapp.Generation,
Reason: "DeploymentNotAvailable",
Message: fmt.Sprintf("Deployment %s/%s has 0 available replicas", dep.Namespace, dep.Name),
})

conditions 的更新应该幂等:如果状态没有变化,避免不必要的 API 调用,减少 etcd 写操作。controller-runtime 的 meta.SetStatusCondition 函数在 condition 未变化时不会触发更新。

Operator 设计的进阶话题

多租户与 Namespace 隔离

Operator 可以是 namespace-scoped(只管理特定 namespace 的资源)或 cluster-scoped(管理整个集群)。kubebuilder 生成的 Controller 默认监听所有 namespace,生产环境通常通过 Manager 的 WatchNamespace 配置限制范围,配合 RBAC 实现租户隔离。

cluster-scoped 的 CRD(scope: Cluster)没有 namespace 字段,适合跨 namespace 共享的资源,如 StorageClass、ClusterIssuer。namespace-scoped 的 CRD 更常见,天然隔离不同团队的资源。

单个 Operator 管理所有 namespace 的资源(cluster-scoped 部署)与每个 namespace 独立部署一个 Operator 实例各有取舍:前者资源效率高但故障影响范围大;后者隔离性强但资源消耗随租户数线性增长,且 CRD 是集群级别的资源,仍然共享 schema 定义。

幂等性与 Owner Reference

Reconcile 可能因为任何原因被反复调用(网络抖动、重启、缓存 resync),因此所有操作必须幂等。controller-runtime 的 CreateOrUpdate 函数是实现幂等的标准工具:先尝试创建,如果对象已存在则更新。

Owner Reference 是 Kubernetes 的对象所有权机制:Operator 创建子资源时,把 MyApp 对象设为该子资源的 OwnerReference。当 MyApp 被删除,Kubernetes 的 GC controller 会自动删除所有 owned 的子资源(前提是未使用 Finalizer 管理外部资源)。Owner Reference 让 Operator 不需要手动维护"我创建了哪些子资源"的列表。

Webhook:准入控制与默认值注入

Operator 通常需要配套两类 Webhook:

Mutating Admission Webhook(变更准入):在对象写入 etcd 之前修改它,常用于注入默认值(用户没填 replicas 时自动设为 1)、添加 label/annotation、规范化字段格式。

Validating Admission Webhook(校验准入):在对象写入 etcd 之前做业务层面的合法性检查,比 OpenAPI v3 schema 更灵活(可以跨字段校验、查询外部资源)。校验不通过返回 400,不写入 etcd。

Webhook 是 HTTPS 服务,需要 TLS 证书。cert-manager 配合 kubebuilder 是最常见的证书管理方式,cert-manager 自动为 Webhook Server 续签证书,kubebuilder 自动把证书注入到 WebhookConfiguration 对象。

测试策略:envtest 与 fake client

controller-runtime 提供 envtest 包,在本地启动一个真实的 API Server 和 etcd(不需要完整的 K8s 集群),用于 Controller 的集成测试。测试代码可以直接 apply 对象、触发 Reconcile、断言 status 变化,比 mock 测试更接近真实环境。

对于单元测试,controller-runtime 提供 fake.NewClientBuilder() 构建内存 client,可以预设对象列表,测试 Reconcile 函数在各种输入下的行为,无需任何网络依赖。

Operator 成熟度模型

OperatorHub.io 定义了 Operator 成熟度的 5 个级别:

  • Level 1(基本安装):自动化部署,等价于 Helm chart
  • Level 2(无缝升级):支持应用版本升级,处理 schema 迁移
  • Level 3(完整生命周期管理):备份、恢复、故障转移
  • Level 4(深度洞察):指标采集、告警规则、自动诊断
  • Level 5(自动驾驶):水平扩缩容、异常自愈、性能调优

大多数生产级 Operator 达到 Level 2-3,数据库 Operator(如 Vitess、TiDB Operator)通常达到 Level 4-5。设计 Operator 时明确目标成熟度级别,避免一开始就过度工程化。

CRD 字段默认值与不可变字段

Kubernetes 1.17+ 支持在 CRD schema 中通过 default 关键字声明字段默认值,API Server 在写入 etcd 前自动补全用户未填写的字段,无需 Mutating Webhook 注入默认值:

1
2
3
4
5
6
7
8
9
spec:
type: object
properties:
replicas:
type: integer
default: 1
minimum: 1
image:
type: string

x-kubernetes-immutable: true 标注不可变字段:对象创建后该字段不允许修改,API Server 收到修改请求时直接返回 422。常用于数据库 Operator 的存储引擎类型、集群名称等创建后不可更改的配置,比在 Operator 代码里校验更早、更可靠。

性能与可扩展性考量

Informer 缓存与 API Server 压力

controller-runtime 的 Manager 为所有 Controller 共享同一个 Informer 缓存(SharedInformerFactory)。这意味着:即使注册了 10 个 Controller 分别监听 10 种资源,到 API Server 的 LIST/WATCH 连接数也只有 10 条,而不是 100 条。共享缓存在内存中维护资源的本地副本,Reconcile 函数调用 r.Get() 时读取的是本地缓存,不产生 API Server 请求。

只有写操作(Create、Update、Delete、Patch)才会产生实际的 API 调用。因此 Operator 的 API Server 压力与写操作频率正相关,而非与 Reconcile 调用频率正相关。减少不必要写操作(如 status 未变化时跳过 Update)是降低 API Server 负载的核心手段。

Reconcile 并发与队列背压

controller-runtime 的 WorkQueue 是 Reconcile 的缓冲层。事件进入 WorkQueue 时自动去重:同一个对象的多次事件只保留一次,避免短时间大量变更触发等量 Reconcile。WorkQueue 支持指数退避:Reconcile 返回 error 后,下次重试等待时间从 1s 开始指数增长,上限默认 1000s,避免对损坏对象无限快速重试。

MaxConcurrentReconciles 控制同时执行的 Reconcile 数量,默认 1(串行)。对于大规模部署(数千个自定义资源),可以调大这个参数,但要注意 Reconcile 函数自身的并发安全:避免使用 Operator 进程内的全局状态,所有需要共享的状态应存储在 Kubernetes 对象的 status 或 annotation 中。

练习

  1. 用 kubebuilder 创建一个 WebSite CRD,spec 包含 url(string 类型)和 checkIntervalSeconds(integer 类型),Controller 每隔 checkIntervalSeconds 用 HTTP GET 检查 url,把结果写入 status.lastCheckTimestatus.healthy。观察 Controller 日志和 kubectl describe website 中的 status 变化,以及 Events 字段的内容。

  2. 为练习 1 的 WebSite CRD 启用 /status subresource,验证直接用 kubectl patch 修改 status 字段会被拒绝,必须通过 --subresource=status 参数才能修改 status 字段。理解 spec 和 status 写权限分离的安全意义。

  3. 为练习 1 的 Controller 添加 Mutating Webhook,当用户不指定 checkIntervalSeconds 时自动注入默认值 30。用 kubebuilder 生成 webhook 脚手架,用 cert-manager 管理 TLS 证书,验证 apply 一个不含 checkIntervalSeconds 的 WebSite 对象后,kubectl get website demo -o yaml 中该字段自动出现。

  4. 部署 cert-manager,观察它注册的 CRD(kubectl get crd | grep cert-manager),创建一个 self-signed Issuer 和 Certificate,追踪 Operator 日志(kubectl logs -n cert-manager deploy/cert-manager -f)直到 Certificate 的 status.conditions 出现 Ready=True。分析日志中出现了哪些 Reconcile 调用,理解 CertificateRequest 对象在签发流程中的作用。

系列导航

参考资料