hacomono TECH BLOG

フィットネスクラブ・スクールなど施設・店舗のための会員管理・予約・決済システム「hacomono」 開発チームの技術ブログ

モノリスなRailsにモジュラーモノリスを導入した話

こんにちは、プラットフォームチーム所属のまこたすです。

昨今、様々な場で「モジュラーモノリスを導入した」という話を目にするようになってきました。弊社でも昨年からモジュラーモノリスの試験導入を進めており、社内でノウハウが徐々に溜まってきたため、今回 技術ブログ で なぜ導入したのかと知見の共有 をさせていただけたらと思います。

想定読者

  • モノリスなアプリケーションの分割を検討している
  • Railsへのモジュラーモノリスの導入を検討している

話さないこと

  • チーム体制がどうあるべきかという観点の話
  • 以下アーキテクチャについての詳細
    • モノリスアーキテクチャ
    • モジュラーアーキテクチャ

背景

今回「モジュラーモノリスを導入した」というタイトルですが、最初に検討・導入に至るまでの背景について触れたいと思います。

hacomonoという組織・サービスの成長

hacomonoというサービスはリリースから現在に至るまで、APIサーバーにRailsが使われており、一つのアプリケーションとして稼働しています。アプリケーションの内部は、CleanArchitectureが採用されており、予約、入退館、マスタ管理など様々な機能が実装されています。2022年4月まではこのアプリケーションを10名前後のエンジニアがワンチームで開発をしていました。

2022年5月からは開発メンバーも増え、これまでワンチームだった開発組織が機能別にチームが別れ、複数チームで一つのアプリケーションを開発する体制になりました。このような体制になってくると以下のような問題/要望が発生してきます。

  • チーム間の改修コンフリクト
    • 機能を修正したいが影響範囲が読めない
    • 正しい機能の仕様がわからない
  • 自チームの担当機能は責任を持って保守したい
    • 他チームによる改修に気づきたい

このような問題は、ワンチームで開発していた頃に比べて開発速度を低下させ、顧客への価値提供を遅らせる要因になります。そこでワンチームで開発をしていたころと同様の開発速度を維持するために、機能を担当するチームが自チームの領域をもてるようにする仕組みの整備が必要になってきました。

今回hacomonoでは、チームが自身の領域をもつ = チームが担当する機能の分離 という観点で捉えてモジュラーモノリスとマイクロサービスという2つの分離方法の導入を検討しました。

モジュラーモノリス ・ マイクロサービス

最初にそれぞれのソフトウェアアーキテクチャについて紹介したいとおもいます。

モジュラーモノリスとは

モジュラーモノリスとは、モノリスな一つのアプリケーションの内部をモジュール(機能)という単位で分割したアーキテクチャのスタイルを指します。内部的な分割のみなので、開発/テストの実施・デプロイの方法などの方法はモノリスの時の資産をそのまま活用できる特徴があります。モジュールという単位で分割できれば、コード上のスパゲッティな状態というものは生まれにくくなり、チーム毎に担当するモジュールのみに専念すればよいという状況を生み出すことができます。

マイクロサービスとは

マイクロサービスは、アプリケーションを小さな独立したサービス(マイクロサービス)に分割するアーキテクチャのスタイルです。各マイクロサービスは、特定のビジネス機能を担当し、物理的に独立して開発、デプロイ、スケーリングされます。通常、各サービスは独自のデータベースを持ち、APIを通じて相互に通信されるという特徴があり、1サービス = 1チームのような担当領域の決め方をする場合が多いです。このアーキテクチャは、スケーラビリティや柔軟性に優れてはいますが、モジュラーモノリスと比べてCI/CDの整備・システムの監視など運用のためのコストが追加で発生してきます。

それぞれの物理的な図のイメージです。

モジュラーモノリス or マイクロサービス

ここまで紹介したようにモジュラーモノリスとマイクロサービスどちらにおいてもチームが担当領域を持つことができることがわかりました。ここからはhacomonoにおいて、どのような検討がありモジュラーモノリスを採用する判断に至ったのかについて説明します。

