AmazonSNSへのサーバサイドからの端末紐付け(python+boto3)


課題

SNSに対しtopicに端末を紐付けたい場合、スマートフォン端末からsdkを使ってSNSに直接アクセスという事例はあるのですが、今後のこと(tokenをdynamoDB側でも管理するとかの後付け対応)を考えてサーバサイド側でやろうとしたところ意外と事例が少なかったのでまとめました。

今回push通知対象はスマートフォンなのでその前提の話になっています。
また実装はLambdaでやっていますがLambda依存にはなっていないと思います。

概要

handler.py
import boto3

def lambda_handler(event, context):

    application_arn = 'arn:aws:sns:us-east-1:999999999999:notify_sample'
    topic_arn       = 'arn:aws:sns:us-east-1:999999999999:notify_sample'

    endpoint = add_endpoint(application_arn, token)
    subscribe_token(topic_arn, endpoint['EndpointArn'], is_subscribe)

大雑把な手順は
1. Applicationにendpointを登録する
2. TopicにendpointをSubscribeする

となります。ここで登場するApplication,TopicのARNは事前にAWS側のダッシュボードを使って登録・取得しておいてください。

Applicationにendpointを登録する

handler.py
def add_endpoint(application_arn, token):
    # token(boto3 doc抜粋) :
    # For example, when using APNS as the notification service, you need the device token.
    # Alternatively, when using GCM or ADM, the device token equivalent is called the registration ID.

    client = boto3.client('sns')
    endpoint = client.create_platform_endpoint(
        PlatformApplicationArn=application_arn,
        Token=token
    )

    return endpoint

ここは追加済みであっても呼び出します。tokenはAPNSもしくはFCM(GCM)から取得するtokenです。
create_platform_endpoint()はendpoint追加済みの場合新規追加をおこなう事なく既存のendpointの情報を返します。いずれにしてもendpoint情報が取得できるので、この情報を元にsubscribeを実施します。

The CreatePlatformEndpoint action is idempotent, so if the requester already owns an endpoint with the same device token and attributes, that endpoint's ARN is returned without creating a new endpoint.

TopicにendpointをSubscribeする

ここで多少面倒な処理をしています。

subscribeは単純にsubscribe()実行すれば良いのですが、unsubscribeの場合対象端末のSubscriptionArnが必要になります。
今回は一度subscribe()を実行してSubscriptionArnを取得し、それを使ってunsubscribe()実行する、という事をやっています。

handler.py
def subscribe_token(topic_arn, endpoint_arn, is_subscribe):

    client = boto3.client('sns')
    subscription = client.subscribe(
        TopicArn=topic_arn,
        Protocol='application',
        Endpoint=endpoint_arn
    )
    if not is_subscribe:
        client.unsubscribe(
            SubscriptionArn=subscription['SubscriptionArn']
        )

疑問点

最後のSubscriptionArnの取得方法なのですが、マニュアルを眺めてみてもこれくらいしか良さげな方法が見つかりません。
適切な方法がありましたらご指摘ください。

追試

実行時ちょっと釈然としないエラーが出たためboto3マニュアルを見直しコードを見直したのですが、多分以下のように書いた方が素直、だと思います。
しかし書き直してもエラーは改善しなかったので、やはりclientはlow-level clientなんだなーと思っています。

handler.py
def add_endpoint(dry_run, application_arn, token):

    sns = boto3.resource('sns')
    platform_application = sns.PlatformApplication(application_arn)
    endpoint = platform_application.create_platform_endpoint(
        Token=token
    )
    return endpoint.arn


def subscribe_token(dry_run, topic_arn, endpoint_arn, is_subscribe):

    sns = boto3.resource('sns')
    topic = sns.Topic(topic_arn)
    subscription = topic.subscribe(
        TopicArn=topic_arn,
        Protocol='application',
        Endpoint=endpoint_arn
    )
    if not is_subscribe:
        subscription.delete()