FLARES LLC
FLARES LLC

Technical document

分割は手段にすぎない ― 多テナント業務SaaS基盤を数十サービスへ抜本分割し、壊さず運用するための横断設計

勤怠・販売・在庫・見積・会計・請求・人事給与・予約・フォーム・POS・コマース・コンテンツ管理・権限管理・監査ログ、さらにAIアシスタントやチャットボットまでを、ひとつの基盤へ束ねた多テナントの業務SaaS群を、準モノリスから数十の独立サービスへ抜本的に分割しました。作業を通じて最も強く実感したのは、難所が「サービス境界をどこで切るか」ではなく「切ったあとに壊れず運用し続けられるか」にある、ということです。命名やポートの衝突、変更のたびに全体が再デプロイされる手書きのリリース、スキーマ台帳と実データベースのずれ、認証セッションが公開ページへ漏れる境界の曖昧さ。これらはサービスを割ったこと自体からではなく、割ったあとの運用設計を後回しにしたときに噴き出します。本稿では、分割と同時に整えた横断的な仕組みを、判断理由とトレードオフとともに整理します。
アーキテクチャ / 運用設計約30分公開日 2026年7月6日更新日 2026年7月6日
分割は手段にすぎない ― 多テナント業務SaaS基盤を数十サービスへ抜本分割し、壊さず運用するための横断設計のアイキャッチ

Summary

この文書の要点

  • 勤怠から会計、POS、AIアシスタントまでを束ねた多テナント業務SaaS基盤を、数十の独立サービスへ再編する。
  • 変更の影響を受けるサービスだけを、1つのマニフェストから差分デプロイする。
  • build→migrate→deployの順序を固定し、スキーマ未反映のコードを本番へ出さない。
  • 環境設定はIaCで一元管理し、デプロイはコンテナイメージだけを更新してドリフトを防ぐ。
  • 識別子のバイト長・未記録・部分適用というマイグレーションドリフトを冪等に自動補完する。
  • API仕様をバリデータの単一ソースから分散生成し、サーバー側で統合する。
  • セッションCookieを認証層サブドメインに閉じ、公開層と物理的に隔離する。
  • AIアシスタント・チャットボット・RAGも、同じマニフェストとデプロイ基盤の上で動かす。

分割そのものより、分割したあとの運用設計が難所になります。

出発点は、性格の異なる多数の業務ドメイン(勤怠、販売、フォーム、予約、権限管理など)が、単一あるいは準モノリスの中に同居している状態でした。ドメインごとにチームや変更の頻度が違うのに、コードとデプロイが一体化しているため、ある機能の小さな修正が別ドメインの再ビルドと再デプロイを巻き込みます。変更の影響範囲が読めず、リリースは常に全体同期になり、障害の切り分けも一体のログの中で行うことになります。

この状態を解くために各ドメインを独立サービスへ分割するわけですが、分割は目的ではありません。サービスを割った瞬間に、これまで暗黙に一体化していたものが表面化します。ホスト公開ポートの衝突、コンテナ名やイメージ名の重複、サービスごとに分かれたデータベースマイグレーションの整合、そしてサービスをまたぐ認証セッションの扱いです。分割の設計と同じ熱量で、これらの横断課題に対する運用設計を先に用意しておかないと、分割した数だけ運用の穴が増えます。

多テナントであることも設計を難しくします。1つのサービスが複数の組織のデータを扱うため、分割にあたっては、テナントの分離をどの層で保証するかを崩してはいけません。サービスを割っても、テナント境界の一貫性は全サービスで揃っている必要があります。分割は、この横断的な不変条件を守りながら進める作業でもありました。

症状はいくつかの形で現れます。あるドメインの共有ライブラリを更新しただけで無関係のドメインまでビルドが走る、リリースのたびに全サービスのデプロイ完了を待つ、片方のドメインの高負荷が同居する別ドメインの応答を巻き添えにする、といった具合です。これらは個別の不具合というより、境界が引かれていないことの表面化であり、機能追加では解けません。コードとデプロイの単位を業務ドメインに沿って分ける、という構造の変更が必要になります。

本稿は、モノリスからマイクロサービスへの移行を検討している設計者・運用者に向けて、「割り方」ではなく「割ったあとに壊れないための仕組み」を具体的に共有するものです。個別の業務内容や案件には触れず、技術的な構造と判断だけを扱います。

何を分割したのか ― 多テナント業務SaaS基盤の全体像。

対象にしたのは、ひとつの業務領域に特化した単機能のツールではありません。勤怠、販売、在庫や部品表、見積、会計、請求、人事給与、予約、フォーム、店頭のPOS、コマース、コンテンツ管理、監査ログやアクセスログ、権限管理、組織管理、そしてAIアシスタントやチャットボットまでを、共通の土台の上に束ねた多テナントの業務SaaS群です。数十のバックエンドと数十のドメイン画面、認証専用のアプリ、複数の公開ポータルが、ひとつの基盤として連携します。単体でも製品として成立する機能群が、これだけの数で一つの屋根の下に同居している点が、この分割を特異なものにしています。

この規模で難しいのは、各ドメインが独立した製品として成り立つほどの機能を持ちながら、認証・組織・権限・通知・監査といった土台は全ドメインで共有しなければならない点です。独立性を高めるほど土台の共有は難しくなり、共有を強めるほど独立したデプロイやスケールが犠牲になります。分割設計の中心にあるのは、この「独立性と共有基盤の両立」という緊張を、どの層でどう解くかという判断でした。以降の各章は、突き詰めればすべてこの一つの緊張への回答です。

さらに同じ基盤を別ブランドの入口として提供するマルチブランド(OEM)にも対応し、外部の開発者に向けた公開APIや、全サービスを横断して参照できる統合APIリファレンスといった開発者向けの面も持ちます。業務アプリ、公開ポータル、AI、開発者基盤という性格の違う面を、一つの運用モデルの上に載せる。分割は、この規模と多様性を、少人数でも壊さず運用し続けられる形へ収めるための再編でした。

  • 勤怠・販売・会計・請求・人事給与・予約・POS・CMS・権限・監査・AIまでを一基盤に束ねる。
  • 各ドメインの独立性と、認証・権限・監査など土台の共有を両立させる。
  • 業務アプリ・公開ポータル・AI・開発者基盤(公開API/統合リファレンス)を一つの運用モデルに載せる。
