ひとつだった Omnilude バックエンドを AI とともに MSA へ移していった過程
はじめに
この記事は Omniludeを作るために、こんなバックエンドを作っています の続きです。前回の記事では、Omnilude バックエンドの現在の構造と責務の境界を、ひとつのプロダクトバックエンドとして紹介しました。今回は、その構造が実際にはどのようにひとつの大きなプロジェクトから複数のサービス境界へと分かれ始めたのかをお話しします。
MSA の話は、どうしても抽象的に聞こえがちです。サービスを分けた、独立デプロイできるようになった、境界を引いたと言うのは簡単ですが、実際のプロジェクトの中で何がどう変わったのかは見えにくいからです。
Omnilude バックエンドには、その変化を説明しやすい材料があります。今は auth-service、backbone-service、storage-service、ai-service、game-service、blog-service、legacy-service のように分かれていますが、最初からそうだったわけではありません。最初はひとつの大きな Spring Boot バックエンドでした。
この記事では、そのひとつのプロジェクトがなぜ分かれ始めたのか、なぜ jspring(common) を先に取り出したのか、なぜ legacy-service を意図的に残したのか、そしてこの移行で AI が実際にどこまで役立ったのかを、できるだけわかりやすく整理します。
最初はただ大きなバックエンドひとつでした
移行前の Omnilude バックエンドは、ひと言でいえば、いろいろなものがひとつの場所に集まっている構造でした。認証もあり、AI もあり、ゲームもあり、ファイル保存もあり、リアルタイムイベントもあり、共通ユーティリティもありました。そのすべてが同じプロジェクトの中に入っていました。
こういう構造は最初は便利です。機能追加も速く、同じコードベースの中でそのまま直せるからです。ですが、プロジェクトが大きくなるほど問題が見えてきます。
- 認証のように慎重に扱うべき領域
- AI のように頻繁に変わり実験も多い領域
- リアルタイムイベントのように安定性が重要な領域
- ゲームのようにドメインロジックが急速に増える領域
それらすべてが同じデプロイ圧力を受けるようになります。
たとえるなら、季節物の服、トレーニングウェア、スーツ、毛布、工具箱を全部ひとつのクローゼットに入れているようなものです。最初は便利でも、ある時点からは何かひとつ変えるだけでも負担が大きくなります。
なぜわざわざ分けようとしたのか
私は最初から MSA という形そのものを目標にして動いていたわけではありませんでした。もっと現実的な理由のほうが大きかったです。
いちばん大きかったのは、長く接続を維持しなければならない機能を通常のデプロイ影響から切り離したかったことです。Omnilude には、backbone-service のように WebSocket、SSE、イベントストリーム、長時間かかる非同期処理に近い領域がありました。こうした領域は、関係のないドメインの修正で揺れると、運用体感がすぐに悪くなります。ブログやゲームの修正ひとつで接続性の高いサービスまで再起動される構造は、長く持たせにくいと感じました。
二つ目はデプロイ時間です。小さな修正ひとつでも、結局は大きなアプリ全体をビルドし、イメージ化し、再デプロイしなければなりませんでした。プロジェクトが大きくなるほど、その時間はどんどん惜しくなります。よく変わる領域の試行速度を上げたくても、デプロイ単位がひとつだと全体が一緒に遅くなります。
その次に出てきたのが責務境界の曖昧さでした。認証ルール、共通例外、内部 HTTP、メッセージング、ファイル保存のようなものがコードベース全体に混ざっていたので、何が共通で何がドメインなのかがだんだん見えにくくなっていきました。
そしてさらに大きな理由もありました。これから作りたいものが明確だったからです。Omnilude では AI 機能も伸ばしたかったですし、ゲームサービスも育てたかったですし、ブログやツールのような別プロダクトも増やしたかったのです。ところが、すべてをひとつのバックエンドの中で大きくしていくやり方は、ある時点から良い選択ではないと判断しました。
つまり、この移行は MSA が格好良く見えたからやったのではありません。接続を守るべきサービスを保護し、デプロイ時間を短くし、これから作りたいものを押し上げるために構造を組み直す必要があった、というほうが近いです。
最初の一歩はサービス分割ではなく共通基盤の切り出しでした
ここで大事なのは、この移行の最初の一歩が auth-service や ai-service を切り出すことではなかったという点です。その前にまず common を作りました。
ここはかなり重要です。サービス分割は外から見るとドメインを切る作業に見えますが、実際にはその前に何が共通規約なのかを外へ出す必要があります。
Omnilude では、その役割を common が担いました。
- 共通の例外処理
- JWT の規約
- 共通 DTO
- Web 層のユーティリティ
- JPA とデータアクセスの基盤
- 内部 HTTP とメッセージングの補助
こうしたものを先に common にまとめておいたことで、その後サービスを分けても、各サービスがばらばらのアプリになるのではなく、同じ言語を話す構造を維持できました。
たとえるなら、部屋をいくつかに分ける前に、各部屋で共通して使うコンセントの規格やドアノブの規格を先にそろえておくようなものです。そこが合っていなければ、部屋を分けても暮らしやすくはなりません。
分離はかなり短い期間で一気に進みました
実際のリポジトリ履歴を見ると、この移行は驚くほど短い期間で進んでいます。2026年1月18日から22日の間に、主要な骨格がほぼ整いました。
流れを単純化するとこうです。
commonを抽出auth-serviceを分離backbone-serviceを分離storage-serviceを分離ai-serviceの分離を進める- 残った領域を
legacy-serviceに隔離 game-serviceを抽出- その後
blog-serviceを新しいサービスとして載せる - Jenkins と Kubernetes を選択デプロイ前提で整理する
この順番には理由がありました。
auth-service は比較的境界がはっきりしていました。認証、アカウント、権限は他サービスから共通に参照されるため、先に切り出すと後続の構造を決める基準になります。
backbone-service はリアルタイムイベント、メッセージング、DTE など、インフラ寄りの性格が強い領域でした。変化の速いプロダクトロジックと分けておくほうが、運用上かなり自然でした。
storage-service には複数サービスが使うファイル保存契約を持たせたかったのです。ファイル保存をひとつの場所にまとめておけば、ブログ、ゲーム、AI 生成結果が同じ流れに乗せられます。
次が ai-service でした。ここは規模も大きく依存関係も多かったので難易度が高かったです。ただ、Omnilude の中でも特に実験頻度が高くなる領域だったので、独立サービスとして扱うのが自然でした。
なぜ legacy-service を残したのか
MSA への移行というと、きれいに全部を分けてモノリスを一気になくすイメージを持ちやすいです。ですが、実際にやってみると、そうした終わり方は思っている以上に非現実的です。
そのため Omnilude では、まだ完全に切り出せていないドメインを legacy-service という名前で別にまとめました。
これは後退というより、緩衝材に近い判断でした。
legacy-service があったからこそ、次のことが可能でした。
- まだ移していない機能を無理に壊さずに済んだこと
- 新しいサービスと古いサービスを同じプロダクトの中で共存させられたこと
- 分離の速度を保ちながら運用を続けられたこと
- 難しいドメインは後回しにして、簡単なものから独立させられたこと
私はここが、むしろ現実的な MSA 移行の核心だと思っています。実際の移行は、完璧な分離よりも、まず運用できる境界を作ることに近いからです。
ゲーム、ブログ、AI はその上でさらに輪郭がはっきりしました
その後は、よりプロダクト色の強いサービスたちが分かれていきます。
ai-service は単なるチャット API ではありません。エージェント実行、ワークフロー、生成パイプライン、メディア生成、ロールプレイ、商品エージェントなど、多くの責務を抱えています。この規模になると、ひとつの独立サービスとして見るほうが自然です。
game-service は、最終的に Omnilude の中で最も大きく育てたい領域に近いサービスでした。ストーリークイズ、シナリオ、アセットスタジオなどが積み上がるにつれて、AI、ストレージ、認証を束ねるドメイン中心のサービスになっていきました。
blog-service は少し性格が違います。これは元のモノリスからそのまま切り出されたというより、すでに分離が進んだ構造の上に新しく 자리を得たサービスに近いです。デプロイと共通規約がすでに分かれていたので、ブログは最初から独立サービスのように設計して運用できました。
つまり、この MSA 移行は古いものを切り離す作業だけではありませんでした。その後に作る新しいプロダクトを、よりよい単位で積み上げるための土台でもありました。
本当に変わったと感じた瞬間はデプロイでした
コードだけを見ると、サービスが分かれたように見えることはあります。ですが、MSA が本当に実感として変わるのは、デプロイ構造が変わったときです。
Omnilude では、その痕跡が Jenkinsfile、Jenkinsfile.prd、そして kubernetes/services/** に残っています。
現在、Jenkins で選択デプロイできるサービスは次の 7 つです。
auth-servicebackbone-servicestorage-serviceai-servicegame-serviceblog-servicelegacy-service
つまり、リポジトリはひとつでも、もはや全部を一緒にデプロイするアプリではありません。必要なサービスだけを選んでリリースできる構造になったということです。
Kubernetes も同じ方向です。各サービスは自分専用の Deployment と Service を持ち、Ingress は api.omnilude.com や dev-api.omnilude.com の下で path prefix を 기준に振り分けています。
ここが重要です。MSA はフォルダを分けることではなく、運用単位を分けることです。Omnilude では、この地点を越えてはじめて、本当に分離されたという感覚が強くなりました。
最後に重要だったのはサービス数ではありませんでした
こうした移行をすると、つい何個のサービスに分けたのかに目が向きます。ですが、今振り返ると重要だったのは数ではありませんでした。
本当に重要だったのは、次の四つでした。
- 共有規約を先に外へ出して、その後の分離を可能にしたか
- よく変わるドメインと、あまり変わらないインフラを区別できたか
- 完璧でなくても運用できる境界を作れたか
- コードの分離とデプロイの分離を実際に結びつけられたか
この基準で見ると、Omnilude の移行はかなり現実的だったと思います。完璧にきれいな図ではなくても、次のプロダクトと次の実験を載せ続けられる構造を先に作れたからです。
まとめ
Omnilude バックエンドの MSA 移行は、最初から大きな戦略文書があって始まったものというより、もっと大きなものを作るために、結局は通らなければならなかった構造整理に近い作業でした。
ひとつの大きなプロジェクトから始まり、common を取り出し、auth、backbone、storage、ai、game、blog を分け、残りは legacy-service にまとめ、最後に Jenkins と Kubernetes 上の実デプロイ単位までそろえていきました。
私はこの流れがとても Omnilude らしいと思っています。Omnilude という名前には、omni- が持つ包摂性と、-lude が持つ遊びの感覚が同時に含まれています。多くの機能やサービスが永遠に散らばるのではなく、最終的にはひとつの大きな遊びの体験に集まっていく。そう考えると、この移行は名前そのものにもよく似ています。目指すものが明確だったからこそ、構造もそれに合わせて変わっていったのです。
次の記事では、構造の話からもう一歩進んで、実際にゲームをどう実装しているのかという話をお届けするつもりです。