Amazon SNS(Simple Notification Service)とは、1回の発行(publish)を、登録しておいた複数の宛先(SQSキュー、Lambda、メールなど)へ同時に配るサービスです。 発行する側は「トピック」という窓口に送るだけでよく、誰が受け取るかはトピック側の「サブスクリプション(購読)」で決まります。1つのトピックに複数のSQSキューをつなぐ形は「ファンアウト」と呼ばれます。

この記事では、トピック作成からSQSでの受信までを AWS CLI と boto3 の両方で並べ、各コマンドの出力と、SQSに届くメッセージのJSONをそのまま載せます。フィルターポリシーは書き方ごとに「届く・届かない」を実際に確かめた表にしました。

確認環境:Ubuntu 24.04、AWS CLI 1.46.1、Python 3.11.15、boto3 1.43.103、jq 1.7。AWS の API をローカルで再現するツール(moto 5.2.3)で実行しています。掲載するコマンドは、実際の AWS でそのまま使える形(接続先の指定なし)にしてあり、出力内のキューURLは AWS での形式に置き換えています。

Amazon SNSの早見表

やりたいこと AWS CLI boto3
トピックを作る aws sns create-topic --name NAME sns.create_topic(Name=...)
SQSキューを購読させる aws sns subscribe --topic-arn T --protocol sqs --notification-endpoint QUEUE_ARN sns.subscribe(TopicArn=, Protocol="sqs", Endpoint=)
発行する aws sns publish --topic-arn T --message TEXT sns.publish(TopicArn=, Message=)
属性を付けて発行する --message-attributes '{"k":{"DataType":"String","StringValue":"v"}}' MessageAttributes={...}
まとめて発行する aws sns publish-batch --publish-batch-request-entries [...] sns.publish_batch(...)
本文だけをSQSに届ける aws sns set-subscription-attributes --attribute-name RawMessageDelivery --attribute-value true Attributes={"RawMessageDelivery": "true"}
条件に合うものだけ届ける --attribute-name FilterPolicy --attribute-value '{"eventType":["order_created"]}' Attributes={"FilterPolicy": json.dumps(...)}
本文のJSONで絞り込む FilterPolicyScope を MessageBody にする Attributes={"FilterPolicyScope": "MessageBody"}
購読の一覧を見る aws sns list-subscriptions-by-topic --topic-arn T sns.list_subscriptions_by_topic(TopicArn=)
購読をやめる aws sns unsubscribe --subscription-arn S sns.unsubscribe(SubscriptionArn=)
トピックを消す aws sns delete-topic --topic-arn T sns.delete_topic(TopicArn=)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Amazon SNSの使い方 CLI・boto3とSQSファンアウト早見表」

SNSの登場人物と流れ

用語 役割
トピック 発行先の窓口。ARN(arn:aws:sns:リージョン:アカウントID:名前)で指定する
サブスクリプション 「このトピックに来たものを、この宛先へ届ける」という登録
プロトコル 宛先の種類。sqs、lambda、email、https など
メッセージ属性 本文とは別に付ける名前つきの値。フィルターポリシーの判定に使える
フィルターポリシー サブスクリプションごとに「届ける条件」を書いたJSON
flowchart LR
    P[発行する側] -->|publish| T[SNSトピック order-events]
    T -->|サブスクリプション1| Q1[SQS order-mail]
    T -->|サブスクリプション2| Q2[SQS order-point]
    Q1 --> W1[メール送信の処理]
    Q2 --> W2[ポイント加算の処理]

SQSは「1つのメッセージを1つの処理が取り出す」、SNSは「1つのメッセージを全員に配る」という違いがあります。両方を組み合わせると、配った先のキューでそれぞれの処理が自分のペースで取り出せます。

AWS CLIでトピック作成からSQS受信まで通す

1. トピックを作る(create-topic)

aws sns create-topic --name order-events
{
    "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:order-events"
}

同じ名前でもう一度実行しても、エラーにはならず同じ ARN が返ります。 そのため、セットアップ用のスクリプトに入れて何度流しても差し支えありません。

2. SQSキューを作り、ARNを調べる

