ArgoCDでマルチテナントのマニフェストをモノレポ管理
ArgoCDで複数テナントのマニフェストをモノレポ管理する際のやっかいな点と、それの回避策を紹介します。
TL;DR
3行で以下のとおりです。
- テナント同士は独立したリリースを行いたいが、
- デプロイ用ブランチをウォッチするArgoCD運用ではそれが難しい。
- なので各 Application CR の targetReivison に個別のコミットハッシュを指定した。
前提
今回の前提となる事情や条件を説明します。
テナントとは
この記事のテナントとはk8sクラスタの直接的な利用者です。アプリケーション開発チームのことだと思ってください。
バージョン
使用するArgoCDバージョンは v1.7.x です。
3つのデプロイステージそれぞれにk8sクラスタ
デプロイステージは development/stagsing/production の3つです。
デプロイステージごとに1つずつk8sクラスタを用意し、それぞれに同じようなマニフェストを使ってアプリケーションをデプロイするイメージです。
kustomizeを使用し、base+overlays 構成でマニフェストを管理します。
マルチテナント
クラスタを複数のテナントが利用します。
テナントはArgoCD Application CRを作って各種リソースをデプロイします。
テナントのk8sマニフェストは基本的にテナントが管理することになっています。(もちろん基盤チームがサポートはする)
そしてこれが重要なことなのですが、テナントはそれぞれ独立したリリースサイクルを持っています。
テナント同士が平行にデプロイできるようにするのは必須条件です。
テナントのマニフェストはモノレポにまとめる
今回はすべてのテナントのマニフェストを単一のリポジトリ(=モノレポ)に集約します。
なぜモノレポにまとめたいのかというと、マニフェスト用リポジトリに配置するconftestポリシーの管理コストをテナントの数に比例させたくないからです。テナントの数だけマニフェスト用リポジトリが存在してしまうと、conftestポリシーの管理が面倒になってしまいます。
またマニフェストリポジトリが沢山あるとGitOpsのオペレーション履歴を時間軸で追うのが面倒になります。
これらの理由からマニフェストリポジトリはモノレポにまとめたいと考えました。
なにが困るのか
ここまででGitOpsで満たしたい要件を列挙しました。次は定番構成のArgoCD GitOpsについて紹介し、この構成をマルチテナントに持っていくとどう困るのかを説明します。
定番の構成
ArgoCD の GitOps 定番構成は、次のようなものになるかと思います。
dev
stage
prod
という3つのデプロイ用ブランチがある
マニフェストリポジトリには dev
stage
prod
という名前のデプロイ用ブランチを用意し、それらのブランチへのpushを起点にして、ArgoCDがk8sクラスタに変更を適用する、という仕組みが採用されることが多いです。
この場合 Application CR の targetRevision
にはデプロイ用ブランチの名前が指定されます。
ソースコードリポジトリのデプロイ用CIを起点にコンテナイメージが更新される
自社で開発するプログラムをGitOpsするときは、
コンテナイメージのビルドとプッシュはソースコードリポジトリの責務とするのがよくある構成です。
イメージが更新された後、マニフェストリポジトリを別のPRで更新するという寸法です。
より高度になパイプラインでは、マニフェストリポジトリのイメージ更新は自動化されているかもしれません。
この構成でのリリース手順
この構成を取ると、次のようなプロダクションまでのリリースフローが形成されます。
- ソースコードリポジトリのCIで新たなイメージをリリースする
- マニフェストリポジトリの
dev
ブランチを更新し、developmentクラスタのArgoCDに適用させる -
dev
ブランチからstage
ブランチにPRを作成・マージし、stagingクラスタのArgoCDに適用させる -
stage
ブランチからprod
ブランチにPRを作成・マージし、productionクラスタのArgoCDに適用させる
デプロイステージを進める際の操作が直前のデプロイステージからのPR作成になるので、デプロイステージを進める際の変更漏れがなくなるのが良いところです。
また、デプロイステージを進めるための操作が単なるPR作成なので、オペレーションミスが起きづらいのも良いところです。
定番構成をマルチテナントで実践するとデプロイ順序の入れ替えが制御しづらい
上述のフローはデプロイステージを進める時、各PRの適用順序が入れ替わることはまず無いことを暗黙の前提としています。
シングルテナントではこの制約を自然に満たせることが多いでしょう。
しかし、マルチテナントではデプロイステージによって適用順序がよく入れ替わります。
dev
ブランチにプッシュしたアプリケーションAをdevで動作確認している間に、
後発のアプリケーションBがトントン拍子でdevelopment→staging→productionとデプロイステージを進んでしまうことは頻繁にありえます。
定番ArgoCD構成で、この「アプリケーションBのデプロイステージをアプリケーションAを追い越してdev
からstage
に進める」という操作は具体的には次のようになります。
-
dev
ブランチのアプリケーションAのマニフェストを更新する -
dev
ブランチのアプリケーションBのマニフェストを更新する -
stage
ブランチのアプリケーションBのマニフェストを更新する。このときアプリケーションAはまだ更新しない
3番目の手順でアプリケーションAは更新してはいけないため、
dev
からstage
に進める操作が、単なるdev
からstage
へのPR作成では対応できなくなります。
その代わりデプロイブランチをそれぞれ独立に管理する必要がでてきます。
具体的にはアプリケーションのデプロイステージを進めるたびにcherry-pickを使うことになるでしょう。
以上の話をまとめると、マルチテナント環境ではアプリのデプロイ順序がステージごとに異なるので、定番構成のArgoCD GitOpsではデプロイステージをそれぞれ独立管理することになります。
この複雑さを許容できるなら、定番ArgoCD GitOps構成でマルチテナントモノレポを実践することができます。
しかしこの複雑さは適用漏れの可能性を上げてしまいます。
productionへのデプロイ操作でミスをすると影響が大きくなるので、難しい操作は行いたくありません。
結局自分は定番構成をマルチテナントで実施するのはのは厳しいと思ったので、
別のマルチテナント用モノレポマニフェスト管理を構成しました。
マルチテナントマニフェストリポジトリをモノレポで管理する構成
結局、現在は以下のような構成でマルチテナントArgoCDのマニフェストリポジトリをモノレポ管理しています。
targetRevision にコミットハッシュを指定する
Application CRの targetRevision に、ブランチ名ではなく特定のコミットハッシュを指定することにしました。
こうすると、各テナントのアプリケーションごとに独立したコミットを指定できるので、テナントがデプロイステージを独立に管理できるようになります。
またデプロイステージごとにブランチを分ける意味も無くなるので、master
(main
)一本ですべて管理できるようになります。
ネックになるのはマニフェストを更新するたびに targetRevision も更新する必要がある点ですが、この変更はCI内で自動実行することでテナントからは隠蔽しています。
アプリケーションのソースコードリポジトリにも targetRevision を自動変更するための CI ジョブを生やしたくなるので、その部分は CircleCI Orb/GitHub Actions として実装しています。
テナント用App Projectを用意する
これは多くのマルチテナントArgoCDで実施していることだと思いますがテナント用AppProjectを用意します。
またテナント用マニフェストリポジトリではApplication.spec.projectにそのAppProjectを指定しなければならないというconftestポリシーを書いています。
テナントのリリース操作
この構成ではテナントのアプリケーションAの、デプロイステージ foo
へのデプロイ処理の流れは以下のようになります。テナントの開発者が行う操作はマニフェストの更新と、最初のリリース用ブランチへのpushだけになります。
- (必要なら) 開発者がマニフェストリポジトリのAのマニフェストを更新する。このPRでは Application A の targetRevision は変えないのでクラスタ上のリソースは変更されない。
- 開発者がAのソースコードリポジトリのリリース用ブランチ
foo
を更新する。 - CIによってAの新たなコンテナイメージが作成される。
- CIによってマニフェストリポジトリのmasterから新たなブランチが作成され、新イメージのタグがマニフェストリポジトリのimageに差し込まれる。(
kustomize edit set image
をCIから実行する)。前項の変更をコミットし、さらにそのコミットハッシュを targetRevision に書き込み、もう一度コミットする。 - CIで↑ブランチからPRを作成し、そのままmasterマージする。
-
foo
環境のArgoCDがアプリケーション Aを管理しているapp-of-appsの変更を検知し、自動同期する。 -
foo
環境のArgoCDがアプリケーションAの管理するマニフェストのdiffを検知し、自動同期する。
おわりに
もっといいやり方があったら教えて下さい。fluxcd/toolkitとかPipeCDを使うともっとエレガントに解決したりする?
Discussion