見出し画像

monorepoをなぜ採用したか、及び大まかな構成: 三井物産デジタルアセットマネジメントでの事例

皆さんこんにちは、代表取締役CTOの松本(@y_matsuwitter)です。最近買ったSonyのlinkbudsがめちゃ快適で感動しています。骨伝導イヤホンから乗り換えました。

さて、再びLayerXアドベントカレンダー(概念)がスタートするようです。もはやアドベントとは何なのか分からなくなっておりますが、継続してブログ書いていこうというチームでの決意表明となります。

今回は私が見ているプロジェクトの一つでmonorepoを採用し、その立ち上げ最中となりますので、まずはその採用目的や大まかにどのような構成をとったのかという話をさせてください。今レポジトリ構成でmonorepoを検討中の方の参考になればと思います。

背景

今回取り上げるmonorepoは、三井物産デジタルアセットマネジメントにおける業務効率化、特に物件等の運用に関わる業務の改善プロジェクトにて採用したものとなります。もともとメールや手作業で行われていた作業のデジタル化に向けて、業務プロセスを支えるツールを内製するべく始まりました。

本プロジェクトのチーム構成としては、自分がPdM兼EM的な立ち位置、それ以外に開発メンバーが3人(この記事が出る頃に4人になります)という陣容で、全員フロントエンド・バックエンド・インフラなど横断的に開発にチャレンジしています。

対象業務領域ではメールだけでなくすでにワークフローシステム(バクラク申請)など他システムが動いており、また利用者としては社内のメンバーだけでなく、社外のパートナー企業も様々いる部分となります。また関連する人々は今後も増えうる中で内部で細かく改善しながら担当する業務領域を少しずつ拡大・改善することの繰り返しで日々デジタル化をすすめることを目指しています。

実際のプロジェクト自体は2ヶ月ほど前にスタートし設計を開始、開発中のものです。

monorepo採用の目的

もともと、社内の他プロジェクトではAPI、バッチ処理、Web、IaCなどに対してそれぞれレポジトリを整備していることが多かったのですが、今回はあえてmonorepoにチャレンジしています。その理由について見ていきましょう。

昨今では、例えばAWSではLambdaやECSなど、ビジネスロジックの実行環境について様々な選択肢が存在しています。よりよいアーキテクチャを目指す上で、「どこでどのロジックを動かすかは、その処理性質に合わせればよい」という前提を置くと、同じビジネスドメインでも、ものによって動かすインフラを変えること、またその状況変化に応じて変更することが求められるようになります。

図:アーキテクチャの変化

この前提の上ではmonorepoという選択肢は有りなのではないかと考えるようになりました。長期間、爆速で開発をすすめる上では如何に変更に強い構成を取れるかが重要です。その際、要求の変化に応じてアプリを柔軟に分割・統合することを考えると、ロジックの共有とその変更による問題のCIレベルでの検知が重要です。

例えば、一つのビジネスドメインに紐づくシステムであっても、このロジックAはLambdaで非同期処理し、BはECSで実行するなどがありえます。適材適所で、クラウド自体も柔軟に活用する場合、ロジックを共有しながら、Lambda用、ECS用のバイナリを作り分けるなどが発生してきます。一方この時、AとBは同じモデルやロジックを共有することになります。monorepoの場合、これは全て一つのレポジトリの中に包含されることとなります。

ソフトウェアに変更を加える場合、AとBそれぞれへの影響が見えねばなりませんが、monorepoではこれを一つのrepository内でテストを実行でき、変更影響をユニットテスト等で検知しやすくなります。もちろん、multi repositoryでも実現はできるのですが、比較して容易だと感じています。

これは、プラットフォームを跨いだ場合でも同じです。

インターフェイスの型共有

例えば、とあるGraphQLのメソッドを変更する場合、クエリの型に変更があればGo側、Typescript側双方で即座に検知され、変更影響が見えるようになります。ビルドの失敗等として見えてくるでしょう。また、DBのスキーマ変更があれば、APIだけでなく、バッチ処理側など同じDBへアクセスする処理系それぞれへの影響が把握しやすくなります。

こうした「変更影響を素早く検知できること」は長期間複雑なインフラ構成となりうる現代のソフトウェアに対して有効な性質だと考えており、この担保のためにmonorepoは有用だと考え、今回monorepoの採用に踏み切りました。

monorepoの大まかな構成

現状のディレクトリ構成は下記のようになっています。

.
├── containers ... Dockerコンテナ周り
├── go ... Go言語関連、APIやバックエンド処理
├── terraform ... インフラ定義
└── typescript ... Webapp関連を中心にしたディレクトリ 

大まかには、言語ないし処理系ごとにルートのディレクトリを分割する形をとっています。また、その内部で更にモジュールごとのディレクトリを切るなどしています。

