Git 接收了提交,Argo 却无法应用
目标
实际观察有问题的配置进入状态仓库后,调谐器(reconciler)会遇到什么,再把 CI 的验证移到仓库入口,让同样的错误到不了 main。 通过 RBAC 判定来确认这样的分工:CI 只写 Git,集群中只有调谐器写入。
为什么重要
GitOps 并不取消 CI/CD,而是挪动边界。CI 生成并验证期望状态,再记录到 Git,
部署则由集群内的调谐器把 Git 拉取过来完成。这样就没有理由把生产集群的写入凭据交给 CI。
代价是,进入 Git 的东西就等同于部署指令。Git 不理解 YAML 的含义,所以 replicas: two 这样的提交也会接受,
调谐器尝试应用它时会不断失败。把配置当作代码来对待(Configuration as Code),意思是像代码一样经过验证之后,
才允许进入源头,本实验用验证专用身份和服务端 dry-run 来建立这道验证。
步骤
- 创建裸仓库
/srv/bare/ci.git,克隆到/root/cgoa-ci/repo,在deploy/web.yaml中提交 Deploymentweb(无 namespace,replicas 2,标签app: web,容器web,镜像nginx:1.27-alpine),并 push 到main。在/root/cgoa-ci/app.yaml中编写并应用 Applicationci-web(argocd 命名空间,project default,仓库git://gitd.gitsrv.svc.cluster.local:9418/ci.git的main分支与deploy路径,目标命名空间ci-web,自动同步 prune、selfHeal,CreateNamespace=true),确认 Synced、Healthy。 - 把
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,并且不能再留有重试有问题提交的操作。 - 创建命名空间
ci与ci-dryrun,并在ci中创建 ServiceAccountvalidator。在ci-dryrun中放入 Roledryrun-deployments(仅对 apps 组的 deployments 有 get、create、patch)以及把它绑定到ci:validator的 RoleBindingvalidator-dryrun。用该账户的令牌(8 小时)创建 kubeconfig/root/cgoa-ci/ci-kubeconfig(服务器地址与 k3s kubeconfig 相同,当前命名空间ci-dryrun)。该账户只能在ci-dryrun中创建 Deployment,在ci-web中不能写入任何东西。 - 把
/root/cgoa-ci/validate.sh <디렉터리>(占位符为目录)做成可执行脚本。用/root/cgoa-ci/ci-kubeconfig对该目录中的清单,在ci-dryrun命名空间中做服务端 dry-run 应用,只要有一个被拒绝,就必须以非 0 值结束。不要依赖环境变量 KUBECONFIG,而要在脚本内部指定该 kubeconfig。不得创建真实对象。 - 把
/srv/bare/ci.git/hooks/pre-receive做成可执行钩子(hook)。每次更新refs/heads/main的 push,都把新提交的deploy目录检出到临时目录,用/root/cgoa-ci/validate.sh验证,失败就拒绝 push。拒绝删除 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。 - 把
/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。 - 在
/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 历史中,布尔值)。
参考
- VM 中有 k3s、Argo CD v3.5.2、git 守护进程(
gitd.gitsrv)。/srv/bare/<이름>.git会显示为git://gitd.gitsrv.svc.cluster.local:9418/<이름>.git(占位符均为仓库名称)。 - 要立即重新读取:
kubectl -n argocd annotate app ci-web argocd.argoproj.io/refresh=hard --overwrite。 - 权限判定:
kubectl auth can-i create deployments.apps -n ci-dryrun --as system:serviceaccount:ci:validator - 常见错误:用
kubectl apply --dry-run=client验证。客户端不像服务端那样严格检查 schema,所以第 2 步的提交也会通过。 - 常见错误:没放钩子之前,main 就一直保持损坏。第 2 步必须先回滚再继续。
- 常见错误:回滚后只看 Synced 就往下走。即使比较结果是 Synced,只要失败的操作还在重试有问题的提交,第 7 步的新提交就不会被同步。
- core 模式命令会在 kubeconfig 的当前命名空间中寻找 Argo CD 配置。请复制 k3s kubeconfig,用
kubectl config --kubeconfig <사본> set-context --current --namespace=argocd(占位符为副本文件名)修改后使用。 - OpenGitOps 原则、Argo CD 自动同步、kubectl apply --dry-run、githooks
从 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 确认后的结果。