Git は受け入れたのに Argo は適用できなかった
目標
壊れた設定がステート用のリポジトリに入ったときに調整器が遭遇することを実際に確かめ、CIの検証をリポジトリの入口へ移して、同じミスがmainに届かないようにします。 CIはGitにだけ書き、クラスターには調整器だけが書くという分担を、RBACの判定で確認します。
なぜ重要なのか
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)をコミットしてmainへpushしてください。/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で適用してみて、1つでも拒否されたら0以外の値で終了する必要があります。環境変数KUBECONFIGに頼らず、スクリプトの中でそのkubeconfigを指定します。実際のオブジェクトを作成してはいけません。/srv/bare/ci.git/hooks/pre-receiveを実行可能なフックとして作成してください。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で検証することです。クライアントはサーバーほど厳密にスキーマを見ないので、ステップ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)をコミットしてmainへpushしてください。/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サービスが、/srv/bare以下のベアリポジトリをgitプロトコルで公開しています。最初の同期のあと、Podが2つ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で適用してみて、1つでも拒否されたら0以外の値で終了する必要があります。環境変数KUBECONFIGに頼らず、スクリプトの中でそのkubeconfigを指定してください。実際のオブジェクトを作成してはいけません。
クライアント側のdry-runは、文字列のreplicasも、誤字のフィールドも通してしまいます。APIサーバーのスキーマ検証を通しつつ、保存はしないモードを選んでください。set -eだけでは、パイプの途中の失敗が表に出ないことがあります。
検証をリポジトリの入口に据える
/srv/bare/ci.git/hooks/pre-receiveを実行可能なフックとして作成してください。refs/heads/mainを更新するpushのたびに、新しいコミットのdeployディレクトリを一時ディレクトリに取り出して/root/cgoa-ci/validate.shで検証し、失敗したらpushを拒否してください。mainの削除は拒否し、ほかの参照は通してください。
フックはベアリポジトリで動くので、作業ツリーがありません。特定のコミットの1つのディレクトリは、git archive <커밋> deploy | tar -x -C <임시>で取り出せます(プレースホルダーはコミットと一時ディレクトリです)。新しいSHAが0を40個並べたものなら、削除です。
同じミスはもう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の履歴に残っているか、ブール値)を書いてください。
ブール値の2つは推測せず、kubectl auth can-iとgit merge-base --is-ancestorで確認した結果を書いてください。