判断をするにあたり以下のようなマトリクスで整理をしました。

様々な軸を設けて現在のhacomonoという組織・サービスの成長という観点を考慮してx, △, ◯の三段階で評価しています。以下それぞれの項目で一言コメントで評価について補足します。(項目ごとで時間軸と比較対象で若干ブレがでてしまいましたmm)

  • コミュニケーション
    • モジュラーモノリス
      • 分割した時点でチーム間のコミュニケーションが必須になるため → △
    • マイクロサービス
      • モジュラーモノリスと同様 →△
  • 開発生産性
    • モジュラーモノリス
      • 元のRailsのアプリケーションのまま、モジュール部分の開発をできるため →◯
    • マイクロサービス
      • 新規アプリケーションで、開発にあたり様々な決め事が発生するため初期は →△
  • 事業成長に対する柔軟性
    • モジュラーモノリス
      • hacomonoのサービスが想定していない方向に成長しても、アプリケーション・データ層が物理的に共通のため修正が行いやすいため →◯
    • マイクロサービス
      • モジュラーモノリスの対局にあるため → ☓
  • 価値提供速度
    • モジュラーモノリス
      • モノリスな場合と変化がないため → △
    • マイクロサービス
      • CI/CDの仕組みができさえすれば、小さい粒度でリリースができるため → ◯
  • 安定性
    • モジュラーモノリス
      • ベースはモノリスなアプリケーション上にあるため、各モジュールやモノリスのミドルウェア等の共通部分の改修による影響はあるため → △
    • マイクロサービス
      • 他チームの影響は受けないため→ ◯
  • 導入コスト
    • モジュラーモノリス
      • 先述の通り既存のRails上での開発になるため、マイクロサービスとの対比で → ◯
    • マイクロサービス
      • ゼロベースからの検討が必要 → x
  • 運用コスト
    • モジュラーモノリス
      • 先述の通り既存のRails上での開発になるため、マイクロサービスとの対比で → ◯
    • マイクロサービス
      • ゼロベースからの検討・構築が必要 → x

長くなってしまいましたがざっくり一言コメントと評価を書き出しました。改めてみてもそれぞれ一長一短であることがわかります。

hacomonoでは、以下の背景がありモジュラーモノリスを選択しました。

背景

  • 開発組織の拡大速度が急である
  • プロダクトの成長に、よりフォーカスするフェーズにある

選択理由

  • プロダクト・事業の成長に合わせてモジュールのAPIを見直せる柔軟性
  • 引き続き同じRails上での開発のため、ほぼほぼプロダクト開発速度を落とさないで導入できる

もしも今よりサービスが成長しドメイン境界が変わらないフェーズまできていて、かつ開発リソースにも余裕がある状態であれば、マイクロサービスを選択していたかもしれません。 ここは企業のフェーズにあわせて判断を変えていく必要があるかと思います。

Rails x モジュラーモノリス

ここからはhacomonoのRailsアプリケーションにモジュラーモノリスをどのように導入したかについて紹介させていただきます。

モジュール分割の実現

Ruby製のアプリケーションにおいてモジュール分割するための壁は、自由な呼び出しをどのように制御するかという点にあると思います。

Rubyにおいてクラスには公開メソッドとプライベートなメソッドがあり、前者は自由にクラス外から呼び出す事ができます。この自由な呼び出しは、同じモジュール内でのクラスであれば問題ないですが、モジュール外から意図せずよばれてしまうと、モジュラーモノリスのモジュールを実現することができません。

そこで今回Railsプロダクトでも最大級のサービスを運営しているShopify社が公開している packwerk というgemを利用しました。こちらは元々Rubyでのモジュラーモノリスをサポートするために作られたライブラリで、1モジュールをpackageという概念で管理することができます。仕組みとしては自身で定義したルールに従って、namespaceがルール通りに呼び出しあっているかを静的解析でチェックを行っています。

