従来のパイプラインでは、CI がコードをビルドし、その結果を kubectl apply や helm upgrade でクラスターにプッシュします。これでも動きますが、パイプラインはすべてのクラスターの管理者権限を必要とし、クラスターに問い合わせなければ何が動いているのか誰も確実には言えず、手で編集されたデプロイはリポジトリの内容から黙ってずれていきます。
GitOps はこの向きを逆にします。望ましい状態を Git が保持し、クラスター内のエージェントがそれを継続的に Pull して、クラスターをその状態に合わせます。この記事では、原則、多くのチームが使う 2 つのリポジトリによるワークフロー、昇格とロールバックの仕組みを説明し、全体の流れを図にします。
4 つの原則
CNCF の OpenGitOps プロジェクト は、GitOps を 4 つの原則で定義しています。
- 宣言的: システムの望ましい状態を宣言的に表現します。
- バージョン管理され不変: その状態は、完全で不変なバージョン履歴が残る方法で保存されます。実際には Git のことです。
- 自動的に Pull される: ソフトウェアのエージェントが、望ましい状態をソースから Pull します。
- 継続的に調整される: エージェントは実際の状態と望ましい状態を比較し続け、差分を修正します。
3 つ目の点が、GitOps と「Git を読むパイプラインからデプロイする」ことを分けています。クラスターが Pull するのであって、外部の何かがクラスターにプッシュするのではありません。
2 つのリポジトリ:コードとコンフィグ
多くのチームは 2 種類の変更を分けています。
- アプリのリポジトリ にはソースコードを置きます。CI がそれをビルドし、テストを実行し、スキャンして、不変のイメージタグをレジストリにプッシュします。
- コンフィグのリポジトリ には、環境ごとの Kubernetes マニフェスト、Helm の values、Kustomize のオーバーレイを置きます。イメージタグも含め、何をどこで動かすべきかを記述します。
両者をつなぐのは、小さく明示的な変更です。CI かイメージ自動化ツールが、ステージングのイメージタグを上げるプルリクエストをコンフィグリポジトリに作成します。このプルリクエストがデプロイの申請であり、レビューと監査証跡が残ります。
クラスター内のコントローラー
Argo CD と Flux は、広く使われている 2 つの GitOps コントローラーです。どちらもクラスター内で動き、コンフィグリポジトリをポーリングするか Webhook を受け取り、マニフェストをレンダリングして稼働中のオブジェクトと比較し、差分を適用します。
調整(リコンシリエーション)の設定で、システムの厳しさが決まります。
- 自動同期: 誰かがボタンを押さなくても、新しいコミットを適用します。
- プルーニング: Git から削除されたオブジェクトを削除します。
- セルフヒール: クラスターで直接行われた変更を元に戻します。Argo CD の 自動同期のオプション では、セルフヒールは自動同期とは別のスイッチです。
Git を本当の信頼できる情報源にするのが、セルフヒールの設定です。これがないと、午前 3 時の kubectl edit が恒久的で、しかも目に見えない変更になります。
昇格とロールバック
環境は、コンフィグリポジトリ内のフォルダー、オーバーレイ、ファイルです。昇格とは、あるバージョンを 1 つの環境から次の環境へコピーすることです。
- プルリクエストでステージングのタグを上げます。マージから数分以内にステージングが更新されます。
- ステージングに対してテストとチェックを実行します。
- 2 つ目のプルリクエストで本番を同じタグに上げます。それをマージすることが本番へのデプロイです。
ロールバックは、そのマージの git revert です。コントローラーは古いタグを検知して適用します。すべての履歴が Git にあるので、「先週火曜日の 14:00 に本番で何が動いていたか」という問いに正確に答えられます。
誰がプルリクエストを作成するか
新しいイメージをコンフィグリポジトリにつなぐ一般的な方法は 2 つあります。
- CI が作成する。 パイプラインの最後のステップでコンフィグリポジトリをクローンし、1 行を変更してブランチにコミットし、プルリクエストを作成します。明示的でデバッグしやすく、パイプラインに必要なのはその 1 つのリポジトリ用のトークンだけです。
- イメージ自動化ツールがコミットする。 Flux のイメージ自動化コントローラーや Argo CD Image Updater プロジェクトは、レジストリを監視し、ポリシーに合う新しいタグを選んで Git に書き戻せます。常に最新のビルドを動かすべきステージング環境に便利です。
多くのチームは両方を組み合わせます。自動化でステージングを最新に保ちつつ、本番の変更は必ず、人かリリースジョブによるレビュー済みのプルリクエストとして届くようにします。どちらを選ぶにしても図に書いておきましょう。「新しいバージョンはどうやってここに来るのか」は、GitOps の構成について誰もが最初に尋ねる質問だからです。
1 つのリリースを最初から最後まで
shop サービスのバージョン 1.8.0 を、コミットから本番まで追ってみます。
- 開発者がアプリのリポジトリに変更をマージします。CI がテストを実行し、イメージをビルドしてスキャンし、
shop:1.8.0をレジストリにプッシュします。 - CI が、ステージングのタグを 1.7.3 から 1.8.0 に変えるプルリクエストをコンフィグリポジトリに作成します。レビュアーが承認してマージします。
- 1〜2 分以内にコントローラーが新しいコミットに気づき、ステージングのマニフェストをレンダリングしてデプロイを更新します。Kubernetes が新しい Pod をロールアウトし、Pod はレジストリからイメージを Pull します。
- ステージングでスモークテストが通ります。リリース担当者が、本番を 1.8.0 にする 2 つ目のプルリクエストを作成します。
- そのマージの後、本番も同じようにロールアウトされます。エラー率が上がれば、マージコミットを revert すると 1.7.3 に戻ります。
クラスターの外にある何かがクラスターの認証情報を必要とした場面は一度もなく、どのステップにもコミットが残りました。
シークレット
平文のシークレットは Git に入れられません。一般的な方法は次のとおりです。
- Git で暗号化する: Sealed Secrets や SOPS などのツールを使い、クラスター内でだけ復号します。
- Git には参照を置き、値は別の場所に置く: シークレットマネージャーから読み取る External Secrets のようなオペレーターを使います。
どちらの方法でも、コンフィグリポジトリはレビュー可能なままで、シークレットの値が差分に現れることはありません。
アーキテクチャ図
ダイアグラムの内容
- Developer
- App repo: code
- CI: build, test, scan
- Container registry
- Config repo: desired state
- Kubernetes cluster
- GitOps controller
- staging
- shop v1.8.0
- production
- shop v1.7.3
- push
- trigger
- image 1.8.0
- PR: bump staging tag
- pull every few minutes
- apply
- nodes pull images
図の読み方は次のとおりです。
- 上段、ビルド: 開発者がアプリのリポジトリにプッシュし、CI がイメージ 1.8.0 をビルド、テストしてレジストリにプッシュします。
- 中段: CI が、コンフィグリポジトリのステージング用タグを上げるプルリクエストを作成します。CI がデプロイに関わるのはここだけです。
- 下段、クラスター: GitOps コントローラーがコンフィグリポジトリを Pull し(破線)、ステージングの名前空間に適用します。ステージングは 1.8.0 で動き、本番は自身のプルリクエストがマージされるまで 1.7.3 のままです。ノードはレジストリからイメージを Pull します(点線)。
- Markdown ブロック にはワークフローのルールを書いています。プルリクエストによる昇格、revert によるロールバック、セルフヒールによるずれの修正です。
チームにとって何が変わるか
- CI がクラスターの認証情報を持たなくなる。 外へ取りに行くのはクラスターのほうで、パイプラインに必要なのはリポジトリへの書き込み権限だけです。
- レビューがデプロイにも及ぶ。 誰が何を、いつ、なぜデプロイしたかが、プルリクエストの履歴に残ります。
- ずれが見えるようになる。 コントローラーは、同期していないリソースを隠さずに表示します。
- デバッグの場所が変わる。 「なぜこれがデプロイされなかったのか」の答えは、コントローラーの同期ステータスとイベントにあります。マージする人たちから見えるようにしておきましょう。
GitOps の図に描くべきもの
- 両方のリポジトリ: それらをつなぐプルリクエストも描きます。
- Pull の向き: パイプラインからクラスターへではなく、コントローラーからコンフィグリポジトリへの向きです。
- 各環境: 動いているバージョンと一緒に描き、昇格が見えるようにします。
- 同期の設定: 自動同期、プルーニング、セルフヒール。
ロールアウト自体をカナリアやブルーグリーンにする方法は デプロイ戦略の図 で、ビルドの各段階の詳細は CI/CD パイプラインの図 で、クラスターそのものは Kubernetes のアーキテクチャ図 で、コンフィグリポジトリのマニフェストを図にする方法は Kubernetes の YAML から図を作る記事 で紹介しています。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- C4 モデルパックから 人物 を、DevOps パックから リポジトリ ノードを 2 つと パイプライン を、Docker パックから レジストリ を追加します。
- 基本パックからクラスター用に フレーム を追加し、その中に GitOps コントローラー用の コンテナ を置きます。
- Kubernetes パックから Namespace ノードを 2 つ追加し、それぞれの中に、動いているバージョンをラベルに書いた Deployment を置きます。
- コントローラーからコンフィグリポジトリへの Pull を破線のエッジで描き、ワークフローのルールを書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 12 個で、Free プランの範囲に収まります。
よくある質問
GitOps と CI/CD の違いは何ですか? CI/CD はソフトウェアをビルドし、テストし、届けます。GitOps はその届ける部分のやり方の 1 つです。望ましい状態を Git に置き、パイプラインが変更をプッシュする代わりに、対象環境のエージェントがそれを Pull して調整します。
Argo CD と Flux のどちらを使うべきですか? どちらも同じモデルを実装しています。Argo CD には充実した Web UI とアプリケーション中心のビューがあります。Flux は組み合わせて使えるコントローラーの集まりで、多くのチームが Git と CLI だけで運用しています。
アプリとコンフィグのリポジトリを分ける必要はありますか? よくある慣習ですが、ルールではありません。リポジトリを分けると、デプロイの履歴がきれいに保たれ、コードを再ビルドせずに設定を変更できます。
GitOps ではどうやってロールバックしますか? コンフィグリポジトリでバージョンを変更したコミットを revert します。コントローラーが以前の状態を適用します。
GitOps ではシークレットをどう扱いますか? Git に暗号化して保存するか、Git には参照を置き、クラスター内のオペレーターにシークレットマネージャーから値を取得させます。