こんにちは、ギフティでエンジニアをしている @egurinko です。普段は toB 向けプロダクトの開発をしていて、趣味は 1 年中スノーボードです!
先日ギフティ社内の LT 大会である TechBash にて、「システムリニューアルに伴う monorepo 構成を考える」というタイトルで LT させていただきましたので、その模様をお届けいたします。
スライドはこちらに公開しております。
背景
ギフティには gift wallet というアプリがあります。gift wallet は URL 形式で発行されたギフトをなくさないように保存する、文字通り「ギフトのお財布」的なアプリです。リリースしてから順調にユーザが増えており、今後さらに大幅な機能追加が見込まれました。また、この段階で UI のフルリニューアルも見込まれ、UI に大きな柔軟性が求められることになりました。
2021年7月現在の gift wallet は Rails で稼働しているのですが、今後の UI の柔軟性を考えフロントエンドを Rails から分離することになり、今回の monorepo の話が持ち上がりました。
ざっくり要件
- システム構成は以下
- バックエンドは Rails
- 社内向け管理画面は慣れている Rails
- エンドユーザ向けフロントエンド React
- バックエンドは Rails
- デプロイは各アプリごと
- CI は変更箇所のアプリのみに対して回す
monorepo vs multi repos ?
冒頭やタイトルに monorepo と書いてしまっているのですが、実際には monorepo か multi repos の検討から始めました。 monorepo は単一 repository に複数のプロジェクトを保持する形であり、multi repos はプロジェクトごとに repository を分ける形になります。
monorepo のメリット/デメリットは主に以下の点が挙げられました。
- メリット
- 開発者は複数 repository を行き来する必要がない
- プロジェクト同士の関連を同じ repository に入れることで表現できる
- コードの再利用を行える
- デメリット
- プロジェクトが増えれば repository がどんどん肥大化していく
- 1 つの repository で複数プロジェクトを管理する仕組みが必要になり、技術難度が上がる
multi repos のメリット/デメリットは monorepo の逆になります。
- メリット
- repository は肥大化しにくい
- 技術難度は上がらない
- デメリット
- 複数 repository を行き来する必要がある
- プロジェクト同士の関連を何かしら別の方法で表現する必要がある
今回のリニューアルは monorepo を採用しました。決め手は、gift wallet のフロントエンドとバックエンドという深い関連のあるプロジェクトに対して repository を分けてしまうとその関連を表現することが難しくなってしまうことと、repository を行き来せずに効率よく開発したいという 2 点でした。
詳細要件
monorepo で行こうと決まってから、さらに細かい要件を出してみました。
- deploy は各アプリごと
- CI は変更箇所のアプリのみに対して回す
- repository の root から各アプリの script は打ちたい
- bundle exec rails s , yarn install を想定
- また、Rails の bin/setup のように、全てのアプリのセットアップを行うコマンド等も想定
- package.json は個別のアプリで管理
- 共通の設定ファイルなどは再利用できる形にしたい
- prettier や eslint, tsconfig 等を想定
- 共通の package (TS とか) は共通で管理したい
実際の monorepo 構成
最終的に作成した repository は以下のようになりました。
- repo root - .github - backend:Rails アプリ - app - db - package.json - .eslintrc - frontend - consumer: エンドユーザ向けフロント (React) - package.json - .eslintrc - tsconfig.json - ... - graphql: GraphQL スキーマ - package.json - .eslintrc - .prettierrc - tsconfig.json
今回は、この構成で工夫した以下 4 点について詳細を解説していきます。
- monorepo ツール
- コードの流用
- CI
- GraphQL
monorepo ツール
複数のアプリを 1 つの repository で管理する場合、それを実現するために monorepo ツールを使うことになると思います。ツールを利用することで、package の管理を効率化できたり、複数アプリに跨ったコマンドを打ったりすることもできます。
今回の gift wallet では、代表的な monorepo ツールである以下を検討しました。
- yarn workspaces
- lerna
- lerna + yarn workspaces
- Nx
Nx は管理するアプリが JavaScript 前提であり、Rails を利用する gift wallet では適さないことは早めにわかっていましたので、詳細は省きます。
yarn workspaces
yarn workspaces は代表的なパッケージ管理ライブラリの yarn が提供している monorepo 機能になります。yarn workspaces を利用することで、以下のようなことが実現可能です。
- 複数 package を単一リポジトリで管理できる
- repository の root から各 package のコマンドを打てる
yarn install
:一斉インストールyarn workspace <workspace名> test
:特定 workspace のコマンド実行yarn workspaces run test
:全 repository に対してyarn test
を実行
- 依存 package の hoisting
- workspace 間で共通の依存 package があれば、root で管理しそれを workspace がシェアする
- yarn.lock は root で単一管理
gift wallet で何より魅力的だったのは馴染みの yarn command を利用できるという点でした。
lerna
lerna は yarn workspaces と違って、monorepo を実現するためのライブラリになります。yarn workspaces で実現できることのほとんどは lerna によっても実現できます。むしろ複数の package を npm に publish したい場合、それに対するたくさんの便利コマンドがあります。
- 複数 package を単一リポジトリで管理できる
- repository の root からいろんなコマンドが打てる
lerna bootstrap
:package の一括インストールlerna run test
:全 repository に対してyarn test
を実行lerna exec -- <command>
:root から sh のコマンドを打てる- 各 package で diff が取れたり、各 package の状態を把握するコマンドが多数
- package の追加が容易
lerna add <名前>
lerna publish
で一括 publish できる
gift wallet では、Rails のアプリの管理も含まれていたため、sh のコマンドを実行できる lerna は非常に魅力的でした。一方で、lerna のコマンドを開発者が覚える必要があり、そこは厳しいなあと考えていました。
lerna + yarn workspaces
2 つを合わせて利用することで、馴染みの yarn command を使いつつ、Rails のコマンドも打てる lerna の柔軟性を教授できそうでした。2 つを合わせて利用する設定も、lerna の設定ファイル (lerna.json) に設定を 1 つ追加するだけでとても簡単なので、gift wallet では 2 つを合わせて利用する形にしました。
参考
参考にした monorepo 構成をとっているリポジトリを以下に挙げておきます。
- chakra
- lerna + yarn workspaces で管理してる
- vue
- yarn workspaces のみ
- react
- yarn workspaces のみ
- jest
- lerna + yarn workspaces
コードの流用
バックエンドは Rails を想定してましたが、Rails を利用した社内向け管理画面には TypeScript の利用が見込まれました。その場合、バックエンドとフロントエンドに、Eslint や Prettier、TypeScript の似たような設定が誕生しそうでした。gift wallet では、なるべく重複した設定を書かないようにベースとなる設定ファイルを root に置くことでこれを解決しています。
- repo root - .github - backend:Rails アプリ - app - package.json - .eslintrc - tsconfig.json - frontend - consumer: エンドユーザ向けフロント (React) - package.json - .eslintrc - tsconfig.json - package.json - .eslintrc - .prettierrc - tsconfig.json
Eslint はカスケードを採用しているため、共通の設定を root の .eslintrc に書き、個別の設定をそれぞれのディレクトリにある .eslintrc に書いています。 また、tsconfig.json は extends を利用し、同じことを実現しています。Eslint と同じく共通の設定を root に記載し、個別の設定が必要な場合は、root の tsconfig.json を extends しています。
CI
gift wallet では、フロントエンドの変更を commit した場合はフロントエンドの CI を、バックエンドの変更はバックエンドの CI のみを回すことを検討していました。これを実現するために、GitHub Actions を採用しています。
- root - .github - workflow - backend.yml - frontend.yml
以上のように 2 つの yml ファイル作成し、GitHub Actions の on
オプションに path を指定し、特定の path の変更に hook して job を回すようにしています
GraphQL
gift wallet では、フロントエンドとバックエンドの通信に GraphQL を採用しており、そこが 2 つの明確な責務境界点になります。また、フロントエンドではバックエンドで作成されたスキーマに対して、GraphQL Code Generator を利用して自動で型定義を作成するようにしています。
そこで、root に graphql ディレクトリを作成し、バックエンドがスキーマ定義を graphql ディレクトリに格納し、フロントエンドがそこを参照し型定義を自動作成するようにしています。
- repo root - backend - frontend - graphql - graphql.schema
終わりに
今回はシステムリニューアルに伴う monorepo の検討過程について掻い摘んでお伝えさせていただきました!ギフティでは事業の立ち上げやその成長に伴走できるチャンスがたくさんあります。事業の成長に伴い技術的チャレンジも増えてきましたが、まさにうれしい悲鳴です。
ギフティではギフティの成長に伴走していただけるエンジニアを絶賛募集中です。今回のブログでギフティを気になった方はぜひ面談などしましょう。
https://giftee.co.jp/recruit/
今回は以上です、ありがとうございました!