Kubernetes API 为什么成了平台的通用语
一句话总结
Kubernetes 赢下的并不是容器编排竞争,而是 API 规范竞争。声明式资源与协调控制器这一成对模式扩展到了容器之外,因此在构建新的平台 API 时,CRD 成为了默认选择。
为什么需要它
平台团队想让“启动一个服务”变得简单时,通常会经历以下路径。
- 把流程写进 Wiki → 没有人持续更新。
- 编写 shell 脚本 → 不同运行环境产生不同结果,失败后还会留下中间状态。
- 开发内部 Web 应用 → 必须自行实现状态存储、认证、审计、重试和并发,而且应用本身成为新的 SPOF。
把第三条路走到底后,就会意识到自己正在重新制造什么:状态存储、乐观并发控制、认证与授权、审计日志、监视(watch)以及协调循环——这些都是 Kubernetes API 服务器已有的能力。
因此,方向会反转:不再新建平台 API,而是扩展 Kubernetes API。注册 CRD 的那一刻,以下能力会免费获得。
- 数据存储在 etcd 中,并通过版本与
resourceVersion实现乐观锁 - 现有 RBAC 直接生效(
kubectl auth can-i create webservices可立即使用) - 操作进入审计日志
kubectl get/describe/edit、-o yaml与--watch可直接使用- OpenAPI 模式会立即拒绝错误值,并填充默认值
- GitOps 工具可以像处理其他资源一样处理它
这就是“Kubernetes API 是平台通用语言”的实际含义:定义新的词汇(CRD),但语法(API 规范)沿用所有人已经熟悉的方式。
它如何工作
CRD + 控制器 = 平台 API
需要两个组成部分。
CRD 定义词汇,包括 group、version、kind、作用域(Namespaced 或 Cluster)以及 OpenAPI v3 模式。模式的作用比想象中更大。
spec:
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [image]
properties:
image: { type: string }
replicas: { type: integer, default: 2, minimum: 1, maximum: 10 }
public: { type: boolean, default: false }
required会立即拒绝缺少的字段。minimum/maximum用于强制范围。提交replicas: 20时,API 服务器会用人类可读的句子拒绝请求。default由服务器填充。用户不写时,值会在保存时加入。这是实现“安全默认值”成本最低的方式。additionalPrinterColumns可在kubectl get输出中显示所需列。它看似细小,却会显著改善开发者体验。
控制器 为这些词汇赋予含义。它是一个协调循环:读取名为 WebService 的 CR,并创建 Deployment、Service、HPA 与 NetworkPolicy。如果只有 CRD 而没有控制器,这个 CR 只是“一份结构经过验证的配置文件”——它仍有一定用途,却还不是平台 API。
作用域选择也是考试常见题。Namespaced 资源被限制在租户边界内,命名空间 RBAC 可直接生效。Cluster 作用域使用全局名称,不同租户可能产生命名冲突,而且只能通过 ClusterRole 授权,不能使用 Role。由租户创建的资源几乎总应采用 Namespaced。
抽象发生泄漏的时刻
优秀的平台 API “只询问必要信息”。WebService 只要求镜像,必要时再要求 replicas;其余内容——标签规范、安全上下文、资源默认值、可观测性注解和网络策略——由控制器填充。
但总有一天抽象会泄漏。
- 某个团队需要添加 sidecar。
- 某个工作负载只能运行在特定节点,例如配备 32GB VRAM GPU 的节点。
- 某个服务使用非标准探针路径。
如果此时回答“平台不支持”,该团队就会放弃平台并回到原始 YAML。一旦离开,通常不会再回来。因此,设计中必须预先提供逃生舱(escape hatch)。
| 逃生舱 | 形式 | 风险 |
|---|---|---|
| 局部覆盖 | 像 spec.podOverrides 这样的自由字段 |
什么都能填会让抽象失去意义 |
| 扩展点 | 只允许 extraEnv、extraVolumes、nodeSelector |
需要维护允许列表 |
| 渲染后脱离 | 复制生成的清单并自行管理 | 无法继续获得平台改进 |
平衡点是:**必须提供逃生舱,但使用逃生舱的事实必须可见。**如果给使用覆盖的服务留下标签或状态条件,平台团队就能收到“已有五个团队通过覆盖绕过这项能力”的信号,并将其提升为正式功能。这就是把平台作为产品运营的反馈循环。
实际现场中的表现
作者的家庭实验室准确呈现了这个问题。GPU Feature Discovery 会自动为节点标记显卡信息:RTX 3090(24576MB,ampere)、5090(32607MB,blackwell),以及两块 4070 Laptop(8188MB,ada-lovelace)。但如果 Pod 只请求 nvidia.com/gpu: 1,需要 32GB 的训练任务也可能被调度到 8GB 笔记本 GPU 上,因为对 Kubernetes 而言,它们都是“一块 GPU”。
GFD 添加的 gpu.memory 标签是字符串,无法使用“至少 24GB”这样的比较选择器。因此,我们自行添加了语义化标签:gpu.homelab/tier=xlarge|large|small 与 gpu.homelab/vram=32g|24g|8g。现在工作负载可以通过 nodeSelector: gpu.homelab/tier: xlarge 选择符合自身规模的节点。
这一行正是平台 API 设计的典型案例:不直接暴露底层物理事实(显卡型号、内存字节数),而是把它翻译成用户可以据此决策的词汇(tier)。同时仍保留逃生舱:如果确实需要某个特定型号,可以直接使用 GFD 原始标签选择。优秀的抽象不是把下层完全隐藏,而是将其覆盖,同时保持可访问。
下一项实验将做什么
我们会在真实集群中创建 CRD webservices.platform.labhub.io,亲眼确认模式违规是否会被立即拒绝,以及默认值是否由服务器填充。随后使用命名空间、ResourceQuota 和 LimitRange 划定租户边界,通过 RBAC 授予自助服务权限,再用 kubectl auth can-i 证明无法操作其他租户。