Laravelで実装するマイクロサービスアーキテクチャ:分割の判断から運用設計まで

Laravelで実装するマイクロサービスアーキテクチャ:分割の判断から運用設計まで

Laravelでマイクロサービスを進めるときは「とりあえず分割」ではなく、境界の決め方・通信方式・データ整合性・デプロイと監視まで含めて設計しないと、モノリスより遅くて壊れやすい構成になりやすい。最初に“分割する理由”を言語化し、境界(ドメイン/責務)と運用(障害時・変更頻度・組織構造)を合わせることで、Laravelでも十分に実務レベルのマイクロサービスを構築できる。

分割の前提:マイクロサービスが向く条件・向かない条件

向く条件:サービスごとに変更頻度が違う、チームが分かれている、障害影響範囲を限定したい、スケール要件が異なる、言語/技術選定をサービス単位にしたい。
向かない条件:チームが小さい、要件が流動的で境界が固まっていない、DBを分ける運用ができない、監視・SREの土台が無い、トランザクション整合性が強く必要で同期が多い。
まず「分割で得たい効果(速度・安定・スケール・組織)」を明確にしてから着手する。

最初の設計:ドメイン境界(Bounded Context)を言語化する

サービス分割は“機能別”より“業務ドメイン別”に寄せた方が破綻しにくい。
例:注文(Order)、支払い(Payment)、在庫(Inventory)、配送(Shipping)、顧客(Customer)など。
境界は「用語の意味がズレる場所」「DBテーブルが増殖して責務が混ざる場所」「変更理由が違う場所」を手がかりに切る。境界が曖昧なうちはモジュラーモノリスで耐え、境界が固まったらサービスへ切り出すのが安全。

API境界:サービス間通信はHTTPかメッセージかを決める

Laravel同士の通信はHTTP/JSONが始めやすい。リアルタイム性が不要で疎結合にしたい場合はメッセージング(キュー/イベント)を優先すると障害耐性が上がる。
・同期HTTP:即時応答が必要、依存が増えやすい
・非同期イベント:依存が減る、整合性は結果整合へ寄る
最初から全部イベント駆動にせず、同期が必要な最小箇所だけ同期にして、残りはイベントに寄せると設計が安定しやすい。

サービスごとにDBを分ける:共有DBを避ける理由

マイクロサービスの“分割”はコードではなくデータで決まる。共有DBでテーブルだけ分けても、結局JOINや外部キーで結合され、変更が全体に波及する。
原則:サービスごとにDB(スキーマ)を分離し、他サービスのDBを直接参照しない。
例外は移行期のみで、期限を決めて解消する。共有DBが残ると「分割したのに運用はモノリス」が起きる。

データ整合性:分散トランザクションを前提にしない

複数サービスを跨ぐACIDトランザクションは現実的に重くなる。代わりに、Saga(補償トランザクション)やアウトボックスパターンで“結果整合”を設計する。
・注文作成 → 支払い要求イベント → 支払い成功/失敗イベント → 注文状態更新
失敗したら補償処理(注文キャンセル、在庫戻し)で整合性を回復させる設計に寄せる。

イベント設計:Outboxパターンで「DB更新とイベント送信」をズラさない

発生しがちな事故:DB更新は成功したのにイベント送信が失敗、またはその逆でサービス間の状態がズレる。
対策:同一DBトランザクション内で outbox テーブルへイベントを書き、別プロセスが確実に配信する。

// 例:Orderサービス(概念の雛形)
DB::transaction(function () use ($data) {
    $order = Order::create($data);

    Outbox::create([
        'event_type' => 'order.created',
        'payload' => json_encode(['order_id' => $order->id]),
        'status' => 'pending',
    ]);
});
// 別ワーカーが pending を取り出して送信し、sent にする(概念)
$events = Outbox::where('status', 'pending')->limit(100)->get();
foreach ($events as $e) {
    // publish($e->event_type, $e->payload);
    $e->update(['status' => 'sent']);
}

