キューポリシーのないSNSとSQSの末路

SNSトピックにSQSキューを購読させただけでは配送されない。Publishは成功し、エラーはどこにも出ず、キューに届かないだけ。必要なのはキューポリシーで、LocalStackはこれを強制しないためローカルのテストは全部通る。デデュープ窓と順序保証の実測も。

SNS トピックに SQS キューを購読させる。AWS のメッセージングで一番よくある形だが、 ここに罠がある。Subscribe が成功していても、キューポリシーが無ければ配送されない。 しかも、

実際にこれを踏んで半日調べたので、再現と対策を独立した記事として残す。

最小の再現

トピックとキューを作り、購読させ、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つのうちどれか、できれば複数:

  1. キューポリシーを購読とセットで作る(IaC やセットアップコードで、購読を作る場所に必ず隣接させる)
  2. DLQ を設定する(配送失敗が痕跡として残るようになる)
  3. 一度は実 AWS で受信まで通す(認可はエミュレータでは検査できない、と割り切る)

ついでに実測した2つの挙動差

実 AWS と LocalStack で FIFO の挙動を突き合わせたので記録しておく。

5分デデュープ窓。同一 MessageDeduplicationId で本文だけ変えて2回 publish:

実 AWSLocalStack
2回目の Publish成功(エラーなし)成功(エラーなし)
配送1通のみ(2回目は飲まれる)1通のみ(同じ)
MessageId2回とも同一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 未満だった。

まとめ

← Back to all posts