TT Lab
はじめる
学ぶ 学習パス コース

CGOA — GitOps認定アソシエイト

refreshは計算で、syncは適用だ

TT Labで続きを見る

一言でいうと

refreshは再レンダリングしてdiffを計算するだけで、何も適用しません。syncだけがクラスターに触れます。この1文を取りこぼすと、Argo CDの半分が理解できなくなります。

なぜ必要なのか

UIでRefreshボタンを押したのにアプリが相変わらずOutOfSyncのままだと、たいていの人は「壊れたのか」と思います。正常です。refreshが行うのは次のことです。

  1. repo-serverに、最新のマニフェストを作り直すよう要求します(キャッシュの無効化の有無によって、hard refreshならクローンからやり直します)。
  2. 対象クラスターからlive状態を読み直します。
  3. 両者を比較して、Sync Status(Synced / OutOfSync)とHealth Statusを更新します。

ここまでです。適用はsyncの仕事で、syncは人が押すか、syncPolicy.automatedが有効なときにだけ起こります。この分離のおかげで、「今Gitとクラスターがどれくらい違うか」を、何も変えずに観察し続けられます。自動同期を切ったまま運用する組織が多い理由が、まさにこれです。監視は常に、適用は人が行います。

どう動くのか

3-way diff: なぜ3つの状態なのか

Argo CDのdiffは、2つではなく3つの状態を見ます。

状態 どこから来るか 何に答えるか
Desired Gitをレンダリングした結果 私たちが望むもの
Live クラスターの現在のオブジェクト 今実際にあるもの
Last-applied オブジェクトのlast-applied-configurationアノテーション 私たちが以前に管理すると宣言したもの

2つの状態だけを比べると、致命的な誤判定が起きます。たとえば、HPAがreplicasを5に上げていて、Gitにはreplicasフィールドそのものがないとします。DesiredとLiveだけを比較すると、「Liveにしかないフィールドだから消そう」となります。3つ目の状態を見ると答えが変わります。last-appliedにもreplicasがないので、これはそもそも私たちが管理したことのないフィールドであり、他のコントローラーが所有する値なので、触れてはいけません。

正規化(normalization)もここに加わります。metadata.resourceVersion、uid、generation、creationTimestamp、managedFields、そしてほとんどのstatusは、diffから取り除きます。Kubernetesが自動的に埋めるデフォルト値(ServiceのclusterIP、イメージタグがlatestのときのimagePullPolicy: Always)も無視します。この正規化がないと、すべてのアプリが永遠にOutOfSyncに見えます。

selfHealが本当に意味すること

syncPolicy.automated.selfHeal: trueは「ドリフトを自動で元に戻す」という意味です。裏返すと、こういう意味になります。

障害対応中に手で直したものが元に戻される。

明け方にPodが落ちて、急いでkubectl scaleでreplicasを増やしたなら、次の調整ループでGitの値に戻ります。これはバグではなく設計です。selfHealを有効にした組織は、「緊急の修正もコミットで行う」という規律も一緒に受け入れたことになります。急ぐときのための逃げ道が必要なら、自動同期を一時的に無効にするか、該当のフィールドをignoreDifferencesに入れて管理対象から外すのが定石です。

ウェーブとフック: 順序を作る2つの仕組み

宣言型は、順序を表現できません。そのため、2つの仕組みが上乗せされます。

Sync waveは、argocd.argoproj.io/sync-waveアノテーションの数字でリソースをグループ化し、小さい番号から適用します。核心は、現在のウェーブのすべてのリソースがHealthyになるまで、次のウェーブへ進まないという点です。そのため、ウェーブの分け方を間違えると、デプロイがその場で止まります。たとえば、絶対にHealthyにならないリソース(受信するトラフィックがなく準備が整わないJobなど)を前のウェーブに置くと、後ろのウェーブがいつまでも来ません。同じウェーブの中では、リソースの種類ごとのデフォルトの順序(Namespace → NetworkPolicy → ResourceQuota → LimitRange → ServiceAccount → Secret/ConfigMap → RBAC → CRD → PV/PVC → Service → ワークロード → Ingress)が適用されます。

