OpenGitOpsの四原則 — なぜpushではなくpullなのか
一言でいうと
GitOpsはデプロイツールの名前ではなく、運用モデルの名前です。OpenGitOpsが定義した4つの原則(宣言的(Declarative)、バージョン管理され不変(Versioned and Immutable)、自動的にプルされる(Pulled Automatically)、継続的に調整される(Continuously Reconciled))をすべて満たして初めてGitOpsです。4つのうち1つでも欠ければ、それは単なる「GitにYAMLを置くCDパイプライン」です。
なぜ必要なのか
10年前のデプロイは、たいていこんな形でした。CIサーバーがビルドを終えると、そのCIサーバーがkubectl applyを実行します。これをpushモデルと呼びます。見た目は単純ですが、3つの点が崩れます。
1つ目は、クラスターの鍵がCIの中にあることです。JenkinsやGitHub ActionsのランナーがkubeconfigまたはServiceAccountトークンを持っていなければなりません。ランナーは外部から来たコードを実行するマシンです。フォークから上がってきたPRのワークフロー1つが本番デプロイの権限を手に入れる経路は、ここから生まれます。
2つ目は、「今クラスターに何が動いているのか」に答えがないことです。パイプラインのログをさかのぼる必要があり、しかもそのログに、誰かが手で直した内容は記録されていません。
3つ目は、パイプラインが止まると状態も止まることです。pushはイベントがあるときだけ動きます。何のイベントもない午前3時に誰かがreplicasを手で書き換えても、誰も気づきません。
pullモデルはこの3つをまとめてひっくり返します。クラスターの中で動くエージェント(Argo CD、Flux)がGitを読みに行きます。CIにはクラスターの認証情報を1つも渡さずに済みます。CIの仕事は「イメージをビルドしてレジストリにプッシュし、設定リポジトリにタグ1行をコミットする」ところまでです。
どう動くのか
4つの原則を1つずつ分解すると、次のようになります。
| 原則 | 意味 | 破ったときに起きること |
|---|---|---|
| 宣言的 | 望ましい最終状態を記述します。そこへ至る手順ではありません | 同じスクリプトを2回実行すると結果が変わります |
| バージョン管理され不変 | すべての状態がコミットとして残り、いったん作られたリビジョンは書き換えません | 「いつからこうなっていたのか」に答えられません |
| 自動的にプルされる | 承認された変更をエージェントが自分で取りに行きます | 人がデプロイを忘れると、Gitと現実が分かれます |
| 継続的に調整される | イベントとは無関係に、絶えず比較して収束させます | 手で直した変更がいつまでも生き残ります |
3つ目の原則の原文が「Pushed Automatically」ではなくPulled Automaticallyである点は、試験でよく出ます。そして4つ目の「Continuously」は「コミットのたびに」ではなく、コミットがなくても続けてという意味です。Argo CDはデフォルトで180秒ごとにこのループを回します。
「GitがSSOT(Single Source of Truth)」という言葉もよく誤解されます。これは「Gitにすべてのファイルがある」という意味ではなく、「クラスターとGitが違うなら、正しいのはGit」という宣言です。向きが決まっていることが核心です。ドリフトを見つけたときにGitをクラスターに合わせて書き直すのはGitOpsではなく、単なる事後ドキュメント化です。
宣言型が常に勝つわけでもありません。順序が本質である作業(データベースのスキーママイグレーション、一回限りのデータ補正、証明書の初回発行)は「最終状態」では表現できません。そのためArgo CDは、フック(PreSync/PostSync)という命令型の逃げ道を別に用意しています。宣言型が既定で、命令型は例外として明示するもの、と理解すれば正確です。
現場での姿
筆者の7ノードのホームラボは、このモデルをそのまま使っています。Giteaが10.0.0.200、Argo CDが10.0.0.201、Harborが10.0.0.202、Grafanaが10.0.0.203で、MetalLBのL2プール10.0.0.200-215から割り当てられたアドレスです。ネットワークはkube-proxyなしでCilium 1.20のeBPFが処理し、その上にkube-prometheus-stackとCloudNativePGが載っています。このスタック全体が、クラスターの外から押し込まれたものではなく、内側から引き寄せられた結果です。
ここで、宣言型の強みと限界を同時に見せる出来事が1つありました。このクラスターのcontrolPlaneEndpointは、VIPやDNSではなく、最初のコントロールプレーンの物理IP10.0.0.120で固定されています。のちにコントロールプレーンを3台に増やしてetcdメンバーを3つそろえたのに、その最初のノードが落ちると、kubectlも7台のkubeletもすべて接続できなくなります。apiserver証明書のSANに他のノードのIPがなく、TLS検証の段階で失敗するからです。
これがGitOpsとどう関係するのかというと、宣言ファイルに書かれていない値は管理されないことを示しているからです。controlPlaneEndpointはクラスターを作った瞬間に一度決まり、その後はGitにもなく、調整の対象でもありません。だから変更するのが地獄になります。GitOpsで管理できる領域と、クラスターのブートストラップのようにその外にある領域を見分ける感覚のほうが、実務ではずっと重要です。
続けて読むこと
すぐ次の理論レッスンで、desired state、drift、reconciliation、convergenceといった試験用語を正確な定義にそろえます。そのあとのモジュールでは、/root/cgoa-repo/の下に実際のGitOpsリポジトリの骨組みを作り、kubectl kustomizeで環境ごとのレンダリング結果がどう分かれるかを目で確認します。