テクノロジー

Omniludeを作るために、このようなバックエンドを組んでいます

AAnonymous
13分で読めます

この記事は、ひとつの機能を紹介するためのものではありません。Omniludeというプロジェクトがどのような構造の上で作られているのか、そしてなぜ私はこのようにバックエンドを組織しているのかを整理するための文章です。

今私がやっていることは、二つの流れが重なっています。ひとつは、コーディングエージェントの限界を試すことです。バイブコーディングと、意識の流れに近い指示だけで、実装がどこまで進められるのか。そのやり方が、本当にプロダクションレベルの開発まで届くのかを継続して確かめています。もうひとつは、その実験を実験のままで終わらせず、最終的には実際のサービスにつなげることです。その中心にあるのがOmniludeであり、私はこの構造の上で、以前から作ってみたかったゲーム開発を通じて、まずこの実験を前に進めたいと考えています。

そのため今回の記事は、実装の自慢というより構造の紹介に近いものです。ただ、単にサービス名を並べるだけで説明したくはありませんでした。このバックエンドはいくつかのモジュールに分かれていますが、私が実際に見ている捉え方は少し違います。私はこれ全体を、ひとつのプロダクトバックエンドとして見ています。認証、AI実行、コンテンツ管理、リアルタイム接続、ファイル保存、イベントストリームが、すべてひとつの目標に向かって結び付いているからです。

長くて少し重たい記事になるかもしれませんが、できるだけ具体的に説明してみます。

Omniludeは何を目指すプロジェクトなのか

Omniludeには、私にとって同時に二つの意味があります。

ひとつ目は実験です。今の時代、開発者にとって新しいツールを少し触ってみるだけでは足りないと私は考えています。AIをうまく使える人とそうでない人の差は、これからますます大きくなる可能性があります。そしてその差は、実装速度そのものよりも、問題設定、構造設計、検証の仕方のほうでさらに大きく広がっていくはずです。私はその変化を外から眺めるだけではなく、実際にプロダクトを作りながら自分の身体で通過してみたかったのです。

ふたつ目は、とても現実的なプロダクト目標です。私はずっとゲームを作りたいと思ってきました。しかし、その目標に昔のやり方だけで近づきたくはありません。AIを積極的に活用する制作ツール、コンテンツ生成パイプライン、運用可能なバックエンド、そしてリアルタイム体験までを、ひとつの流れとして結び付ける形で取り組みたいと考えています。Omniludeは、まさにその方向の上に置かれているプロジェクトです。

つまりこのプロジェクトは、AIを最大限に活用するためのAIエージェント実験であると同時に、最終的にはリリース可能なサービスへと収束しなければならないプロダクト開発プロジェクトでもあります。

複数のサービスですが、私はひとつのプロダクトバックエンドとして見ています

この記事を書いている現在、Omniludeのバックエンドはまだひとつのモノリシックサービスです。とはいえ、私はこのモノリスを単なる巨大なサーバーひとつとしては見ていません。内部に複数の責務境界がすでに共存している構造として見ています。この記事に登場する auth-serviceai-serviceblog-servicegame-servicestorage-servicebackbone-servicemmorpg-service、そして共有基盤である common(jspring) は、今この瞬間のデプロイ単位というより、私がこのシステムを理解し、拡張していくための基準に近いものです。この記事では、この内部共通レイヤーを便宜上 common と呼びます。

ですから、ここで言う「複数のサービスだがひとつのプロダクトバックエンドとして見る」という表現は、現在の構造を大げさに飾り立てるためのものではありません。むしろ、まだひとつにつながっているシステムの中で、どの責務がすでに分かれていて、これからどの方向により明確になっていけるのかを説明するための見方に近いです。重要なのはサービスの数そのものではなく、それぞれの責務がどのような共通規約の上で、ひとつのプロダクトに向かって結び付いているのかです。

私は今のOmniludeを、巨大なサーバーひとつとしてだけではなく、ひとつのプロダクトバックエンドの中に複数の境界が共存している構造として見ています。こうした境界が将来的にリポジトリやデプロイ単位として実際に分かれていく過程は、別のポストとして残すつもりです。この記事では、現在のモノリシック構造の中にすでに入っている責務分離を、今の言葉でもう一度広げて説明してみます。

