キューポリシーのないSNSとSQSの末路
SNSトピックにSQSキューを購読させただけでは配送されない。Publishは成功し、エラーはどこにも出ず、キューに届かないだけ。必要なのはキューポリシーで、LocalStackはこれを強制しないためローカルのテストは全部通る。デデュープ窓と順序保証の実測も。
SNS トピックに SQS キューを購読させる。AWS のメッセージングで一番よくある形だが、 ここに罠がある。Subscribe が成功していても、キューポリシーが無ければ配送されない。 しかも、
- Publish はエラーなく成功する(送信側からは完全に正常に見える)
- 配送だけが捨てられ、DLQ を設定していなければ痕跡がどこにも残らない
- LocalStack はキューポリシーを強制しないので、ローカルのテストは全部通る
実際にこれを踏んで半日調べたので、再現と対策を独立した記事として残す。
最小の再現
トピックとキューを作り、購読させ、publish する。ポリシーだけ設定しない。
aws sns create-topic --name demo-topic.fifo --attributes FifoTopic=true
aws sqs create-queue --queue-name demo-queue.fifo --attributes FifoQueue=true
# 購読は**成功する**(ポリシーの有無は検査されない)
aws sns subscribe --topic-arn <topic-arn> --protocol sqs --notification-endpoint <queue-arn>
# publish も**成功する**(MessageId が返る)
aws sns publish --topic-arn <topic-arn> --message hello \
--message-group-id g --message-deduplication-id d1
# しかしキューは空のまま
aws sqs receive-message --queue-url <queue-url> --wait-time-seconds 10
# (何も返らない)
どのコマンドも成功する。失敗するものが1つも無いまま、メッセージだけが消える。
原因: SQS の既定は「所有者以外は何もできない」
SQS キューは既定で所有者にしか操作を許さない。SNS がキューへ SendMessage するのは
SNS というサービスプリンシパルによる操作なので、キューポリシーで明示的に
許可しないと拒否される。そして拒否は publish 側には返らない——SNS から見れば
配送先の一つが失敗しただけで、Publish API の応答はとっくに成功で返っている。
必要なポリシーはこれだけ:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "sns.amazonaws.com" },
"Action": "sqs:SendMessage",
"Resource": "<キューの ARN>",
"Condition": { "ArnEquals": { "aws:SourceArn": "<トピックの ARN>" } }
}]
}
aws:SourceArn の条件を忘れると、同一リージョンの任意の SNS トピックがこの
キューへ書けるようになるので、許可はトピック単位に絞る。
なお、マネジメントコンソールから購読を作ると、この形のポリシーが自動で付くことがある。 手で作ると動き、コード(SDK / CLI / IaC)で作ると動かない、という食い違いの原因も たいていここにある。
なぜテストで気づけないか
この失敗の性質が悪いのは、確かめる側の道具がどれも「成功」を返すことにある。
| 確認手段 | 結果 |
|---|---|
sns publish の戻り値 | 成功(MessageId が返る) |
sns subscribe / list-subscriptions | 成功(購読は存在する) |
| CloudWatch などのエラーログ | 何も出ない(DLQ 未設定なら痕跡ゼロ) |
| LocalStack 上の結合テスト | 全部通る(キューポリシーを強制しないため) |
とくに最後が决定的で、LocalStack でどれだけ丁寧に「publish → 受信 → 重複排除」の 一気通貫テストを書いても、実 AWS の認可は一度も検査されていない。 「送れた」は「届く」を含意しない。届いたことは受信側でしか確認できず、 その確認を認可を強制しない環境でやっている限り、この穴は見えない。
検査の仕組みが壊れているとき、失敗ではなく成功が返ってくる——この構造は fail open として別記事に整理した分類の一種で、 「検査は正しいが、検査の走る環境に検出対象が存在できない」パターンにあたる。
対策は3つのうちどれか、できれば複数:
- キューポリシーを購読とセットで作る(IaC やセットアップコードで、購読を作る場所に必ず隣接させる)
- DLQ を設定する(配送失敗が痕跡として残るようになる)
- 一度は実 AWS で受信まで通す(認可はエミュレータでは検査できない、と割り切る)
ついでに実測した2つの挙動差
実 AWS と LocalStack で FIFO の挙動を突き合わせたので記録しておく。
5分デデュープ窓。同一 MessageDeduplicationId で本文だけ変えて2回 publish:
| 実 AWS | LocalStack | |
|---|---|---|
| 2回目の Publish | 成功(エラーなし) | 成功(エラーなし) |
| 配送 | 1通のみ(2回目は飲まれる) | 1通のみ(同じ) |
| MessageId | 2回とも同一 | 2回とも別 |
配送が1通に抑えられる挙動は LocalStack も再現する。違うのは MessageId で、 実 AWS は2回目にも1回目と同じ MessageId を返す。これは送信側が重複を検知できる 唯一の信号だが、LocalStack では毎回別の ID が返るため、この信号に依存する コードはローカルで検証できない。
順序保証。同一 MessageGroupId へ5メッセージ連投して、受信は 1→5 の完全順序
だった(実 AWS)。グループ内は順序保証・グループ間は並行、という FIFO の契約は
額面どおりに動く。
踏んだ経緯
検証リポジトリの outbox 構成 (DB に書いたイベントを relay が SNS へ、SQS 経由で consumer が受けて inbox が 重複を弾く)を、LocalStack から実 AWS に差し替えて再演したときに踏んだ。 アプリのコード変更は 0 行で済んだ一方、送信までの全チェックが通るのに受信だけが 沈黙し、キューポリシーに行き着いた。修正後は一気通貫の検査が実バスで全部通り、 使い終わったトピックとキューは当日削除、費用は $0.01 未満だった。
まとめ
- SNS → SQS は購読だけでは配送されない。キューポリシー(SNS からの SendMessage 許可、SourceArn 条件付き)が要る
- 無いときの症状は「全 API 成功・配送だけ消える・痕跡なし」。DLQ が無いと どこにもエラーが出ない
- LocalStack はこれを強制しないので、認可の検査だけはエミュレータの外で 一度は通すこと