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

GitOpsとArgo CD

このフィールドは誰のものか

TT Labで続きを見る

一言でいうと

サーバーサイド適用は、オブジェクトのフィールドごとに持ち主を記録しておく仕組みです。その記録(managedFields)を読めて初めて、「なぜ自分の値が何度も元に戻るのか」と、Argo CDのmanagedFieldsManagers無視ルールを説明できます。

なぜこの記録が必要だったのか

以前のkubectl applyは、クライアントで計算していました。「自分が前回送ったマニフェスト」をkubectl.kubernetes.io/last-applied-configurationアノテーションにまるごと入れておき、新しいマニフェストと比べて、何を消すかを決めていました。この方式には、持ち主という概念がありません。そのため、2つのツールが同じオブジェクトを扱うと、互いのフィールドを静かに消します。Argo CDが適用した次の瞬間にオートスケーラーがreplicasを変え、次の適用でその値が消え、オートスケーラーがまた書くという具合に、果てしなく行き来します。誰が悪いのかもわかりません。

サーバーサイド適用は、この計算をAPIサーバーに移し、フィールドごとにどのマネージャーが所有するかを記録します。kubectl apply --server-side --field-manager=gitopsのgitopsが、その名前です。

どう動くのか

記録はmetadata.managedFieldsに入ります。ここで最初に引っかかる落とし穴が1つあります。kubectl get ... -o jsonは、このフィールドをデフォルトで隠します。--show-managed-fieldsを付けないと、常にnullになって、jqが落ちます。

kubectl -n demo get deploy web -o json --show-managed-fields   | jq -r '.metadata.managedFields[] | .manager + " | " + .operation'
gitops | Apply
kubectl | Update

自分が所有していないフィールドに値を書こうとすると、サーバーが拒否し、どのフィールドが誰のものかを一覧で教えてくれます。--force-conflictsを与えれば通りますが、このとき何が起きるかを正確に知っておく必要があります。衝突がなくなるのではなく、所有権が移ります。以前の持ち主はそのフィールドから外れ、次に以前の持ち主が自分の値をもう一度適用すると、今度はそちらが止められます。自動化2つが強制を習慣のように使うと、終わらない争いになります。

反対の方向も、直感とは違います。マニフェストからフィールドを除くと、そのマネージャーは所有を手放します。ところが、値は以前の持ち主の値には戻りません。誰も所有しないフィールドは、APIのデフォルトになります。replicasを5に奪っていったマネージャーがその行を消すと、2ではなく1になるのです。

サブリソースも別に記録されます。kubectl scaleは、オブジェクト全体ではなくscaleサブリソースに書くので、所有の記録にマネージャーkubectl、サブリソースscaleという項目が別にでき、衝突のメッセージもその事実を知らせてくれます。オートスケーラーが作る衝突が、まさにこの形です。

Argo CDとどうつながるか

Argo CDは、同期オプションServerSideApply=trueで、この方式を使えます。そして、無視ルールのmanagedFieldsManagersは、フィールドのパスではなく、この所有の記録で比較の対象を選びます。

resource.customizations.ignoreDifferences.apps_Deployment: |
  managedFieldsManagers:
  - kubectl
  jsonPointers:
  - /spec/replicas

「オートスケーラーが書いたフィールドは比較しない」を、パスを1つずつ書かずに表現できることが利点です。その代わり、限界があります。プレビューのコマンドは、パスのルールだけをレンダリングします。マネージャー名のルールが実際に何を除くかは、所有の記録を人が読んで判断する必要があります。

現場での姿

最もよくある事故は、「HPAとArgo CDが争う」ことです。リポジトリにreplicasが書かれていて、HPAもその値を書きます。所有の記録を見れば、答えがすぐに出ます。2つのマネージャーが同じフィールドを持っているのです。解決策は強制ではなく、リポジトリからreplicasの行を除くことです。ただし、上で見たとおり、除いた瞬間に値がAPIのデフォルトに落ちるので、HPAがもう一度計算するまで、Podが一時的に減ります。この事実を知らずに、運用時間中にやると事故になります。

2つ目は、移行中の混乱です。クライアントサイドで管理していたオブジェクトをサーバーサイドに移すと、last-applied-configurationアノテーションが残っていて、そのアノテーションが指すフィールドと新しい所有の記録が食い違い、予期しない削除が起きることがあります。そのため、移行はオブジェクト単位で、所有の記録を確認しながら行います。

このラボ環境の限界

ラボのPodには、Argo CDコントローラーがありません。そのため、ServerSideApply=trueの同期が実際に動く様子は見られません。その代わり、kwokが立ち上げるのは本物のkube-apiserverなので、所有の記録・衝突・強制・手放しは、本番とまったく同じように起き、Argo CD側のつながりは、無視ルールのレンダリングで確認します。

次のラボですること

マネージャーgitopsで適用して、所有の記録を読みます。2つ目のマネージャーhotfixで同じフィールドを書いて衝突を作り、強制で奪ったあとでもう一度手放して、値がどこへ行くかを見ます。クライアントサイド適用が残すアノテーションを比べ、kubectl scaleが作るサブリソースの衝突を起こします。最後に、所有の記録が入った実物のオブジェクトに、managedFieldsManagersルールを掛けてレンダリングしてみます。