多テナント業務SaaS基盤で複数の業務サービスが共通の認証、権限、監査、デプロイ基盤に載る全体像
多数の業務ドメインを独立させても、認証、権限、監査、デプロイの共通基盤は横断的に揃えます。

前提と制約を先に固定すると、判断がぶれません。

全体はモノレポで管理し、依存の向きを一方向に規律しました。アプリケーション本体(各サービスのエントリ)が、ドメインごとの機能パッケージに依存し、機能パッケージが共有ライブラリに依存する。逆向きの参照 ― 共有ライブラリからアプリを参照する、機能パッケージ同士が横に依存する ― は禁止します。この一方向依存は地味ですが、分割後に「どこを変えるとどこが再ビルドされるか」を機械的に判定できる土台になり、後述する差分デプロイの前提そのものになります。

運用上の最大の制約は、稼働中のサービスを止められないことです。したがって移行は段階的にしか進められません。仕様の棚卸しを先に済ませ、次に注釈や設定を足し、最後に古い実装を退役させる、という順序を守ります。「一気に作り替える」方式は綺麗に見えても、移行の途中期間に本番と設計のずれを長く抱えることになり、リスクが高いと判断しました。

一方向依存を実際に守らせるには、規約を口頭で共有するだけでは足りません。ワークスペースの境界としてパッケージを分け、依存の向きに反する参照はビルドやチェックで弾く仕組みにしておきます。こうしておくと、後からサービスを1つ切り出すときに、そのサービスが本当に依存しているものだけを持ち出せばよく、モノリス時代のように「どこまで芋づるで付いてくるか分からない」状態を避けられます。分割の容易さは、分割前の依存規律の副産物です。

実行基盤にはコンテナ化したサービスをマネージドなサーバーレス実行環境(Cloud Run 相当)へ載せる構成を採り、状態はPostgreSQLに集約、インフラはTerraformで宣言的に管理、ローカルはDocker Composeで本番トポロジを再現できるようにしました。いずれも「宣言と実態の差分を検出できること」を重視した選定です。

ローカルでの再現性は、この規模の分割では検証の生命線です。本番と同じ構成をコンテナで手元に立てられるようにし、サービス間通信、認証のCookie挙動、マイグレーションの適用までを開発機で確認できるようにしました。本番でしか再現しない問題を減らすほど、リリースの不安は小さくなります。逆に、手元で本番トポロジを再現できないと、境界に起因するバグを本番で初めて踏むことになります。

サービス境界は、命名とポートの統一から現実になります。

分割の第一歩は、意外にも命名とポートの整理でした。個別に発展してきたサービスは、コンテナ名・イメージ名・公開ポートがばらばらで、同じホストや同じ開発マシンに同居させると衝突します。そこで全サービスを統一接頭辞のもとに置き、コンテナ基盤上でひとつの論理グループとして扱えるようにしました。名前が揃うだけで、一覧性・起動順・ログ集約の運用が大きく楽になります。

ホストが外部へ公開するポートは、他プロジェクトと衝突しない専用の番号帯へ一括で寄せました。ここで重要なのは、コンテナ内部のポートやコンテナ間通信のポートは変えない、という線引きです。外向きに見えるポートだけを移動し、内部の名前解決による通信はそのまま保つことで、変更の波及を最小化しました。開発サーバーのホットリロードのように、ブラウザがホストポート経由でつなぎ直す経路だけは、クライアント側ポートの明示など追随設定が必要になります。

統一されたサービス群は、観測の面でも効きます。基盤上でひとつのグループとして見えることで、横断したログの追跡や、どのサービスが動いているかの把握が一目で済みます。分割するとサービスの数だけ監視対象が増えますが、命名とグループ化が揃っていれば、その増加を一覧性である程度相殺できます。逆に、名前がばらばらのまま数だけ増えると、分割は運用者にとって「散らかった」状態にしか見えません。

命名の統一には、別ブランド向けの入口のように「同じ実体を設定だけ変えて出す」派生の扱いも含みます。派生ごとにコードを複製すると、その数だけ保守対象が増えます。そこで実体は共通に保ち、ブランドを示す変数やビルド時の引数で差分を表現し、入口だけを分ける方針にしました。サービスの実体を無闇に増やさないことも、分割の一部です。ポート整理とあわせて、基盤上で「このプロジェクトのサービス群」がひとまとまりに見える状態を作り、起動・停止・ログ確認を一括で回せるようにしています。

分割に伴い、実装から乖離して久しい多数のテストや設定ファイルも一括で退役させました。動かないテストを残すことは、CIの信頼性をむしろ下げます。「割るときに負債も一緒に清算する」判断です。ただし退役は後述するテスト基盤の再建とセットで行い、カバレッジの空白を放置しないようにしました。

  • 外向きの公開ポートだけを衝突回避帯へ移し、コンテナ内部通信は不変に保つ。
  • 命名を統一接頭辞へ寄せ、基盤上でひとつのサービス群として扱えるようにする。
  • 陳腐化したテストは退役させるが、必ず再建計画とセットにする。
  • 共通の派生(別ブランド向けの入口など)は、実体を増やさず設定差分で表現する。

単一マニフェストを真実の源にすると、差分デプロイが成立します。

分割後のCI/CDで最初に決めたのは、「サービスの一覧と属性を、人が書いた手続きではなく1つのデータに集約する」ことでした。各サービスについて、所有するファイルの範囲、ビルド定義、デプロイ種別(常駐サービス/バッチジョブ/静的配信)、マイグレーションの要否、テスト対象パッケージ、共有パッケージへの依存区分を、ひとつのマニフェストに宣言します。これがデプロイ判断の唯一の真実の源になります。

