第 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”概括。一个新增字段真正的成本,通常藏在更新、删除和恢复路径中。
评审案例:一个字段带来的三条路径
团队提出增加“暂停协调”字段,看起来只要在函数开头返回即可。仔细展开,会发现创建、更新和删除的含义都变了:暂停前已经存在的资源是否继续自愈,暂停期间用户删除主对象是否仍要清理,恢复后是否一次应用积压的所有变更?如果暂停也跳过删除分支,用户就可能得到永远无法删除的实例。
可以把暂停定义为只停止正常配置推进,删除责任仍然有效,并在状态中记录实际处理版本。这样用户提交新配置后,不会误读旧的成功条件为本次更新已完成。代价是用户不能用暂停阻止清理,应另有受控的运维流程。
这说明字段命名只是入口,关键是对每种生命周期状态定义行为。评审时把暂停值分别放入新建、稳定、更新、待删除和重启恢复现场,逐格说明预期,通常比先写实现更容易发现契约空白。本书没有实现暂停字段;此例提供需求展开的方法,避免把未实现能力混进当前发布。