なぜ比較対象が三つも要るのか
一言でいうと
Argo CDのApplicationコントローラーは、デフォルトで180秒ごとに目を覚まし、Desired(Git)、Live(クラスター)、Last-Applied(アノテーション)の3つの状態を比較します。2つではなく3つである理由と、比較の前に正規化を経ないと、なぜ永遠にOutOfSyncのまま残るのかが、このレッスンのすべてです。
なぜ必要なのか
Gitとクラスターを、そのまま比較してはいけないのでしょうか。いけません。Kubernetesは、オブジェクトを保存するときに、数多くのデフォルト値を埋め込むからです。ServiceをclusterIPなしで作成すると、APIサーバーが1つ割り当て、イメージタグがlatestなら、imagePullPolicyにAlwaysを入れ、すべてのオブジェクトにresourceVersion・uid・generation・creationTimestamp・managedFieldsを付けて、statusを埋めます。Gitのマニフェストには、これらのどれもありません。そのため、「違う」という結論が常に出て、自動同期を有効にしていると、コントローラーが3分ごとに、無意味なapplyを永遠に繰り返します。
3つ目の比較対象が必要な理由は、別の種類の問題です。Gitとクラスターだけを知っていても、「Gitにないのにクラスターにあるフィールド」が、自分が以前入れて消したものなのか、ほかのコントローラー(HPA、サイドカーインジェクター、デフォルター)が入れたものなのかを区別できません。Last-Appliedは、「直前に自分が宣言したもの」を覚えています。以前自分が宣言したが、いまGitから消えたフィールドは、消すべきもので、自分が一度も宣言したことのないフィールドは、他者のものなので、触ってはいけません。この判断を、3-way diffが行います。Argo CDは、この計算に、KubernetesのServer-Side Applyが使うものと同じStructured Merge Diffライブラリを使います。
どう動くのか
正規化の段階で取り除かれる代表的なフィールドは、metadata.resourceVersion、metadata.uid、metadata.generation、metadata.creationTimestamp、metadata.managedFields、そして、ほとんどのリソースのstatusです。それでも残る差は、ignoreDifferencesで自分で除外する必要があります。最もよくある例がHPAです。Gitにはreplicas: 2と書かれているのに、HPAが8に上げていると、永遠にOutOfSyncで、selfHealまで有効になっていると、Argo CDが2に下げて、HPAが8に上げるという争いが始まります。答えは、/spec/replicasをdiffから除外することです。
同期は3つの段階に分かれます。PreSync → Sync → PostSyncで、Syncが失敗するとSyncFail段階が別に実行されます。各段階で実行されるものがフックで、フックリソースの削除ポリシーのデフォルトはBeforeHookCreationです。次の同期のときに、以前のフックオブジェクトを先に削除してから、新しく作るという意味なので、失敗したマイグレーションJobの痕跡が、次のデプロイまで残ることになります。
同じ段階の中での順序は、ウェーブが決めます。小さい番号から実行され、1つのウェーブのすべてのリソースがHealthyになるまで待ってから、次のウェーブに進みます。どのウェーブでも失敗すれば、同期全体が中断されます。この「待つ」ことが重要です。ウェーブは、applyの順序を変える仕組みではなく、準備完了を待つ仕組みです。
リトライは指数バックオフです。limit 5、duration 5s、factor 2、maxDuration 3mにすると、5秒、10秒、20秒、40秒、80秒の間隔で5回試行します。そして、Argo CDが自分のものを見分ける方法は、追跡アノテーションで、形式はAPP_NAME:GROUP/KIND:NAMESPACE/NAMEです。コアグループは、グループ名が空なので、my-app:/Service:default/nginx-svcのように、コロンのすぐあとにスラッシュが来ます。この形式を手で書いてみると、なぜラベル方式よりアノテーション方式が推奨されるのか、見当がつきます。ラベルには63文字の制限があり、ネームスペースが入りません。
現場での姿
筆者のホームラボのArgoCDは、MetalLBのプールから受け取った10.0.0.201で動いていて、同じ帯域の10.0.0.200にGiteaがあります。クラスターの登録で、必ず押さえておくべき落とし穴が1つあります。argocd cluster addは、対象のクラスターにargocd-manager ServiceAccountを作り、それをデフォルトでcluster-adminに結び付けるという点です。ホームラボでは見過ごせますが、本番でこのままにしておくと、GitOpsコントローラー1つが、すべてのクラスターのルートになります。必要なapiGroupと動詞だけを含むClusterRoleを別に作って、バインディングを差し替えるのが定石です。
もう1つ、このクラスターにはCilium Gateway APIが載っているのですが、Gateway API CRDをv1.2にしておいたところ、tlsroutesとreferencegrantsがv1ではないと言って、コントローラーが起動を拒否しました。v1.6.1に上げたら起動しました。Argo CDでCRDをデプロイするときに、このようなことがよく起きます。CRDと、そのCRDを使うカスタムリソースが同じ同期に入っていると、CRDがまだ登録される前にCRがapplyされて、失敗します。ウェーブを分けて、CRDを先に送るのが標準的な解決策で、これがウェーブの存在する理由を最もよく示す事例です。
次のラボですること
/root/capa-app/に、Applicationマニフェストを1フィールドずつ積み上げます。source・destination・syncPolicy・retry・ignoreDifferencesを順に埋め、最後に、そのアプリがデプロイするネームスペースとDeploymentを、実際にクラスターに載せ、追跡アノテーションまで手で付けてみます。