以下の図は、現在の非レガシーなバックエンドを、ひとつのプロダクトバックエンドとして束ね直して見たオーバービューです。

この図で私が重要だと見ているポイントは三つあります。

第一に、common が全体の土台になっていることです。各サービスは独立して存在していますが、認証規約、内部HTTP呼び出しの方式、イベント発行、ストレージ処理、分散ジョブ処理は、すべて共有基盤の上で繰り返されています。だからこそ構造が散らばりません。

第二に、プロダクトドメインと運用基盤が分かれていることです。authaigamemmorpgblog はプロダクト機能を表し、storagebackbone はそれらが実際に動くために必要な共通運用基盤を担っています。

第三に、この分離そのものがAIにとって扱いやすい構造だということです。コーディングエージェントは、文脈のない巨大なコードベースをうまく扱えません。一方で、役割が分かれ、規約が明確な構造では、はるかに安定して動きます。私はこの構造を、人間が保守しやすいようにするためだけでなく、AIにとってもよりよい作業環境を与えるために設計しています。

この構造の中心には common があります

このプロジェクトを理解するとき、最初に説明しなければならないのは common です。最終的に、全体がひとつのプロダクトのように動く理由がそこにあるからです。

common は単なるユーティリティ集ではありません。私はこれを、内部プラットフォームSDKに近いものとして見ています。実際にサービス間の接続を見ていくと、すでに多くのものが common の中で標準化されています。

  • common-core は JWT、共通例外、基本ユーティリティのような最も低い基盤を提供します。
  • common-web は内部HTTPクライアントと共通Webレイヤー規約を提供します。
  • common-data は JPA、QueryDSL、共通データアクセス基盤を束ねます。
  • common-messaging はイベント発行経路を統一します。
  • common-cloud はストレージコミットやアクセスURLなど、ファイル処理連携を標準化します。
  • common-dte は分散ジョブ、ワークフロー、Job Eventフローを提供します。

このレイヤーが重要だと考える理由は明確です。AI-First開発では、コードをどれだけ速く書けるかよりも、システムがどれだけ反復可能なパターンを持っているかのほうが重要になります。新しい機能を追加するたびに、人間でもエージェントでも、毎回まったく新しい構造を理解しなければならないのであれば、生産性はすぐに落ちます。逆に共有基盤が強ければ、責務をより小さな単位に分けて任せることができ、レビューすべきポイントも明確になります。

たとえば game-service がAI機能を呼び出すとき、いきなり外部モデルへ直接つながるのではなく、内部的に ai-service を呼びます。ファイル保存も、各サービスがそれぞれS3を扱うのではなく、storage-service を経由する形に整理されています。イベントも必要に応じて backbone-service を中心に流れます。こうした一貫性があるからこそ、私はAIに対してより正確な指示を出せますし、結果の検証もより速くできます。

開発者にとっても、この構造は重要です。サービスが多く見えても、実際には同じ言語、同じフレームワーク、同じ規約、同じ内部プラットフォームを共有しているため、思考の単位が完全に断絶しません。私はここがOmnilude構造の大きな強みのひとつだと考えています。

プロダクトドメインはなぜこのように分かれているのか

ここからは、各サービスがどのような役割を担っているのかを、もう少し具体的に説明してみます。単にサービス名を並べるより、なぜそう分けたのかを見るほうが重要です。

auth-service: 信頼の出発点

auth-service は、ログイン、アカウント、デバイス、権限、クレジット、アクティビティログを担当します。見た目には比較的よくある認証サーバーに見えるかもしれませんが、私はこれを単なるログインAPIとは考えていません。このサービスは、プロダクト全体の信頼境界を担っています。

ユーザーが誰なのか、どのセッションを持っているのか、どんな権限を持っているのか、どのようなクレジットや利用履歴を持つのかは、ほとんどここから始まります。ほかのサービスがより速く機能実験を進め、新しい能力をつないでいくためには、この基礎的な信頼レイヤーこそ、より保守的で明確である必要があります。