# 静的解析のチェックは以下を実行するだけ

bundle exec packwerk check

ルールはymlで定義でき、以下のような設定が可能です。

# package.yml

enforce_dependencies: true # パッケージの依存チェックを有効にする
enforce_privacy: true      # プライバシーチェックを有効にして、public api以外での呼び出しがないかチェックする
public_path: public/       # packageの公開APIディレクトリを設定する
dependencies:
  - "packages/xxx"         # packageが依存してもいい、パッケージを設定する

これにより、モジュールとしてどういう振る舞いができるかを定義でき、Railsアプリケーションで論理的に分割されたモジュールを実現できます。

hacomonoでは、packageのディレクトリ毎に担当チームをCodeOwnerとして設定することで、他チームが担当領域に手を入れてしまうケースを検知できるようにしています。

ここまでの設定は、Rails generatorを用いて自動でおこなえるようになっており、コマンド一発で新規のpackageの作成が行えるように整備されています。

また、アディショナルな対応としてpackageが公開しているAPIのスキーマをprotoで定義できるようにしています。これは将来サービスが成長した際の切り出し先のアプリケーション構成として、go x gRPCを利用したマイクロサービスを想定していて、現段階からprotoの定義に沿ったコードを公開API部分に利用することで、切り出し時の移行の(引数の型の見直しやチェックなどの)コストを低くするという狙いです。

protoからRubyコードを生成する仕組みは、grpcが公開しているgrpc_tools_ruby_protocを利用して生成しています。

永続化層の分割の実現

次にRailsでモジュラーモノリスを導入となった際の壁として、データベースのテーブルへのアクセスの分割があります。コードベースだけで分離しても、コードから各テーブルへのアクセスが自由に行われると将来的に物理的な分離を行う際の移行で大変苦労します。そのためpackageからアクセスできるテーブルは限定される必要があるのですが、Railsが利用しているActiveRecordは様々な記法ができるため全てのケースに対して愚直にチェックを行うと手間がかかります。

今回このチェックはcookpad社が公開されている arproxy を用いて、実行される直前のSQLに対して正規表現でチェックをするというPluginを作成しチェックを行うという地道な戦略を取っています。本番で全てのクエリに対してこの方法を取ってしまうとオーバーヘッドが大きくなるため、検証環境・ローカル・テストの場合のみでチェックを行なっています。今回のチェックはあくまで社内事情であるためこの方針で事足りています。

※スライド中のRequestContextは、RailsのThread.current相当の挙動をしています。

ここまでが執筆時点での、モジュラーモノリスの導入の現状になります。

今後の展望

ここまで導入を進めてきまして、いくつか課題も見えてきました。

1つ目は、packageのインターフェース定義として一律でprotoを採用しているが、将来的に物理的に切り出されないであろうモジュールに対しては、protoを利用することで発生する手間のコストが上回ってしまうかもしれないというところです。慣れやエコシステムの整備不足の問題もあるかもしれませんが、そういった特性のモジュールの場合はリスクを減らした上でもっと手軽に開発ができないかを検討していく必要があると感じています。

2つ目は、モジュールとしての切り出し単位の精度のあげ方です。ここを決めきるのが難しいという考えもありマイクロサービスではなくモジュラーモノリスという手段を取ってはいるのですが、やはり後から手直しするコストはなるべく少なくしたいので、精度をあげるための考え方のフレームワークを見出さねばと考えています。

まとめ

いかがでしたでしょうか。これまでhacomonoで取り組んできたRails x モジュラーモノリスの取り組みがこれからモジュラーモノリスを導入していこうと考えている方の参考になれば幸いです。

またコードベースでどうしてるの?などその他の質問がございましたら、お気軽にお問い合わせください。

参考リンク


株式会社hacomonoでは一緒に働く仲間を募集しています!
採用情報や採用ウィッシュリストも是非ご覧ください!