仓库怎么拆 — 以及把晋级做成一行 PR
一句话总结
GitOps 的首项设计决策不是选择工具,而是确定repository boundary。是否分离 app source 与 deployment config,是否按团队拆分 repository,是否用 branch 或 directory 区分 environment——这四个问题的答案会决定之后全部运维成本。
为什么需要它
最初,在 app repository 中建立 k8s/ directory 并一起管理 manifest 很方便,但很快会遇到以下问题。
提升 image tag 的 commit 进入 app repository 后,会再次触发 CI。CI 创建新 image,再 commit 新 tag,形成无限循环。为了绕过问题开始添加 [skip ci] 后,pipeline 会越来越混乱。
第二个问题是权限。app code review 权限与 production deployment config review 权限,并不属于同一组人员。放在同一 repository 时,只能通过 CODEOWNERS 勉强拆分,一次失误就可能突破边界。
第三个问题是变更周期。app code 一天变化十次,deployment config 可能一个月才变一次。历史混在一起后,为了查找“replicas 从什么时候开始是 3”,就要翻阅数百个 commit。
因此,实际工作的基础形态是分离app repository(code + Dockerfile + CI)与config repository(manifest + kustomize/Helm values)。考试中称之为 separation of concerns。
工作原理
config repository 再拆成几个,就是 monorepo 与 polyrepo 的争论。
| Monorepo(一个 config repo) | Polyrepo(每团队/app 一个) | |
|---|---|---|
| 一致性 | 在一个位置强制使用共同 base | 每个 repository 的标准可能不同 |
| RBAC | 只能按 directory 控制,精细权限较难 | 可通过 repository permission 清晰分离 |
| 原子变更 | 可在一个 PR 中同时修改多个 app | 需要跨 repository 协调 |
| agent 负载 | 多个 app 同时 polling 一个 repository | 分散 |
| 适用规模 | 一个或多个团队、数十个 app | 多个组织、数百个 app |
environment 也有三种隔离方式。
- Branch 隔离——
dev/staging/mainbranch。直观,但 branch 间 diff 会混合环境差异与时间差异,并陷入 cherry-pick 困境,如今通常不推荐。 - Directory 隔离(overlay)——
overlays/dev、overlays/stage、overlays/prod。可以在同一个 commit 中并排查看三个环境的差异,是当前事实标准。 - Repository 隔离——只把 production config 放入独立 repository。常用于监管行业,以 repository 划定 audit boundary。
让 promotion 只改一行
判断 repository structure 好坏只有一个标准:“把 stage 中运行良好的版本提升到 prod,PR diff 有几行?”
如果 base 与 overlay 分离正确,答案就是一行。
# apps/checkout/overlays/prod/kustomization.yaml
images:
- name: ghcr.io/labhub/checkout
newTag: 1.5.0 # ← 1.4.0 에서 이 줄만 바뀐다
这一行 diff 向 reviewer 提供了非常丰富的信息:可以直观看出“进入 prod 的变更只有 image tag,其余配置与 stage 相同”。反过来,如果 promotion PR 同时修改 replicas、resource 与 environment variable,那就不是 promotion,而是一次新 deployment。
此处 tag 必须不可变。如果使用 latest 或 branch name tag,Git commit 不变,实际运行 image 却可能变化。这样 desired state 就无法完全由 Git 决定,违反第二项原则。
app-of-apps
app 数量增加后,Argo CD Application resource 也会达到数十个。若手动创建,“部署 deployment tool 的流程”又会退回手工作业。app-of-apps 是结束这种递归的 pattern。
一个 root Application 指向 bootstrap/ directory,该目录包含 child Application manifest。同步 root 后会创建 child Application,再由 child 分别同步自己的 app。添加新 app 只需向 bootstrap/ commit 一个文件。
它与用途相近的 ApplicationSet 仍有区别。app-of-apps 通过文件明确列出 child,ApplicationSet 则通过 generator(directory scan、cluster list、PR list)计算 child。希望 app list 明确可见时选择 app-of-apps;列表频繁变化并可用规则表达时选择 ApplicationSet。
现场会遇到的情况
作者家庭实验室中,Argo CD 位于 10.0.0.201,Gitea 位于 10.0.0.200。Argo CD 从同一集群中的 Gitea pull config。该架构有一个有趣的 bootstrap 陷阱:Gitea 故障后,Argo CD 无法读取 desired state,而 Gitea 自身 manifest 也保存在该 Gitea 中。因此,GitOps 管理区域与手动 bootstrap 区域之间的边界,应画在哪里,会成为真实设计问题。
还有一点:该集群的 Gateway API 需要 CRD v1.6.1。使用 v1.2 时,tlsroutes 与 referencegrants 尚不是 v1,导致 Cilium gateway controller 拒绝启动。这类事故说明,CRD version 等集群 prerequisite 不应放在 app overlay,而应置于platform layer repository,并让其 sync wave 更早执行。
下一项实验要做什么
在 /root/cgoa-repo/ 下亲自创建 apps/checkout/base 与 overlays/dev|stage|prod,再通过 kubectl kustomize 查看各环境渲染结果如何不同。随后,把 Application manifest、app-of-apps root 与 AppProject 都写入文件。接下来的实验会通过 kubectl apply -k 把同一结构部署到真实集群。