Files
ayatori/docs/decisions/0004-modular-controller-boundaries.md
T

110 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0004:采用可拆分的模块化 Controller 架构
- 状态:Accepted
- 日期:2026-09-17
## 背景
Ayatori 将逐步提供任务执行、虚拟机、数据库、负载均衡、对象存储、托管 Kubernetes 和
人工操作等领域能力。这些能力拥有不同的生命周期、权限、网络位置和后端实现,若直接在
一个 controller 中相互调用并共享内部状态,后续接入新后端时容易形成代码耦合,也难以
独立扩缩容、发布和隔离故障。
另一方面,在首个领域能力完成前就拆分为多个独立服务,会立即引入镜像与部署管理、服务
间认证、版本兼容、分布式观测和故障处理成本,而这些成本尚未由真实运行需求证明。
Job 是首个领域对象。它既是最初的单次任务调度 API,也用于验证领域状态机与 Kubernetes
Job、OpenSandbox 和未来执行后端之间的适配边界。
## 决策
Ayatori 初期采用模块化单体:多个领域 controller 可以编译进同一个 controller manager,
但代码、API 所有权和依赖方向按照未来可独立部署的服务边界组织。
### 领域所有权
每个领域模块拥有自己的:
- CRD 与 API 版本;
- reconciler 和状态机;
- finalizer、conditions、删除及恢复语义;
- backend adapter contract;
- 领域测试。
初始领域包括但不限于 execution、compute、database、networking 和 human operations。领域
模块不得导入其他领域的内部实现,也不得直接修改其他领域所拥有对象的 spec 或 status。
### 跨领域协作
领域间的持久协作通过 Kubernetes API 对象、typed reference、owner reference 和
conditions 完成,而不是通过进程内 service 方法调用。
例如虚拟机完成创建后需要执行 provision,应创建或引用 Run 对象并观察其状态,而不是
直接调用 execution 模块的内部 Go API。更高层的资源组合由专门的领域对象或 GitOps 声明
完成,不引入统一包装所有底层能力的 Application controller。
该约束使 controller 即使暂时位于同一进程,其通信、失败和恢复行为仍与未来分进程部署
一致。
### Backend adapter
领域状态机只依赖本领域定义的最小 adapter contract,不依赖 Kubernetes Job、Crossplane、
OpenTofu、OpenSandbox 或具体厂商 SDK 类型。Adapter 负责:
- 幂等地确保期望外部资源存在;
- 观察并翻译外部状态;
- 执行取消、删除或 orphan 策略;
- 返回稳定的外部引用、能力和分类错误。
不同领域分别定义 adapter contract,不建立能包装所有资源类型的万能 Provider 接口。
后端不具备的能力必须显式报告,不通过虚假的统一语义隐藏差异。
Crossplane、OpenTofu 和 Ansible 等系统是可替换的 backend 实现或执行机制,不构成 Ayatori
面向用户的稳定 API。它们的 ProviderConfig、Workspace、playbook 等实现细节不得直接成为
领域 API 的必填契约。
### 共享代码
默认不建立跨领域的万能 service、repository 或 util 层。共享并非禁止,但必须来自已经
存在的真实重复,并同时满足:
1. 至少有两个真实调用者;
2. 重复的行为和语义一致,而不只是代码形状相似;
3. 调用方对其生命周期和预期演化方向一致;
4. 共享包不依赖任何具体领域的内部包。
适合共享的通常是机制,例如 conditions 操作、typed reference、重试退避、Secret 引用
读取、观测初始化和测试环境。领域状态机、资源策略、错误含义及后端选择不得为了消除少量
重复而抽取到共享层。
共享包使用表达具体职责的窄名称,初期保留在 `internal/shared/`。不建立内容持续膨胀的
通用 `util` 包,也不在实现稳定前承诺公共 Go API。
### 部署与拆分
controller manager 应支持按领域或 controller 集合选择性启用。初期可以使用一个二进制和
一个 Deployment;需要隔离时,优先使用同一制品部署为多个 Deployment。只有独立版本和
依赖关系成为真实需求后,才进一步拆分二进制或仓库。
出现下列任一情况时,应评估拆分部署:
- 需要独立扩缩容或显著不同的 reconcile 并发;
- 权限边界要求独立 ServiceAccount 与 RBAC;
- 后端只能从特定网络或节点访问;
- 沉重、不可信或冲突的 SDK 需要隔离;
- 一个领域的故障不应影响其他控制循环;
- 发布节奏或维护责任已经明确分离。
拆分不得改变领域 API,也不应将原本通过 API 对象完成的协作改为同步 RPC 链路。
## 结果
- 首个 Job 实现需要同时建立 execution 状态机和 adapter 边界,不能把 Kubernetes Job
细节写入领域模型。
- 初期避免承担不必要的微服务运维成本,同时保留按权限、网络位置和故障域拆分的路径。
- Kubernetes API 成为领域间异步协作和恢复边界,领域 controller 必须正确处理最终一致性。
- 部分机械代码会有意保留重复,直到共享语义被至少两个真实实现证明。
- 代码评审需要检查跨领域 import、对象写入所有权和后端类型泄漏。
- 当 Job 同时拥有 Kubernetes Job 与 OpenSandbox adapter 后,应复审 adapter contract,确认
它来自真实后端差异而非单一实现假设。