デプロイ時は、変更されたファイルの一覧とマニフェストを突き合わせ、影響を受けるサービスだけを選び出します。ファイル範囲の指定はglobで書き、内部では正規表現へ変換して照合します。共有パッケージが変わった場合は、その区分(バックエンド共有/フロントエンド共有)に紐づく全サービスへ影響を波及させます。選定結果に加えて、フロントエンドを含むか(CDN無効化が要るか)、インフラ定義が変わったか(IaCの差分計画を回すか)、マイグレーションが要るか、テスト対象パッケージは何か、といったフラグを同時に出力します。

この設計の狙いは明確で、無関係なサービスの再ビルド・再デプロイを避けることです。全体同期のリリースは、時間もコストもかかるうえ、変更していないサービスにまで事故の機会を作ります。差分デプロイは、そのリリースあたりの影響面を、実際に変わったものだけへ絞り込みます。

ファイル範囲の照合には、少し地味な正規化が要ります。globの記法(任意階層を表す記号や、階層をまたがない記号)を、内部の正規表現へ機械的に変換し、変更ファイルのパスと突き合わせます。ここで、あるサービスが広いディレクトリを所有しつつ、その一部だけは別サービスの管轄、という重なりが生じます。そこで所有範囲に加えて除外範囲も宣言できるようにし、「このディレクトリのうち、この部分は自分の変更とはみなさない」を表現します。マニフェストの表現力が、そのまま差分の精度になります。

選定ロジックには、いくつかのモードを持たせました。既定は変更ファイルに基づく差分選定ですが、基盤側の大きな変更時には全サービスを対象にするモード、特定サービスだけを名前で指定するモードも用意します。緊急時に「これだけを出す」と明示できる逃げ道は、自動選定の万能を過信しないための保険です。テスト対象パッケージは、選定された各サービスが宣言する集合の和をとり、重複を除いてまとめて一度に依存解決します。サービスごとに個別インストールを繰り返すより、和集合を一括で扱う方がCIの総時間を大きく削れます。

限界もあります。共有パッケージを1つ触ると、それに依存する多数のサービスが一斉に再ビルド対象になります。共有の粒度が粗いほど、この「巻き込み」は大きくなります。対策として、影響範囲を締めるための除外指定をマニフェストに持たせ、共有の粒度自体も定期的に見直す運用が要ります。マニフェストが真実の源である以上、その正確さの維持がそのまま運用品質になります。

  • サービスの属性(範囲・種別・移行要否・テスト対象・共有依存)を1データに宣言する。
  • 変更ファイル×マニフェストで、影響サービスと付随フラグを機械的に導く。
  • 共有パッケージ変更の波及は避けられないので、除外指定と粒度見直しで締める。
変更ファイルを単一マニフェストで照合し、影響を受けるサービスだけを選んでデプロイする流れ
変更ファイルをマニフェストに照合し、実際に影響するサービスだけをビルド、移行、デプロイ対象にします。

build→migrate→deployの順序が、壊れ方を先回りで塞ぎます。

デプロイのパイプラインは、ビルド、マイグレーション、ロールアウト(+疎通確認)、後処理という精鋭のフェーズに再設計しました。ここで最も重要なのは順序です。コンテナイメージのビルドと、実際に新リビジョンを流すロールアウトを分離し、その「あいだ」でデータベースのマイグレーションを実行します。そしてマイグレーションが失敗したら、ロールアウトを中止します。

この順序には理由があります。もし新しいコードを先に流してからスキーマを変更すると、スキーマがまだ古い状態の本番で新コードが動く瞬間が生まれます。逆にスキーマだけ先に壊れて新コードが来なければ、旧コードが新スキーマに直面します。build→migrate→deployの直列化は、「スキーマが期待どおりに更新できたときだけ、新コードを本番へ出す」という不変条件を作ります。マイグレーション失敗は、新コードを旧スキーマに触れさせないための停止線として機能します。

ロールアウトは同期実行にしました。新リビジョンがReady(受信可能)になるまで待ち、簡単な疎通確認まで通って初めて成功と見なします。一時的な基盤側の操作競合を吸収するため、数回のリトライと短い待機を挟みます。デプロイ種別ごとに手法は変わり、常駐サービスは同期更新と疎通確認、バッチジョブはリトライ付きのループ、静的配信はビルド成果物をオブジェクトストレージへ同期してCDNを無効化する、という三系統を1つのパイプラインで扱います。

デプロイの後処理も、単なる締めの作業ではありません。フロントエンドを含むリリースではCDNのキャッシュを無効化し、バックエンドやフロントエンドのリリースでは認証の疎通をもう一度確かめます。そして、反映したイメージのタグ、公開URL、万一のときのロールバック手順を、リリースの要約として残します。切り戻しのコマンドをあらかじめ要約に含めておくのは、障害時に手が止まらないための地味だが効く工夫です。復旧手順を「そのとき考える」状態にしないことが、平均復旧時間を縮めます。

デプロイ種別ごとの手法をもう少し具体化します。常駐サービスは、イメージだけを指定した同期更新を行い、新リビジョンが受信可能になるのを待ってから、簡単なリクエストで疎通を確かめます。失敗時は数回のリトライと短い待機で一時的な競合を吸収し、それでも駄目なら失敗として扱います。バッチジョブは、複数の対象をループで順に更新し、こちらもリトライを挟みますが、非同期実行のため疎通確認は行いません。静的配信は、ビルド成果物をオブジェクトストレージへ同期し、不要になったオブジェクトの削除まで含めて反映したうえで、CDNのキャッシュを無効化します。1つのパイプラインでこの三系統を扱えるのは、種別をマニフェストの属性として持っているからです。