そして興味深いのは、このサービスが完全に孤立しているわけではないことです。起動時には backbone-service やプラットフォーム設定と同期し、バックエンド全体の共通JWT規約を一緒に形作ります。つまり認証は、独立した機能であると同時に、システム全体の接続言語でもあります。

ai-service: AI実行のハブ

ai-service は、このプロジェクトで最も象徴的なサービスです。LLM呼び出し、マルチエージェントワークフロー、メディア生成、商品エージェント、ロールプレイ、各種システム系AI APIがここに集まっています。

重要なのは、このサービスが単に外部モデルAPIをプロキシするだけではないという点です。私はこれをAI実行のハブだと見ています。どのモデルを呼ぶのか、どのワークフローを通すのか、どのイベントを発行するのか、結果物をどこに保存するのか、進捗を外部へどうストリーミングするのかを決める中心レイヤーです。

この構造は今後さらに重要になると考えています。AIを積極的に活用するプロジェクトほど、外部モデル呼び出しがあちこちに散ると、すぐに制御が難しくなります。コスト、失敗時の扱い、プロンプト規約、実行記録、結果保存、プロバイダー切り替え戦略まで含めて考えると、AIは一箇所で調整したほうがよい場合が多いです。ai-service はまさにその役割を引き受けています。

game-service: 私が最初に現実化したいドメイン

game-service は、このプロジェクトの中核となるプロダクトドメインです。シナリオ、ストーリークイズ、ゲームセッション、共通ゲームメタ、アセットスタジオのような機能がここに集まっています。

このサービスが重要な理由は単純です。Omniludeで最初にリリースしたいものが、最終的にはこの領域につながっているからです。だからこそ、このサービスは単なるデータ保存場所ではなく、AIと最も多く相互作用するドメインとして設計されています。シナリオを作り、アセットを生成し、セッションを作り、プレイフローを運用する過程が、すべてこちらに集まります。

特に game-serviceai-service と直接つながっている点は重要です。コンテンツ生成、レビュー、補助作業、アセット制作、一部の自動化フローは、どうしてもAIによって加速されます。私はこの接続を無理に隠すより、構造として最初から見えるようにしておくほうがよいと考えました。

mmorpg-service: リアルタイムランタイムは別軸として扱います

mmorpg-service は同じゲーム領域に属していますが、性格はかなり異なります。このサービスはHTTPベースの管理APIだけでなく、別のゲートウェイポートでリアルタイムWebSocketランタイムも運用します。

なぜこれを同じサービスに混ぜなかったのかと聞かれれば、答えは明確です。CRUD中心のコンテンツAPIとリアルタイムゲームサーバーでは、失敗の仕方も、性能要件も、状態管理の方式もまったく違うからです。私はこの二つを同じ思考単位として扱わないほうが自然だと考えました。

認証方式も興味深いです。リアルタイム接続時には、auth-service が発行したJWT規約を mmorpg-service 内部のゲートウェイがローカルで検証します。つまり、ランタイムのたびに認証サービスへHTTPで問い合わせ直すわけではありません。これは、リアルタイムシステムが通常のビジネスAPIとどこで分かれるべきかをよく示しています。

blog-service: プロジェクトの外部記憶装置

blog-service は、他と比べると複雑さが少なく見えるかもしれませんが、私にとっては重要な位置を持っています。このサービスは、単にブログを運営するためのCMSではありません。今何を作っていて、どのような判断をしていて、どんな実験をしているのかを外部へ蓄積していく通路です。

Omniludeが内側だけで完結するプロジェクトではないなら、その構造や実験や試行錯誤は、外からも説明できなければなりません。blog-service はその記録を担います。ポスト、コメント、翻訳、SEO、静的アセット連携といった機能がここに集まっているのも、プロダクトに外向きのコミュニケーションレイヤーが必要だからです。

運用基盤をなぜ別に立てているのか

プロダクト機能だけではサービスは運用できません。結果を保存し、ファイルライフサイクルを管理し、進捗をストリーミングし、イベントを流し、長時間実行ジョブを追跡し、リアルタイム接続を維持するレイヤーが必要です。

