深色模式
CRD 与自定义资源:扩展 Kubernetes API 的基石
摘要:本文面向生产 SRE / 平台工程师,系统讲解
CustomResourceDefinition(CRD)的工作机制:它如何把一个新的资源类型注册进 Kubernetes API、结构化校验如何生效、多版本如何共存,以及何时应该选择 CRD、何时应该选择 Aggregated API。覆盖版本:Kubernetes v1.28+(CRDapiextensions.k8s.io/v1自 1.16 GA)。
适用版本与前提
- Kubernetes:v1.28+(本文以
apiextensions.k8s.io/v1为准,该 API 自 v1.16 起 GA;旧的v1beta1已在 v1.22 移除)。 - 工具:
kubectlv1.28+,集群 RBAC 权限需能create/getcustomresourcedefinitions.apiextensions.k8s.io。 - 前提:理解 Pod / Deployment /
kubectl get的基本模型,以及 API Group / Version / Kind 的概念。
背景与问题
Kubernetes 内置的资源(Pod、Deployment、Service……)是有限的,但真实业务需要管理的"对象"远不止这些:一个数据库实例、一个消息队列、一个 CI 流水线。把它们塞进 ConfigMap 或 Annotation,会丢失类型安全、校验能力与 kubectl 原生体验。
官方文档给出的判断标准很清晰(来源:Kubernetes 官方文档 - Custom Resources):当你想要用 kubectl 读写新类型、想要 .spec/.status 约定、想要 watch 新对象并驱动自动化时,就该用自定义资源(CRD 或 Aggregated API)。否则,独立的服务或 ConfigMap 可能更合适。
心智模型
CRD 是"资源的模具"。你定义一个 CRD,API Server 就自动拥有了"接收、存储、校验、序列化某种新对象"的能力——你不需要写一行 API Server 代码。
核心概念
Custom Resource 与 CRD
- Custom Resource(CR):某个自定义资源类型的"实例",就像
default命名空间里的一个 Pod。用户用kubectl get <plural>读写它。 - CustomResourceDefinition(CRD):描述"这种资源长什么样"的声明式对象。它告诉 API Server:我的 group 是什么、有哪些 version、每个 version 的 schema 是什么、有哪些子资源。
一个 CRD 对象本身的名字必须是合法的 DNS 子域名(由 <plural>.<group> 派生),而 CR 实例的名字同样必须是合法 DNS 子域名。
apiextensions.k8s.io/v1 关键结构
一个 CRD 的核心字段:
spec.group:API Group,如example.com。spec.names:kind(如Database)、listKind、plural(databases)、singular(database)、shortNames。spec.scope:Namespaced或Cluster。这是创建后不可变的字段。spec.versions[]:可服务多个版本,每个版本有name(v1)、served、storage、schema、additionalPrinterColumns、subresources。
结构化 Schema 与校验
v1 要求结构化 OpenAPI v3.0 schema(spec.versions[].schema.openAPIV3Schema)。它带来的能力:
- 类型与必填字段校验(
type、required、properties)。 - 取值范围(
minimum/maximum、enum、pattern、maxLength)。 preserveUnknownFields: false(v1 强制要求),即拒绝 schema 之外的未知字段,保证对象可被无歧义解码。- 默认值:
default关键字(GA 于 1.17),在创建时填充。
版本相关
CRDValidationRatcheting 特性开关(kube-apiserver 侧)允许"部分对象未变更时,忽略其违反的新校验规则",用于已有数据的字段收紧场景。该特性各版本默认开关不同,生产启用前请以目标版本 release notes 为准,并先在测试集群验证。
CEL 校验规则(x-kubernetes-validations,用 Common Expression Language 表达跨字段约束)GA 于 v1.30([版本相关],请以目标版本为准)。在更早版本可能仅为 alpha/beta。
CRD 与 Aggregated API 的边界
官方文档明确给出两种扩展方式的取舍(来源:Kubernetes 官方文档 - Custom Resources / API Aggregation):
| 维度 | CRD | Aggregated API(APIService) |
|---|---|---|
| 是否需要编程 | 否,纯声明式 | 需要编写并部署独立 API Server |
| 额外服务 | 无,由原生 API Server 处理 | 需部署并运维一个聚合 apiserver |
| 校验 | OpenAPI v3 schema;任意校验需 Validating Webhook | 可在自定义 API Server 内写任意校验 |
| 默认值 | OpenAPI default 或 Mutating Webhook | 自定义实现 |
| 多版本 | 支持(served/storage 分离) | 支持,且你可自己实现 conversion |
| 自定义存储 | 否(只能用 etcd) | 可(如时序库、加密隔离存储) |
| 自定义子资源 | 仅 status / scale | 可定义任意子资源(exec/logs) |
| strategic-merge-patch / protobuf | 不支持 | 支持 |
选型结论
如果每个资源字段不多、主要在团队/公司内部使用、不需要自定义存储或特殊 REST 路径,优先 CRD。只有当需要自定义存储后端、任意子资源、strategic-merge-patch、或无法接受 etcd 这个存储语义时,才考虑 Aggregated API。绝大多数 Operator 场景都用 CRD。
完整 CRD 示例
下面是一个单版本、带校验、带 status 子资源与打印列的 Database CRD:
yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.example.com
spec:
group: example.com
scope: Namespaced
names:
kind: Database
listKind: DatabaseList
plural: databases
singular: database
shortNames:
- db
versions:
- name: v1
served: true
storage: true
additionalPrinterColumns:
- name: Engine
type: string
jsonPath: .spec.engine
- name: Replicas
type: integer
jsonPath: .spec.replicas
- name: Age
type: date
jsonPath: .metadata.creationTimestamp
subresources:
status: {}
scale:
specReplicasPath: .spec.replicas
statusReplicasPath: .status.replicas
labelSelectorPath: .status.selector
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- engine
- replicas
properties:
engine:
type: string
enum:
- postgres
- mysql
replicas:
type: integer
minimum: 1
maximum: 10
default: 1
storageGi:
type: integer
minimum: 1
status:
type: object
properties:
replicas:
type: integer
selector:
type: string
phase:
type: string1
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
创建并验证:
bash
kubectl apply -f database-crd.yaml
# 确认 CRD 处于 Established 状态
kubectl get crd databases.example.com
kubectl api-resources | grep databases
# 创建一个 CR 实例
kubectl apply -f - <<'EOF'
apiVersion: example.com/v1
kind: Database
metadata:
name: orders-db
namespace: default
spec:
engine: postgres
replicas: 3
storageGi: 50
EOF
kubectl get databases
kubectl get db orders-db -o yaml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
多版本与转换
当一个 CRD 同时 served 多个 version 时,只有一个版本 storage: true,其余为只读服务。字段重命名/结构变更需要 conversion:
spec.conversion.strategy: None:直接在不同版本间做无转换透传(字段名必须一致),最简单。spec.conversion.strategy: Webhook:部署一个 conversion webhook,在 API Server 内部做版本间对象转换。这是生产演进的标准做法,但引入了额外的可用性依赖。
生产危险
CRD 的 spec.scope 与 spec.conversion 部分字段创建后不可变更。字段类型(如 string 改 integer)属于破坏性变更,存储在 etcd 中的旧对象将无法反序列化。演进 API 必须新增 version + conversion webhook,而非原地改类型。
删除一个被 Controller 依赖的 CRD 会级联删除所有该类型的 CR 实例,且该操作不可恢复。生产变更前务必备份 etcd 或导出 CR 清单。
生产实践
- 命名与分组:group 用公司/产品域名反写(
apps.example.com),kind 用大驼峰单数,plural 用小写复数;避免与社区同名冲突。 - 始终开启 status 子资源:让 RBAC 能区分"用户写 spec"与"控制器写 status",并利用
metadata.generation触发 reconcile。 - printer columns 要实用:把排障最关心的字段(状态、副本数、版本)放进
additionalPrinterColumns,减少kubectl describe次数。 - Schema 从严不从宽:早期把字段设成
type: object且不限properties看似灵活,后期收紧会触发上述破坏性变更。一开始就用结构化 schema 约束。 - RBAC 最小权限:为 Controller 的 ServiceAccount 仅授予该 CR 及它实际管理的资源权限。
常见失败模式
- Schema 过松导致脏数据:
x-kubernetes-preserve-unknown-fields: true或在 object 上不约束 properties,会让非法字段长期沉淀,后续收紧困难。 - printed 列 jsonPath 指向不存在字段:
kubectl get直接报错或显示<none>,排障信息缺失。 - 删除 CRD 误伤:在 CI/测试里
kubectl delete crd --all会清掉所有自定义对象。 - version 演进无 conversion:新增 version 后老客户端仍读旧 version,字段映射错乱。
故障排查
bash
# CRD 是否 Established(未就绪会卡在 NamesAccepted/Establishing)
kubectl get crd databases.example.com -o jsonpath='{.status.conditions[*].message}'
# 校验失败的具体原因
kubectl apply -f bad-cr.yaml
# 查看 schema 是否如预期
kubectl explain databases.spec.replicas
# CR 创建被拒时的 API 错误(4xx)通常直接指明违反的 schema 路径1
2
3
4
5
6
7
2
3
4
5
6
7
回滚与清理
- CRD 变更不可逆部分(scope、字段类型):只能删除重建。重建前先
kubectl get <plural> -A -o yaml > backup.yaml。 - 版本下线:
served: false某个 version 即可停止服务,但storage: true的版本不能直接设为未服务,需先通过 conversion 迁走数据。
安全与合规
- CRD 本身是集群级对象,创建需
customresourcedefinitions的create权限——应仅授予平台管理员,避免任意命名空间用户注册资源类型。 - 校验规则(尤其是 CEL、webhook)是防止越权/过量配置(如
replicas: 10000)的第一道防线。
性能、容量与成本
- 每个 CRD 会在 API Server 内存中缓存其 OpenAPI schema,CR 实例随 etcd 增长。大量 CR(十万级)时需注意 API Server 内存与 watch 风暴。
additionalPrinterColumns过多会在kubectl get时增加序列化开销,列数建议 ≤6。
FAQ
Q:apiextensions.k8s.io/v1beta1 还能用吗? A:不能。v1beta1 已在 Kubernetes v1.22 移除。任何仍引用它的清单都会创建失败,需迁移到 v1。
Q:CRD 能否替代数据库? A:不能。CRD 对象存于 etcd,适合低基数、强一致、需 watch 的配置型数据;高吞吐、大数据量应放外部存储,CR 仅持有引用。