さらに、デプロイの前段に軽量な認証回帰ガードを置きました。OAuth 2.0の開始からリダイレクトの連鎖までを、データベースを使わない単体テストで素早く検証します。認証はほぼ全サービスが依存する土台なので、ここが壊れたリリースは何を措いても止める価値があります。限界として、この設計はスキーマの前進のみを扱い、マイグレーションのロールバックは持ちません。したがって「出す前に検証を厳格化する」方向に倒しています。疎通確認も浅く、正常応答を返してもその奥で接続が切れている状態までは拾えないため、必要に応じて後処理でより深い検査を足します。

  • ビルドとロールアウトを分け、あいだにマイグレーションを挟む。
  • マイグレーション失敗はロールアウトの停止線。新コードを旧スキーマに触れさせない。
  • 常駐サービス/バッチ/静的配信の3種を1パイプラインで、種別ごとの手法で扱う。
  • ロールバック非対応を前提に、出す前の検証(認証ガード等)を厚くする。
コンテナイメージのビルド、データベースマイグレーション、ロールアウトを順番に実行し、失敗時に停止するデプロイ設計
build、migrate、deploy の順序を固定し、スキーマ更新に失敗したコードを本番へ出さない停止線を置きます。

設定とコードを別経路に分けると、両方が安定します。

環境変数、シークレット、スケーリング設定、ネットワーク設定といった「環境の形」は、すべてIaC(Terraform)で一元管理し、レビューを通したapplyでのみ変更します。一方、日々のデプロイでは、実行環境のコンテナイメージだけを更新します。実行環境の更新コマンドが、指定しなかったフィールドを既存値のまま保つ挙動を利用し、イメージ以外の設定に触れないようにしています。

ここで一つ工夫が要ります。IaCと日々のデプロイが同じ「イメージ」というフィールドを取り合うと、互いの変更を上書きし合ってドリフトが生じます。そこでIaC側では、イメージフィールドの変更を無視する宣言を入れ、ロールアウトの所有権をデプロイ側に一本化しました。IaCは環境の形を、デプロイはコードのバージョンを、それぞれ排他的に担当します。

どちらがどのフィールドを持つかを明確にしておくことが要点です。イメージのバージョンはデプロイが、それ以外の環境の形(環境変数、シークレット参照、同時実行数やスケーリング、ネットワーク接続)はIaCが持ちます。この持ち分がはっきりしていれば、片方の変更がもう片方の意図を巻き戻すことがありません。逆に、両者が同じフィールドに書き込む設計のままだと、applyのたびにデプロイ済みのイメージが古い宣言値へ引き戻される、という分かりにくい事故が起きます。無視する宣言は、その事故を設計で封じるための一手です。

シークレットの更新も、この分離の恩恵を受けます。接続情報や鍵の差し替えはIaCの経路で行い、日々のデプロイはそれに一切触れません。コードのリリースとシークレットの更新が別々のタイミング・別々の承認で進むため、片方の作業がもう片方の秘匿情報を露出させる事故が起きにくくなります。運用の責務が経路で分かれていることが、そのまま安全性の担保になります。

この分離の効果は運用のリズムに現れます。環境設定の変更は、プルリクエスト、レビュー、applyという慎重な経路を通ります。コードの変更は、ビルドからマイグレーション、デプロイまで自動で流れます。慎重に扱いたいものと素早く回したいものを別経路に置くことで、安定性とデリバリー速度を同時に確保できます。

マイグレーションドリフトは、正規化と冪等な補完で吸収します。

サービスを分割し、複数の経路でスキーマを進化させると、「マイグレーションの台帳」と「実際のデータベースの状態」がずれていきます。これがマイグレーションドリフトです。ある環境では適用済みなのに台帳に記録がない、途中まで適用されて中断した、名前の解釈が環境によって違う、といった不整合が積み重なると、次のマイグレーションが安全に流せなくなります。本稿の作業ブランチが主目的としたのは、まさにこのドリフトを検出して自動的に埋め戻す仕組みでした。

そもそもドリフトがなぜ生じるかを補足します。分割の過程では、複数の作業ブランチが並行してスキーマを変更し、環境ごとに適用のタイミングもずれます。台帳は「あるべき順序」を記録しますが、実データベースは「実際に適用された結果」でしかありません。この二つは、手作業の介入や中断、環境差があると容易に乖離します。だからこそ、台帳を信じて上書きするのではなく、実データベースの現状を読み取ってから、台帳との差を安全な方向へ収束させる、という順序が要になります。実体を正とし、記録を後から合わせにいく設計です。

第一の対策は、識別子のバイト長の正規化です。データベースの識別子には最大バイト長の制限があり、長いテーブル名やカラム名は境界で切り詰められます。台帳側の名前と実データベース側の切り詰められた名前が一致しなくなると、「同じ対象なのに別物に見える」誤検出が起きます。そこで台帳と実体の両方に同一の正規化規則を適用してから比較キーを生成し、対応関係を復元します。文字とバイトの境界を尊重した切り詰めが要点です。

第二の対策は、未記録・部分適用マイグレーションの冪等な補完です。台帳にあるが実行状態が曖昧なマイグレーションを、実データベースの現状を読んだうえで、いくつかのパターンに分けて安全に収束させます。対象がまだ何も存在しなければ安全に適用して記録し、すでに実体が存在して内容も一致していれば記録だけを追加し、一部だけ適用されていれば欠けた操作を補完し、実質的に空の作成マイグレーションであれば安全に作り直します。いずれの経路も、繰り返し実行しても同じ結果になる冪等性を持たせているため、再デプロイで二重適用の事故が起きません。

第三の対策は、サービス再作成時のデータ保護です。差分デプロイやインフラ変更の過程で、サービスや接続リソースは作り直されることがあります。しかし、主データベースのインスタンス、そのユーザー、接続文字列のシークレットといった「消えたら復旧できないもの」だけは、絶対に削除させてはなりません。そこで、インフラの差分計画の段階で保護対象リソースへの削除操作を検出したら、applyそのものを止めます。「サービスや接続は再作成してよいが、データベースの実体は不可」という境界を、計画の検査で機械的に強制します。加えて、マイグレーション用のスキーマとシード(初期データ投入)のクライアントを分離し、ルート境界の外部キーを整理して、分割後の依存関係を素直にしました。