Omniludeでは、その役割を主に storage-servicebackbone-service が担っています。

storage-service: ファイルの本当の寿命はここで管理されます

多くのプロジェクトでは、ファイル保存は後から付け足される機能のように扱われます。しかし実際のプロダクト運用では、ファイルアップロード、仮状態、commit / cancel、アクセスURL、サムネイルURL、静的オブジェクトメタデータのすべてが重要です。ファイルは単に上げて終わりではなく、ライフサイクルを持っています。

だからこそ私は storage-service を別に置きました。ブログ画像であれ、AIが生成した成果物であれ、ゲームアセットであれ、できる限りこの経路に統合しようとしました。そうしておけば、どのサービスがファイルを扱っても共通ルールの上で動けますし、後からアクセス方針や保存戦略が変わっても、影響範囲を狭めやすくなります。

backbone-service: 運用と非同期の中心

backbone-service は、その名前どおり、システムの背骨に近い存在です。イベントハブ、SSE、WebSocket、プラットフォーム設定、DTE Jobイベントフローがここに集まります。

このサービスが特に重要なのは、AI実行と非常によく噛み合うからです。今作っている機能の多くは、単純な同期APIひとつで終わりません。ユーザーがジョブを開始したら、中間進捗が必要で、完了イベントが必要で、場合によってはリアルタイム接続や通知も必要です。こうした要件を各ドメインサービスが個別に解き始めると、構造はすぐに複雑になります。

そのため私は、非同期配信と運用型リアルタイムレイヤーを backbone にまとめました。イベント発行の基本経路を HTTP -> backbone -> Kafka/RabbitMQ のように整理し、DTE関連の進捗はSSEで流すほうが、より管理しやすいと考えています。

以下のフローは、AI生成ジョブが実際にどのようにつながっていくのかを簡略化して示しています。

私がこの構造を気に入っている理由は明確です。生成は ai-service、結果保存は storage-service、進捗の外部配信は backbone-service が担います。責務が分かれているからこそ、それぞれをより明確に検証できます。

データストアとメッセージングレイヤーはこのように使っています

この構造をより立体的に見るには、ストレージとメッセージングレイヤーも合わせて見る必要があります。現在のOmnilude非レガシーバックエンドは、インフラを無秩序に混ぜて使ってはいません。各レイヤーが担う役割はかなり明確です。

  • PostgreSQL は、ほぼすべてのサービスにとっての標準永続ストアです。authaibloggamestoragebackbonemmorpg がこの軸を共有しています。
  • Redis は、単純なキャッシュというより Pub/Sub やセッション性のあるイベント配信に重心があります。複数サービスに広く入っていますが、用途はかなり戦略的です。
  • Cassandra は現在、auth-servicebackbone-service 側のアクティビティログやイベント書き込みレイヤーに近い位置付けです。どのサービスも無差別につながらないよう、境界を設けています。
  • Kafka と RabbitMQ は、実質的には backbone-service を中心に扱われます。各サービスが直接メッセージブローカーにぶら下がるのではなく、まず backbone へイベントを送り、backbone が外部メッセージング層へ伝播させる構造です。
  • S3 は storage-service が直接扱い、ほかのサービスはできる限りそこを経由します。ファイル処理ルールを一箇所にまとめるための選択です。

最初は少し遠回りに見えるかもしれません。しかし私は、こうした分離こそ長期的にははるかに重要だと考えています。機能サービスがストレージとメッセージングを直接抱え始めると、責務境界は時間とともにすぐ曖昧になります。反対に、今のようにインフラアクセスをある程度ゲートウェイサービス経由に整理しておけば、運用方針が変わったときや障害点を絞り込みたいときに有利です。

AIを活用するという観点でも、この構造には大きな利点があります。ある機能をエージェントに任せるとき、その機能がどこまでを直接処理し、どこから共通経路を使うべきかが明確であればあるほど、結果は安定します。

認証付きリクエストとリアルタイム接続は、なぜ別の経路を通るのか

Omniludeの構造をより具体的に理解するには、同期リクエストとリアルタイム接続を分けて見るほうがわかりやすいです。

