脱・巨大なモノリシックアプリ!DRYなRails分割方法

Housmartの松江です。

Railsでアプリケーションを開発していると、管理者機能や周辺機能も含んだ巨大なプロジェクトになってしまうこと や、 分割したプロジェクト間の管理・同期が煩雑になってしまうこと ありますよね。

今回は以下の要望に応えるべくカウルの開発現場で実施した、Railsプロジェクトの分割と運用をご紹介します。

  1. 巨大な1つのRailsプロジェクトにせず、管理機能部分を分けたい
  2. 分割後はプロジェクト間で必要なロジックを共有したい

複数のRailsプロジェクト間でロジックを共有したい状況

不動産を扱うカウルの裏側では以下のような多種多様なデータを扱っています。

  • 詳細情報: 最寄り駅・エレベータの有無・ペット可否など
  • 部屋情報: 間取り・方角・契約の進行状況など

上記を社員が効率よく管理するため、管理者用のアプリケーションが別途用意されています。

  • ユーザ用のRailsプロジェクト
  • 管理者用のRailsプロジェクト

上記のように2つのプロジェクトに分割しているのは、巨大な1つのRailsプロジェクトになり管理が難しくなることを避けるためです。

しかし、複数のプロジェクトに分割した後、DBテーブルの追加・変更があった場合等に “2つのプロジェクトそれぞれに同じ変更を行う” 必要のある状況が発生するようになりました。 これは以下の様な問題を引き起こします。

  • 2つのプロジェクトに変更を行うコストがかかる
  • プロジェクト間での同期できていなかった場合に予期しないエラーが発生する

カウルではこの状況を Gitのsubmodule を用いて改善しています。

実際の構成

Gitのsubmoduleとは1つのGitレポジトリの中に、他のGitレポジトリも含めてバージョン管理を行う機能です。

カウルのRailsプロジェクトでは ${RAILS_ROOT}/app/models 以下がsubmodule扱いになっており、複数のRailsプロジェクトから同じモデルクラス群が扱える ようになっています。

rails_app_1
├── app
│    ├── controllers
│    ├── views
│    ├── models   -> ../models_repo/app/models   # シンボリックリンク
│    ├── ...
├── models_repo # submodule
├── ...

rails_app_2
├── app
│    ├── controllers
│    ├── views
│    ├── models   -> ../models_repo/app/models   # シンボリックリンク
│    ├── ...
├── models_repo # submodule
├── ...

この構成にすることでRailsのもともとのディレクトリ構成を変えることなく、モデル周りにあるロジックを複数のRailsプロジェクトで共有することができる ようになります。

submodule以下は最新のmasterブランチを追うことで簡単に同期がとれるため、ユーザ用のアプリケーションで追加されたロジックが管理者用アプリケーションで使えないという問題は起きなくなりました。

また、図では簡単化のためにモデルクラスのみが共有されていますが、実際には他のクラスも共有されています。

submoduleの難しさ

submoduleでの運用を開始した当初、以下の部分で使い難さがありました。

  1. 単一のGitレポジトリとpush手順が変わる
  2. プルリクエスト間でコンフリクトが起きやすい

1つ目のpush手順の違いは、submoduleを含むレポジトリの操作に慣れないうちは特に不便に感じます。 submoduleを含まないレポジトリであれば git add して git push するのは1回だけで済みますが、submoduleを含む場合は2回行う必要があります。

# submoduleを含まないレポジトリでのpush
project_dir $ git add
project_dir $ git push

# submoduleを含むレポジトリでのpush (submodule内にも差分がある場合)
project_dir $ cd submodule_dir
submodule_dir $ git add
submodule_dir $ git push
submodule_dir $ cd ..
project_dir $ git add
project_dir $ git push

2つ目のコンフリクト問題は、submodule内のファイルに変更のあるプルリクエストが複数来ていた場合に発生します。

上記の図のようにsubmoduleを扱うGitレポジトリでは、submodule内のどのコミットからどのコミットへの参照に切り替えるか という内容を含むコミットが発生します。

  • コミット”ロ”: submodule内のコミット”1”から”2”へ参照を変えるコミット
  • コミット”B”: submodule内のコミット”1”から”3”へ参照を変えるコミット

上記のような2つのコミットがプルリクエストに並んでいる状態で片方をマージすると、もう一方で必ずコンフリクトが起きます。後からマージされるコミットの内容はsubmodule内のコミット"4"から◯◯へ参照を変えるコミットでなければならないからです。

submoduleを簡単に運用するためのbot運用

Railsのmodel層を共有できるようになり当初の悩みを解決できた一方で発生した、Gitでの管理上の不便さを補う仕組みも用意しました。 submoduleレポジトリへのプルリクエストがマージされた際のwebhookをきっかけに、自動でsubmoduleを含むプロジェクトも更新されます。

submoduleレポジトリでメインブランチ上にマージコミット発生すると、botが自動でsubmodule更新のコミット(図の”ロ”と”B”)を行います。 これによりコミット”2”と”3”が両方含まれているコミットを コンフリクトすることなく使う ことができます。

また、bot導入前はsubmoduleのマージコミットに対して参照をしようとするとsubmoduleレポジトリ内のプルリクエストが閉じるのを待つ必要がありましたが、この運用では submoduleレポジトリ上のプルリクエストがマージされるのを待たず にマージコミットをそれぞれのRailsレポジトリで使っていくことができるので、コミット”2”や”3”を含め損ねることはありません。

その他にもbotによるアプリケーションのリリース時には、GitのタグをRailsレポジトリ内とsubmoduleレポジトリ内の両方につけることで、使用されていたsubmoduleレポジトリのバージョンを分かりやすくしています。

まとめ

今回のご紹介した運用の概要は以下です。

  1. 管理機能は別Railsプロジェクトに分けた
  2. model部分をsubmodule化して、複数のRails間で共有した
  3. submoduleの運用はbotに任せた

カウルでは今後もより良い運用を目指して日々改善を行っていきます!