SNSの購読先には、キューのURLではなく ARN を渡します。

aws sqs create-queue --queue-name order-mail
aws sqs get-queue-attributes \
  --queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789012/order-mail \
  --attribute-names QueueArn
{
    "QueueUrl": "https://sqs.ap-northeast-1.amazonaws.com/123456789012/order-mail"
}
{
    "Attributes": {
        "QueueArn": "arn:aws:sqs:ap-northeast-1:123456789012:order-mail"
    }
}

3. キューポリシーでSNSからの送信を許可する

AWS の公式ドキュメントには「SNSトピックがキューへメッセージを送るには、そのトピックに sqs:SendMessage を許可するポリシーをキューに設定する必要がある」と書かれています。CLI や boto3 で購読を作るときは、この設定を自分で入れます。

policy.json を用意します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"Service": "sns.amazonaws.com"},
      "Action": "sqs:SendMessage",
      "Resource": "arn:aws:sqs:ap-northeast-1:123456789012:order-mail",
      "Condition": {
        "ArnEquals": {"aws:SourceArn": "arn:aws:sns:ap-northeast-1:123456789012:order-events"}
      }
    }
  ]
}

set-queue-attributes の Policy は「JSONを文字列にしたもの」を値に取るので、jq の tojson で包むと手で引用符をエスケープせずに済みます。

QUEUE_URL=$(aws sqs get-queue-url --queue-name order-mail --query QueueUrl --output text)
aws sqs set-queue-attributes --queue-url "$QUEUE_URL" \
  --attributes "$(jq -c '{Policy: tojson}' policy.json)"

# 入ったことを確かめる
aws sqs get-queue-attributes --queue-url "$QUEUE_URL" --attribute-names Policy \
  --query Attributes.Policy --output text | jq -c '.Statement[0] | {Effect, Principal, Action}'
{"Effect":"Allow","Principal":{"Service":"sns.amazonaws.com"},"Action":"sqs:SendMessage"}

set-queue-attributes は成功すると何も表示しません。Condition の aws:SourceArn にトピックのARNを書くと、そのトピックからの送信だけを受け付けるキューになります。

4. 購読させる(subscribe)

aws sns subscribe \
  --topic-arn arn:aws:sns:ap-northeast-1:123456789012:order-events \
  --protocol sqs \
  --notification-endpoint arn:aws:sqs:ap-northeast-1:123456789012:order-mail
{
    "SubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:order-events:bd4f92c7-1e32-4cf6-92a1-bff395c3903c"
}

サブスクリプションARNは「トピックARN+UUID」の形です。あとで属性を変えたり購読をやめたりするときに使うので控えておきます。同じトピックとキューの組でもう一度 subscribe を実行すると、同じ SubscriptionArn が返りました。

一覧は list-subscriptions-by-topic で確認できます。

aws sns list-subscriptions-by-topic \
  --topic-arn arn:aws:sns:ap-northeast-1:123456789012:order-events
{
    "Subscriptions": [
        {
            "SubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:order-events:bd4f92c7-1e32-4cf6-92a1-bff395c3903c",
            "Owner": "123456789012",
            "Protocol": "sqs",
            "Endpoint": "arn:aws:sqs:ap-northeast-1:123456789012:order-mail",
            "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:order-events"
        }
    ]
}

5. 発行する(publish)

aws sns publish \
  --topic-arn arn:aws:sns:ap-northeast-1:123456789012:order-events \
  --subject "order created" \
  --message '{"orderId": 1001, "status": "created"}' \
  --message-attributes '{"eventType":{"DataType":"String","StringValue":"order_created"},"amount":{"DataType":"Number","StringValue":"4800"}}'
{
    "MessageId": "af73bb08-1807-48c0-bbe4-6677a7f5da96"
}

メッセージ属性は DataType(String、Number、Binary など)と値の組で書きます。Number でも値は StringValue に文字列として入れる点が最初につまずきやすい箇所です。

6. SQSで受け取る(receive-message)

