SNS ファンアウト

SNS トピックから複数の SQS キューや Lambda に同時配信し、1 つのイベントで複数の処理を並行実行するパターン

AWSメッセージング

SNS ファンアウトとは

SNS ファンアウトは、SNS トピックに 1 つのメッセージを発行すると、サブスクライブしている複数の SQS キューLambda 関数にメッセージが同時配信されるパターンである。1 つのイベントをトリガーに、複数の独立した処理を並行実行できる。

[注文サービス] → SNS トピック → SQS (在庫引き当て) → Lambda A
                             → SQS (メール通知)   → Lambda B
                             → SQS (分析データ)   → Lambda C
                             → Lambda D (直接呼び出し)

SNS → SQS の利点

SNS から Lambda を直接呼び出すこともできるが、SQS を挟むメリットがある。

構成メリットデメリット
SNS → Lambdaシンプル、低レイテンシ配信リトライを使い切ると破棄される
SNS → SQS → LambdaDLQ でエラー管理、バッチ処理、スロットリング若干のレイテンシ増加

SQS を挟むと、Lambda が失敗した場合にメッセージがキューへ戻り、可視性タイムアウトの経過後に再び受信される。受信回数が maxReceiveCount を超えたメッセージは SQS の DLQ (デッドレターキュー) へ移るので、後から原因を追える。

SNS から Lambda を直接呼ぶ構成にはこの受け皿が無い。SNS は配信リトライを使い切るとメッセージを破棄するため、購読に DLQ を設定していないと痕跡なく失われる。ここで言う DLQ は SNS の購読に付ける DLQ で、配信そのものが失敗したメッセージを受ける。SQS キューに付ける DLQ が受けるのは、届いた後の処理が繰り返し失敗したメッセージである。前半の失敗と後半の失敗は別の受け皿で拾うため、片方だけ用意すると残る方が盲点になる。SNS 購読の DLQ は通常の SQS キューで、redrive ポリシーの deadLetterTargetArn に ARN を書いて指定する。同一アカウント・同一リージョンのキューであることが条件で、FIFO トピックの購読には FIFO キューを使う。

配信形式と権限の落とし穴

SNS から SQS への配信は、既定ではペイロードが SNS の通知エンベロープに包まれる。キューから取り出した本文は Type や MessageId、TopicArn、Timestamp を含む JSON で、発行した内容はその Message フィールドに文字列として入っている。素の JSON が届くつもりで書いたコンシューマーは、この二重構造で解析に失敗する。購読属性の RawMessageDelivery を true にすると SNS のメタデータが外れ、発行した内容がそのまま届く。どちらの形式で受けるかを決めたら、そのキューを読む処理すべてで揃えておく。

権限はキュー側に書く。購読を作れていても、キューのポリシーで sns.amazonaws.com からの sqs:SendMessage を許可していなければメッセージは届かない。購読の状態は確認済みのまま送信だけが落ちるので、気付くのが遅れやすい失敗である。

{
  "Sid": "AllowSnsTopic",
  "Effect": "Allow",
  "Principal": { "Service": "sns.amazonaws.com" },
  "Action": "sqs:SendMessage",
  "Resource": "<キューの ARN>",
  "Condition": {
    "ArnEquals": { "aws:SourceArn": "<トピックの ARN>" }
  }
}

aws:SourceArn の条件は、第三者が自分のトピック経由でこのキューへ送り込むことを防ぐために置く。いわゆる混乱した代理問題への対策である。複数のトピックから受けるキューでは、トピックごとにステートメントを分けるか、aws:SourceAccount でアカウント単位に束ねる。

キューをサーバー側暗号化している場合、AWS マネージドキーのままでは SNS からのメッセージを受け取れない。カスタマー管理の KMS キーを作り、そのキーポリシーで SNS のサービスプリンシパルに暗号化操作を許可する。SNS 購読に付ける DLQ を暗号化する場合も同じ手当てが必要になる。

メッセージフィルタリング

フィルターポリシーは、トピックではなく購読に付ける JSON である。フィルターポリシーを持たない購読は、そのトピックに発行された全メッセージを受け取る。絞り込みは受け取る側の設定だと押さえておくと、配線を追いやすい。

発行側は、判定に使う値をメッセージ属性として付ける。

// メッセージ発行時に属性を付与
await sns.send(new PublishCommand({
  TopicArn: ORDER_TOPIC_ARN,
  Message: JSON.stringify(order),
  MessageAttributes: {
    orderType: { DataType: 'String', StringValue: 'premium' },
    amount: { DataType: 'Number', StringValue: '50000' },
  },
}));

購読側には、その属性に対する条件を置く。

{
  "orderType": ["premium"],
  "amount": [{ "numeric": [">=", 10000] }]
}

配列は「いずれかに一致」を意味し、キーを並べると全キー一致になる。数値比較を効かせるには発行側で DataType を Number にしておく必要があり、String のままでは文字列一致しか働かない。属性ベースの落とし穴は、発行側が属性を付け忘れたメッセージが条件を満たさず静かに配信対象外になることである。属性を増やしたくない場合は購読の FilterPolicyScope を MessageBody に切り替え、本文の JSON を直接条件にできる。この場合は本文が常に整形式の JSON である前提が要る。

EventBridge との使い分け

どちらも 1 つのイベントを複数の宛先へ届けるが、得意な形が違う。SNS は同じメッセージを多数の購読先へ配るのが速く、EventBridge は中身を見て宛先を選び分ける方が得意である。

観点SNS ファンアウトEventBridge
フィルタリング購読フィルターポリシー (属性または本文)イベントパターン (JSON パス)
宛先の数標準トピックで 1 トピックあたり 12,500,000 サブスクリプション (FIFO トピックは 100)1 イベントバスあたり既定 300 ルール、1 ルールあたり 5 ターゲット
スキーマ管理なしSchema Registry
適するケースシンプルなファンアウト複雑なルーティング

数値は 2026 年 8 月時点の既定クォータである。EventBridge のルール数は引き上げを申請できるが、1 ルールあたりのターゲット数 5 は引き上げできない。ルール数の既定が 300 ではなく 100 のリージョンもあるので、多数のルールを前提にするなら先に確認する。宛先を 6 つ以上にしたいときはルールを分けるか、ルールのターゲットに SNS トピックを置いてそこからファンアウトさせる。

購読数の上限が問題になることは実務ではまずない。先に当たるのは発行側で、標準トピックへの発行はリージョンとアカウント単位のクォータになる。東京リージョンの既定は毎秒 1,500 メッセージで、Publish と PublishBatch を合算した値である。ファンアウトは 1 回の発行で下流の処理数が倍になる構造なので、実際に詰まるのは下流の同時実行数や下流サービス側のクォータであることが多い。

設計時の判断は 2 つに絞られる。1 つは下流ごとに受け皿を持たせるかで、再処理や失敗調査が必要な処理は SQS を挟む。もう 1 つは絞り込みをどこでやるかで、購読フィルターで落とせるものは下流に届く前に落とす。届いてから捨てる設計は、下流の同時実行数と課金を無駄に使う。

この記事は役に立ちましたか?

関連用語

関連する記事