こんにちは。技術本部Sansan Engineering Unit Master Data Groupの古本です。
普段は、営業DXサービス「Sansan」の名刺交換した人や企業に関するニュースを表示し、お知らせする「企業ニュース」や「企業情報」を扱うシステムの開発をしています。
最近、マイクロサービスで作られた企業ニュースのシステムをモノレポ構成に移行しました。
今回はその時に行ったことについて話します。
モノレポ(mono repo)とは
本ブログで類似の記事があったので引用します。
一連のソースコードを単一のリポジトリで管理している状態のことです。
特に、実装言語、またはサブシステムやドメインといった何らかの区切りでリポジトリを分けている場合に、それらを集約することをモノレポ化と言います。
マイクロサービスアーキテクチャのリポジトリ構成を漸進的にモノレポに移行した話
今回も複数レポジトリで管理されているサービスを1つのレポジトリに集約するという点は同じです。
今回は具体的に行ったことについて話そうと思います。
なぜモノレポ構成に移行することにしたのか
理由を一言で書くと運用負荷を減らすためです。
現状では主に運用面でマイクロサービスアーキテクチャから得られるメリットよりデメリットの方が大きいという課題がありました。
この課題を解決するためにアーキテクチャを見直すことが検討されました。
アーキテクチャを見直すに当たり、運用負荷の分析を進めると以下3点の要因に集約できました。
- 複数リポジトリ -> 調査のしづらさ
- 複数データベース -> データの追いづらさ
- 複数Gemfile -> バージョンアップ、ライブラリ更新がつらい
まずは複数リポジトリを解消を目指しモノレポ構成へ移行することになりました。
具体的にやったこと
次の6つのステップを実行してモノレポ構成に移行しました。
- git subtreeを使ってソースコードを移行
- サービスごとにbuildが走るようにCircleCIを設定
- サービスごとにdeployできるようにAWS CodeBuildを設定
- サービスごとに走るようにGithub Dependabotを設定
- テスト
- リリース
1. git subtreeを使ってソースコードを移行
まずモノレポ構成で移行するリポジトリを新しく作成しました。
モノレポ構成下ではマイクロサービスの各サービスをサービス単位で置くことにしました。
つまり既存のロジックの変更やサービスの統廃合は行いません。
そのため各リポジトリのソースをコピーすることになりますが、コミット履歴は引き継ぎたい欲求がありました。
gitにはsubtreeというリポジトリ集約用のコマンドがあります。
このコマンドならコミット履歴を引き継げるので、このコマンドを使って移行しました。
# 移行サービス: event_archiver % git subtree add --prefix=event_archiver git@github.com:********/event_archiver.git master # 途中で更新があった場合はsubtree pullで同期 % git subtree pull --prefix=event_archiver git@github.com:********/event_archiver.git master
注意点として、Github上にあるissuesやPull requestsは引き継がれません。
調べた限りでは簡単に移行する方法は見つからなかったのですが、チームの欲求としては過去のissuesやPull requestsは検索できれば問題ありませんでした。
そこで過去のリポジトリをアーカイブして残すことで検索できるようにしました。
2. サービスごとにbuildが走るようにAWS CircleCIを設定
CircleCIはCI/CDを行うSaaSサービスです。
引き続き利用したいので、モノレポ構成になっても動くように設定します。
またCIはサービスごとに独立して動く形を変えたくなかったので、CircleCIにあるダイナミックコンフィグの仕組みを利用しました。
.circleciディレクトリにconfig.ymlとcontinue_config.ymlの2ファイルを用意して次のように設定しました。
# config.yml orbs: path-filtering: circleci/path-filtering@1.1.0 workflows: main_build: jobs: - path-filtering/filter: name: check-updated-files base-revision: main mapping: | event_archiver/.* run-build-event-archiver-job true event_dispatcher/.* run-build-event-dispatcher-job true config-path: .circleci/continue_config.yml
mappingで指定したディレクトリに変更があった場合は、CI実行時にrun-build-[サービス名]-jobパラメータがtrueで実行されます。
実行されるworkflowはcontinue_config.ymlのworkflowsに定義されています。
when句に指定したパラメータの値を見て、trueなら配下のjobが走る仕組みとなっています。
# continue_config.yml parameters: run-build-event-archiver-job: type: boolean default: false run-build-event-dispatcher-job: type: boolean default: false (中略) workflows: # event_archiver event-archiver-build: when: << pipeline.parameters.run-build-event-archiver-job >> jobs: - run_setup: - run_rubocop: target_service: event_archiver requires: - run_setup - run_rspec: run_executor: event_archiver_executor target_service: event_archiver requires: - run_setup # event_dispatcher event-dispatcher-build: when: << pipeline.parameters.run-build-event-dispatcher-job >> jobs: (中略)
モノレポ構成にしたことでbuildを走らせたいディレクトリが1階層深くなりました。
そのため走らせたいディレクトリをworking_directoryで指定することが必要となりました。
ディレクトリを固定値で指定すると冗長になるため、ディレクトリ名と同じであるサービス名をtarget_serviceパラメータとして渡せるようにしました。
受け取ったパラメータをworking_directoryに入れることで、buildを走らせたいディレクトリを指定できるようになりました。
# continue_config.yml workflows: event-archiver-build: when: << pipeline.parameters.run-build-event-archiver-job >> jobs: - run_setup: - run_rubocop: target_service: event_archiver requires: - run_setup (中略) jobs: run_rubocop: executor: default_executor parameters: target_service: type: string environment: BUNDLE_APP_CONFIG: ~/news_monorepo_services/<< parameters.target_service >>/.bundle steps: - attach_workspace: at: ~/news_monorepo_services - install_bundler: target_service: << parameters.target_service >> - exec_to_rubocop: target_service: << parameters.target_service >> (中略) commands: exec_to_rubocop: parameters: target_service: type: string steps: - run: name: Run rubocop working_directory: << parameters.target_service >> command: bundle exec rubocop install_bundler: (中略)
サービスごとにDockerfileの設定も異なっていました。
その差分を埋めるためCircleCIのExecutorという仕組みを利用しました。
サービスごとにExecutorを用意し、実行したいjobでrun_executorを指定することで解決しました。
# continue_config.yml default_executor: docker: - image: cimg/ruby:3.3.4 environment: BUNDLER_VERSION: 2.5.11 RAILS_ENV: test event_archiver_executor: docker: - image: cimg/ruby:3.2.5 environment: BUNDLER_VERSION: 2.4.19 RAILS_ENV: test (中略) workflows: event-archiver-build: when: << pipeline.parameters.run-build-event-archiver-job >> jobs: - run_setup: - run_rubocop: target_service: event_archiver requires: - run_setup - run_rspec: run_executor: event_archiver_executor target_service: event_archiver requires: - run_setup (中略) jobs: run_rspec: executor: << parameters.run_executor >> parameters: run_executor: type: string default: default_executor target_service: type: string environment: BUNDLE_APP_CONFIG: ~/news_monorepo_services/<< parameters.target_service >>/.bundle steps: - attach_workspace: (中略)
3. サービスごとにdeployできるようにCodeBuildを設定
企業ニュースのdeployはAWSのCodeBuildを利用して行っています。
引き続き利用したいので、モノレポ構成でも動くように設定しました。
実用を考えるとCI同様サービスごとにdeployする環境は必要です。
そのためbuildspec.ymlも統合せず各サービス配下に残しました。
# 各サービスディレクトリ配下にbuildspec.ymlがある ├─ event_archiver │ └─ buildspec.yml ├─ event_dispatcher │ └─ buildspec.yml (中略)
CodeBuildでは読み込むbuildspec.ymlを指定できます。
各サービス配下のファイルを読みに行くことで、サービスごとのdeployを実現しました。
# terraform locals { codebuild_applications = { event-archiver = { location = "https://github.com/********/news_monorepo_services.git" buildspec = "event_archiver/buildspec.yml" }, event-dispatcher = { location = "https://github.com/********/news_monorepo_services.git" buildspec = "event_dispatcher/buildspec.yml" }, (中略) }
buildspec.ymlは既存の設定がほぼそのまま利用できました。
しかし、モノレポ構成の影響を受けてディレクトリの指定が必要な場合もありました。
その場合は次のようにディレクトリを移動する設定を追加しました。
# buildspec.yml commands: - echo Build started on `date` - echo Building the Docker image... - cd event_archiver # ディレクトリの移動を追加 - docker build -t $IMAGE_REPO_NAME . (中略)
4. サービスごとに走るようにGithub Dependabotを設定
企業ニュースのbundle updateはGithubのDependabotを利用して行っていました。
引き続き利用したいため、モノレポ構成でも動くように設定しました。
モノレポ構成ではサービスごとにGemfileが存在します。
Dependabotはディレクトリ単位で設定できるので、設定ファイルにサービスごと個別の設定を追加して利用できるようにしました。
# dependabot.yml updates: # サービスごとに設定を追加する # event_archiver - package-ecosystem: bundler directory: "/event_archiver/" (中略) # サービスごとのPRをまとめられるようにサービスごとのgroupsの設定を追加 groups: event-archiver-group: patterns: - "*"
各サービスで個別にbundle update用のPRが作られると収拾がつかなくなります。
それを防ぐためにgroups設定を追加しました。
この設定でサービスごとPRが作られるようになりました。
5. テスト
構成を変えただけなのでテスト観点としては、移行前後でdeployされたシステムに変わりがないこと、となります。
変わっていないことを確認するのは意外と難しいのですが、今回は次の3点を見ることで変わっていないことを確認しました。
- buildが通ること
- 作成されたAWS ECSのタスク定義が変わらないこと
- staging環境で一連の処理を流してOutputに変わりがないこと
6. リリース
本番展開は新しいCodeBuildの設定で各サービスをdeployすることで展開します。
既にサービスごとのビルドプロジェクトは存在するため、buildspec.ymlの参照先を変えた後に実行してdeployを行いました。
このdeploy手順はモノレポ構成に移行する前と同様のやり方で行えました。
モノレポ構成に移行してどうなったか
マイクロサービス構成自体は温存されているので、システム的には変わりはありません。
サービス単位でCircleCIやdeployが行えるので、開発体験も変わらずです。
ただし、調査でソースコードを見たくなった時にリポジトリを跨いで見る必要がなくなったので、そこは楽になりました。
まとめ
当初はコードをコピーして、カスタマイズのような設定をいくつも行わないとならないと思っていました。
実際にやってみるとモノレポ構成に移行するための方法が数多くあって、モノレポもだいぶ市民権を得てきたと感じました。
今回のモノレポ構成への移行を検討している方に本稿が参考になれば幸いです。
また、モノレポ構成に移行してもプロダクトが持つ運用課題はまだ解決されていません。
そのため改善施策はまだ続きますが、それはまた別の記事でお話しできればと思います。
私たちのチームではWebアプリ開発エンジニアを募集しています。Webアプリ開発だけではなくデータエンジニアリングなど、幅広い業務を経験できます!
open.talentio.com