TT Lab
开始
学习 学习路径 课程

CGOA — GitOps 认证助理

Git 接收了提交,Argo 却无法应用

在 TT Lab 中继续学习

目标

实际观察有问题的配置进入状态仓库后,调谐器(reconciler)会遇到什么,再把 CI 的验证移到仓库入口,让同样的错误到不了 main。 通过 RBAC 判定来确认这样的分工:CI 只写 Git,集群中只有调谐器写入。

为什么重要

GitOps 并不取消 CI/CD,而是挪动边界。CI 生成并验证期望状态,再记录到 Git, 部署则由集群内的调谐器把 Git 拉取过来完成。这样就没有理由把生产集群的写入凭据交给 CI。 代价是,进入 Git 的东西就等同于部署指令。Git 不理解 YAML 的含义,所以 replicas: two 这样的提交也会接受, 调谐器尝试应用它时会不断失败。把配置当作代码来对待(Configuration as Code),意思是像代码一样经过验证之后, 才允许进入源头,本实验用验证专用身份和服务端 dry-run 来建立这道验证。

步骤

  1. 创建裸仓库 /srv/bare/ci.git,克隆到 /root/cgoa-ci/repo,在 deploy/web.yaml 中提交 Deployment web(无 namespace,replicas 2,标签 app: web,容器 web,镜像 nginx:1.27-alpine),并 push 到 main。在 /root/cgoa-ci/app.yaml 中编写并应用 Application ci-web(argocd 命名空间,project default,仓库 git://gitd.gitsrv.svc.cluster.local:9418/ci.git 的 main 分支与 deploy 路径,目标命名空间 ci-web,自动同步 prune、selfHeal,CreateNamespace=true),确认 Synced、Healthy。
  2. 把 deploy/web.yaml 中的 replicas 改为字符串 two,提交并 push,然后对 ci-web 执行 hard refresh。大约 20 秒后读取 Application 状态,在 /root/cgoa-ci/broken.json 中写入 commit(有问题的提交 SHA)、sync_status(status.sync.status)、operation_message(status.operationState.message)、live_replicas(ci-web 命名空间中 Deployment 实际的 spec.replicas,数字)。观察结束后,用 git revert 回滚这个有问题的提交并 push。回滚之后,如果 status.operationState 仍在以有问题的提交重试而处于 Running(重试中),就用 /root/cgoa-ci/kubeconfig(k3s kubeconfig 副本,当前命名空间 argocd)执行 argocd app terminate-op ci-web --core,结束该操作。ci-web 必须在新的 main 上处于 Synced、Healthy,并且不能再留有重试有问题提交的操作。
  3. 创建命名空间 ci 与 ci-dryrun,并在 ci 中创建 ServiceAccount validator。在 ci-dryrun 中放入 Role dryrun-deployments(仅对 apps 组的 deployments 有 get、create、patch)以及把它绑定到 ci:validator 的 RoleBinding validator-dryrun。用该账户的令牌(8 小时)创建 kubeconfig /root/cgoa-ci/ci-kubeconfig(服务器地址与 k3s kubeconfig 相同,当前命名空间 ci-dryrun)。该账户只能在 ci-dryrun 中创建 Deployment,在 ci-web 中不能写入任何东西。
  4. 把 /root/cgoa-ci/validate.sh <디렉터리>(占位符为目录)做成可执行脚本。用 /root/cgoa-ci/ci-kubeconfig 对该目录中的清单,在 ci-dryrun 命名空间中做服务端 dry-run 应用,只要有一个被拒绝,就必须以非 0 值结束。不要依赖环境变量 KUBECONFIG,而要在脚本内部指定该 kubeconfig。不得创建真实对象。
  5. 把 /srv/bare/ci.git/hooks/pre-receive 做成可执行钩子(hook)。每次更新 refs/heads/main 的 push,都把新提交的 deploy 目录检出到临时目录,用 /root/cgoa-ci/validate.sh 验证,失败就拒绝 push。拒绝删除 main,放行其他引用。
  6. 在 /root/cgoa-ci/repo 中,创建一个把 deploy/web.yaml 的 replicas 键改成拼写错误 replica 的提交并尝试 push,把被拒绝的完整输出保存到 /root/cgoa-ci/blocked.txt。然后把本地 main 还原为远程 main(git reset --hard origin/main)。远程 main 必须保持不变,ci-web 必须一直是 Synced、Healthy。
  7. 把 /root/cgoa-ci/bump.sh <태그>(占位符为镜像标签)做成可执行脚本。把 /root/cgoa-ci/repo 对齐到远程 main 之后,把 deploy/web.yaml 的镜像改成 nginx:<태그>,用消息 ci: web nginx:<태그> 提交并 push。不使用 kubectl。执行 bump.sh 1.28-alpine,hard refresh 之后,确认 ci-web 在该提交上处于 Synced、Healthy,且 Deployment 镜像已变为 nginx:1.28-alpine。
  8. 在 /root/cgoa-ci/report.json 中写入 ci_writes(git)、cd_writes(cluster)、ci_can_write_prod(ci:validator 能否对 ci-web 中的 deployments 执行 patch,布尔值)、gate(pre-receive)、deployed_revision(现在 ci-web 的 status.sync.revision)、broken_reached_git(第 2 步中有问题的提交是否仍在 main 历史中,布尔值)。

参考

从 Git 部署的基线

创建裸仓库 /srv/bare/ci.git,克隆到 /root/cgoa-ci/repo,在 deploy/web.yaml 中提交 Deployment web(无 namespace,replicas 2,标签 app: web,容器 web,镜像 nginx:1.27-alpine),并 push 到 main。在 /root/cgoa-ci/app.yaml 中编写并应用 Application ci-web(argocd 命名空间,project default,仓库 git://gitd.gitsrv.svc.cluster.local:9418/ci.git 的 main 分支与 deploy 路径,目标命名空间 ci-web,自动同步 prune、selfHeal,CreateNamespace=true),确认 Synced、Healthy。

gitd 服务会通过 git 协议对外提供 /srv/bare 下的裸仓库。首次同步后,两个 Pod 都变为 Ready 才算 Healthy。

仓库已经收下,Argo 应用却失败了

把 deploy/web.yaml 中的 replicas 改为字符串 two,提交并 push,然后对 ci-web 执行 hard refresh。大约 20 秒后读取 Application 状态,在 /root/cgoa-ci/broken.json 中写入 commit(有问题的提交 SHA)、sync_status(status.sync.status)、operation_message(status.operationState.message)、live_replicas(ci-web 命名空间中 Deployment 实际的 spec.replicas,数字)。观察结束后,用 git revert 回滚这个有问题的提交并 push。回滚之后,如果 status.operationState 仍在以有问题的提交重试而处于 Running(重试中),就用 /root/cgoa-ci/kubeconfig(k3s kubeconfig 副本,当前命名空间 argocd)执行 argocd app terminate-op ci-web --core,结束该操作。ci-web 必须在新的 main 上处于 Synced、Healthy,并且不能再留有重试有问题提交的操作。

Git 不知道它是字符串还是数字。直到调谐器要应用的那一刻,API 服务器才会拒绝。请看这期间集群中留下了什么。自动同步操作失败后,会用同一个 revision 重试固定的次数,这期间不会开始同步新提交。请确认 status.operationState.operation.sync.revision。

不能用于生产的 CI 身份

创建命名空间 ci 与 ci-dryrun,并在 ci 中创建 ServiceAccount validator。在 ci-dryrun 中放入 Role dryrun-deployments(仅对 apps 组的 deployments 有 get、create、patch)以及把它绑定到 ci:validator 的 RoleBinding validator-dryrun。用该账户的令牌(8 小时)创建 kubeconfig /root/cgoa-ci/ci-kubeconfig(服务器地址与 k3s kubeconfig 相同,当前命名空间 ci-dryrun)。该账户只能在 ci-dryrun 中创建 Deployment,在 ci-web 中不能写入任何东西。

用 kubectl create token 获取令牌,用 kubectl config --kubeconfig <파일> set-cluster/set-credentials/set-context(占位符为文件名)组装新文件。CA 直接沿用 k3s kubeconfig 的 certificate-authority-data 即可。用 kubectl auth can-i --as system:serviceaccount:<ns>:<이름>(占位符为名称)确认判定结果。

把服务端会拒绝的清单先在 CI 中拒绝

把 /root/cgoa-ci/validate.sh <디렉터리>(占位符为目录)做成可执行脚本。用 /root/cgoa-ci/ci-kubeconfig 对该目录中的清单,在 ci-dryrun 命名空间中做服务端 dry-run 应用,只要有一个被拒绝,就必须以非 0 值结束。不要依赖环境变量 KUBECONFIG,而要在脚本内部指定该 kubeconfig。不得创建真实对象。

客户端 dry-run 会放过字符串 replicas,也会放过拼写错误的字段。请选择会经过 API 服务器 schema 验证但不保存的模式。仅靠 set -e,管道中间的失败可能不会暴露出来。

把验证设在仓库入口

把 /srv/bare/ci.git/hooks/pre-receive 做成可执行钩子。每次更新 refs/heads/main 的 push,都把新提交的 deploy 目录检出到临时目录,用 /root/cgoa-ci/validate.sh 验证,失败就拒绝 push。拒绝删除 main,放行其他引用。

钩子在裸仓库中运行,所以没有工作树。特定提交的某个目录可以用 git archive <커밋> deploy | tar -x -C <임시>(占位符依次为提交与临时目录)取出。新 SHA 是 40 个 0 时表示删除。

同样的错误再也到不了 main

在 /root/cgoa-ci/repo 中,创建一个把 deploy/web.yaml 的 replicas 键改成拼写错误 replica 的提交并尝试 push,把被拒绝的完整输出保存到 /root/cgoa-ci/blocked.txt。然后把本地 main 还原为远程 main(git reset --hard origin/main)。远程 main 必须保持不变,ci-web 必须一直是 Synced、Healthy。

被拒绝的提交不在远程,所以只需还原本地即可。这一次,调谐器连遇到失败的机会都没有。

CI 写入 Git,由 Argo 部署

把 /root/cgoa-ci/bump.sh <태그>(占位符为镜像标签)做成可执行脚本。把 /root/cgoa-ci/repo 对齐到远程 main 之后,把 deploy/web.yaml 的镜像改成 nginx:<태그>,用消息 ci: web nginx:<태그> 提交并 push。不使用 kubectl。执行 bump.sh 1.28-alpine,hard refresh 之后,确认 ci-web 在该提交上处于 Synced、Healthy,且 Deployment 镜像已变为 nginx:1.28-alpine。

这个提交也必须通过第 5 步的钩子,才能进入 main。脚本只改变 Git 中的期望状态,应用则交给调谐器。

看清谁在哪里写入

在 /root/cgoa-ci/report.json 中写入 ci_writes(git)、cd_writes(cluster)、ci_can_write_prod(ci:validator 能否对 ci-web 中的 deployments 执行 patch,布尔值)、gate(pre-receive)、deployed_revision(现在 ci-web 的 status.sync.revision)、broken_reached_git(第 2 步中有问题的提交是否仍在 main 历史中,布尔值)。

两个布尔值不要猜测,请写入用 kubectl auth can-i 和 git merge-base --is-ancestor 确认后的结果。