補完の仕組みを支えるために、マイグレーションを流すためのクライアントと、初期データを投入するためのクライアントを分離しました。両者を混ぜると、スキーマ変更と初期データ投入の責務が絡まり、片方の失敗がもう片方の状態を巻き込みます。あわせて、分割の過程で生じたルート境界の外部キーを整理し、サービス間で参照が循環しない素直な依存にしました。ドリフトの自動補完は、こうした「補完しやすい形にスキーマ側を整える」下ごしらえがあって初めて安定します。仕組みだけでなく、対象のスキーマ設計も同時に整えることが、再現性のある収束につながります。

限界として、文法エラーや一意制約違反のように、状態を読むだけでは安全に収束できない失敗は、自動補完の対象外です。これらは人手の介入を要するため、任意のスクリプトを一度きり実行できる運用口(手動オペレーション用のジョブ)を別途用意し、復旧や補正を安全に流せるようにしています。

  • 識別子はバイト長で切り詰められる。台帳と実体を同一規則で正規化して突き合わせる。
  • 未記録・部分適用は、現状を読んでパターン別に冪等へ収束させる。
  • 主データベースの削除は計画段階で検出して止める。再作成可と不可の境界を機械化する。
  • 自動補完できない失敗のために、一度きりの手動オペレーション経路を残す。
マイグレーション台帳と実データベースの差を比較し、未記録や部分適用を冪等に補完する仕組み
台帳と実データベースを同じ規則で照合し、未記録や部分適用を安全な方向へ補完します。

API仕様は、実装を単一ソースにして分散生成・統合します。

サービスが増えると、手書きのAPI仕様書は必ず実装より早く古くなります。そこで、各サービスが自分の仕様を自己記述で配信し、それを1か所へ統合する「分散生成+マージ」の基盤を用意しました。各バックエンドは、リクエスト検証に使っているスキーマ(Zod)をそのままOpenAPI仕様の単一ソースとして、自分の仕様エンドポイントを提供します。検証ロジックと仕様書が同じ定義から生まれるため、二重管理とドリフトが構造的に消えます。

補足すると、ドメインごとの機能はライブラリとして実装し、それをサービスのプロセスにマウントする構成にしています。ライブラリ単体は仕様エンドポイントを持たず、マウント先のサービスが自分の仕様として束ねて配信します。この「機能はライブラリ、配信はサービス」という分担により、機能の再利用と、サービス単位の独立配信を両立させています。仕様の分散生成も、この構成の上に自然に載ります。

導入方式には、既存ルートへ記述を「追記する」やり方を選びました。ルーティング全体を型安全な別方式へ全面的に書き換える選択肢もありましたが、多数のサービスを一斉に書き換えると、移行期間に長いドリフトを抱えます。追記型なら、既存の構造を保ったまま段階的に注釈を足していけ、途中段階でも空の項目を含めて棚卸しができます。型安全性を最大化する全面書き換えの魅力と引き換えに、移行の安全性と可逆性を優先した判断です。

統合するアグリゲータは、各サービスの仕様を集めてマージします。ここでの設計判断がいくつかあります。同じパスが複数サービスから来た場合は先勝ちで維持し、衝突を警告として記録します。スキーマ名の衝突は、サービス名で名前空間化(サービス名を接頭辞に付ける)して回避し、仕様書内の参照を再帰的に貼り替えます。各操作には出自を示すタグを付け、どのサービス由来かを追えるようにします。到達できないサービスの仕様は、全体を止めずに警告付きでスキップします。

配信面では、統合ビューを提供する専用サービスが、サーバー側またはビルド時に各仕様を収集します。ブラウザからサービスをまたいで取得させないのは、後述する認証と公開の分割トポロジのもとでは、跨ドメインの取得がCORSやCookieの制約に直面するためです。サーバー間で束ねる経路にすることで、この制約を回避します。展開は、規約とアグリゲータの用意、パイロット1サービスでの実証、全サービスの棚卸し配信、ドメインごとの注釈の横展開、最後に手書き仕様の退役、という段階を踏みます。

パスの衝突を先勝ちにしたのは、統合の停止を避けるためです。厳密には衝突は是正対象ですが、統合の段階でエラーとして全体を止めてしまうと、1つの衝突が全サービスのドキュメントを不可視にします。そこで、統合は続行しつつ衝突を警告として残し、是正は別のタイミングで行う運用にしました。仕様書を「常に最新で、常に見られる」状態に保つことを、厳密さより優先した判断です。API仕様が実装と同期し、いつでも参照できることは、サービス間の連携を設計する際の共通言語になります。

仕様の取得元は、環境に応じて切り替えられるようにしました。個別に上書きする環境変数があればそれを、コンテナ基盤上ではサービス間の内部ネットワーク越しのアドレスを、ローカル開発ではホスト上のアドレスを使います。共通の認証方式の定義は、重複を避けて未登録のものだけ取り込みます。展開の段階も、一度に全部ではなく、規約とアグリゲータの確立、1サービスでのパイロット、全サービスの最小配信、ドメインごとの注釈の充実、最後に手書き仕様の退役、という順で進めました。移行の各段階で常に動く状態を保つことが、長い移行を破綻させないための条件です。

検証は、アグリゲータ自体を単体テストで守ります。複数サービスの統合結果、名前空間化に伴う参照の貼り替えの正確さ、到達不能サービスのスキップを、テストで固定します。限界として、API構造の全体開示はそれ自体が攻撃面になり得るため、本番の統合ドキュメントは管理者認証の背後に置くことを前提にしています。

  • 検証スキーマを仕様の単一ソースにし、二重管理とドリフトを消す。
  • 全面書き換えではなく追記型を選び、移行の安全性と可逆性を優先する。
  • マージは、パス衝突は先勝ち+警告、スキーマは名前空間化、操作に出自タグ。
  • 統合はサーバー側で束ね、跨ドメイン取得の制約を回避する。
