内容提要
Kubernetes 虽有 CRD、控制器、CEL 等组件,却缺少“编排配方”,即没有机制说明多个资源如何组成一体、字段依赖如何排序,导致 Helm、Argo CD、Crossplane 等各自重复实现同一套组合逻辑。kro 项目提出 ResourceGraphDefinition,用 CEL 引用自动推导依赖顺序,以数据形式描述资源连接,一个控制器即可解释所有定义;其 Graph 功能可仅做组合而无需创建 API。文章认为,组合可能是 Kubernetes 缺失的基础原语。
延伸解读
重复造轮子的根源:缺少“配方”原语
文章指出,Kubernetes 提供了 CRD、控制器、CEL 等“原料”,却没有说明多个资源如何组成一体、字段依赖如何排序的“配方”。于是 Helm、Argo CD、Crossplane 等工具各自实现组合逻辑,平台团队也反复编写仅做“接线”的控制器。这种重复并非因为功能复杂,而是因为底层缺少表达资源间依赖与值流动的原语,导致同一套组合逻辑在成千上万的组织中重复出现。
用引用推导顺序,避免两套事实来源
kro 的 ResourceGraphDefinition 将接线写成数据,依赖图由 CEL 表达式中的引用自动推导,而非显式声明顺序。文章强调,显式顺序是对清单中已有关系的二次描述,重命名或重排后两者可能不一致;而引用本身就是依赖,推导出的顺序不会漂移。这提示读者:当顺序与引用分离时,就存在两个事实来源和漂移风险,应优先让依赖从引用中自然产生。
抽象成本从编写转移到长期维护
文章提醒,接线变成数据后创建 API 变得廉价,但廉价 API 仍是承诺。应用团队会围绕它编写清单、RBAC 和流水线,平台团队此后删除字段可能破坏自己看不见的东西。kro 默认拒绝会破坏现有实例的 schema 变更,除非作者显式允许。因此团队应统计需要长期维护的 API 数量,而非控制器数量,并意识到抽象的成本已从编写转移到拥有和治理。
Graph 揭示组合才是缺失的原语
kro 0.10 的 Graph 只保留组合部分,无需 CRD 和实例,可响应集群中已有资源的变化,例如根据带标签的 Service 生成 Ingress 规则。文章由此指出,API 那半只是 CRD,Kubernetes 本就有;组合那半才是各工具重复构建的缺失原语。读者可据此重新审视现有平台:命名约定、同步波次、复制状态字段的脚本,往往都是在别处编码本应由组合原语表达的依赖关系。
Q&A
为什么说 Kubernetes 缺少“编排配方”?
Kubernetes 提供了 CRD、控制器、CEL 等组件,但没有机制说明多个资源如何组成一体、字段依赖如何排序。因此每个管理资源集的工具(如 Helm、Argo CD、Crossplane)都必须自己实现组合逻辑,重复构建同一层。
kro 的 ResourceGraphDefinition 如何自动推导资源依赖顺序?
ResourceGraphDefinition 通过 CEL 表达式中的引用自动推导依赖图。例如 ConfigMap 模板引用了 Service 的 hostname 字段,kro 就推断出 Service 必须先于 ConfigMap 创建,无需显式声明顺序。依赖关系就是引用本身,因此不会因重命名或重排而漂移。
kro 的 Graph 功能和 ResourceGraphDefinition 有什么区别?
Graph 只包含组合部分,是命名空间级别的,没有 schema、CRD 和实例,用于响应集群中已有资源的变化(如给 Service 打标签自动生成 Ingress 规则)。ResourceGraphDefinition 则在组合之上加了一个 CRD 作为 API,供应用团队创建实例。
使用 kro 将接线逻辑变为数据后,平台团队需要承担什么新成本?
虽然编写控制器的成本降低(十个胶水控制器变成一个控制器解释十个定义),但创建 API 变得廉价后,平台团队需要承担拥有 API 的责任。应用团队会依赖这些 API,删除字段可能破坏他们看不见的东西。kro 默认拒绝会破坏现有实例的 schema 变更,除非作者显式允许。
文章认为组合为什么可能成为 Kubernetes 缺失的基础原语?
因为几乎所有平台都需要组合:创建对象、在对象间复制值、持续检查状态。无论是 Operator、流水线阶段还是 runbook,去掉触发方式后核心都是组合。Kubernetes 已经以类似方式演进过(如滚动更新从 kubectl 子命令变为 Deployment 控制器),组合也适合成为共享原语。
目前平台中常见的“隐式资源图”有哪些表现形式?
常见形式包括:用命名约定让两个清单约定一个字符串、用同步波次或钩子将依赖图编码为步骤顺序、用脚本将一个状态字段复制到另一个对象。这些做法都是因为底层工具无法表达对象间的依赖或无法感知集群当前状态,导致依赖关系藏在字符串、步骤编号或脚本中。