こんにちは!株式会社DearOneの長谷川です。
DearOneのAI推進室でRAGの開発やRAGを使った社内システムの開発に従事しています。
この記事でわかること
- AI時代(マルチクラスタ・高頻度更新・LLMエージェント運用)におけるGitOpsの新たな要件
- Argo CD / Flux と比較した際の「Fleet」の構造的メリット(コスト・性能・機械可読性・セキュリティ)
- Fleet運用時に直面しやすい「WaitAppliedデッドロック」や「pollingInterval集中」などの罠と具体的な回避策
AI 時代に GitOps が背負う要件は変わった
大規模言語モデルの学習・推論、推論の地理分散、モデル差し替えの高頻度化、そして LLM エージェントによる運用自動化 — この数年で GitOps に求められる要件は静かに、しかしはっきりと変わりつつあると感じています。
要点は以下の4つです。
マルチクラスタが常態化する。GPU を積んだ推論ノードプールを複数リージョンに配置し、開発用・ステージング用・本番用の系統を分け、さらに顧客テナントごとに独立クラスタを切ることも珍しくありません。「1つのクラスタに Argo CD を入れて終わり」のモデルはもはや当てはまりにくくなっています。
ロールアウトの粒度と頻度が上がる。モデル重みの入れ替え、推論パラメータの調整、A/B テスト用の分岐配信。従来の「週次リリース」的なリズムを大幅に上回る頻度で、しかも大きな (数 GB〜数十 GB の) アーティファクトが飛び交うようになっています。
運用の主体にマシン(AI)が入ってくる。LLM エージェントによる自動化が現実になった今、GitOps の状態が「マシン可読で、境界が実在し、どこで詰まったかをマシンが診断できる」設計であることの価値が跳ね上がりました。人間しか読まない前提のダッシュボードやログでは足りません。
そして、トークンコストと権限。エージェントを運用に載せると、彼らが 1 回のタスクで消費する LLM トークンが直接コストになります。同時に、エージェントに Kubernetes cluster への書き込み権限をどこまで渡すかは、事故率と信頼性を大きく左右します。この 2 つは AI 時代に固有の、しかしまだ十分に語られていない要件です。
この 4 つの要件 — 多クラスタ、高頻度ロールアウト、機械可読、そしてトークン・権限 — を並べて、私は Standalone Fleet が現実的かつ非常に強力な選択肢の 1 つだと考えています。
なぜ Fleet か: 4 つの選定軸
Argo CD と Flux が現行の主要選択肢であることに異論はありません。しかし、AI 時代の要件を上記の 4 つに整理したとき、Fleet が構造的に有利な点がいくつかあります。
1. コスト: ランタイムフットプリントが軽い
Argo CD は Application Controller、Repo Server、API Server、Redis、Dex/Web UI で最小構成でも数コンテナが常時走ります。多クラスタ環境で ApplicationSet や Cluster Controller まで入れると、GitOps レイヤーだけで 1クラスタあたり数 GB のメモリを取ることも普通にあります。
Fleet は Fleet manager クラスタに fleet-controller と gitjob の 2 つの Deployment が主です。downstream には fleet-agent が 1 つだけです。管理対象クラスタ側の負荷が小さいのは、GPU ノードでコストを 1 コアでも節約したいときに効いてきます。エージェントは Kubernetes API と Fleet manager にしか話さないので、コンテナランタイムに追加サイドカーを差し込む必要もありません。
「GitOps ツールが軽い」ことは、AI ワークロードでは機能の 1 つとして数えられます。
2. 性能: controller 側でスロットリング可能
マルチクラスタ・高頻度なロールアウトを扱うとき、GitOps ツール側の処理能力そのものがボトルネックになります。Fleet は GITREPO_RECONCILER_WORKERS (goroutine 数) と GITREPO_SYNC_PERIOD (全体 safety net) で controller のスループットを明示的に制御でき、加えて GitRepo ごとに pollingInterval を YAML で設定できます。
これは後で詳しく書きますが、要は「manager 側の処理能力を GitRepo 側の要求と釣り合うようにチューニングできる」ということで、マルチクラスタでスケールさせるときに極めて重要な性質です。
3. マシン可読性: リソース境界が実在する
Fleet の GitOps パイプラインは 4 層で、それぞれが独立の Kubernetes リソースとして存在します。
git push
→ GitRepo.status.pollingCommit (Fleet manager 側)
→ Bundle.metadata.labels[commit] (Fleet manager 側)
→ BundleDeployment (per-cluster ns) (downstream cluster 側)
→ Pod (downstream cluster 側)
エージェントが「どこで詰まったか」を診断するとき、この分離は決定的に効きます。Argo CD が Application 1 個に畳み込むのと違い、Fleet では「manager 側は正しく Bundle を作った」「downstream 側で apply が詰まっている」を Kubernetes API 越しに単純な get で切り分けられます。
LLM エージェントに kubectl get gitrepo → kubectl get bundle → kubectl get bundledeployment の順で状態を見せれば、どのフェーズで何が起きているかを言語化できます。この「マシン可読な境界」は、AI 時代の運用では単なる好みではなく、実装コストを左右します。
4. AI 運用: トークンコストと権限分離
ここが、実運用の中で Fleet の特に大きなメリットだと実感したポイントです。
トークンコストの構造的な削減。エージェントに Kubernetes リソースを直接更新させる運用を想像してみてください。ConfigMap にコード (例えば Python スクリプトや Nginx の設定) を埋め込んでいる場合、たった 1 か所の修正でも、エージェントはまず ConfigMap 全体をコンテキストに読み込み、kubectl apply 用の YAML を再構成する必要があります。数 KB の ConfigMap でも、複数のロールアウトを重ねればトークン消費は積み上がります。
Fleet を挟むと、エージェントの仕事は「git のファイルを編集して commit する」ことだけになります。ファイル編集自体は行単位の str_replace 系ツールで済むので、ファイル全体をコンテキストに載せる必要はありません。Fleet の reconcile loop が、その差分を Kubernetes リソースに落とすところを完全に代行します。
具体例で言えば、ConfigMap 1 個の 1 行修正なら:
-
直接 apply する場合: エージェントが (1)
kubectl get configmap -o yamlで全体を取得、(2) 全体をコンテキストで保持して差分を作る、(3)kubectl apply用の YAML を再構成、(4) apply する。数千トークンが吹き飛ぶ。 -
Fleet 経由の場合: エージェントが (1) 対象行を
str_replaceで置換、(2) git commit + push する。数百トークン。Fleet が残りをやる。
このコスト差は、対象ファイルの規模が大きいほど、またロールアウト頻度が高いほどはっきりします。もちろん実際の削減率はプロンプト設計や差分ツールの実装に依存しますが、ConfigMap にモデル推論のプロンプトテンプレートや大きな設定を書いている環境での独自検証では、その差が 10 倍〜100 倍のオーダーになるケースもありました。GitOps の伝統的な利点である「宣言的」「Git を真実源」というのは、実は AI 運用時代においては エージェントのトークン経済性を守る構造 でもある、というのが実装してみての実感です。
権限の最小化。もう 1 つ、これと表裏の重要な利点があります。Fleet がデプロイを代行してくれるということは、エージェント自身は Kubernetes cluster への書き込み権限を持つ必要がない、ということです。
エージェントに与える cluster RBAC は view ClusterRole 程度で済みます。診断のための get / list / logs はできますが、create / patch / delete はできません。書き込みの経路は完全に「Git commit → Fleet controller (専用の高権限 ServiceAccount で reconcile)」に限定されます。
これがどれだけ効くかは、AI エージェントを本番運用に載せたことがある人なら分かってもらえると思います。プロンプトインジェクションで暴走したエージェントが、間違った namespace の Pod を消したり、CustomResource を誤って改変したりする事故 — この blast radius が構造的に 0 になります。エージェントは Git という「レビュー可能な、rollback 可能な、監査可能な」ゲートを必ず通ることになるので、事故の下限が「悪い commit が git log に残る」ところで止まります。
「エージェントに権限を渡さずに、エージェントで運用する」— これは AI 時代の運用アーキテクチャの中心的な問いの 1 つですが、Fleet はこの問いに構造的な答えを持っています。
効いた設計判断 3 つ
1. self-managed-fleet: ブートストラップの再帰を GitOps に載せる
Fleet の一番きれいなイディオムは、Fleet manager 自身も Fleet で管理することです。
bootstrap/GitRepos/self-managed-fleet.yaml 1 個だけを手動 apply の対象にして、これが managed/ 配下を pull しに行くように仕込みます。そこに全 GitRepo CR や、Fleet UI / oauth2-proxy 等の Deployment を全部入れておきます。
bootstrap/ ← 初回だけ手動 kubectl apply
GitRepos/self-managed-fleet.yaml
Deployments/fleet-controller
Deployments/gitjob
...
managed/ ← self-managed-fleet が reconcile
GitRepos/(全プロジェクトの GitRepo CR)
Deployments/fleet-ui
...
これで「新規 GitRepo を追加する = git commit する」だけになります。kubectl apply で直接 CR を作るのは禁じ手 (= GitOps 違反) にできます。CR の source of truth が Git に一元化されるので、監査は git log で完結します。
エージェント運用の観点でこれが重要なのは、「エージェントが GitOps 対象を変更するときの経路が git commit ただ 1 つに固定される」ことです。前節で書いた「エージェントは read-only cluster RBAC で済む」という設計が、bootstrap レイヤーまでほころびなく貫徹されます。
2. Fleet を「唯一の真実源」として扱う
マルチクラスタ環境の運用ツール (ダッシュボード、CLI、内製の運用台帳) を作るとき、最初はモジュールと bundle の対応表を手書きで持ちたくなります。「このアプリはこの bundle 経由で配信されている」というのを、コード側で列挙しておくやり方です。
これは、Fleet の実態と乖離します。GitRepo の spec.paths を変えれば bundle は付け替わるし、新規アプリを追加すれば bundle も増えます。手書きの対応表を追随させ続けるコストは、規模に対して二乗で効いてきます。
答えは単純で、「Fleet こそが正しい」と決めて、他のメタデータはそこから引くことです。GitRepo の spec.paths とモジュールのパス prefix を前方一致照合すれば、「今どの bundle がどこに属しているか」は動的に導出できます。
# 導出ロジックの本質はこれだけ
for gitrepo in gitrepos:
for path in gitrepo.spec.paths:
module = longest_prefix_match(path, all_module_paths)
module.bundles.append(gitrepo.name)
Fleet 自体がこの設計を可能にしているのがポイントで、Bundle CR に commit label があり、GitRepo CR に spec.paths と targets.clusterSelector が declaratively 揃っています。これらを問い合わせるだけで「今何がどこに配信されているか」の全景を再構築できます。Fleet は Kubernetes API 越しに読めるデータモデルとしてよく設計されていて、この点はエージェント運用の観点でもコスト観点でも効きます。派生ツールに独自のインデックスを持たせずに済むからです。
3. pollingInterval を段階化する — 性能の急所
これは効いたというより、痛い目を見てから学んだ話です。同時に、性能設計を語るうえで一番実践的な例でもあります。
単純に GitRepo を作っていくと、みんな pollingInterval: 1m0s になってしまいます。ある日、数十個の GitRepo が 1 分ごとに同時発火し、gitjob controller が並列で Job を走らせすぎる状態が発生。monorepo の shallow clone に数分かかると、前回 Job の完了前に次の polling が入り、Deleting previous job to avoid conflicts のログとともに Delete/Create loop に陥ってしまいます。新 commit から反映まで数分〜10 分の遅延、しばしば完全にストールします。
対策は単純で、用途別に interval を階層化することです。開発中で即反映が要るものは 1m、通常運用は 5m、静穏な低頻度モジュールは 15m。実質停止扱いの 87600h (= 10 年、事実上手動同期) すら使います。
# 開発中で即反映したいもの
pollingInterval: 1m0s
# 通常運用
pollingInterval: 5m0s
# 低頻度で十分なもの
pollingInterval: 15m0s
同時発火数が数分の一に落ちるだけで、並列 Job 数はほぼ 0 で常時静穏になります。
これは Fleet が「性能設計を運用者に開いている」ことの現れでもあります。GitOps ツールの中には、内部の polling 挙動をブラックボックスにしていて、規模が大きくなったときに手が出せないものがあります。Fleet はデフォルトが素朴なため、最初はハマるが、YAML 1 行で直すことができます。
Fleet manager の内部処理量は有限 (GITREPO_RECONCILER_WORKERS の goroutine 数で決まる) なので、GitRepo が増えるほど polling interval の設計は必須になります。ここに気付かず「Fleet は遅い」と思って捨ててしまうケースは多いはずで、Fleet の名誉のために書いておきます。
踏み抜いた罠を 2 つ
WaitApplied デッドロック
GitRepo の spec.repo や spec.paths を変更すると、Bundle 名が変わります。すると新しい Helm release が既存リソースを adopt しようとして失敗します:
ServiceAccount "X" exists and cannot be imported into the current release:
invalid ownership metadata
厄介なのは、この失敗によって self-managed-fleet Bundle 自体が WaitApplied で stuck することです。「takeOwnership を有効にする新しい fleet.yaml」を後から git に push しても、self-managed-fleet が動いてないので届きません。まさに鶏と卵のデッドロック状態に陥ってしまいます。
予防策としてはシンプルで、GitRepo の repo/paths を変更する 前に、対象 fleet.yaml を先に整えておくことです:
helm:
takeOwnership: true
atomic: true
これで新 Helm release は既存リソースの meta.helm.sh/release-name annotation を書き換えて強制 adopt します。失敗時は rollback で安全です。この予防策を効かせるだけで、大規模なリポジトリ再編でも事故なく進められます。
一度 stuck した場合の脱出手順は、対象 GitRepo CR を kubectl delete で直接消します (keepResources: true のおかげで配下リソースは残る)。これは GitOps 原則の例外運用なので、承認プロセスを通して実施します。
forceSyncGeneration bump という緊急ハンドル
Bundle の反映が数分で終わるはずが 10 分待っても来ない、というときがあります。GitRepo の pollingCommit は最新なのに、BundleDeployment の spec.deploymentID が古いままなのです。
このとき Fleet manager 側で以下を叩くと、パイプラインを強制的に進められます:
kubectl patch gitrepo <name> -n fleet-default --type=merge \
-p "{\"spec\":{\"forceSyncGeneration\":$(date +%s%N)}}"
epoch nanosecond を毎回突っ込むイディオムは Fleet の中では非常手段(プラクティス)としてよく知られているアプローチです。ただしこれは症状対処で、原因は前述の pollingInterval 集中や、ネットワーク瞬断による git clone 失敗であることが多いです。安易に頼らず、Bundle の status.conditions[].message を必ず読んで根本原因を見に行くことが大切です。
エージェントに委ねる場合、「forceSyncGeneration bump を打つ前に必ず conditions を読ませる」ようにプロンプトを設計するのがよいです。緊急ハンドルの存在は救いですが、それを日常的に頼ると状態が読めなくなるからです。
選択肢としての Fleet — Argo CD / Flux と並べて
正直に書いておくと、Fleet はすべてのユースケースで最良ではありません。UI 面の完成度は Argo CD に劣るし、Flux ほど CNCF エコシステムの中心にいるわけでもありません。単一クラスタの GitOps なら、Argo CD の Application モデルのシンプルさは大きな利点です。
一方で、以下の要件が揃うとき、Fleet は明確に良質な選択肢になります。
- クラスタ数が多い (5 以上)。特に地理分散した推論クラスタや、テナントごとに切られたクラスタを扱う場合
- GitOps レイヤーのランタイムコストを抑えたい。GPU クラスタでは 1 コア、1 GB が惜しい
- エージェント / 自動化ツールから GitOps 状態を機械的に扱いたい。境界の実在は診断コストを大きく下げる
- エージェントに Kubernetes への書き込み権限を渡したくない。Fleet の代行モデルは、AI 運用時代のセキュリティ設計に直接効く
- エージェントのトークン消費を抑えたい。ConfigMap 内のコードや設定を、AI に全体保持させずに差分編集で回したい
- Helm と生 manifest を混在させたまま漸進的に整理したい。takeOwnership の Kubernetes ネイティブな書き方は他ツールに真似しにくい
- Fleet manager 自体を GitOps 対象にしたい。self-hosting の素直さは Fleet の際立った特徴
これらは AI 時代のインフラでは頻出する要件だと思います。「Argo CD が主流だから Argo CD」で決めるのではなく、要件と Fleet の設計とを突き合わせて選ぶ価値があります。
おわりに
Fleet は「地味だが素直な」ツールで、CNCF での知名度は Argo CD や Flux に劣るかもしれません。しかし、AI 時代の GitOps 要件 — 多クラスタ、高頻度ロールアウト、機械可読、低ランタイムコスト、そしてエージェントのトークン・権限最小化 — を並べたとき、Fleet の設計はこれらに構造的に噛み合っています。
特にトークンコストと権限分離の観点は、GitOps ツールを選定するときにまだ十分議論されていない切り口だと思います。しかしエージェント運用を本気で回そうとすると、この 2 つは必ずぶつかる壁になってきます。「エージェントに ConfigMap を直接触らせない」「エージェントに cluster への書き込み権限を渡さない」— これらを構造で実現できるのは、Git を真実源とし、controller が代行する GitOps モデルの本質的な力です。
この記事に書いた設計判断や罠は、いずれも実際に痛い目を見てから覚えたものです。GitOps ツールをこれから選ぶ人、あるいは Argo CD からの乗り換えを検討している人にとって、Fleet を「コストと性能を考慮した良質な選択肢」の 1 つとして真面目に俎上に乗せる参考になれば嬉しいです。