一般的なコンテンツ参照や管理操作の多くは、auth-service でログインしたあとにドメインAPIを呼ぶ流れを取ります。この場合に重要なのは、共通JWT規約と権限体系が維持されているかどうかです。blog-servicegame-serviceai-service はこの共通規約を共有しているため、サービスが分かれていても、ユーザーは比較的一貫した体験を得られます。

一方、リアルタイム接続では要件が変わります。特に mmorpg-service は、別のゲートウェイポート :9088/ws を使います。ユーザーはすでに auth-service が発行したトークンを持っており、ゲートウェイはそれをローカルで検証します。毎回認証サービスへ往復しなくても、素早くセッションを確立するための構造です。

以下のフローは、その違いを示しています。

この違いは、見た目以上に重要です。私はこの構造を通じて、一般的なビジネスAPI、長時間AIジョブ、リアルタイムゲームセッションが、それぞれ異なる重心を持つべきだという点を明確にしたかったのです。すべてをひとつの方式に統一すると、一見シンプルに見えても、実際の運用では問題を大きくすることが少なくありません。

この構造は、どのようにAI-First開発とつながるのか

ここまで読むと、ひとつの疑問が自然に出てきます。なぜこのような構造が、AIをうまく使うこととつながるのかという疑問です。

私はこの問いがとても重要だと思っています。なぜなら、今でも多くの開発者がAIを、コードを書いてくれる便利な道具程度にしか見ていないからです。しかし実際に生産性が大きく伸びるのは、単にコードをもっと生成する段階ではありません。AIが理解しやすい構造を、システム自体が持ち始める段階だと私は考えています。

たとえば。

  • 役割が曖昧な巨大サービスひとつより、責務が分かれたサービス群のほうが、プロンプトに分割しやすくなります。
  • common のような内部プラットフォームがあるコードベースは、共通規約のないコードベースより、エージェントにとってはるかに安定した文脈になります。
  • ファイル処理、イベント処理、AI実行、リアルタイム接続が分かれていれば、どの境界で問題が起きたのか追跡しやすくなります。
  • 最後に人間がレビューすべきポイントも、ずっと明確になります。

結局のところ、AI-First開発とは、AIをあちこちに貼り付けることではありません。AIがより一貫して働けるように、システムを整理することに近いものです。私はOmniludeを作りながら、まさにその点を実験しています。

その意味で、今の開発者に必要なのは、最新モデルのニュースそのものより訓練だと思っています。よりよいプロンプトを書く訓練だけではありません。よりよい構造を作り、よりよい基準でレビューし、AIが本当に働ける文脈を設計する訓練です。私はこのプロジェクトが、最終的にはそうした訓練の結果であるべきだと考えています。

それでも目的地はOmniludeのリリースです

私がこの記事を単なる構造紹介だけで終わらせたくなかった理由がひとつあります。この構造は、説明するための構造ではありません。実際にリリースする何かを作るための構造だからです。

今私がやっているのは、AIベースの制作ツールを作り、その上にゲームプラットフォームを実装することです。このバックエンドは、その二つの流れが出会うように設計されています。AIがコンテンツ制作を押し進め、ゲームドメインがそれをプロダクト体験へ変え、運用基盤が全体を実サービスレベルまで引き上げる方向です。

私は今でも、よくできたゲームをリリースしたいと思っています。そしてその過程が、感覚や情熱だけでなく、よりよい構造、より速い実験、より正確な検証によっても可能になるのかを、自分の手で確かめたいと思っています。Omniludeは、私にとってそうした期待が集まるプロジェクトです。

だからこそ、このブログではアーキテクチャだけを語るつもりはありません。実際にどんな機能が付き始めているのか、AIがどこまで押し進めているのか、人間はどこで判断しなければならないのか、そしてこの構造が本当にリリース可能なプロダクトへつながるのかを、これからも記録していきたいと思っています。

今の私はバックエンド構造を紹介していますが、心の中でより重要に見ている問いはひとつです。このやり方は、本当に最後まで行けるのか。

私はその答えを言葉だけで済ませたくありません。Omniludeを実際に作りながら確かめていきます。