aws sqs receive-message --queue-url "$QUEUE_URL" --wait-time-seconds 1 --max-number-of-messages 10
{
    "Messages": [
        {
            "MessageId": "e358186a-fe82-4640-b227-e56d45258550",
            "ReceiptHandle": "ngndxnkurizuaqltkksd…(省略)…",
            "MD5OfBody": "84f15d45a24a31358cfbe0a50d68d6f4",
            "Body": "{\n  \"Message\": \"{\\\"orderId\\\": 1001, \\\"status\\\": \\\"created\\\"}\",\n  \"MessageAttributes\": {\n    \"amount\": {\n      \"Type\": \"Number\",\n      \"Value\": \"4800\"\n    },\n …(省略)… \"Type\": \"Notification\",\n …(省略)…}"
        }
    ]
}

Body は1本の文字列で、その中にSNSが付けた外側のJSON(この記事では「封筒」と呼びます)が入っています。

SQSに届くメッセージJSONの中身

上の Body を整形すると次のとおりです(署名に関する Signature、SigningCertURL と、UnsubscribeURL は省略しています)。

{
  "Message": "{\"orderId\": 1001, \"status\": \"created\"}",
  "MessageAttributes": {
    "amount": {
      "Type": "Number",
      "Value": "4800"
    },
    "eventType": {
      "Type": "String",
      "Value": "order_created"
    }
  },
  "MessageId": "af73bb08-1807-48c0-bbe4-6677a7f5da96",
  "Subject": "order created",
  "Timestamp": "2026-10-05T06:00:25.745Z",
  "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:order-events",
  "Type": "Notification"
}
キー 中身
Type 通常の配信は Notification
MessageId publish の戻り値と同じID(SQS側の MessageId とは別物)
TopicArn どのトピックから来たか
Subject --subject で付けた件名
Message 発行した本文。JSONを送った場合も「文字列」として入る
MessageAttributes 発行時の属性。キー名は Type と Value(発行時の DataType、StringValue と名前が変わる)
Timestamp SNSが受け付けた時刻(UTC)

本文のJSONに手が届くまで、デコードが2回必要です。jq なら次のように書けます。

aws sqs receive-message --queue-url "$QUEUE_URL" --query 'Messages[0].Body' --output text \
  | jq -r '.Message' | jq '.orderId'
1003

(orderId が 1003 のメッセージを発行したあとに実行した結果です。)

RawMessageDelivery の有無で本文がどう変わるか

封筒が不要なら、サブスクリプションの RawMessageDelivery を true にします。

aws sns set-subscription-attributes \
  --subscription-arn arn:aws:sns:ap-northeast-1:123456789012:order-events:bd4f92c7-1e32-4cf6-92a1-bff395c3903c \
  --attribute-name RawMessageDelivery --attribute-value true

aws sns publish \
  --topic-arn arn:aws:sns:ap-northeast-1:123456789012:order-events \
  --message '{"orderId": 1002, "status": "created"}' \
  --message-attributes '{"eventType":{"DataType":"String","StringValue":"order_created"}}'

aws sqs receive-message --queue-url "$QUEUE_URL" --wait-time-seconds 1 --message-attribute-names All
{
    "Messages": [
        {
            "MessageId": "99e72ee1-c84a-46b9-b52b-113053c2fa39",
            "ReceiptHandle": "zoxjhfesjussjwimxjof…(省略)…",
            "MD5OfBody": "c70c5ce875174c9aec9d5f61ae59a1d5",
            "Body": "{\"orderId\": 1002, \"status\": \"created\"}",
            "MD5OfMessageAttributes": "414dc44afc30dafb954152107fb39479",
            "MessageAttributes": {
                "eventType": {
                    "StringValue": "order_created",
                    "DataType": "String"
                }
            }
        }
    ]
}
項目 RawMessageDelivery なし(既定) RawMessageDelivery あり
SQSの Body SNSの封筒JSON(Type、Message など) 発行した本文そのまま
本文JSONの取り出し デコード2回 デコード1回
メッセージ属性 封筒の中の MessageAttributes(Type/Value) SQSのメッセージ属性(DataType/StringValue)。受信時に --message-attribute-names All が必要
TopicArn、Subject、SNSの MessageId 封筒から読める 届かない
属性の数 ― 公式ドキュメントによると、SQS向けは10個まで。超えたメッセージはそのサブスクリプションには配信されない