各サービスが生成したAPI仕様をサーバー側で集約し、統合APIリファレンスとして配信する構成
各サービスは自分のAPI仕様を生成し、集約層が衝突や出自を整理して統合ビューへまとめます。

AIアシスタントとRAGも、同じ運用の土台に載せます。

この基盤には、業務を横断して質問に答えるAIアシスタントや、問い合わせに応じるチャットボット、そして社内の知識や資料を検索して回答に使うRAG(検索拡張生成)の機能が組み込まれています。見た目には最も派手な機能ですが、運用の設計方針はむしろ逆で、「特別扱いしない」ことを徹底しました。AI機能だけが独自のデプロイ経路や独自の監視を持つと、その部分だけが運用の孤島になり、いずれ誰も面倒を見られなくなるからです。

たとえばRAGの知識ベースは、元になる資料やデータが更新されるたびに再同期が必要です。この再同期は、リクエストを受け続ける常駐サービスではなく、非同期のバッチジョブとして実装し、前章までのデプロイ基盤が持つ「ジョブ」種別へそのまま載せました。マニフェストにジョブとして宣言すれば、変更検知による差分実行も、失敗時のリトライも、既存の仕組みがそのまま効きます。AIだからといって、別系統のパイプラインを新設しない、という判断です。

AIアシスタントのAPIも、他のドメインと同じく自己記述のAPI仕様を出し、同じ認証と権限の土台に乗ります。外部のAIサービスやモデルの呼び出しはドメインサービスの内側に閉じ込め、鍵や利用上限の管理はIaCのシークレット経路へ寄せます。こうして、AI機能を「動くかもしれない実験」ではなく、権限・監査・デプロイまで含めて運用可能な製品機能として、他のドメインと同じ土俵に立たせています。派手さの下で、地味な運用規律が効いている状態です。

一方で、AI機能に固有の難しさ ― 応答が毎回同じとは限らない非決定性、外部サービスのレイテンシや障害、利用量に応じて動くコスト ― は、この横断設計だけでは解けません。これらはドメイン側で、タイムアウトやフォールバックの用意、利用量の観測として個別に向き合います。共通の土台に載せることと、その機能ならではの難しさに個別に取り組むことは、別の仕事だと切り分けています。

  • RAGの再同期は常駐ではなくジョブとして宣言し、既存の差分デプロイに載せる。
  • AIのAPIも自己記述の仕様と共通の認証・権限に乗せ、特別扱いしない。
  • 非決定性・外部障害・コスト変動は土台では解けず、ドメイン側で個別に扱う。

認証と公開は、2階層のサブドメインで物理的に隔離します。

分割で最も神経を使ったのは、認証セッションの境界です。認証ライブラリのクロスサブドメインCookie機能を使うと、セッションCookieは親ドメイン配下の全サブドメインへ到達します。便利な反面、埋め込み可能な公開ページ(フォームや予約の入口)にまで認証済みセッションが届いてしまうと、セキュリティ上の面が広がります。そこで、認証を要するコンソール群を認証層サブドメイン(app.example.com 相当)に、匿名アクセスの公開ポータルを公開層サブドメイン(pub.example.com 相当)に分け、セッションCookieのスコープを認証層だけに閉じました。

この2階層化の狙いは、Cookieのスコープをそのままセキュリティ境界にすることです。認証層でログインしても、そのCookieは公開層へは届きません。公開ページは常に匿名のまま動き、認証済みユーザーの資格情報を受け取ることがありません。結果として、公開ページ経由の攻撃面が縮小し、未同意の訪問者に対するセッション付与も避けられます。

公開ポータルからバックエンドを叩く経路は、公開API用のファサードに集約し、オリジンの許可リストで制御します。許可リストはハードコードせず環境変数で一元管理し、公開アプリを増やすたびに設定で追加します。ファサードが生の応答を返す構造のため、CORSヘッダをどの層で付与するか(応答生成の前段で付ける必要がある)に注意が要ります。

公開ファサードには、もう一段の割り切りがあります。複数の公開機能(フォーム、予約、注文)を1つのファサードが束ねるため、機能ごとに細かく許可元を分けるのではなく、公開層全体の許可リストを共有します。手軽な反面、公開機能を増やすたびに許可リストへの追加が要り、機能単位の細かなアクセス制御は別の層で担保する必要があります。境界を単純にするほど運用は楽になりますが、粒度の細かさは犠牲になる、という典型的なトレードオフです。

Cookieの属性にも本番と開発で差をつけます。本番はHTTPS前提で、別サブドメイン間の送受信を許すために安全属性と緩めの同一サイト属性を組み合わせ、開発は平文のため通常の同一サイト属性にします。ドメイン属性は認証層の親ドメインに固定し、そこから外へ広がらないようにします。これらは一つでも取り違えると、ログインできない、あるいは逆に公開層までCookieが漏れる、という形で表面化するため、設定値そのものをレビュー対象として扱います。

トレードオフは明確です。公開層では認証セッションを保持できないため、顧客が公開ポータルでログイン状態を持ちたい場合は、バックエンド発行のトークンや招待リンクを介した別設計が必要になります。段階的にログインしていくハイブリッドなページは、この分離のもとでは作りにくくなります。また、認証の開始URLを別サブドメインへ移すと、OAuth 2.0のコールバックURLを認可プロバイダ側に登録し直す必要があり、サブドメインを動かすたびにこの同期を運用として抱えます。境界を明確にする代わりに、これらの制約を受け入れる判断です。

  • セッションCookieのスコープを認証層に閉じ、公開層と物理隔離する。
  • 公開ページは常に匿名。認証情報を受け取らせないことで攻撃面を縮める。
  • 公開層でのログイン保持は捨て、必要ならトークンや招待リンクで代替する。
認証が必要なコンソール層と匿名公開ポータル層を分け、Cookieを認証層に閉じるサブドメイン設計
認証層と公開層を分けることで、セッションCookieを公開ページへ渡さず、攻撃面を小さくします。

