ArgoCDでマルチテナントのマニフェストをモノレポ管理

公開:2020/10/18
更新:2020/10/18
5 min読了の目安(約4500字TECH技術記事

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で更新するという寸法です。

より高度になパイプラインでは、マニフェストリポジトリのイメージ更新は自動化されているかもしれません。

この構成でのリリース手順

この構成を取ると、次のようなプロダクションまでのリリースフローが形成されます。

  1. ソースコードリポジトリのCIで新たなイメージをリリースする
  2. マニフェストリポジトリのdevブランチを更新し、developmentクラスタのArgoCDに適用させる
  3. devブランチからstageブランチにPRを作成・マージし、stagingクラスタのArgoCDに適用させる
  4. stageブランチからprodブランチにPRを作成・マージし、productionクラスタのArgoCDに適用させる

デプロイステージを進める際の操作が直前のデプロイステージからのPR作成になるので、デプロイステージを進める際の変更漏れがなくなるのが良いところです。
また、デプロイステージを進めるための操作が単なるPR作成なので、オペレーションミスが起きづらいのも良いところです。

定番構成をマルチテナントで実践するとデプロイ順序の入れ替えが制御しづらい

上述のフローはデプロイステージを進める時、各PRの適用順序が入れ替わることはまず無いことを暗黙の前提としています。
シングルテナントではこの制約を自然に満たせることが多いでしょう。

しかし、マルチテナントではデプロイステージによって適用順序がよく入れ替わります。
devブランチにプッシュしたアプリケーションAをdevで動作確認している間に、
後発のアプリケーションBがトントン拍子でdevelopment→staging→productionとデプロイステージを進んでしまうことは頻繁にありえます。

定番ArgoCD構成で、この「アプリケーションBのデプロイステージをアプリケーションAを追い越してdevからstageに進める」という操作は具体的には次のようになります。

  1. devブランチのアプリケーションAのマニフェストを更新する
  2. devブランチのアプリケーションBのマニフェストを更新する
  3. 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だけになります。

  1. (必要なら) 開発者がマニフェストリポジトリのAのマニフェストを更新する。このPRでは Application A の targetRevision は変えないのでクラスタ上のリソースは変更されない。
  2. 開発者がAのソースコードリポジトリのリリース用ブランチ foo を更新する。
  3. CIによってAの新たなコンテナイメージが作成される。
  4. CIによってマニフェストリポジトリのmasterから新たなブランチが作成され、新イメージのタグがマニフェストリポジトリのimageに差し込まれる。(kustomize edit set image をCIから実行する)。前項の変更をコミットし、さらにそのコミットハッシュを targetRevision に書き込み、もう一度コミットする。
  5. CIで↑ブランチからPRを作成し、そのままmasterマージする。
  6. foo環境のArgoCDがアプリケーション Aを管理しているapp-of-appsの変更を検知し、自動同期する。
  7. foo環境のArgoCDがアプリケーションAの管理するマニフェストのdiffを検知し、自動同期する。

おわりに

もっといいやり方があったら教えて下さい。fluxcd/toolkitとかPipeCDを使うともっとエレガントに解決したりする?