どのトピックから来たかを受信側で見分けたいなら既定のまま、本文だけあれば足りるなら Raw、と選ぶと迷いません。公式ドキュメントでは、Raw配信に対応する宛先は SQS、HTTP/S、Firehose とされています。

boto3でファンアウトを通す

同じ流れを boto3 で書き、キューを2つつないで1回の publish が両方に届くことを確かめます。sns_fanout.py として保存し、python3 sns_fanout.py で実行します。

import json
import boto3

sns = boto3.client("sns", region_name="ap-northeast-1")
sqs = boto3.client("sqs", region_name="ap-northeast-1")

# 1. トピックを作る(同じ名前なら同じ ARN が返る)
topic_arn = sns.create_topic(Name="member-events")["TopicArn"]
print("topic:", topic_arn)

# 2. キューを2つ作り、ARN を調べて購読させる
queues = {}
for name in ["member-mail", "member-point"]:
    url = sqs.create_queue(QueueName=name)["QueueUrl"]
    arn = sqs.get_queue_attributes(QueueUrl=url, AttributeNames=["QueueArn"])["Attributes"]["QueueArn"]
    # SNS からの SendMessage だけを許可するキューポリシー
    policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": {"Service": "sns.amazonaws.com"},
            "Action": "sqs:SendMessage",
            "Resource": arn,
            "Condition": {"ArnEquals": {"aws:SourceArn": topic_arn}},
        }],
    }
    sqs.set_queue_attributes(QueueUrl=url, Attributes={"Policy": json.dumps(policy)})
    sub = sns.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=arn, ReturnSubscriptionArn=True)
    print("subscribe:", name, "->", sub["SubscriptionArn"])
    queues[name] = url

# 3. 発行する(1回の publish が2つのキューに届く)
res = sns.publish(
    TopicArn=topic_arn,
    Subject="member joined",
    Message=json.dumps({"memberId": 501, "plan": "standard"}),
    MessageAttributes={"eventType": {"DataType": "String", "StringValue": "member_joined"}},
)
print("publish MessageId:", res["MessageId"])

# 4. 各キューで受け取り、SNS の封筒(外側の JSON)を開けて本文を取り出す
for name, url in queues.items():
    msgs = sqs.receive_message(QueueUrl=url, WaitTimeSeconds=1, MaxNumberOfMessages=10).get("Messages", [])
    for m in msgs:
        envelope = json.loads(m["Body"])        # 1回目:SNS の封筒
        body = json.loads(envelope["Message"])  # 2回目:自分が送った JSON
        print(name, "| Type:", envelope["Type"], "| same id:", envelope["MessageId"] == res["MessageId"],
              "| memberId:", body["memberId"], "| eventType:", envelope["MessageAttributes"]["eventType"]["Value"])
        sqs.delete_message(QueueUrl=url, ReceiptHandle=m["ReceiptHandle"])
topic: arn:aws:sns:ap-northeast-1:123456789012:member-events
subscribe: member-mail -> arn:aws:sns:ap-northeast-1:123456789012:member-events:ce3a6192-9aec-47f5-b135-9a9f7175ac9c
subscribe: member-point -> arn:aws:sns:ap-northeast-1:123456789012:member-events:8f34dc4f-6464-450d-b281-982f160cf932
publish MessageId: a7f08d43-986f-41db-8be6-fdac03a6184e
member-mail | Type: Notification | same id: True | memberId: 501 | eventType: member_joined
member-point | Type: Notification | same id: True | memberId: 501 | eventType: member_joined

2つのキューに同じ内容が届き、封筒の MessageId は publish の戻り値と一致しました。 重複の確認や追跡には、SQS側ではなくこのSNS側のIDを使うと、どのキューでも同じ値で照合できます。

まとめて発行する(publish-batch)

1回のAPI呼び出しで複数のメッセージを発行できます。Id はバッチ内で結果を見分けるための任意の文字列です。