Hookは、フェーズ(phase)そのものを分けます。PreSync → Sync → PostSyncであり、失敗するとSyncFailが動きます。フックは通常Jobで、アノテーションargocd.argoproj.io/hook: PreSyncで指定します。削除ポリシーargocd.argoproj.io/hook-delete-policyにはHookSucceeded、HookFailed、BeforeHookCreationの3つの値があり、デフォルトはBeforeHookCreationです。つまり、フックのリソースは成功しても残っていて、次のsyncで新しく作る直前に削除されます。失敗したマイグレーションJobのログを事後に見られるのは、このデフォルトのおかげです。

リトライとバックオフ

syncが失敗すると、指数バックオフでリトライします。duration: 5s、factor: 2、maxDuration: 3mなら、待ち時間は5s → 10s → 20s → 40s → 80sと増え、3分で上限に達します。リトライがトリガーされる状況は、リソースの適用失敗、Health Checkのタイムアウト、フックJobの失敗、一時的なネットワークエラーです。

pruneの危険

prune: trueは、Gitから消えたリソースをクラスターからも削除します。判別の基準は、「クラスターにあってGitにないもの」のすべてではなく、Argo CDが自分のものとして印を付けたもののうちGitにないものです。印の付け方がリソース追跡(resource tracking)で、デフォルトはアノテーション方式です。

argocd.argoproj.io/tracking-id: APP_NAME:GROUP/KIND:NAMESPACE/NAME
예) checkout-prod:apps/Deployment:cgoa-prod/prod-checkout

レガシー方式はラベルapp.kubernetes.io/instanceを使います。このラベルはHelmなどの他のツールも使うため、所有権の判定が衝突する可能性があり、アノテーション方式が推奨されます。

pruneが怖い理由は、パスを誤って変えたコミット1つが、そのまま大量削除になるからです。source.pathを誤字で空のディレクトリに変えると、レンダリング結果が0個になり、そのアプリが管理していたすべてのリソースがpruneの対象になります。防御の仕組みは、allowEmpty: false(空のレンダリング結果を拒否)、PruneLast=true(他のリソースの同期が終わったあと、最後にprune)、そして個別のリソースのargocd.argoproj.io/sync-options: Prune=falseです。

現場での姿

筆者のホームラボで、この感覚が必要になった場面は、GPUノードの拡張のときでした。GPU Feature Discoveryが新しいノードにラベルを自動で付けます。RTX 3090 24GB、5090 32GB、4070 Laptop 8GBが2台です。ところがPodがnvidia.com/gpu: 1だけを要求すると、32GBが必要な学習が、8GBのノートPC用GPUに載ってしまう可能性があります。Kubernetesにとっては、どちらも「GPU 1つ」だからです。

そこで、意味に基づくラベルを手で追加しました(gpu.homelab/tier=xlarge、vram=32gのような形です)。ここから、GitOpsの観点での教訓が出てきます。コントローラーが付けるラベルと、人が宣言するラベルが、同じオブジェクトに共存しているのです。このNodeオブジェクトをGitOpsで管理するなら、GFDが付けたラベルは必ずignoreDifferencesで外さなければなりません。そうしないと、調整ループとコントローラーがお互いのフィールドを消し合って争います。3-way diffとフィールドの所有権がなぜ必要なのかが、この場面にすべて入っています。

次のラボですること

実際のクラスターにマニフェストを載せたあと、手でreplicasを変えてドリフトを作り、kubectl diffでその差をファイルに残してから元に戻します(人が行うself-heal)。続いて、sync waveアノテーションの付いたマニフェスト3つとPreSyncフックのJobをファイルとして作成し、最後にクラスターを調べて、pruneの対象が何かを自分で判別します。