第 2 / 23 篇

第 1 章:先定义托管契约,再写控制器

用户交出的是什么责任

设想平台团队允许业务创建一个网关实例。用户希望一个名称对应一组可以访问的代理进程,改变期望副本数后系统自动调整,误删 Service 后系统自动补回,删除实例后关联资源也得到清理。单次执行的安装脚本只承诺执行过一组命令;托管能力承诺在服务存续期间反复检查这些关系。

我们把托管入口命名为 GatewayInstance。它的 spec 只包含 replicas 和 message:前者限定在零到三个,后者决定 Envoy 对所有路径返回的正文。字段少不是为了回避工程问题,而是让资源身份和失败处理足够清晰。若一开始就加入证书、插件、路由、跨集群和配额,每次失败都难以确认究竟是哪层契约失效。

用户提交意图后并没有把 Kubernetes 所有权完全交给控制器。控制器必须说明自己管理哪些字段、哪些对象,以及哪些修改会被恢复。本文实现管理 Deployment 的副本、代理镜像与参数、部分容器字段、Pod 标签,以及 Service 的选择器和端口。它保留对象上的无关注解,不擅自接管一个同名但没有正确 owner UID 的 Service。

期望状态需要可判断

“网关已经部署”过于含糊。Deployment 创建成功,只能说明 API 已接受对象。Pod 被调度,不代表 Envoy 成功加载配置;HTTP 探针通过,也不能代表所有业务路由正确。不同层级的事实应分别命名,避免把一个方便检查的信号当成最终业务结果。

本项目的 Reconciled 条件表示控制器已经应用本次 generation 对应的子资源配置,不表示代理流量已经可用。实际 HTTP 验证是独立实验:等待 Deployment 观察其自身 generation,确认更新副本和可用副本达到期望,再由集群内客户端访问 Service DNS。这个区分使控制器可以及时报告资源协调进展,同时保留对业务验证的严格要求。

期望副本数可以为零。这表示用户暂时停止数据面,而不是删除实例。Service 和用于教学的外部登记仍然存在,finalizer 仍承担删除责任。若把零副本误当作删除,后续恢复就可能丢失原对象身份或稳定入口。生命周期状态与容量状态应分开设计。

一张明确的责任表

对象或字段 谁决定 失败时的处理
GatewayInstance.spec 用户与准入规则 拒绝无效输入
Deployment 托管字段 控制器 下次协调恢复
Service ClusterIP API server 更新时保留
Reconciled 条件 控制器 只确认实际处理的 generation
外部登记 ConfigMap finalizer 清理流程 清理完成才移除自身 finalizer

这张表也是评审工具。发现实现将整个 ServiceSpec 覆盖为新值时,立即检查是否误删 API 分配的 ClusterIP。发现 status 直接引用最新对象 generation 时,检查它是否与本轮实际处理的 spec 相同。契约越具体,测试越容易提出可证伪的断言。

把不可承诺的能力写在入口

单实例运行不等于高可用。两个控制器进程即使都使用乐观锁,也可能同时调用外部 API;资源版本冲突只能保护同一个 Kubernetes 对象的条件写入。随书程序没有 leader election,因此部署说明必须要求一个活动进程,而不能靠“重复协调应该幂等”暗示可以任意增加控制器副本。

同样,Envoy 的 direct-response 是真实 HTTP 能力,却不是生产网关治理。它没有上游重试、限流、鉴权和动态路由。我们使用固定镜像 digest 和简单配置,让实验关注控制器能否将 spec 映射为可验证行为。扩展网关业务能力时,需要为每项新增承诺设计独立验证,而不是沿用一次成功响应作为总证明。

从用户操作倒推恢复要求

按用户操作列出路径,比按代码函数列出任务更可靠。创建需要补齐资源;更新需要区分旧状态和新意图;误删子资源需要恢复;控制器重启需要重新发现已有对象;删除需要先处理外部责任;版本演进需要保证旧对象依然可读。这些路径共同构成控制器的产品定义。

本章的练习是为证书字段写出同样的责任表。证书内容由谁写入、错误证书如何报告、旧证书何时撤销、Secret 删除后怎样恢复,不能仅用“支持 TLS”概括。一个新增字段真正的成本,通常藏在更新、删除和恢复路径中。

评审案例:一个字段带来的三条路径

团队提出增加“暂停协调”字段,看起来只要在函数开头返回即可。仔细展开,会发现创建、更新和删除的含义都变了:暂停前已经存在的资源是否继续自愈,暂停期间用户删除主对象是否仍要清理,恢复后是否一次应用积压的所有变更?如果暂停也跳过删除分支,用户就可能得到永远无法删除的实例。

可以把暂停定义为只停止正常配置推进,删除责任仍然有效,并在状态中记录实际处理版本。这样用户提交新配置后,不会误读旧的成功条件为本次更新已完成。代价是用户不能用暂停阻止清理,应另有受控的运维流程。

这说明字段命名只是入口,关键是对每种生命周期状态定义行为。评审时把暂停值分别放入新建、稳定、更新、待删除和重启恢复现场,逐格说明预期,通常比先写实现更容易发现契约空白。本书没有实现暂停字段;此例提供需求展开的方法,避免把未实现能力混进当前发布。