aws sns publish-batch \
  --topic-arn arn:aws:sns:ap-northeast-1:123456789012:member-events \
  --publish-batch-request-entries '[{"Id":"1","Message":"first"},{"Id":"2","Message":"second"}]'
{
    "Successful": [
        {
            "Id": "1",
            "MessageId": "50a8dd6a-a3a4-474d-b4cb-aa7d4d576fc3"
        },
        {
            "Id": "2",
            "MessageId": "1021233c-c78e-4138-b6c4-900b597318cc"
        }
    ],
    "Failed": []
}

戻り値は Successful と Failed に分かれます。一部だけ受け付けられないこともあるので、Failed が空かどうかを必ず見ます。

フィルターポリシーの書き方早見表

フィルターポリシーは、サブスクリプションごとに設定する「届ける条件」のJSONです。キー同士は AND、配列の中の値同士は OR として判定されます。既定ではメッセージ属性が判定の対象です。

条件 書き方 意味
完全一致 {"eventType": ["order_created"]} 属性が order_created のとき
どれかに一致(OR) {"eventType": ["order_created", "order_canceled"]} 2つのどちらか
prefix(前方一致) {"eventType": [{"prefix": "order_"}]} order_ で始まる
suffix(後方一致) {"eventType": [{"suffix": "_joined"}]} _joined で終わる
anything-but(〜以外) {"eventType": [{"anything-but": ["order_canceled"]}]} order_canceled 以外(属性があること)
numeric(数値の比較) {"amount": [{"numeric": [">=", 5000]}]} 5000 以上
numeric(範囲) {"amount": [{"numeric": [">", 0, "<", 5000]}]} 0 より大きく 5000 未満
exists(属性がある) {"coupon": [{"exists": true}]} coupon 属性が付いている
exists(属性がない) {"coupon": [{"exists": false}]} coupon 属性が付いていない
AND(複数キー) {"eventType": ["order_created"], "amount": [{"numeric": [">=", 5000]}]} 両方を満たす

このほか公式ドキュメントの文字列マッチングのページには、大文字小文字を区別しない {"equals-ignore-case": "tennis"}、ワイルドカードの {"wildcard": "*ball"}、{"anything-but": {"prefix": "order-"}}、IPアドレス範囲の {"cidr": "10.0.0.0/24"} が載っています。

届く・届かないを確かめる

10種類のポリシーを付けたキューを1つずつ用意し、属性の違う5通のメッセージを発行して、どのキューに届いたかを数えます。sns_filter.py として保存して実行します。

import json
import boto3

sns = boto3.client("sns", region_name="ap-northeast-1")
sqs = boto3.client("sqs", region_name="ap-northeast-1")
topic_arn = sns.create_topic(Name="filter-demo")["TopicArn"]

policies = {
    "exact":        {"eventType": ["order_created"]},
    "or":           {"eventType": ["order_created", "order_canceled"]},
    "prefix":       {"eventType": [{"prefix": "order_"}]},
    "suffix":       {"eventType": [{"suffix": "_joined"}]},
    "anything-but": {"eventType": [{"anything-but": ["order_canceled"]}]},
    "numeric-ge":   {"amount": [{"numeric": [">=", 5000]}]},
    "numeric-range": {"amount": [{"numeric": [">", 0, "<", 5000]}]},
    "exists-true":  {"coupon": [{"exists": True}]},
    "exists-false": {"coupon": [{"exists": False}]},
    "and":          {"eventType": ["order_created"], "amount": [{"numeric": [">=", 5000]}]},
}
urls = {}
for name, policy in policies.items():
    url = sqs.create_queue(QueueName="f-" + name)["QueueUrl"]
    arn = sqs.get_queue_attributes(QueueUrl=url, AttributeNames=["QueueArn"])["Attributes"]["QueueArn"]
    sns.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=arn,
                  Attributes={"FilterPolicy": json.dumps(policy), "RawMessageDelivery": "true"})
    urls[name] = url

def attr(**kw):
    out = {}
    for k, v in kw.items():
        out[k] = {"DataType": "Number" if isinstance(v, int) else "String", "StringValue": str(v)}
    return out