またこうしたmonorepoではBazelの利用事例もみられますが、今回はbazelの採用を見送っています。依存関係を明示することで巨大なmonorepoでも効率的ビルドを実現できるbazelは個人的にとても好きな技術です。一方、bazel自体が利用者にある程度の習熟を必要とすること、bazel採用時のプロジェクト管理はbazelに密に結合したものとなることなど踏まえ、現状の少人数チームではこのコストを取るより、他プロジェクトでも慣れた管理を採用すべきと判断し、Makefileでの管理にしています。

それぞれの言語のビルド等の処理についてはMakefileを別々に配置しています。例えばGoディレクトリのMakefileでは下記のようにプロジェクトセットアップやCIに必要なコマンドを用意しました。社内用helpなので分かりにくいところもあるかもしれませんがご容赦ください、個別の採用ツールなどについては今後解説などできればと思います。

$ make help
 Choose a command run in amdx:

  mod-download        Download modules from go.sum
  mod-tidy            Tidy up modules
  golangci-lint       run golangci-lint
  format              run golanci-lint + go fmt
  format-fix          run golanci-lint fix + go fmt
  gen-api-model       generate ent models.
  migrate-api-model   migrate with ent schemas.
  install-air         install 'air' for golang api live reloading.
  serve-api           Serve api server with live reloading
  serve-api-debug     Serve api with DEBUG=true
  seed                seed data for development.
  test-setup-db       setup db and fixture for test.
  test-clean-db       clean up test db.
  test-go             run go tests only.
  test                setup and run go tests.

一つのMakefileで全て記述することも検討しましたが、分割しようがまとめようがあまり変わらないこと、可読性など考慮しgo, terraform用にバラバラに用意しています。typescriptではyarn runにて定義しています。

Makefile運用にしておくことで、既存のプロジェクトとあまり変わらない環境でmonorepoに取り組めるため、いま時点でmonorepoゆえの問題にぶつかったことは無く、各メンバースムーズに開発を進めています。

RepositoryのCI/CDの定義自体も全て一つのレポジトリ内で完結しており、変更されたディレクトリに応じて必要なgithub actionsが動作するよう設定されています。例として、terraformのplan実行を行うactionsでは下記のようにterraformディレクトリの変更時のみ実行するよう設定しています。

name: "terraform aws plan"
on:
  pull_request:
    types: [opened, synchronize]
    paths:
      - "terraform/**"

defaults:
  run:
    working-directory: ./terraform

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  terraform:
    name: "Terraform plan"
    runs-on: ubuntu-latest
...以下省略....

今回はあっさりとした紹介のみとなりますが、今後それぞれの言語での工夫やDevOps周りの取り組みなども共有させていただければと思います。

現時点での感想

今の所、大胆なコード変更などもユニットテストを組み合わせて即座に依存する全てのレポジトリにおいて検証できており、開発初期特有の大胆な設計変更なども恩恵を受けながら開発できているように思います。

また、レビューする場合にも、達成したいアウトカムに対して一つのプルリクエストで完結させることで、必要な機能が実現できているのか、関連するものは何が必要だったのか、一度に把握でき、レビューのしやすさにも繋がっています。例として、ユーザーがログインできる、というストーリーに対して一つのプルリクエスト内にてスキーマの定義、APIの作成、Webappの作成、必要なインフラの変更を含めることができます。マージされれば即座に機能がデプロイされる、ということでレビューやブランチ運用も楽だと感じています。

一方、まだまだデプロイの工夫などが必要ですので、Github ActionsやCodePipelineでの工夫など今後も取り組みが必要なように感じます。特に長期間開発を進めていけば、依存が複雑になる一方、それを加味したデプロイパイプラインになっていないことによる問題が起きるなど容易に想像できます。こうした問題への対処は今後の課題になるため、継続的に改善していきたいところですね。

最後に

どのような開発手法もトレードオフを持ちますが、monorepoならではのデメリット等にも冷静に目を向けつつ、爆速開発やハタラクをバクラクにできるプロダクト開発を実現するべく積極的に新しいものにも取り組んでいきたいと考えております。

LayerXではユーザーに向き合いながら、長期間健全にスケールできる組織を実現するため様々なポジションで募集をさせていただいております。
ぜひご興味ある方こちらから弊社の情報を見ていただけると幸いです。

また、キャリアや技術周りの壁打ちなどmeetyで行っておりますので、ご興味ある方はぜひご応募ください。

SaaS.techという技術勉強会コミュニティの立ち上げもはじめました。こちらはLayerXというよりもSaaSに取り組むエンジニア、という視点で純粋に技術的なトピックに向き合える会になればなと思いますので、こちらご興味ある方もぜひご応募ください!

この記事が気に入ったらサポートをしてみませんか?