💎

モノリシックなRailsアプリを、モジュラーモノリスを経てマイクロサービスに分解する

2023/10/13に公開

はじめに

Railsアプリをマイクロサービスに移行する専門チームにjoinして3ヶ月が経ちました。
これまでの学びをまとめておこうと思います。

マイクロサービスに移行するモチベーション

  • 外部サービスからの利用性向上
    例えば、ECサービスにおける「商品」や「顧客」といったマスタデータは異なるアプリケーション間で共通して使いたいモジュールです。これらのモジュールをマイクロサービスとして分離することで、外部アプリケーションから直接利用が可能となり、アプリケーション間の依存を減らして開発効率やユーザ体験が向上します。
  • アーキテクチャの最適化
    切り出したマイクロサービスに最適化した言語やフレームワーク、DBを選定できます。
    特にRDBはスケールアウトが難しく、各ユースケースに最適化した分散DBを選択できることは大きなメリットになります。
  • 開発速度と開発体験の向上
    コードベースが小さくなり、コードの見通しが良くなり開発速度が向上します。テストも小さく済むのでCIの待ち時間も減ります。
  • デプロイ頻度の向上
    コード変更の影響範囲が狭まり、デプロイサイクルが小さくなります。デプロイサイクルが小さくなれば、デグレの原因特定やロールバックも容易になります。

マイクロサービスに移行するデメリット

  • トラフィックの増加とパフォーマンスの劣化
    サービス間のトラフィックが増え、通信のオーバーヘッドが大きくなります。これによりパフォーマンスが劣化し、クラウド利用料も増加します。
  • 観測性の低下
    各サービス間でのバグやエラーの特定が難しくなります。サービスメッシュやAPMなどの対策が必須となります。
  • 強い整合性を保てない
    サービスを跨ぐトランザクションの設定は難しく、サービス間でデータ整合性の担保が難しくなります。また、通信相手のサービスが落ちている可能性を前提にした設計が必須となります。
  • サービス間でテーブルのjoinができない
    仮にサービス間でDBを共有している場合でも、サービス感でテーブルのリレーションを切る必要があります。
    このとき、例えばテーブル間のリレーションを利用してソートすることができなくなります。リレーションを使わない代替案は可能ですが、パフォーマンス要件が厳しくなってしまいます。
  • インフラの複雑化
    インフラのアーキテクチャが複雑化し、学習コスト、運用コスト、クラウド利用料などが増加します。

マイクロサービスの切り出し

ソフトウェアアーキテクチャ・ハードパーツにマイクロサービスの切り口やトレードオフが詳しくまとめられています。
どう言った切り口や粒度でマイクロサービスを切り出すかは本当に難しいです。自分は経験が浅く、うまく語ることはできませんが、下記を特に意識する必要があると感じています。

  • DDDの文脈での集約や境界づけられたコンテキスト
  • トランザクションがサービスを跨がない

移行手順

モノリシックなRailsアプリからマイクロサービスへの移行手順の概要をまとめておきます。
大きく3つのステップがありますが、実際には同時に実装を進めたり、機能ごとに小さく分割してリリースすることになります。

モノリシックなRailsアプリ

例として下記のような簡単なECアプリを想定します。
Active Recordが本当に優秀で、associationを自由にたどって簡単にViewを組み立てることができます。
例えば、@item = Item.find_by(xx)をControllerからViewに渡してあげるだけで、下記のようなJSONを構築できます。

{
  item_name: @item.name
  producer_name: @item.producer.name
  producer_account_number: @item.producer.account.number 
  costomer_name: @item.purchase.customer.name     
}

Step1: モジュラーモノリスへの移行

Railsの中でドメインを分割し、モジュラーモノリスを作ります。
今回の例ではCustomer Domain Account Domain Item Domainに分割することにします。
このとき、ドメインを跨ぐassociationを切る必要があります。
実際にmodelのassociationを切る必要はないでのですが、ドメインを跨ぐassociationを無いものとしてリファクタする必要があります。
目的のJSONを構築するためには、Controller側で3つのActive RecordをViewにに渡す必要があります。

  • @item = Item.find_by(xx)
  • @customer = Customer.find_by(xx)
  • @account = Account.find_by(xx)
{
  item_name: @item.name
  producer_name: @item.producer.name
  producer_account_number: @account.number 
  costomer_name: @customer.name     
}


上記は参照の例ですが、全てのactionでドメインを跨ぐassociationを切るリファクタを行います。
また、移行の前後でドメインを跨ぐassociationが新たに実装されてしまうことを防ぐために、arproxypackwerkをつかった静的解析をCIに組み込むことを検討する必要もありそうです。

Step2: Serviceクラスの作成

ControllerとActive Recordを分離します。
ControllerとModelの間にServiceクラスを設置し、ServiceクラスからControllerへのデータの受け渡しのためのAPIモデルを定義し、ActiveRecordから取得した情報をAPIモデルに詰め直します。
ActiveModel::ModelActiveModel::SerializationをincludeしたこのAPIモデルを定義することで、Active Recordと同様の挙動を与えてViewでのロジック修正を最小限に抑えることができます。

また、ドメイン間のやり取りも全てこのServiceクラスを通して行います。

api_model.rb
  module ApiModel
    class Item
      include ActiveModel::Model
      include ActiveModel::Serialization

      attr_accessor :id, :name, :producer_id
      ...
      
      def method_used_at_view
        ...
      end
    end
    
    class Customer
      ...
    end
  end

最終的にはドメイン分離の一環として別ドメインに対するbelongs_toやhas_one、has_manyを削除していきます。

Step3: マイクロサービスへの移行

Modelの中にあったドメインをマイクロサービスに移行します。
マイクロサービスではユースケースに最適な言語やフレームワークを選定できます。
マイクロサービスとのインターフェースをServiceクラスで吸収し、Controller目線では同じ挙動が担保されることが必須です。

さいごに

マイクロサービス化において難しいのは、もちろん実装面だけではありません。
バグを出さずに確実に切り替えを行うために考慮点は多くあり、深い技術的知見と共にビジネススキルも大切です。

  • 依存関係を整理しつつ細かくFeature Flagを設定し、機能ごとに少しづつリリースできること
  • 開発やQAのスケジュールを調整し、チーム間のalignが取れること

Discussion