messages = {
    "A": attr(eventType="order_created", amount=4800),
    "B": attr(eventType="order_created", amount=12000, coupon="AUTUMN"),
    "C": attr(eventType="order_canceled", amount=12000),
    "D": attr(eventType="member_joined"),
    "E": {},
}
for label, attrs in messages.items():
    sns.publish(TopicArn=topic_arn, Message=label, MessageAttributes=attrs)

print(f"{'policy':<14}| A B C D E")
for name, url in urls.items():
    got = set()
    while True:
        msgs = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=10).get("Messages", [])
        if not msgs:
            break
        for m in msgs:
            got.add(m["Body"])
            sqs.delete_message(QueueUrl=url, ReceiptHandle=m["ReceiptHandle"])
    print(f"{name:<14}| " + " ".join("o" if x in got else "-" for x in "ABCDE"))

(キューポリシーの設定は前の節と同じなので、このスクリプトでは省いています。実際の AWS で動かすときは sns_fanout.py の set_queue_attributes の部分を加えてください。)

policy        | A B C D E
exact         | o o - - -
or            | o o o - -
prefix        | o o o - -
suffix        | - - - o -
anything-but  | o o - o -
numeric-ge    | - o o - -
numeric-range | o - - - -
exists-true   | - o - - -
exists-false  | o - o o o
and           | - o - - -

o が届いた、- が届かなかったメッセージです。送った5通は次のとおりです。

メッセージ eventType amount coupon
A order_created 4800 なし
B order_created 12000 AUTUMN
C order_canceled 12000 なし
D member_joined なし なし
E (属性なし) なし なし

この結果から読み取れるポイントは3つです。

  • 属性が1つも付いていないメッセージ E は、exists: false 以外のどのポリシーにも届きません。 フィルターを付けたのに何も届かないときは、発行側が属性を付けているかを最初に確かめます。
  • anything-but は「その属性があって、値が違う」ときに届きます。 属性そのものが無い E には届いていません。
  • 数値の比較は、属性を DataType: Number で発行したときに働きます。A(4800)は >= 5000 に届かず、範囲指定の > 0 かつ < 5000 にだけ届きました。

既存のサブスクリプションに後から付ける

aws sns set-subscription-attributes \
  --subscription-arn arn:aws:sns:ap-northeast-1:123456789012:order-events:bd4f92c7-1e32-4cf6-92a1-bff395c3903c \
  --attribute-name FilterPolicy \
  --attribute-value '{"eventType":["order_created"]}'

aws sns get-subscription-attributes \
  --subscription-arn arn:aws:sns:ap-northeast-1:123456789012:order-events:bd4f92c7-1e32-4cf6-92a1-bff395c3903c
{
    "Attributes": {
        "PendingConfirmation": "false",
        "ConfirmationWasAuthenticated": "true",
        "Endpoint": "arn:aws:sqs:ap-northeast-1:123456789012:order-mail",
        "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:order-events",
        "Protocol": "sqs",
        "SubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:order-events:bd4f92c7-1e32-4cf6-92a1-bff395c3903c",
        "Owner": "123456789012",
        "RawMessageDelivery": "true",
        "FilterPolicy": "{\"eventType\":[\"order_created\"]}",
        "FilterPolicyScope": "MessageAttributes"
    }
}

FilterPolicyScope の既定値は MessageAttributes(メッセージ属性で判定)です。フィルターを外すときは、--attribute-value '{}' と空のJSONを設定します。

本文のJSONで絞り込む(FilterPolicyScope を MessageBody に)

発行側が属性を付けていない場合は、FilterPolicyScope を MessageBody にすると、本文のJSONのキーで判定できます。入れ子のキーも書けます。

import json
import boto3

sns = boto3.client("sns", region_name="ap-northeast-1")
sqs = boto3.client("sqs", region_name="ap-northeast-1")
topic_arn = sns.create_topic(Name="body-filter-demo")["TopicArn"]
url = sqs.create_queue(QueueName="gold-only")["QueueUrl"]
arn = sqs.get_queue_attributes(QueueUrl=url, AttributeNames=["QueueArn"])["Attributes"]["QueueArn"]

