schema、子资源、版本 — CRD 的三条轴
一句话总结
编写 CRD 不是列出字段,而是设计能把多少验证工作交给 API Server。
为什么需要了解这一点
当控制器代码开始堆积 if spec.Replicas < 1 { return error } 之类的检查时,会出现两个问题。第一,检查发生得太晚。错误对象已经存入 etcd,用户却以为 kubectl apply 成功了。第二,规则没有记录在任何文档中。用户必须阅读控制器日志,才能知道哪里出错。
把规则移到 schema 后,情况正好相反:kubectl apply 会当场失败,错误消息会说明哪个字段为什么无效;如果是枚举,甚至会同时列出允许的值。而且 kubectl explain webservice.spec 本身就会成为文档。
工作原理
支撑 CRD 的轴有三个。
1) structural schema。 Kubernetes 1.16 以后,所有 CRD 都必须用 OpenAPI v3 明确声明每个字段的类型。满足这一条件后,pruning、默认值注入、Server-Side Apply 和 CEL 验证才能工作。这里必须清楚区分三种情况。
| 分类 | 含义 | 使用场景 |
|---|---|---|
required |
为空就拒绝 | 没有合理默认值的核心标识符 |
default |
为空时由 API Server 填充 | 大多数人会使用相同值的字段 |
| 什么也不加 | 允许为空,也不填充 | 真正的可选功能 |
可以提供默认值的字段若硬设为 required,用户每次都要重复写相同的样板值,以后也很难再降级为可选字段。反之,像镜像这样错误默认值会造成风险的字段,明确拒绝更好。
pruning 是默认行为。 schema 中不存在的字段会在保存前被裁掉。拼错的字段悄然消失,起初可能令人困惑,但这正是强制执行“schema 即契约”的机制。需要接收任意键值的位置,可以用 x-kubernetes-preserve-unknown-fields: true 开放例外,但只应开放最小范围。滥用会彻底失去 structural schema 的好处。
2) 子资源。 启用 status 子资源后,spec 与 status 会成为不同端点。用户只写 spec,控制器只写 status。由此产生一个关键性质:写入 status 不会增加 metadata.generation。 因而控制器能区分“用户修改了 spec”和“我刚刚写入 status”,这是避免无限 reconcile 的基础。
scale 子资源只需提供 specReplicasPath、statusReplicasPath、labelSelectorPath 三条路径,就能让 kubectl scale 与 HPA 支持自定义类型。需要 selector 路径,是因为 HPA 要用该 selector 统计 Pod。
3) 版本。 一个 CRD 可同时提供多个版本,每个版本都有两个标志。
served:是否接受该版本的请求storage:是否以该版本写入 etcd——必须且只能有一个 true
存储只采用一种表示,因此所有版本之间必须能够无损转换。status.storedVersions 会记录“曾经用于保存该 CRD 对象的版本”。切换存储版本后,若不重新保存现有对象,旧版本会继续留在列表中;此时删除旧 schema,就可能无法读取已存储对象。大多数版本删除事故,都是跳过了重新保存这一步。
本练习环境无法提供 webhook 端点,因此不讨论 conversion webhook。我们会在 strategy: None 下同时提供多个版本,并确认存储版本规则。
实际工作中的表现
第一,第一天就可能卡在 CRD 命名规则。 metadata.name 必须是 <복수형>.<그룹>。只写 webservices 会被 API Server 拒绝。练习提供的损坏 CRD 正是这种情况。之所以有此规则,是因为 CRD 本身是集群范围资源,名称就是全局唯一键。
第二,自定义列会改变运维质量。 执行 kubectl get webservices 时,如果只显示 NAME 与 AGE,就没人愿意使用该命令。把故障处理中操作人员最想知道的两三个值设为列,一条命令就能变成仪表板。
第三,枚举就是文档。 添加 enum 后,拒绝消息会同时列出允许值。用户无需翻找 Wiki,只读错误消息就能得到答案。把验证移到 schema 的真正收益,不只是拒绝错误,而是提供这种指引。
下个练习将做什么
接下来有两个练习。第一个练习从零编写 apps.labhub.io/v1 的 WebService——包括命名规则、schema、自定义列、status 与 scale 子资源,以及 v1alpha1 与 v1 两个版本。第二个练习会故意提交违反规则的资源,收集 API Server 的拒绝措辞;亲眼确认默认值注入与 pruning 后,再通过 CEL 规则把字段间约束加入 schema,并制作验证矩阵。