EventBridge Pipes・Schedulerを3ヶ月本番運用して見えた、地雷と活用パターン
Lambda + SQS + Step Functionsの複雑な絡み合いをEventBridge Pipes・Schedulerで整理。実装のコツと、本番で痛い目を見たトラブルシューティングをまとめました。
EventBridge Pipes・Schedulerで本番サーバーレスを3ヶ月回した話
先日プロジェクトで、Lambda + SQS + Step Functions の複雑な絡み合いを整理する必要があって、EventBridge Pipes と Scheduler を本格導入してみました。正直、最初は「こんなのわざわざ使うほど?」って懐疑的だったんですけど、3ヶ月回してみたら、複雑さが一気に減ったんですよね。
うちのチームでは、複数のマイクロサービスから発火するイベントを集約して、フィルタリングして、異なるフォーマットで別システムに送る。みたいな処理が増えていて、その度にLambdaを挟んで「中継役」を作ってた。それが累積すると、どれがどのイベントを変換してるのかわからなくなるんですよ。
PipesとSchedulerはそこを一気に解決してくれました。今回は実装した話と、実務で痛い目を見たポイントを共有します。
EventBridge Pipes:イベント変換の中心が生まれた
まず、Pipesについて。これ、要するに「EventBridgeのバージョンアップ版」じゃなくて、専用の「イベント変換・ルーティング」サービスなんですよね。流れとしては Source → Optional Filter → Optional Enrichment → Optional Dead Letter Queue → Target という感じ。
うちで導入した構成を図にするとこんな感じです。
graph TB
subgraph "Source Services"
SVC1["Order Service<br/>DynamoDB Streams"]
SVC2["Payment Service<br/>SNS"]
SVC3["Inventory Service<br/>SQS"]
end
subgraph "EventBridge Pipes"
PIPE1["Pipe: OrderToPayout<br/>Filter + Enrichment"]
PIPE2["Pipe: PaymentToNotif<br/>Transform"]
PIPE3["Pipe: InventoryToAnalytics<br/>Batch Enrichment"]
end
subgraph "Targets"
TGT1["Payout Lambda"]
TGT2["Notification SQS"]
TGT3["Analytics S3"]
end
subgraph "Monitoring"
DLQ["Dead Letter Queue<br/>SQS"]
CW["CloudWatch Logs"]
end
SVC1 --> PIPE1
SVC2 --> PIPE2
SVC3 --> PIPE3
PIPE1 --> TGT1
PIPE2 --> TGT2
PIPE3 --> TGT3
PIPE1 -.-> DLQ
PIPE2 -.-> DLQ
PIPE3 -.-> DLQ
PIPE1 -.-> CW
PIPE2 -.-> CW
PIPE3 -.-> CW
これが地味に便利なんですよね。具体例でいうと、注文サービスのDynamoDB Streamsで「OrderCreated」イベントが発火する。でも、Payout(支払い)サービスが期待するフォーマットって別で、かつ特定の注文ステータス(例:paid_not_settled)のみ処理したい。そういう場合、従来はLambdaを挟んで変換・フィルタをしてました。
Pipesなら、JSON Path で条件を書いて、EventBridge Input Transformer または Lambda Enrichment で形式を変えるだけ。結果として、Lambda関数を4個減らせたんですよ。
実装した filtering + enrichment の例
ここが実務的なポイントです。フィルター条件をJSON Pathで書きます:
{
"source": ["order.service"],
"detail-type": ["Order Created"],
"detail": {
"status": ["paid_not_settled"],
"amount": [{"numeric": [">", 1000]}]
}
}
これで「支払い済みで未決済、かつ金額1000以上」の注文だけを通す。シンプルですね。
Enrichmentは、Lambda関数を指定するか、API呼び出しもできます。うちの場合、顧客マスター情報を Redis から取得する必要があったので、Lambda enrichment を書きました:
import json
import redis
def lambda_handler(event, context):
rc = redis.StrictRedis(host=os.environ['REDIS_ENDPOINT'], decode_responses=True)
customer_id = event['detail']['customer_id']
customer_info = rc.get(f"customer:{customer_id}")
return {
**event['detail'],
'customer_tier': json.loads(customer_info)['tier']
}
PipesがこのLambdaを自動で呼び出して、戻り値をイベントにマージしてくれるんです。冷却時間?ほぼないです。SnapStart対応なので、99%のケースで100ms以内に実行される。
ただ、ここで痛い目を見ました。Enrichment Lambdaのタイムアウトが想像以上に効くんですよね。初期段階で「取得失敗時のリトライ」をEnrichment側で実装してたんですけど、それが延々とリトライされると、Pipeがハング状態になっちゃう。だから、Enrichment Lambdaは「絶対に成功するか、すぐに失敗する」設計にすべき。失敗時の詳細な補正は、Target側のLambdaで処理する方が安全です。
EventBridge Scheduler:定期実行が単純になった
Scheduler は、従来の EventBridge Rules の「定期実行」機能を独立させて強化したもの。cron式と柔軟なタイムゾーン設定、そしてめっちゃ便利な「最大試行回数」「バックオフ戦略」があります。
うちでは、毎日深夜2時にデータウェアハウスの増分ロードを走らせてたんですけど、従来は Step Functions + EventBridge Rules でやってました。今はSchedulerで十分ですよ。
# CDK での定義例
from aws_cdk import aws_scheduler as scheduler
from aws_cdk import aws_iam as iam
from aws_cdk import aws_sqs as sqs
queue = sqs.Queue(self, "DataLoadQueue")
schedule = scheduler.Schedule(
self, "DailyDataLoadSchedule",
expression=scheduler.ScheduleExpression.at("cron(0 2 ? * * *)"), # 毎日2時(UTC)
timezone="Asia/Tokyo",
target=scheduler.ScheduleTarget(
arn=queue.queue_arn,
role=iam.Role(
self, "ScheduleRole",
assumed_by=iam.ServicePrincipal("scheduler.amazonaws.com")
),
retry_policy=scheduler.RetryPolicy(
max_event_age=Duration.hours(2),
max_retry_attempts=3
),
dead_letter_queue=sqs.Queue(self, "DataLoadDLQ")
)
)
これが地味に便利な理由が、タイムゾーン対応なんですよね。東京時間で「毎日午後3時」と指定できるんです。従来の EventBridge Rules は UTC 固定で、計算間違えて朝4時に走ってた。みたいなことが何度もあった。
痛い目を見た話
運用を始めて2週間で、Scheduler が SQS に 1時間ごとに無限再送信を続けてた事件がありました。原因は、ターゲットの SQS 権限が不足してたから。Scheduler はリトライを自動で続けるので、権限エラーに気づくのが遅れたんですよね。
学んだポイント:
- Scheduler は「失敗した = リトライする」という設計。権限周りは本番投入前に3回は検証すべき
- Max Event Age を設定しないと、古いメッセージが永遠に溜まる
- Dead Letter Queue はほぼ必須。これがないと失敗の詳細が見えない
複合構成:Pipes + Scheduler で非同期処理パイプライン
うちでやった本格的な構成は、Scheduler で定期発火したイベントを、Pipes で変換・フィルタして、複数のターゲットに分散させるやつです。
xychart-beta
title EventBridge Pipeline処理量 (1日)
x-axis [00:00, 06:00, 12:00, 18:00, 24:00]
y-axis "イベント数" 0 --> 5000
line [200, 1800, 3200, 2100, 450]
line [150, 900, 1400, 980, 200]
line [50, 200, 400, 300, 100]
グラフの見方としては、上の線がSchedulerからのスケジュール実行、中がPipesでフィルタされたもの、下がDLQに送られたエラーイベント。ほぼ一貫性がありますね。
実装上の大事なポイントをまとめると:
Input Transformer のシンプル化 Pipesのデフォルト変換じゃ足りないケースが多いので、Lambda enrichmentにしがちですが、まずは Input Transformer(EventBridge ネイティブな JSON 変換)を試すんですよ。冷却時間ゼロですから。
DLQ の監視 DLQ にメッセージが溜まってることに気づかずに、本番で「あれ、イベント処理されてない」となりました。CloudWatch Alarms で DLQ の ApproximateNumberOfMessagesVisible を監視することが必須です。
Pipes と Scheduler の権限設計 IAM ロールの最小権限原則で、Pipes は「SQS SendMessage」だけ、Scheduler は「SQS SendMessage」だけ、という感じで分けるんです。共有ロールにしたらアクセス権のデバッグが地獄になります。
実運用で見えた良い点・しんどい点
ここからは、3ヶ月運用してわかったリアルな話です。
良かった点:
- Lambda関数を4個削減できた → CloudWatch Logs のコストが月3万円減った
- イベント変換の仕様が可視化された。Pipes の定義ファイル見たら「あ、ここでフィルタされてるのか」がわかる
- Enrichment で外部API呼び出しする場合、Pipes側でタイムアウト・リトライを一括管理できる
- テスト環境と本番環境で、cron式だけ変えて同じ定義を使い回せる
しんどかった点:
- CloudWatch ログで Pipes のエラーを追跡しづらい。「なぜフィルタされたのか」「Enrichment で何が起きたのか」が黒箱になりやすい
- Input Transformer の JSON Path 記法が複雑。
$.detail.nested[0].valueみたいなのはいいけど、条件付き変換とかになると、結局Lambda enrichmentに逃げることになる - Scheduler のタイムゾーン(Asia/Tokyo)と Lambda のデフォルトタイムゾーン(UTC)のズレで、初期段階で想定と違う時刻に走る事案が2回あった
- DLQ を見落とすと、失敗したイベントがどこへ行ったのか不明。CloudFormation で DLQ も自動作成することが大事です
実装パターンの選択
実装パターンを比較するなら、こんな感じですね:
| 要件 | EventBridge Pipes | Lambda + SQS | Step Functions |
|---|---|---|---|
| イベント変換 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| スケーラビリティ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 運用のしやすさ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ |
| コスト | 最安 | 中程度 | 高い |
| デバッグの容易さ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 複雑なワークフロー | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
正直に言うと、Pipes は「シンプルなイベント変換・ルーティング」に特化してる。複雑なビジネスロジックが必要なら、Step Functions + Lambda の方が長期的には楽ですよ。でも、「イベント A をフィルタしてイベント B に変換して SQS に送る」みたいなのなら、Pipes 一択です。
Scheduler は、従来の EventBridge Rules + Lambda(定期実行用)を完全に置き換えました。タイムゾーン対応と DLQ がデフォルトで使えるのがでかい。ただし、非常に複雑なスケジュール(例:「毎月第2営業日の午前9時」)は、Step Functions や Lambda + EventBridge Rules の組み合わせの方が安全かもしれません。
チームで共有した実装ガイドライン
うちのチームで、Pipes・Scheduler を導入する際の最低限のチェックリストを作ったんですけど、紹介しますね:
DLQ は必須
- CloudFormation で Pipes 定義と同時に DLQ (SQS) を作成
- CloudWatch Alarms で DLQ メッセージ数を監視
Input Transformer で足りるか、Lambda enrichment が必要か
- JSON Path で変換できるなら Input Transformer
- 外部 API 呼び出しや複雑なロジックなら Lambda enrichment
- Lambda enrichment の場合は、必ず SnapStart 有効化
Scheduler のタイムゾーン
- Asia/Tokyo に統一
- cron 式は UTC ではなく、指定したタイムゾーンで解釈される点を確認
ログ戦略
- CloudWatch Logs に Pipes のすべてのイベントを送出
- CloudWatch Insights で「失敗したイベント」を集計するクエリを用意
テスト環境での検証
- 本番投入前に、DLQ へのリトライが正しく動くか確認
- 権限不足時の挙動(無限リトライ)が起きないか確認
イベント駆動とサーバーレスの相乗効果
EventBridge Pipes・Scheduler をサーバーレス基盤に組み込むと、マイクロサービス間の疎結合が飛躍的に高まります。
従来構成では、サービス A → Lambda(中継)→ サービス B みたいに、Lambda が「宛先を知る」必要がありました。Pipes を導入すると、サービス A はイベントを発火するだけ。「誰が使うのか」は Pipes の設定で一元管理される。結果、サービス間の依存性が減って、デプロイ順序も気にならなくなるんですよ。
Specifically、うちで見えた効果:
- デプロイリスク低下:サービス A を更新しても、Pipes が変わらなければ他のサービスに影響なし
- スケーリングが独立:Pipes のリージョン間レプリケーション設定 1 つで、複数リージョン運用が可能
- 監視の一元化:CloudWatch で Pipes のスループットを見れば、全体的なイベント処理量が把握できる
まとめ
3ヶ月運用して、EventBridge Pipes と Scheduler は「小規模〜中規模のサーバーレスアーキテクチャに最適」という確信が持てました。特に以下の場合、導入を検討する価値があります:
-
複数のマイクロサービスからイベントが発火する → Pipes でフィルタ・変換を一元管理すると、Lambda関数を大幅削減できる
-
定期実行タスク(バッチ処理など)がある → Scheduler で cron 管理を一元化。タイムゾーン問題も消える
-
イベントの形式が複数混在している → Pipes の Input Transformer で標準化。ただし複雑すぎるなら Lambda enrichment
-
本番環境での信頼性が重要 → DLQ + CloudWatch Alarms で、失敗したイベントを確実に追跡できる
もし Lambda 関数が「単なる中継」になってたら、Pipes への置き換えを検討する価値あり。権限周りのテストは 3 回やって、DLQ は必ず用意する。正直まだ完全には使いこなせてないけど、今のところ地味に便利な選択肢だなと感じてます。