sns.subscribe(
    TopicArn=topic_arn, Protocol="sqs", Endpoint=arn,
    Attributes={
        "FilterPolicyScope": "MessageBody",
        "FilterPolicy": json.dumps({"member": {"rank": ["gold"]}, "total": [{"numeric": [">=", 3000]}]}),
        "RawMessageDelivery": "true",
    },
)
for body in [
    {"orderId": 1, "member": {"rank": "gold"}, "total": 5200},
    {"orderId": 2, "member": {"rank": "silver"}, "total": 9800},
    {"orderId": 3, "member": {"rank": "gold"}, "total": 1200},
]:
    sns.publish(TopicArn=topic_arn, Message=json.dumps(body))

for m in sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=10).get("Messages", []):
    print("received:", m["Body"])
received: {"orderId": 1, "member": {"rank": "gold"}, "total": 5200}

rank が gold で、かつ total が 3000 以上の1通だけが届きました。

フィルターポリシーの上限(公式ドキュメントより)

項目 上限
1つのポリシーに書けるキーの数 5個(本文で判定する場合は末端のキーだけを数える)
値の組み合わせの総数 150(各配列の要素数を掛け合わせた数)
ポリシーのサイズ 256 KB
数値の範囲 -10億〜10億、小数点以下5桁まで
1トピックあたりのフィルターポリシー数 200(既定値)
文字列の比較 大文字小文字を区別する

届かないときの確認ポイント

publish は MessageId を返したのにSQSが空、というときは、次の順に確かめると原因に早くたどり着けます。

順番 確かめること 確かめ方
1 サブスクリプションがあるか aws sns list-subscriptions-by-topic に目的のキューARNが出るか
2 キューポリシーがあるか aws sqs get-queue-attributes --attribute-names Policy で sns.amazonaws.com への sqs:SendMessage 許可があるか
3 aws:SourceArn が正しいか ポリシーの条件に書いたトピックARNが、実際のトピックと一字一句同じか
4 フィルターポリシーに合っているか get-subscription-attributes の FilterPolicy と、発行時の属性を見比べる
5 属性の型が合っているか 数値比較なら DataType が Number か
6 判定の対象が合っているか FilterPolicyScope が MessageAttributes か MessageBody か

キューポリシーについては、公式ドキュメント「Amazon SQS キューを Amazon SNS トピックにサブスクライブする」に、SNSトピックへ sqs:SendMessage を許可するポリシーがキューに必要と明記されています。CLI や boto3 で購読を作るときは、キューポリシーの設定をセットで行うと覚えておくと安心です。

エラー文の一覧と直し方

実行して得られたエラー文です。CLI では An error occurred (コード) when calling the 操作名 operation: メッセージ の形で表示されます。

操作 エラーコード メッセージ 原因と直し方
subscribe NotFound Topic does not exist トピックARNの打ち間違い、またはリージョン違い。aws sns list-topics で確かめる
subscribe InvalidParameter Invalid parameter: SQS endpoint ARN --notification-endpoint にキュー名やURLを渡している。キューの ARN を渡す
create-topic InvalidParameter Topic names must be made up of only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, and must be between 1 and 256 characters long. 名前に空白やピリオドがある。英数字・アンダースコア・ハイフンで付ける
publish InvalidParameter The message attribute 'n' has an invalid message attribute type, the set of supported type prefixes is Binary, Number, and String. DataType に Int などを書いている。String/Number/Binary にする
publish InvalidParameter The message attribute 'n' must contain non-empty message attribute value for message attribute type 'S'. 属性の値が空文字。値が無いときは属性ごと付けない
set-subscription-attributes InvalidParameter Invalid parameter: FilterPolicy: "eventType" must be an object or an array 値を配列にしていない。"order_created" ではなく ["order_created"] と書く

boto3 では、エラーコードとメッセージを e.response["Error"] から取り出せます。

import boto3
from botocore.exceptions import ClientError

sns = boto3.client("sns", region_name="ap-northeast-1")
topic_arn = sns.create_topic(Name="member-events")["TopicArn"]

try:
    sns.subscribe(TopicArn=topic_arn + "-typo", Protocol="sqs",
                  Endpoint="arn:aws:sqs:ap-northeast-1:123456789012:member-mail")
