はじめに:サーバーレスの甘い誘惑と厳しい現実
サーバーレスアーキテクチャは、最初は非常にシンプルに見えます。Lambda関数をいくつか作成し、SQSで非同期処理を組み、DynamoDBにデータを保存すれば完了です。しかし、この構造が数百、数千のAWSアカウントに拡大し、Lambda関数が100万を超えると、状況は一変します。
本記事では、ProGlove社が実際に経験した事例をもとに、サーバーレスシステムが大規模に拡張される際に発生する予期せぬ問題とその解決策を詳しく解説します。単なる「Lambdaのコスト削減方法」を超えて、アーキテクチャパターンそのものを再考する必要に迫られたプロセスに焦点を当てます。
参考資料: 本記事はAWS Architecture Blogの原文を基に、日本の開発者コミュニティ向けに再構成しました。

フェーズ1:アカウント単位のテナント分離 — シンプルさがもたらした恩恵と呪い
ProGlove社は**アカウント単位のテナントモデル(1 account per tenant)**を採用しました。セキュリティ分離、コストの透明性、サービス所有権の明確化というメリットがありました。しかし、この選択が後のすべてのスケーリング問題の根源となります。
CloudFormation StackSets:最初は神ツールだったが…
最初の50アカウントまでは、CloudFormation StackSetsが優秀に機能しました。管理アカウントから一度のデプロイコマンドで、数十のアカウントに同時にインフラを更新できたのです。
# 初期のStackSets構成例(簡略化)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'マイクロサービス基本スタック - 全テナントアカウントにデプロイ'
Parameters:
TenantId:
Type: String
Description: 'テナント一意識別子'
Resources:
TenantLambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${TenantId}-processor'
Handler: index.handler
Runtime: python3.12
Code:
ZipFile: |
import json
def handler(event, context):
# ... 実際のビジネスロジック
return {'statusCode': 200}
Role: !GetAtt LambdaExecutionRole.Arn
しかし、Lambda関数が100万を超えると、StackSetsはパフォーマンスの限界に達しました。デプロイエラーが蓄積し、ロールバックに失敗するケースが頻発しました。結局、独自のデプロイエンジンを構築し始めたところ、この話を聞きつけたAWS CloudFormationチームが直接協力を申し出たというエピソードは非常に興味深いです。

フェーズ2:「セルフDDoS」と可観測性のパラドックス
同期スケジュールが招いた悲劇
すべてのLambda関数が同じ5分間隔でCloudWatchアラームを送信するように設定されていました。アカウントが数百を超えると、この「同時多発的リクエスト」が内部APIをまるでDDoS攻撃のように圧倒してしまったのです。
解決策は「ジッター(Jitter)」の導入でした。
# ジッター導入前:すべての関数が正時に実行
import boto3
def scheduled_handler(event, context):
# 全アカウントが同時にこの関数を実行
client = boto3.client('cloudwatch')
# ... メトリクス送信
pass
# ジッター導入後:ランダムな遅延を追加
import random
import time
def scheduled_handler(event, context):
# 0〜300秒のランダムな遅延
delay = random.uniform(0, 300)
time.sleep(delay)
client = boto3.client('cloudwatch')
# ... メトリクス送信
pass
基本原則: 「決してすべての場所で同じことを同時に行ってはいけない。」
可観測性コスト:隠れた落とし穴
アカウントあたり月額3ドルのモニタリングコストは、数百アカウントまでは「小さなコスト」でした。しかし、数千アカウントに拡大すると、モニタリングコストがコンピューティングコストを上回る状況が発生しました。
ProGlove社は、すべてのメトリクスを外部モニタリングツールに転送する代わりに、優先度に応じてデータを分類し、転送コストをアカウントあたり0.7ドルまで削減しました。非アクティブなアカウントは基本メトリクスのみを監視し、ほぼゼロに近い状態を維持しました。

フェーズ3:SQSの罠 — 「無料」だと思っていたコスト
従来のサーバーレスのベストプラクティスでは、SQSを使用してイベントをバッファリングし、再試行メカニズムを提供します。しかし、数千のアカウントでそれぞれのSQSキューをポーリングすると、メッセージが一つもないのに継続的にリクエストが発生し、コストが積み上がります。
解決策:SQSの排除と集中型DLQ
# 従来:EventBridge -> SQS -> Lambda(アカウント別DLQ)
# 変更:EventBridge -> Lambda(集中DLQ)
# 集中DLQ構成例
Resources:
CentralDeadLetterQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: 'central-dlq'
MessageRetentionPeriod: 1209600 # 14日
TenantProcessorFunction:
Type: AWS::Lambda::EventInvokeConfig
Properties:
FunctionName: !Ref TenantLambdaFunction
MaximumRetryAttempts: 2
DestinationConfig:
OnFailure:
Destination: !GetAtt CentralDeadLetterQueue.Arn
Type: SQS
重要なトレードオフ: 集中DLQはデータ分離(isolation)の原則を破ります。複数のテナントの障害イベントが一箇所に集まるため、AWSアカウントIDをテナントIDとして使用する「ブリッジドモデル」への移行が必要でした。
まとめ:効率性が成長よりも速くなければならない
ProGlove社の事例は、サーバーレスが「自動スケーリング」だけでは解決できない問題を明確に示しています。100万のLambda関数を運用するために必要なのは、容量ではなく効率性です。
日本の開発現場での適用コンテキスト
日本のSIerやスタートアップ環境では、「アカウント単位のテナント」モデルはコスト面で負担に感じられるかもしれません。しかし、重要な教訓はアカウント分離そのものではなく、**「規模に応じたアーキテクチャパターンの変化」**にあります。
- 初期スタートアップ: SQSベースの標準パターンに従いつつ、事前にジッターを導入しましょう。
- 成長段階(50〜200アカウント): モニタリングコストをコンピューティングコストと同等に管理しましょう。
- 大規模(200+アカウント): SQSの排除、集中DLQの導入、独自デプロイエンジンの検討を進めましょう。
本技術の限界と注意点
- データ分離 vs コスト: 集中DLQは分離性を弱めます。規制の厳しい業界(金融、医療)では適用が難しい場合があります。
- StackSets依存: AWSサービスチームとの緊密な連携が可能な環境でのみ推奨します。
次のステップとしての学習方向
- Serverless Landで様々なイベント駆動パターンを探索してください。
- AWS LambdaのProvisioned ConcurrencyとScale-to-Zeroのバランスを実験してみてください。
- エンタープライズAIエージェントの新しい時代で扱う自動化パターンもサーバーレスと組み合わせると効果的です。
合わせて読みたい記事