※こちらはバイセルテクノロジーズ Advent Calendar 2023の11日目の記事です。
前回の記事は、神保さんのフロントエンドの新規開発でNext.jsの採用を見送った話でした。
はじめに
こんにちは、福田です。
バイセルでは買取管理システムGYROというサービスを開発・運用しています。 GYROはマイクロサービスで構築されているのですが、モノレポを導入する事によって開発効率の課題を解決できました。
この記事では、モノレポ化に至るまでの過程、変更の内容、そしてその効果や考察について紹介します。
前提(一般的な話)
モノレポ(Monorepo)とは
Monorepo(モノレポ)とは、アプリケーションやマイクロサービスの全コードを単一のモノリシックなリポジトリ (普通は Git) に保存するパターンを指します。
マイクロサービスアーキテクチャにおいて、Git管理の方法は2つ考えられます。それは、各サービスごとにリポジトリを作成するポリレポ(Polyrepo)方式と、1つのリポジトリに複数のサービスを入れるモノレポ(Monorepo)方式です。
GYROではリリース時からポリレポが採用されていたのですが、モノレポに変更しました。この変更は、コード管理とそれに付随するCI/CDの戦略に関するものです。マイクロサービスのアーキテクチャ自身には変更を加えません。
対象となるサービス(GYRO)について
GYROのマイクロサービスアーキテクチャは、GCP上のGKEを中心に構築されています。
- バックエンドはRailsで構築され、gRPCサーバーはbigcommerce/grufを使用
- BFF × 2 + バックエンドサービス × 7
- フロントエンドはVue.jsとiOSアプリ
- BEとFEはOpenAPIで定義されたREST APIを介して連携
詳細な構成は、本ブログの関連記事で確認できます。
- メインシステムのアーキテクチャ紹介 (GCPで、GKEを中心にシステムを構築しました) - バイセル Tech Blog
- 依存関係地獄におさらば〜マイクロサービスのバージョン管理について〜 - バイセル Tech Blog
上記を1チームで運用・保守しており、対象となるリポジトリ数は20ほどになっています。
課題とモノレポ化による解決
変更前には、モノレポ化を検討するきっかけとなるいくつかの課題がありました。
- 開発効率の低下
- サービス間の通信を定義しているgRPC定義の変更等、多くのリポジトリに修正を適用する必要がある場面がある
- 1つのPull Requestを作成するのに10分だとしても、10リポジトリを対象にすると100分かかる
- サービス間の通信を定義しているgRPC定義の変更等、多くのリポジトリに修正を適用する必要がある場面がある
- 対象のリポジトリ数が多いためPull Requestを出すリポジトリが漏れてしまう
- 漏れはレビューでも気がつくのが難しい
- 依存があるリポジトリ同士でPull Requestのmerge順に制約が発生し、開発フローが複雑になっている
- Pull Requestの数がリポジトリ数に依存するため、Pull Requestベースでの計測ができない
- バイセルは開発生産性を高める活動に力を入れており、Pull Request数を指標の1つとしている
- 1日に10Pull Request出したとしても、1つの同じ変更を10のリポジトリに対して行っただけの可能性
- Pull Requestの数が多い=開発生産性が高いという計測ができない
- Git submodule(リポジトリの依存関係を設定しています)が難しい
- submoduleを参照している側のhashを変更しても、submoduleで管理しているコードは切り替わらないため、ローカルでコードの最新化を忘れてしまう事がある
- submoduleの参照がconflictした時にhashしか表示されないため、conflictの解消が難しい
モノレポ化のデメリット
上記の課題については、リポジトリが複数にまたがっている事による弊害です。モノレポ化によって1リポジトリにまとまれば解決されます。
もちろん銀の弾丸ではなくデメリットもありますが、GYROチームでは問題にならないと考えました。
過去のコミット履歴やPull Requestが追いにくくなる
後述のsubtreeコマンドを使う事でコミット履歴の移行は可能ですが、GitHubで管理しているPull Requestは移行できません。そのため、過去のコミット履歴や関連するPull Requestを追いにくくなります。
Pull Requestを見る時は、コミット履歴に含まれるPull Requestの番号から追うか、モノレポ化前のリポジトリから探す必要があります。
こちらは不便ではあるものの、メリットが上回ると判断しました。
リポジトリごとのアクセス権限設定ができない
リポジトリごとに、細かくアクセス権の設定をしている場合、モノレポとして1つにまとめてしまうと同様の設定はできなくなります。
GYROチームでは1チームで運用しており、細かい制御は不要なため問題ではありませんでした。
Pull Requestが1リポジトリに大量にあふれる可能性がある
モノレポのリポジトリに全ての修正が集まるため、Pull Requestが多すぎて管理が難しくなってしまう可能性があります。
GYROチームの規模では問題無いと判断しました。
Gitリポジトリのサイズが大きくなる
1つの巨大なGitリポジトリを作成する事により、Gitのパフォーマンス低下が起きる可能性があります。
GYROチームでは実際にモノレポを作成した結果、特に影響は感じられませんでした。
モノレポ移行の手順
具体的なモノレポ移行の検討内容、手順内容をご紹介します。
1. モノレポ化対象のリポジトリの決定
今回は、BE側のサービスと関連リポジトリをターゲットにする事にしました。
Vue.jsやiOSアプリは、デプロイ方法が大きく異なる事、担当者がBEと分かれる事が多いため、モノレポ化によるメリットが小さいと考えたためです。
具体的には、
- BFF × 2
- バックエンドサービス × 7
- OpenAPIの定義
- BFFからGitのsubmoduleで参照
- gRPCの定義
- BFFとバックエンドサービスからGitのsubmoduleで参照
- 共通ライブラリ(自作gem)
の12リポジトリを対象にしました。
2. Gitの移行
移行ツールとして、Git subtreeを選択しました。このツールはGitコマンドに含まれているため安心感があった事と、継続して取り込み(モノレポへの移行期間中に、移行前リポジトリの変更内容を継続して再取り込みするフローを想定)ができる事が決め手となりました。
移行作業
以下のコマンドで移行ができました。予めモノレポ用のリポジトリは作成済とします。
# subtree化するリポジトリの登録 $ git remote add polyrepo1 https://github.com/buysell-technologies/polyrepo1 # subtree化(ディレクトリを切って、そこに登録 $ git subtree add --Pull Requestefix=polyrepo1 polyrepo1 master remote: Enumerating objects: 16076, done. remote: Counting objects: 100% (1891/1891), done. remote: ComPull Requestessing objects: 100% (343/343), done. remote: Total 16076 (delta 1630), reused 1710 (delta 1546), pack-reused 14185 Receiving objects: 100% (16076/16076), 12.31 MiB | 6.05 MiB/s, done. Resolving deltas: 100% (9752/9752), done. From github:buysell-technologies/polyrepo1 * branch master -> FETCH_HEAD * [new branch] master -> gyro_doc/master Added dir 'polyrepo1' # 確認 $ tree . ├── README.md └── polyrepo1 ├── Dockerfile ├── Makefile ├── README.md ├── cloudbuild.yaml ├── cloudbuild_endpoints.yaml ├── cloudbuild_mock_builder.yaml ├── openapi-generator-config.json ├── openapi.yaml ├── openapi_ext.yaml 〜中略〜 # commitがそのまま取り込まれている。 $ git log --oneline --graph * XXXXX (HEAD -> subtree_test_no_squash) Add 'gyro_doc/' from commit 'XXXXXXXXX' |\ | * YYYYYY (gyro_doc/master) Merge pull request #1617 from XXXXXXX | |\ | | * ZZZZZZ Merge pull request #1677 from XXXXXX
Gitのcommit履歴が膨大になってしまう場合は、squashオプションを検討してみてください。 移行元のcommitが1commitに集約されます(mergeのsquashコマンドと同じです)。 今回は過去履歴の追いやすさを重視したため、採用しませんでした。
また、再取り込み(最新化する)場合は、subtree pull
コマンドを打てば良いです。
git subtree pull --prefix=polyrepo1 polyrepo1 master
この際、モノレポで加えた修正とconflictする場合がありますが、merge時のconflictと同じ状態になります。通常通り、conflictを解消してmergeすれば良いです。
Auto-merging XXXXXX/README.md CONFLICT (content): Merge conflict in XXXXXX/README.md Automatic merge failed; fix conflicts and then commit the result.
3. モノレポ管理ツール(モノレポツール)の検討
モノレポを導入する際、モノレポツール(monorepo tool)と呼ばれるツールを導入することがあります。これは、モノレポの管理を支援するツールで、モノレポの構成管理や依存関係の管理を行うことができます。
今回はモノレポ管理ツールは導入せず、手動での管理を選択しました。Rubyがメインでありビルド内容がシンプルであることと、依存関係が少なかったことから、手動での管理で十分と判断しました。
4. CI/CDの変更
CI/CDの設定を変更し、モノレポ化に合わせた運用を行いました。
何も考えずに、毎回全ファイルに対してCI/CDを回しても技術的には問題ありませんが、それでは余計なCI/CDリソースを食ってしまいます。また、軽微な修正であってもCI/CDの待ち時間も長くなってしまう可能性があります。
そこで、変更内容検知のために、ファイルの差分を正確に判定できるような仕組みを導入しました。
前提として、CI/CDはCloud Buildをメインで使用しています。
Cloud Buildの 含まれるファイル
を指定する
Cloud Buildではファイル指定機能があって、これを使えるか検討しました。
実際にfile filterを設定して検証してみました。
- triggerをbranch push
- 前回push時とのdiff(pushされる前のHead?)と比較して、ファイル指定と突き合わせてくれる
- 新規ブランチの場合は、最新commitの内容だけで判断される
- triggerをPull Request
- Pull Requestに含まれるファイル全体を見て判断してくれる
という挙動になりました。つまり、
- 特定のブランチを指定した(develop、master等)branch pushのtrigger
- Pull Requestのtrigger
の2つは、修正した差分ファイルのみを検知できる事がわかりました(tagのpushは動作確認していませんが、tagの性質上比較対象がないため、含まれるファイルを指定しても意味がないと思われます)
ちなみに、GCPのドキュメントでは
注: 含まれているファイルと無視されるファイルを指定できるのは、[イベント] で [ブランチに push する] または [新しいタグを push する] を選択した場合のみです。
とありました。ドキュメントが古いだけなら良いのですが、検証結果とは違う事が書いてあるので注意が必要です。
Cloud Buildの設定変更
triggerの設定
検証結果を踏まえて、Triggerの設定を以下のようにしました。
- CI実行
- before: branchなんでもいいのでpush
- after: Pull Request作成
- featureブランチのpushだと、変更内容を正常に検知できないため
- 開発環境デプロイ
- before: 特定のブランチ(develop等)にpush
- after: 変更無し
- 本番デプロイ
- before: 特定の命名のtag push
- after: 特定のブランチ(release)にpush
ディレクトリごとのCI/CD作成
特定のディレクトリごとに差分があったかの検知ができる事を確認したため、ディレクトリごとにCloud BuildのTriggerを作成しました。
元々のリポジトリ単位とモノレポ移行後のディレクトリ単位は同じなので、リポジトリごとのCloud Build設定をほぼ流用できます。
具体的には、既存のCloud Build設定をコピーして作成した上で、
- name: 'gcr.io/cloud-builders/docker' entrypoint: 'bash' dir: '$_REPO_NAME'
のような形で、ディレクトリを指定するようにしました。
$_REPO_NAME
という変数は、Triggerで設定します(中身はディレクトリ名=移行前のリポジトリ名)
5. submoduleの廃止
OpenAPIの定義と、gRPCの定義ファイルをsubmoduleで参照していたのを、モノレポ内のディレクトリにコピーして参照するようにしました。Makefileで便利コマンドを作成する運用が定着していたので、別ディレクトリからcp
するコマンドを作成しています。
docker-composeのvolumeマウント設定で直接参照する方法も検討しましたが、contextを変更してDockerfileを修正する必要があり、修正範囲が大きくなってしまうため、初回の導入では採用しませんでした。
今後、Makefileコマンドでも不便を感じるようであれば、再検討予定です。
6. 自作gemの参照方法変更
共通ライブラリとして自作したgemを参照している箇所がありました。これは、gemfileに以下のように記述していました。
gem 'gyro_common', '~> 0.X.YY', git: 'https://github.com/buysell-technologies/XXXXXX.git', branch: 'master'
モノレポ後は該当のリポジトリは存在しないため、ローカルのディレクトリをgemとして扱う事ができる path
で指定するように変更しました。
gem 'gyro_common', path: 'common_plugin'
前述のMakefileコマンドで、別ディレクトリからコピーしてある前提ですが、変更頻度が低いため問題ないと判断しました。
gemを使わずに単純にファイルコピーで済ませる事も検討しましたが、gemとして使われる前提になっているコードが多く、影響範囲が広くなってしまったので見送りました。
リリース/リリース後の流れ
全体としては、以下のような流れになります。
- モノレポを新規作成
- 上記の手順による修正を加えたコードをpush
- 既存リポジトリのコードを適宜取り込み
- テスト環境でリグレッションテスト
- 本番デプロイ
- Pull Requestの手動移行
Gitのコードはsubtreeコマンドで引き継げますが(ブランチを指定すればOK)、GitHubのPull Requestは引き継げないため、手動で作成が必要です。
まとめ
事前の見積もりでは数ヶ月を想定していたのですが、実際には作業時間としては1ヶ月もかからない時間で対応ができました。
GYROをリリースした2020年の段階では知見が少なく導入は難しかったと思います が、現在はモノレポを支える技術や知見が十分に提供されています。マイクロサービスを検討しているチームや既に運用しているチームにとって、モノレポ化は検討する価値があると言えるでしょう。参考になれば幸いです。
※マイクロサービスは難易度が高いアーキテクチャだと思いますが、モノレポで解決できるのはあくまでソース管理の課題のみです。そもそものアーキテクチャ的な難しさは解消できませんので、注意が必要です。
明日の記事は伊与田さんの開発のボトルネックを解消してチームの生産性を上げた話です!
また、BuySell Technologiesではエンジニアも絶賛募集中です。こちらも興味のある方はぜひご確認ください!