【AWS】Amazon SNSからHTTP/Sで通知する時、エンドポイントでやることを調べた

# はじめに
Amazon SNSを使ってHTTP/SでAPIを叩いていきます。
ここでは各AWSサービスの説明は省きます。
AWS超初心者です。

# 目的
- Amazon SNSからHTTP/SでAPIを実行する。
- Amazon SNSからWEBAPIを実行するとき、エンドポイント（受信側）でやることはなにか調べる。
（記事がほとんど見つからなかった）

# 構成
今回は、APIはAPIGateway + Lambdaで用意。
![amazon_sns_image.dio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395395/ada46e8f-8a2d-d9db-7858-875a73b593ef.png)


# エンドポイント（受信側）でやること
[AWSの公式ドキュメント](https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html)を見たら、やることは大きく分けて3つあるらしい。

1. 受信準備が完了していることを確認する（エンドポイントで「私はAmazon SNSからの通知を受け取れる状態にありますよ～」ということを確認する）
2. Amazon SNSからの通知を受け取る
3. Amazon SNSからサブスクリプション登録解除の情報を受け取る

最初は、ただAmazon SNSから通知先のエンドポイントを設定してメッセージを送信すればよいと思っていましたが
それだけだと上手くいかないようです。

## APIの用意
まず、エンドポイントであるAPIを作っていきます。
やりたいことができれば何でもいいのですが、今回は入り口をAPI Gateway、実行される処理をLambdaにしてみました。

### API Gatewayに紐づける
こんな感じでLambdaをAPI Gatewayに紐づけてあげましょう。
![image008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395395/2d5a7762-669a-87df-610d-3aacee7519c0.png)

### エンドポイントの確認
上記のようにLambdaのコンソールから見れます。
「APIエンドポイント」のURLをコピーしておきましょう。

## Amazon SNSでトピックの作成
次に[Amazon SNS](https://console.aws.amazon.com/sns/v3/home?region=us-east-1#/dashboard)を用意して、トピックを作成後、通知先のエンドポイントを設定します。

### 通知先のエンドポイントを設定する
Amazon SNSのコンソール画面から通知先のエンドポイントを設定してみるとこんな画面になります。
![image004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395395/48111b0a-6858-12a1-44e7-dfdb139a2d4a.png)

ステータスが「保留中の確認」となっています。
ステータスを「確認済み」にしないと、トピックからメッセージを発行してもエンドポイントに通知されません。
[AWSドキュメント](https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-message-and-json-formats.html)
>受信登録が確認されるまで、Amazon SNS はこのエンドポイントに通知を送信しません。


## APIをLambdaで実装
pythonで作りました。boto3は使っていません。

```lambda_function.py
import requests
import json

def lambda_handler(event, context):

    path = event['path']
    user_agent = event['headers']['User-Agent']

    # この関数がAmazon SNS経由で呼ばれた場合
    if path == '/testFunction' and user_agent == 'Amazon Simple Notification Service Agent':
        
        type = event['headers']['x-amz-sns-message-type']
        
        # 文字列をdictに変換してbodyの中身を参照できるようにする
        body_unicode = event['body']
        body_dict = json.loads(body_unicode)

        if type == 'SubscriptionConfirmation':
            # サブスクライブされた場合の受信登録確認用処理
            subscribeurl = body_dict['SubscribeURL']
            
            # 受信登録の確認用URLを叩く
            response = requests.get(subscribeurl)
            if response.status_code == 200:
                print("SubscriptionConfirmation")
            
        elif type == 'Notification':
            # 通知がきた場合の処理
            message = body_dict['Message']
            print('Amazon SNSからのメッセージ：' + message)
            print("Notification")
            
        elif type == 'UnsubscribeConfirmation ':
            # 退会通知がきた場合の処理
            print("UnsubscribeConfirmation")
        
        # レスポンス整形
        res = {
            "isBase64Encoded": False,
            "statusCode": 200,
            "headers": {},
            "body": type
        }

        return json.dumps(res)

    else :
        print('SNS経由じゃないよ')
        
        res = {
            "isBase64Encoded": "false",
            "statusCode": "200",
            "headers": {},
            "body": "not testFunction for Amazon SNS"
        }
        
        return json.dumps(res)
```

### コードの説明
基本的には前述している「エンドポイントでやること」をもとに作成しています。
Amazon SNSのメッセージタイプがわかる`event['headers']['x-amz-sns-message-type']`の値によって処理を分けています。
参考：[AWS公式ドキュメント](https://docs.aws.amazon.com/ja_jp/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html#SendMessageToHttp.prepare)

>コードは、Amazon SNS がエンドポイントに送信する HTTP POST リクエストの HTTP ヘッダーを読み取る必要があります。また、コードは Amazon SNS が送信したメッセージのタイプを示すヘッダーフィールド x-amz-sns-message-type を探す必要があります。

- エンドポイントでやること

>1. 受信準備が完了していることを確認する（エンドポイントで「私はAmazon SNSからの通知を受け取れる状態にありますよ～」ということを確認する）
>2. Amazon SNSからの通知を受け取る
>3. Amazon SNSからサブスクリプション登録解除の情報を受け取る

### 「1. 受信準備が完了していることを確認する」をやってみる
1は、`response = requests.get(subscribeurl)`で実現しています。
Amazon SNSのコンソール画面から「リクエストの確認」をします。
![image010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395395/2d42ff57-a0fa-146d-cf34-7a43389fd7a4.png)

リロードすると、ステータスが「保留中の確認」から「確認済み」になっていることを確認できます。
![image012.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395395/c7af5140-f8ca-689b-577e-8780a2072d4c.png)


### 注意点
さて、ここで2点ほど注意点（というかはまったポイント）を紹介します。

- **`import requests`できない**

上記のコードはそのままでは実行できません。
requestsは外部モジュールなので、Lambdaで使えるように環境構築してあげる必要があります。
まずrequestsモジュールをローカルにインポート後zip化してから、zipをLambdaにアップロードします。
以下の記事を参考にしました。
https://hacknote.jp/archives/48083/

zip化できるならコマンドでもツール（7zipとか）でも構いません。
pipコマンドは使えるようにしなきゃいけませんね…。


- **SubscribeURLを取得できない**

これ、めちゃくちゃはまりました。
Amazon SNSから取得したリクエスト`event`はdict型だと思っていたのですが、
`event`を掘り下げていくと`body`部分が文字列になっていて、`SubscribeURL`を参照できなかった。。。
以下の素晴らしい記事を参考に解決できました。よかった。
https://dev.classmethod.jp/articles/lambda-python-tips-all-events-are-not-dict/

## Amazon SNSから通知を送る
最後にPublishしていきましょう。
今回は特にトリガーとなる処理を用意していないので、コンソールから手動でエンドポイントに通知します。

### メッセージの発行
![image014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395395/e53866e1-d046-ddf7-25a3-faf0d5bf6db7.png)
![image016.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395395/95e3b398-4261-10c2-d20a-072cc24aa3b0.png)

### エンドポイントでメッセージを受信したことの確認
![image018.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/395395/def6c523-8fd8-34e3-e746-524471dc4c5a.png)
ちゃんとエンドポイントに設定したLambdaの処理が行われていることを確認できました。

# 最後に
これでなんとなくAmazon SNSからHTTP/Sで受信するときの動きがわかったような気がします。
また何か発見があったら更新します。