認証・認可・課金の3サービスをどう繋ぐか — 統合パターンと設計方針

認証(auth)・認可(authz)・課金(billing)の3サービス間の通信パターンを整理。同期API・イベント駆動・共有データベースの判断基準、未実装の要件を含めた設計方針を体系的にまとめる。

はじめに

マルチプロダクト基盤を「認証・認可・課金」の3サービスに分離する設計は前回の記事で整理しました。次の問いは「この3つをどう繋ぐか」です。

選択肢は大きく3つ:

  1. 同期 API — リクエスト処理中に呼び出す
  2. イベント駆動 — 非同期で通知する
  3. 共有データベース — 同じ DB を読み書きする

この記事では、それぞれの特性と判断基準を整理した上で、現時点で実装済みの部分だけでなく 未実装だがいずれ必要になる要件 も含めた設計方針を示します。

大原則: アプリが組み立てる

3サービスが直接通信するのではなく、プロダクト(アプリ)がオーケストレーターになるのが基本設計です。

[アプリ]
  ├─ 1. JWT 検証      → auth(JWKS 取得のみ)
  ├─ 2. プラン取得     → billing(API 呼び出し)
  └─ 3. 認可判定       → authz(context にプランを渡す)

auth / authz / billing の3つは互いを知らない。アプリが「auth から得た sub」と「billing から得た plan」を組み合わせて、authz に context として渡す。

この設計の利点:

共有データベースを選ばない理由

「同じ DB に書けば簡単じゃない?」という誘惑がありますが、これは3つに分けた意味を根本から否定します。

問題具体例
結合度が最大billing のスキーマ変更で authz が壊れる
デプロイが連動1つのマイグレーションで3サービス止まる
責任境界が曖昧このテーブルはどのサービスが所有者?
スケーリングが困難auth のトラフィックが billing の DB を圧迫
障害の伝播DB の障害で全サービスが同時に死ぬ

3つに分けたなら、データストアも分ける。 これは原則です。

同期 API とイベントの使い分け

同期 APIイベント
いつ使うリクエスト処理中に結果が必要結果を待たなくていい
プラン取得、認可判定ユーザー削除通知、キャッシュ無効化
失敗時エラーをユーザーに返すリトライ(DLQ で補償)
結合度中(API 仕様に依存)低(イベントスキーマだけ)
一貫性強い(同期なので即時反映)結果整合性(遅延あり)

判断基準は単純です:

通信マトリクス(現状 + 将来)

同期 API(アプリ → サービス)

呼び出し用途実装状況
アプリ → authJWT 検証(JWKS)実装済み
アプリ → authz認可判定(IsAuthorized)実装済み
アプリ → billingプラン取得未実装
アプリ → billing使用量取得(上限チェック)未実装
アプリ → billingエンタイトルメント取得未実装

イベント(サービス → サービス)

イベント発行元消費側用途実装状況
UserCreatedauthbilling組織にデフォルトプラン設定未実装
UserDeletedauthbilling組織の最後のユーザーなら課金停止検討未実装
UserSuspendedauthbilling, authzアクティブユーザー数の更新、セッション無効化未実装
PlanChangedbillingauthz認可キャッシュの無効化未実装
PlanChangedbillingアプリUI の機能制限切り替え未実装
UsageLimitReachedbillingアプリ上限到達の通知、機能制限未実装
PolicyCreated/Deletedauthz監査ログポリシー変更の記録未実装
LoginFailed (多数)authauthz, 通知不正アクセス検知、アカウントロック未実装
PaymentFailedbillingauth, 通知支払い失敗時のアカウント制限未実装
TrialExpiredbillingauth, アプリトライアル期限切れの処理未実装

イベント基盤の選択

AWS での選択肢

サービス特徴向いてるケース
EventBridgeルールベースのルーティング、スキーマレジストリサービス間イベント(推奨)
SNS + SQSPub/Sub + キューイングファンアウト + バッファリング
SQS 単体シンプルなキュー1対1の非同期処理
Step Functionsワークフローオーケストレーション複雑な多段処理

推奨: EventBridge

3サービス + 将来のサービス追加を考えると、EventBridge が最適です。

auth ──publish──→ EventBridge ──rule──→ billing の Lambda

billing ─publish──→   │ ──rule──→ authz の Lambda(キャッシュ無効化)

                      │ ──rule──→ 通知サービス(メール・Slack)

理由:

イベントスキーマの例

{
  "source": "rellf-auth",
  "detail-type": "UserDeleted",
  "time": "2026-04-12T10:00:00Z",
  "detail": {
    "sub": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "reason": "user_requested",
    "deletedBy": "admin:taishi"
  }
}
{
  "source": "rellf-billing",
  "detail-type": "PlanChanged",
  "time": "2026-04-12T11:00:00Z",
  "detail": {
    "orgId": "org-abc",
    "previousPlan": "free",
    "newPlan": "pro",
    "changedBy": "admin:taishi",
    "effectiveAt": "2026-04-12T11:00:00Z"
  }
}

未実装だがいずれ必要になる要件

1. トライアル管理

[ユーザーサインアップ]
  auth: UserCreated イベント発行

  billing: 14日間のトライアルプランを自動付与

[14日後]
  billing: TrialExpired イベント発行

  アプリ: 機能制限 UI を表示
  auth: (任意)ログイン時に「プランを選んでください」画面を挟む

トライアルの状態管理は billing の責任です。auth がトライアル期限を知る必要はなく、billing が「期限切れ」イベントを出せば、各サービスが反応する形になります。