フロントは共通シェルへ寄せ、例外だけ残します。

認証層のドメインフロントは、共通アプリケーションシェル(サイドバー、アプリ切替、組織選択、個人設定などを備えたレイアウト)へ統一しました。1つのテンプレート実装を正典として参照し、各ドメインはその上に自分の画面を載せます。UIの一貫性が上がるだけでなく、認証やナビゲーションの共通処理を1か所に集約でき、ドメイン横断の変更が容易になります。

ただし、すべてを無理に共通化はしません。独立した情報設計や認証フローを持つアプリ ― 別の利用者層に向けた独立Webなど ― は、共通シェルの外に例外として残しました。共通化の利益より、独自の体験や境界を保つ利益が上回る場合があるからです。「原則は統一、例外は明示」という線引きを持つことが、共通化の暴走を防ぎます。

どのドメインがどのオリジンで動くかは、環境で駆動するレジストリに集約しました。コンソール側は、参照してよいアプリとそのオリジンの一覧を環境から受け取り、画面のアプリ切替や遷移に使います。オリジンをコードに埋め込まず環境で与えることで、同じコードを開発・検証・本番の異なるトポロジへそのまま載せられます。反面、環境の設定漏れがそのまま機能欠落になるため、必要なオリジンが揃っているかをデプロイの前提条件として検査します。

共通シェルは、見た目の統一以上のものを提供します。サイドバーの固定と自動開閉、幅のドラッグ変更、テーマ・言語・表示密度といった個人設定の集約、検索付きの組織セレクタ、ブランドを示す配色の一貫適用。これらを各ドメインが個別に実装すると、実装の重複だけでなく挙動の微妙な差が利用者の混乱を生みます。横断的なUIと操作の規約を1か所に閉じ込め、各ドメインは業務画面に集中する、という分担にしています。共通化の利益が最も大きいのは、こうした「どのドメインでも同じであるべき」部分です。

各フロントが参照するオリジンは、すべて環境変数で駆動し、ハードコードを廃しました。これは分割トポロジで必須の設計ですが、運用リスクも生みます。デプロイ時にオリジンの環境変数を設定し損ねると、参照先が既定へフォールバックし、本番でCORSエラーやリダイレクトのループを引き起こします。オリジンの一覧を漏れなく注入することが、そのまま安定稼働の条件になります。

本番の配線は証明書とルーティング、開発は本番トポロジの再現で担保します。

2階層のワイルドカード(認証層と公開層それぞれの配下すべて)を1枚の従来型証明書で賄うのは、証明書の対象名の扱い上、素直ではありません。そこで、DNS検証によるマネージド証明書を採り、認証層と公開層をそれぞれ別の証明書として発行しました。証明書のマッピングをHTTPSの入口に配線し、ホスト名に応じて適切な証明書を提示します。ルーティングは、ドメインごとにサーバーレスの実行環境をバックエンドとして束ね、入口からの経路(証明書、プロキシ、サーバーレスNEG、URLマップ、DNS)を宣言的に構成しました。

この手の分割で最も再現しづらいのが、開発環境でのCookieの到達性です。ローカルの単純なポート指定では、サブドメイン間のCookieの挙動を検証できません。そこで、ワイルドカードでループバックへ解決する開発用ドメインとリバースプロキシを使い、認証層と公開層に相当するホストを立て、ログイン後にCookieが認証層へは届き公開層へは届かないことを実機で確認できるようにしました。本番と同じトポロジを手元で再現できることが、境界設計のバグを本番前に潰す唯一の現実的な方法です。

証明書の設計には、従来型の証明書の制約が絡みます。従来型は対象ホスト名を列挙する方式のため、2階層のワイルドカードを1枚で扱うのが素直ではありません。そこでDNSによる所有確認を用いるマネージド証明書に切り替え、認証層と公開層をそれぞれ別の証明書として発行し、ホスト名に応じて提示する証明書を選ぶマッピングを入口に配線しました。開発環境の再現では、ワイルドカードでループバックへ解決する開発用ドメインを使い、開発サーバー側でもそのホスト名を許可リストに加えておく必要があります。この一手を忘れると、ホスト名の検証に弾かれて接続できないため、新しいアプリを足すたびの定型手順として明文化しています。

あわせて、静的なランディングは静的サイト生成(プリレンダリング)へ寄せ、配信を軽くしました。動的な要素の薄いページを、実行環境ではなく静的配信+CDNに載せることで、コストと堅牢性の両方を改善しています。

テスト基盤は、マニフェスト駆動で再建します。

分割にあわせて陳腐化したテストを退役させた以上、カバレッジの空白を埋める再建が必須です。ここでも真実の源はマニフェストにしました。デプロイと同じ選定ロジックで、変更に関係するサービスのテスト対象パッケージだけを集め、重複を除いた集合として1回の依存インストールにまとめて実行します。単体テストとエンドツーエンドのテストの双方を復元し、変更に応じて必要な範囲だけを回す構成です。

検証の思想は、「壊れ方を観測する」ことに置きました。デプロイ前の認証回帰ガード、ロールアウト時の疎通スモーク、API仕様アグリゲータの単体テスト、マイグレーション補完の冪等性。これらは、分割によって増えた「壊れうる箇所」に対して、それぞれの層で早期に異常を可視化するための観測点です。どこを自動で塞ぎ、どこを人手の確認に残すかを意図的に決めることが、テスト量そのものより効きます。

検証を二重に置く判断もしました。プルリクエストの段階と、本流へ取り込んだあとのデプロイ段階の両方で、同じ認証ガードやインフラの差分検査を回します。重複は無駄に見えますが、片方をすり抜けた変更をもう片方が捕まえる多層防御になります。特に、保護対象リソースの削除を止める検査は、取り返しがつかない操作を対象にするため、複数の関門で確認する価値があります。