except sns.exceptions.NotFoundException as e:
    print("NotFoundException:", e.response["Error"]["Message"])

try:
    sns.publish(TopicArn=topic_arn, Message="hi",
                MessageAttributes={"amount": {"DataType": "Int", "StringValue": "1"}})
except ClientError as e:
    err = e.response["Error"]
    print(err["Code"], "/", e.response["ResponseMetadata"]["HTTPStatusCode"], "/", err["Message"])
NotFoundException: Topic does not exist
InvalidParameter / 400 / The message attribute 'amount' has an invalid message attribute type, the set of supported type prefixes is Binary, Number, and String.

NotFound は sns.exceptions.NotFoundException で個別に捕まえられます。それ以外は ClientError で受けて Code で分けると書きやすくなります。

後片付け

aws sns unsubscribe --subscription-arn arn:aws:sns:ap-northeast-1:123456789012:order-events:9eef4ac0-8250-4d72-9e9d-398a3b15f6c9
aws sns delete-topic --topic-arn arn:aws:sns:ap-northeast-1:123456789012:order-events
aws sqs delete-queue --queue-url "$QUEUE_URL"

3つとも、成功すると何も表示せずに終了コード 0 で終わります。

よくある質問

Q. SNSとSQSはどう使い分けますか? A. 1つのメッセージを複数の処理に同時に渡したいときはSNS、1つの処理が自分のペースで順に取り出したいときはSQSです。両方を組み合わせたファンアウトなら、配る役をSNS、ためておく役をSQSが受け持ちます。

Q. 発行した本文がJSONなのに、受信側で文字列になっているのはなぜですか? A. 既定ではSNSが封筒のJSONで包み、本文は Message キーに文字列として入るためです。json.loads を2回行うか、サブスクリプションの RawMessageDelivery を true にして本文だけを届けます。

Q. フィルターポリシーはトピックに設定しますか、サブスクリプションに設定しますか? A. サブスクリプションです。同じトピックにつないだキューごとに別々の条件を持てるので、発行側は1か所に送るだけで、受け取る側が自分に必要なものを選べます。

Q. フィルターを付けたら1通も届かなくなりました。 A. 発行時にメッセージ属性を付けているか、数値比較なら DataType が Number かを確かめます。属性が無いメッセージは、今回の実行結果でも exists: false 以外のポリシーには届きませんでした。本文で判定したい場合は FilterPolicyScope を MessageBody にします。

まとめ

  • SNSは「トピックに発行 → サブスクリプションの数だけ配る」サービス。SQSをつなぐとファンアウトになる
  • CLI も boto3 も create-topic → subscribe → publish の3つで通せる。購読先には キューのARN を渡す
  • SQSに届く Body は SNSの封筒JSON。本文は Message に文字列で入り、デコードは2回
  • RawMessageDelivery を true にすると本文だけが届き、属性はSQSのメッセージ属性になる
  • フィルターポリシーは キー同士がAND、配列の値同士がOR。属性の無いメッセージは exists: false 以外に届かない
  • 届かないときは サブスクリプション → キューポリシー → フィルター の順に確かめる
  • エラーは InvalidParameter のメッセージ本文に原因が書かれているので、ARN・属性の型・配列の書き方を見直す

参考・出典

  • AWS 公式ドキュメント「Subscribing an Amazon SQS queue to an Amazon SNS topic」 https://docs.aws.amazon.com/sns/latest/dg/subscribe-sqs-queue-to-sns-topic.html
  • AWS 公式ドキュメント「String value matching」 https://docs.aws.amazon.com/sns/latest/dg/string-value-matching.html
  • AWS 公式ドキュメント「Filter policy constraints」 https://docs.aws.amazon.com/sns/latest/dg/subscription-filter-policy-constraints.html
  • AWS 公式ドキュメント「Amazon SNS raw message delivery」 https://docs.aws.amazon.com/sns/latest/dg/sns-large-payload-raw-message-delivery.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Amazon SNSの使い方 CLI・boto3とSQSファンアウト早見表」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?