本番のサーバーレス:エンタープライズAWSプラットフォームからの教訓
50個超のLambda関数、ヘキサゴナルアーキテクチャ、SQS駆動のバッチパイプライン、マルチリージョンDR——エンタープライズ級サーバーレスを支える構造・キュー設計・運用判断。
私は「退屈に聞こえる要件」を尊重することを学びました。「法人顧客がCSVファイルをアップロードする」は週末プロジェクトのように読めます——そのファイルをマスターデータと突合して検証し、SFTP経由で外部の基幹システムに登録し、リアルタイムで結果を返し、リージョン障害を数時間のRTOで生き延び、監査人があなたのVPC Flow Logsを読む、という条件が付くまでは。
私はまさにその要件を持つB2Bデータ受付プラットフォームの技術デリバリーをリードしました。全面的にサーバーレスなAWS構成です。この記事は、それをエンタープライズ級で成立させたアーキテクチャ上の判断と、価値が後から見えてきた判断のツアーです。
システムの形
- 50個超のLambda関数(TypeScript、Node.js 22)。同期APIティアと非同期バッチ処理に分かれ、AWS SAMでビルド・デプロイ
- Aurora PostgreSQL に Prisma 経由でアクセスし、前面に RDS Proxy
- 多段の検証・登録パイプラインを駆動する SQSキュー
- レガシーな外部システムとの SFTP連携
- 処理状況をブラウザにリアルタイムで届ける WebSocket API
- 10パッケージのモノレポによる React 19 SPA(Vite、TanStack Router/Query)、認証は Cognito(OAuth2 + SAMLフェデレーション)
- Terraform 約24モジュール・約10環境、DRのため 東京・大阪の2リージョン にデプロイ
Lambdaの中のヘキサゴナルアーキテクチャ
1つのコードベースを共有する50個の関数は、構造なしでは急速に腐ります。すべての関数が同じヘキサゴナルなレイアウトに従いました:
src/
core/domains/<domain>/ # domain、application (ユースケース)、ports
adapters/
primary/ # httpハンドラー、スキーマ、プレゼンター
secondary/ # 永続化 (Prisma)、ストレージ、キュー
containers/ # ユースケースのファクトリー — 配線を1か所に
これを定着させたルール:ハンドラーは10行。 イベントをパースし、ユースケースを呼び、結果を写像する。ビジネスロジックはすべて、ポート(インターフェース)を受け取るユースケースに住みます。つまりユニットテストはAWSのエミュレーションなしにインメモリのフェイクで走る。ローカルのテストスイートは数百ミリ秒に収まり続け、同じユースケースが今日はAPI Gatewayハンドラー、明日はSQSハンドラーの前面に立てます。
これが最も目に見えて回収されたのはバッチパイプラインでした。同じ検証ユースケースが3つの文脈で動きます:APIからの同期的な単票チェック、SQSからの非同期バルク検証、そして登録時の再検証パスです。
非同期パイプライン:背骨としてのSQS
ファイル処理はステージの連鎖として動きます。各ステージはキュー1本とコンシューマーLambda1つ:
アップロード → パース/正規化 → 検証(マスターデータ突合)→ 登録(SFTP)→ 通知
効いた設計判断:
- 大きな1本ではなく、ステージごとに1本のキュー。 各ステージが独立にスケールし、絞られ、失敗します。登録は外部システムの営業時間に縛られますが、検証は縛られない。キューを分けたことで、検証を止めずに登録だけを夜間に滞留させられました。
- DLQを全箇所に、リドライブはランブックの手順として。 すべてのコンシューマーにデッドレターキューとDLQ深度のアラームがあります。毒メッセージが止めるのはそのレコードであって、パイプラインではありません。
- キャッシュされたマスターデータに対する検証。 存在チェック(支店コード、住所マスター)は同じ参照を叩き続けるため、Postgresの前にキャッシュを重ねました——ネガティブ(不存在)結果のキャッシュ込みで。検証ワークロードでは「このコードは存在しない」は「存在する」と同じ頻度で照会され、同じだけキャッシュに値するからです。この1つの変更で、バルクアップロード時のDB負荷スパイクというクラスが丸ごと消えました。
- WebSocketの進捗イベント。 長時間バッチ+無言のUIはサポートチケット製造機です。各ステージが進捗をパブリッシュし、小さなLambdaがWebSocket APIでブロードキャストし、SPAがライブ表示する。作るのは安く、UXの見返りは不釣り合いに大きい。
Aurora + Lambda:コネクション問題
LambdaのスケーリングモデルとPostgresのコネクションモデルは天敵です——同時実行のバーストは一瞬でコネクションを食い尽くします。RDS Proxy が間に立ち、呼び出しをまたいでコネクションをプールします。Prismaでの実務ルールは:クライアントはハンドラーの外で一度だけ生成、インスタンスあたりのプールは最小限、多重化はプロキシに任せる。バルクアップロードのバースト下で、プロキシは本来ならデータベースを倒していたはずのコネクションスパイクを平らにしました。
実際にリハーサルするマルチリージョンDR
プラットフォームは東京をプライマリ、大阪をスタンバイとしてデプロイされました。Terraformは同じモジュール群から両リージョンに適用し、Auroraはクロスリージョンでレプリケーションし、S3バケットは非同期にレプリケーションします。教訓は2つ:
- DRはインフラの後付けではなくプロダクト機能である。 フェイルオーバーのランブック——DNS、データベース昇格、キューのドレイン順序——は文書化され、リハーサルされました。テストされていないDRは能力ではなく図です。
- Terraformモジュールは2つ目のリージョンで元を取る。 大阪の構築はプロジェクトではなく変数ファイルでした。東京を手作業で組んでいたら、DRリージョンは初日からドリフトしていたでしょう。
ネットワークも同様に地味で、しかし荷重を支えます:4層のVPC(ファイアウォール/パブリック/プロテクテッド/プライベートのサブネット)、エグレス制御のNetwork Firewall、そしてフル監査スタック——CloudTrail、GuardDuty、Security Hub、Config——に加えて、「何が何と通信したかを証明せよ」への答えとしての VPC Flow LogsへのAthenaクエリ。
可観測性
X-RayがAPI Gateway → Lambda → SQS → Lambda → Auroraを横断してリクエストをトレースし、「アップロードが遅い」を考古学からフレームグラフに変えました。CloudWatchアラームは、インシデントを実際に予告する退屈なものたちを見張ります:DLQ深度、キューの滞留時間、Lambdaのエラー率とスロットル、RDS Proxyのコネクション飽和。バイリンガルのログクエリマニュアルがチームのLogs Insightsレシピを文書化しました。昨日の失敗を数える方法が部族の知識のままでは、監視戦略とは呼べないからです。
正直なトレードオフ
サーバーレスはここでは正解でした——スパイクの激しいバッチワークロード、エンタープライズのセキュリティ要件、小さなチーム——しかし無料ではありません:
- コールドスタートはバッチでは許容範囲、APIティアでは体感されました。レイテンシに敏感な一握りの関数へのprovisioned concurrencyが現実的な対処でした。
- ローカル開発には投資が要ります。ヘキサゴナル構造がユニットテストを救いましたが、エンドツーエンドのフローには依然デプロイ済み環境が必要です。Terraform管理の環境が10個あるのは、すべてのエンジニアとステージに実物を与えるためです。
- 50個の小さな関数は5個のサービスより運用が重い。その代償として、各関数は仕事1つ・キュー1本・アラーム1つを持ちます——何かが壊れたとき、爆発半径が自分で名乗り出てくれるのです。
どれもエキゾチックではありません。中心には今もAPI Gatewayの後ろのLambdaがあり、本番とはその周りに巻きつけたすべてです:関数の内側の構造、関数の間のキュー、データベースの前のプロキシ、2つ目のリージョン、そして「動かないことを祈る部分」をテストする規律。
この記事は同じプラットフォームを扱う短いシリーズの起点です:Lambdaの中のヘキサゴナルアーキテクチャ(コード付き)、実際にリハーサルするマルチリージョンDR、ログをAPIのように設計する。