2. 使用量ベースの制限

[APIリクエストごと]
  アプリ → billing: 使用量を記録(POST /api/orgs/:id/usage)

[上限到達時]
  billing: UsageLimitReached イベント発行

  アプリ: 429 Too Many Requests を返す
  通知: 管理者にメール/Slack 通知

使用量の記録は同期 APIで行いますが、上限到達の通知はイベントで行います。毎リクエストで上限チェックする場合は billing API を同期で叩く必要がありますが、レスポンス速度が気になるならキャッシュ + イベントで上限変更時だけ無効化する方式もあります。

3. 支払い失敗時のアカウント制限

[Stripe Webhook: 支払い失敗]
  billing: PaymentFailed イベント発行

  auth: アカウントを「制限付き」状態にする(ログインは可能だが機能制限)
  通知: ユーザーと管理者に通知

[猶予期間終了後]
  billing: AccountSuspendRequired イベント発行

  auth: アカウントを Suspended にする

支払い失敗でいきなりアカウントを停止するのではなく、猶予期間を設けるのが一般的です。この状態遷移は billing が制御し、auth はイベントを受けて実行するだけ。

4. 不正アクセス検知

[ログイン失敗が短時間に多発]
  auth: LoginFailedBurst イベント発行

  authz: 一時的にそのIPからのアクセスを拒否するポリシーを追加
  通知: セキュリティチームに通知

[一定時間後]
  authz: 一時ポリシーを自動削除

auth 単体でのレート制限に加えて、authz と連携した動的なポリシー追加で多層防御を実現します。

5. 監査ログの統合

auth  ──→ EventBridge ──→ 監査ログサービス ──→ DynamoDB / S3
authz ──→                  (全イベントを永続化)
billing ──→

3サービスのイベントをすべて監査ログサービスに流して永続化します。SOC 2 や ISO 27001 の要件で「誰が何をいつしたか」を追跡可能にする必要がある場合に必須です。

6. マルチテナント対応

[テナント(組織)作成]
  billing: 組織を作成、デフォルトプランを付与
  auth: 組織に初期管理者を紐付け
  authz: 組織専用の Policy Store を作成

テナント作成はイベントだけでは足りず、Saga パターン(各サービスでの処理を順番に実行し、失敗時は補償トランザクション)が必要になる場面です。

billing: CreateOrg
  → 成功
auth: AssignAdmin
  → 成功
authz: CreatePolicyStore
  → 失敗!
  → 補償: auth で Admin 解除、billing で Org 削除

これは Step Functions で実装するのが適切です。

7. SSO セッション連携

[auth でログアウト]
  auth: UserLoggedOut イベント発行

  各プロダクト: セッションを無効化(Back-Channel Logout)

OIDC の Back-Channel Logout を実装する場合、auth が各プロダクトの back_channel_logout_uri に POST を送ります。これは同期通信ですが、失敗してもログアウトの UX に影響を与えない設計にする必要があります。

8. 機能フラグとの統合

[billing: プラン変更]
  PlanChanged イベント

  Feature Flag サービス: プランに応じたフラグを更新

  アプリ: フラグに基づいて UI / API の機能を出し分け

エンタイトルメントと Feature Flag を統合すると、プラン変更が即座に機能の有効/無効に反映されます。LaunchDarkly などの Feature Flag サービスとの連携、または自前の仕組みで実装します。

段階的な実装ロードマップ

フェーズ通信方式実装内容
Phase 1(現在)同期 API のみアプリ → auth / authz / billing の API 呼び出し
Phase 2+ EventBridgeUserDeleted → billing、PlanChanged → authz キャッシュ無効化
Phase 3+ 監査ログ全イベントの永続化
Phase 4+ Sagaマルチテナント作成、支払い失敗時の状態遷移
Phase 5+ Back-ChannelSSO ログアウト連携

Phase 1 で実運用を始めて、実際の問題が発生したタイミングで次のフェーズに進むのが現実的です。「ユーザー削除したのに billing にゴミが残る」が起きたら Phase 2 へ、「監査要件が来た」ら Phase 3 へ。

イベント設計の原則

1. イベントは「何が起きたか」であって「何をしてほしいか」ではない

# 良い: 事実を伝える
{ "detail-type": "UserDeleted", "detail": { "sub": "user-123" } }

# 悪い: 命令を伝える
{ "detail-type": "DeleteBillingAccount", "detail": { "sub": "user-123" } }

事実を伝えれば、受信側が何をするかは受信側の責任です。発行側は受信側の存在を知らなくていい。

2. イベントに十分な情報を含める

受信側がイベントを処理するために発行元に API を叩き返す必要があるなら、イベントの情報が不足しています。

// 良い: 必要な情報が揃ってる
{
  "detail-type": "PlanChanged",
  "detail": {
    "orgId": "org-abc",
    "previousPlan": "free",
    "newPlan": "pro",
    "effectiveAt": "2026-04-12T11:00:00Z"
  }
}

// 悪い: orgId だけだと billing API を叩き返す必要がある
{
  "detail-type": "PlanChanged",
  "detail": { "orgId": "org-abc" }
}

3. べき等に処理する

同じイベントが2回届いても結果が変わらないように設計します。EventBridge は at-least-once delivery なので、重複配信は前提です。

4. 失敗に備える

DLQ(Dead Letter Queue)を設定し、処理失敗したイベントを退避させます。後からリプレイできるようにしておくと、障害復旧が楽になります。

まとめ

← Back to all posts