サービス間認証:内部通信と外部公開を分ける

外部(ユーザー/クライアント)向けの認証と、内部(サービス間)向けの認証は分けた方が安全。
・外部:JWT/セッション/トークン(要件次第)
・内部:mTLS、サービスアカウント、署名付きトークンなど
内部認証を雑にすると、1サービス侵害で横展開(ラテラルムーブメント)が起きやすい。

API Gateway:入口を統一して認可・レート制限・集約を担う

マイクロサービス化すると、クライアントが複数サービスを直接叩き始めて辛くなる。Gatewayで入口を統一すると、以下を一箇所で制御できる。
・認証/認可
・レート制限
・ルーティング(/api/orders → Orderサービス)
・レスポンス集約(BFF的にまとめる)
Laravel側は「サービスは中身に集中」「入口で共通機能」を意識すると整理しやすい。

通信の失敗を設計する:タイムアウト、リトライ、サーキットブレーカ

発生しがちな事故:下流サービスが遅い→上流が待ち続けて詰まる→全体が落ちる。
最低限のルール:
・タイムアウトを短く固定(無限待ちをしない)
・リトライは回数と間隔を固定(雪だるま式増加を防ぐ)
・冪等性(同じ要求を複数回投げても結果が壊れない)
・サーキットブレーカ(失敗が続くと遮断し回復を待つ)
LaravelでHTTPクライアントを使う場合でも、タイムアウトとリトライは必ず設計に含める。

// Laravel HTTP Client の例(方針を固定する)
$response = Http::timeout(2)
    ->retry(2, 200) // 2回、200ms間隔
    ->post($url, $payload);

ログとトレーシング:分散環境では「関連付け」が本体

マイクロサービスは、障害調査が「複数ログを追う」作業になる。
必須:リクエストID(Correlation ID)を入口で生成して全サービスへ伝播し、ログに必ず出す。
これが無いと「どのユーザーのどの操作が原因か」が追えず、復旧が遅くなる。

// 例:ヘッダからX-Request-Idを受け取る/無ければ生成する(概念)
$requestId = request()->header('X-Request-Id') ?? (string) \Illuminate\Support\Str::uuid();
// logger()->withContext(['request_id' => $requestId]);

デプロイ戦略:サービス単位のリリースと互換性を保つ

マイクロサービスは「片方だけ先に更新される」前提になる。
・API/イベントのバージョニング
・後方互換(新フィールド追加はOK、削除は段階的)
・スキーマ変更は“追加→移行→削除”の順
これを守らないと、片方のデプロイで別サービスが落ちる事故が起きる。

テスト戦略:契約テストとE2Eの役割分担

単体テストだけではサービス間の不整合を検出しにくい。
・契約テスト:API/イベントの形式を固定し、互換性を守る
・E2E:重要シナリオだけ通す(全ケースをE2Eにしない)
・スタブ/モック:下流が落ちても上流のテストが回るようにする
テストは「境界の守り」を強くするほどマイクロサービスの価値が出る。

移行パス:モノリスからの切り出しはStranglerパターンが安定

既存モノリスを一気に分割すると、運用も設計も破綻しやすい。
Strangler(絞め殺し)パターン:
1) 入口をGatewayで統一
2) 変更頻度が高い/独立性が高い機能から切り出す
3) DBも徐々に分離(移行期間は同期や複製を前提)
4) モノリス側の該当機能を縮退
この順で進めると、止めずに移行しやすい。

まとめ:Laravelでマイクロサービスを成功させる最重要ポイント

・分割はコードよりデータ(DB分離)が本体
・同期HTTPは最小限、イベントで疎結合に寄せる
・結果整合を前提にSaga/Outboxでズレを制御する
・Gatewayで入口を統一し、共通機能を集約する
・タイムアウト/リトライ/冪等性/遮断を最初から設計する
・リクエストIDを伝播して、ログと調査を成立させる
・互換性を守るデプロイと契約テストが生命線