再建にあたっては、テストを「いつ・どこまで回すか」も設計しました。変更に関係する範囲は各リリースで回し、全体を通した重いエンドツーエンドのテストは、頻度を落として定期実行します。全リリースで全テストを回すのは理想的に見えても、時間がかかりすぎて結局回されなくなる、という別の失敗を招きます。回し続けられる分量に収めることも、テスト設計の一部です。どのテストを常時、どれを定期、どれを人手の確認に委ねるかを明示することで、「テストはあるが誰も見ていない」状態を避けます。

たとえばマイグレーションのロールバックは自動化せず、事前検証を厚くする方向に倒しました。疎通スモークは浅いままにして、深い検査は必要な後処理でのみ足します。全てを自動化しようとせず、リスクの高い停止線(新コードを旧スキーマに触れさせない、主データベースを消させない)に検証資源を集中させる、という優先順位づけです。

採用しなかった選択肢と、この設計が向かない条件を明示します。

ここまでの設計は、いくつかの魅力的な選択肢を意図的に捨てています。マイグレーションのロールバックは持たず、前進のみとしました。取り消し可能なマイグレーションは運用が複雑になりがちで、出す前の検証と保護ガードに投資する方が、この規模では堅いと判断したためです。API仕様は全面書き換えではなく追記型を選び、型安全性の最大化より移行の安全性を採りました。

運用上の弱点も正直に挙げます。共有パッケージを触ると広範囲が再ビルドされること、ロールアウトの疎通スモークが浅いこと、公開層で認証を保持できないこと、モノレポの一方向依存を規律し続けるコストがかかること。これらは設計の裏返しであり、消せないトレードオフです。重要なのは、弱点を認識したうえで、それぞれに緩和策(除外指定、深い後処理検査、トークン設計、依存方向の検査)を用意しておくことです。

「そもそもモジュラーモノリスで十分ではないか」という問いにも触れておきます。境界を明確にするだけなら、1つのプロセスの中でモジュールを分ける方が、デプロイもデータ整合も単純です。物理的にサービスを割る利益は、独立したデプロイ・スケール・障害隔離が本当に必要なときに初めて上回ります。今回は、ドメインごとに変更頻度と負荷特性が大きく異なり、片方の重い処理が全体を巻き込む問題が現実に起きていたため、物理分割に踏み込みました。裏を返せば、その必要がないなら、モジュラーモノリスに留めるのが賢明です。分割は、痛みが実在するときにだけ見合う投資です。

もう一つ、モノレポの一方向依存を維持し続けるコストも挙げておきます。依存の向きは、放っておくと便利さに負けて崩れます。共有ライブラリからアプリの都合を参照したくなる場面、機能パッケージ同士を直接つなぎたくなる場面は必ず来ます。これを継続的に検査で弾き続けなければ、せっかくの差分デプロイの前提が腐ります。分割の利益は、この規律を保つ地道な運用の上に成り立っており、規律を手放した瞬間に、分割は元の絡まりへ逆戻りします。

この設計が向くのは、性格の異なる多数のドメインを抱え、ドメインごとに変更頻度やチームが分かれ、かつ全体を止められない前提のシステムです。逆に、ドメインが少なく変更も一体で回せるうちは、分割のための横断設計そのものが過剰投資になります。分割は、それを支える運用設計を用意できて初めて利益に転じます。

  • ロールバックより事前検証、全面書き換えより追記型 ― 可逆性と安全性を優先。
  • 弱点(巻き込み再ビルド、浅いスモーク、公開層の非認証)を認め、緩和策を持つ。
  • ドメインが少なく一体で回せる段階では、横断設計は過剰投資になりうる。

分割は手段。壊れないための横断設計が本質です。

振り返れば、この作業の価値の大半は、サービスを割ったこと自体ではなく、割ったあとに壊れないための横断設計にありました。単一マニフェストによる差分デプロイ、build→migrate→deployのfail-safeな順序、設定とコードの経路分離、マイグレーションドリフトの冪等な自動補完、API仕様の単一ソース化、そして認証と公開のCookie境界。どれも「分割したせいで増えた壊れ方」を、仕組みで先回りして塞ぐためのものです。

仕組みの話に終始しましたが、分割の成否はチームの持ち方にも関わります。サービスの境界は、変更の責任の境界でもあります。マニフェストが真実の源であるということは、どのチームがどのサービスを持つかも、そこに映るということです。技術的な境界と組織的な境界を、無理のない範囲で揃えていくことが、分割を長続きさせます。仕組みだけを整えても、持ち主が曖昧なサービスは、いずれ誰も面倒を見なくなります。

これから同じ移行に臨む読者へ、固める順序を提案します。まず、サービスの一覧と属性を宣言するマニフェストを真実の源として置くこと。次に、マイグレーションとロールアウトの順序を固定し、スキーマ未反映のコードを本番へ出さない不変条件を作ること。そのうえで、ドリフトを検出して冪等に補完する仕組みと、消えたら困るものを守る保護ガードを用意すること。最後に、認証セッションの境界を物理的に設計すること。この順で土台を固めれば、分割の数だけ穴が増える事態を避けられます。

マイクロサービスは、それ自体が目的になった瞬間に運用を蝕みます。割る前に、割ったあとをどう運用し続けるかを設計に織り込む。その順序こそが、抜本分割を破綻ではなく前進に変える鍵だと考えます。

ここに書いた仕組みは、どれも単体では地味です。単一マニフェスト、順序の固定、経路の分離、ドリフトの補完、仕様の単一ソース、Cookieの境界。しかし、この地味な横断設計の総体こそが、勤怠からAIアシスタントまでを一つの基盤で束ねる多テナント業務SaaSを、少人数でも日々壊さずに動かし続けています。派手な機能の下で静かに効いているこれらの設計を、この分割で到達した技術の結晶として、ここに記しておきます。

Technical documents

技術文書を増やしていきます。

AI、クラウド、業務アプリ開発、要件定義、運用設計に関する考え方を、今後も文書として整理します。

技術文書一覧へ