肥大化したRailsモノリスを段階分割する進め方
「1行直すだけのはずが、関係ない機能まで壊れてリリースが止まる」「デプロイに30分かかり、その間は誰も触れない」——立ち上げ期に一気に機能を積み上げたRailsアプリが数年運用されると、こうした声をよく聞きます。最初は正しい選択だったモノリスが、チームとコードの成長とともに足かせに変わっている状態です。
この記事では、Railsモノリスが肥大化して変更が遅くなった状態を、サービスを止めずに段階的に分割していく進め方を、発注者向けに整理します。結論から言うと、いきなりマイクロサービス化を目指すのは多くの場合失敗の近道です。まずは「モノリスの中を整理する」段階を踏むのが現実的です。
なぜ「一気にマイクロサービス化」が失敗するか
肥大化したモノリスを見ると「サービスごとに切り離してマイクロサービス化すればいい」と考えたくなります。しかし、これを最初のステップとして選ぶと高確率でつまずきます。
- モノリス内部の責務境界が曖昧なまま切り出すと、サービス間で密結合な同期呼び出しが大量発生し、分散モノリス(distributed monolith)という「マイクロサービスの見た目をした、より障害に弱いモノリス」ができあがる
- トランザクション整合性がDB1つで担保されていたものが、サービスをまたぐと途端に難しくなり、その対策(Saga、Outboxパターン等)を運用する体力がチームにない
- 運用対象がサービスの数だけ増える(デプロイパイプライン・監視・ログ集約・障害対応)。分割前より開発速度が落ちるケースも珍しくない
マイクロサービス化は「組織がサービスごとに独立してデプロイ・スケールする必要に迫られてから」選ぶ手段であり、コードの見通しを良くする目的だけで選ぶ手段ではありません。多くの相談案件では、実際に必要なのはネットワーク越しの分割ではなく、モノリスの中の責務を整理することです。
まずモジュラモノリスを目指す
段階分割の最初のゴールは、プロセスは1つのままでもモジュール間の境界が明確な状態、いわゆるモジュラモノリスです。
- Railsで言えば、
app/modelsに全ドメインのモデルが並列に置かれている状態から、app/domains/billing・app/domains/inventoryのようにドメイン単位でディレクトリを切り、モジュール間の依存を一方向に整理する - モジュール間でDBテーブルを直接JOINしない、直接ActiveRecordを呼ばずサービスクラス経由でアクセスするなど、「将来サービスを分けるとしたらここが境界になる」線を先にコード上に引く
- Packwerk(Shopify製)のようなツールで、モジュール間の意図しない依存をCIで検知できるようにする
この段階を踏むと2つの利点があります。1つは、変更の影響範囲がドメイン単位に閉じるため、「1行直したら関係ない機能が壊れる」問題がその時点でかなり解消されること。もう1つは、将来本当にサービスとして切り出す必要が出たときに、モジュールの境界がそのままサービスの境界の候補になり、分割コストが大きく下がることです。
ストラングラーパターンで無停止に進める
モジュールが整理できたら、必要な部分だけを外に切り出していきます。ここで使うのがストラングラーパターン(Strangler Fig Pattern)です。名前の由来は、既存の木に巻きついて少しずつ覆っていく絞め殺しの木(ストラングラーフィグ)で、旧システムを一気に置き換えるのではなく、新しい実装で少しずつ「覆っていく」進め方を指します。
具体的な進め方は次の通りです。
- 切り出す機能への入り口を1つに集約する(ルーティングやAPI Gatewayで、その機能へのリクエストを判別できるようにする)
- 新しい実装(別サービス、あるいは同一Rails内の疎結合なモジュール)を用意し、まずは読み取り専用の機能から新実装に向ける
- 書き込みを伴う機能は、新旧両方に書き込んで結果を突き合わせる期間(シャドーイング)を挟み、差分がないことを確認してから新実装に完全移行する
- 移行が終わった機能から、旧モノリスの該当コードを削除する
この進め方の最大の利点は、常にどちらか片方が本番で動いている状態を維持できることです。「移行作業のためにメンテナンス画面を出す」必要がなく、問題が起きても入り口の振り分けを戻すだけでロールバックできます。一気に全部を書き換える計画は、途中で止めた瞬間に「新旧どちらも中途半端」という最悪の状態に陥りやすく、この点でもストラングラーパターンのほうがリスクが低い進め方です。
切り出す順番の決め方
「どこから手をつけるか」は分割の成否を左右します。私たちが優先順位をつけるときに見る観点は次の通りです。
- 変更頻度が高く、かつ影響範囲が読みにくい領域から。ここを整理すると、日々の開発速度への効果が最も早く出る
- 他ドメインとの依存が少ない(疎結合な)機能を先に。通知・レポート生成・バッチ処理のような、他機能からの参照が少ない領域は切り出しやすく、練習台としても適している
- スケール特性が違う機能を優先。例えば画像処理や集計バッチのように、アプリ本体と負荷特性が大きく異なる処理は、独立させることでインフラコストの最適化にもつながる
- 逆に、決済やユーザー認証のような強い整合性が求められる中核機能は最後に回す。分割の経験値が低いうちに手をつけると、事故が発生したときの被害が大きい
「一番汚いコードから直したい」という気持ちはよくわかりますが、汚さと分割の優先度は必ずしも一致しません。汚くても変更頻度が低く依存が少ないコードは、実は後回しにして問題ないことが多いです。
デプロイが重い問題への即効性のある対策
段階分割は数ヶ月単位の取り組みですが、「デプロイに30分かかる」といった即効性の高い課題には、分割と並行して打てる手もあります。
- テストスイートの並列実行化、CI上でのキャッシュ活用によるビルド時間短縮
- Docker イメージのレイヤーキャッシュ最適化
- デプロイのたびに全アセットを再ビルドしていないか(差分ビルドで済む場合が多い)
これらはモジュラモノリス化やストラングラー分割を待たずに着手でき、開発体験の改善を早期に体感してもらえるため、私たちは段階分割の計画と並行してこうした「すぐ効く」施策も一緒に洗い出すようにしています。
マイクロサービス化の罠、もう一つ
仮に将来本格的にサービス分割へ進む場合でも、注意すべき罠があります。それは「組織がサービスの数に追いついていない」状態での分割です。1つのチームが5つのサービスを掛け持ちで見る体制は、サービスをまたぐ変更のたびに調整コストが跳ね上がり、モノリス時代より遅くなることが実際にあります。Conway's Law(組織構造がシステム構造に反映される)はこの逆も真で、チーム体制と分割の粒度は揃える必要があります。分割そのものが目的化しないよう、常に「今のチーム規模で運用しきれるか」を基準に判断することをおすすめします。
まとめ
- 肥大化したRailsモノリスの分割は、いきなりマイクロサービス化を狙うのではなく、まずモジュール境界を明確にする「モジュラモノリス化」から始めるのが現実的
- 実際の切り出しは、入り口を1つに集約してから新実装へ少しずつ移行する「ストラングラーパターン」で進めると、サービスを止めずにロールバック可能な状態を保てる
- 切り出す順番は「汚さ」ではなく変更頻度・依存の少なさ・スケール特性で決め、中核機能は経験を積んでから最後に回す
関連する論点は、技術的負債は改修とフルリプレイスどちらを選ぶべきかやモノリスからAPIファースト構成への移行でも詳しく解説しています。既存プロダクト改善の全体的な進め方は、既存プロダクトの改善、外部チームが最初に見る観点をご覧ください。torcheees ではRuby on Railsを中心にこうしたモノリス分割・モダナイゼーションを手がけており、モダナイゼーションのサービスとして提供しています。
「今のモノリスをどこから分割すればいいか分からない」という段階でも構いません。まずは1〜4週間の「要件整理・開発診断」で、現状のコード構造と依存関係を一通り確認し、分割の優先順位と概算費用をご提示します。お問い合わせフォームからお気軽にご連絡ください。