4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[AWS CloudFormation] Amazon SNS プッシュ通知環境構築

Posted at

前提

  • 通知先デバイスのOSはAndroid
  • 通知先デバイスは1台 ※複数台に通知する場合はトピックを使用
  • Mobileアプリの環境構築、実装については本記事では言及しない

構成要素

Amazon SNSを使用してプッシュ通知を行う場合、以下の構成となる

image-20210123091531916.png

準備

Amazon SNSからFCM経由でMolblieアプリにプッシュ通知を行うためには、
通知先のデバイスを特定するためのデバイストークンと、
Firebaseのプロジェクトを特定するためのサーバーキーが必要となる
そのため以下の準備を行う

  • Firebaseに任意のプロジェクトを作成

  • Firebase Cloud MessagingにMoblieアプリを登録

  • Firebaseの以下からサーバーキーを取得する

image-20210122170324954.png

  • Mobileアプリからデバイストークンを取得

    ※取得方法については参考記事等を参照
    ※デバイストークンはMobileアプリのインストール毎に変化する

マネジメントコンソールでの環境構築

プッシュ通知から、
プラットフォームアプリケーションの作成をクリック

1.png

任意のアプリケーション名を入力
プッシュ通知プラットフォームはFCMを選択
APIキーに準備で取得したサーバーキーを入力
プラットフォームアプリケーションをクリック

2.png

アプリケーションエンドポイントの作成をクリック

3.png

デバイストークンに準備で取得したデバイストークンを入力
アプリケーションエンドポイントの作成をクリック

4.png

マネジメントコンソールからのプッシュ通知テストでは、
配信プロトコルごとにカスタムペイロードを選択

5.png

メッセージの内容は、
Mobileアプリがバックグラウンドでもプッシュ通知するためには以下のようなフォーマットとなる
android_channel_idはMobileアプリの実装に合わせる

{
  "GCM": "{ \"notification\": { \"body\": \"message body\", \"title\": \"message title \", \"sound\":\"default\" ,\"click_action\": \"FLUTTER_NOTIFICATION_CLICK\", \"android_channel_id\":\"1\"} , \"data\" : {\"key1\" : \"value\", \"key2\" : \"value\" } }"
}

CloudFormationでの環境構築

Amazon Simple Notification Service リソースタイプのリファレンスからわかるように、
マネジメントコンソールで作成したプラットフォームアプリケーション、アプリケーションエンドポイントはCloudFormationに対応していない
今回は、カスタムリソースを使用してプラットフォームアプリケーションを作成し、
Lambdaにてアプリケーションエンドポイントを追加する

カスタムリソースにはcfn-responseモジュールが必要となる、cfn-responseモジュールについては以下を参照
cfn-responseモジュール
今回は、cfn-responseモジュールをLayer化して使用している

カスタムリソースによるプラットフォームアプリケーション作成

以下、プラットフォームアプリケーションを作成するためのテンプレート

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  child stack (SNS)

Globals:
  Function:
    Timeout: 3
    Runtime: python3.8
    Handler: function.lambda_handler

Parameters:
  StackName:
    Type: String
  CfnResponseArn: # cfn-responseモジュールLayer
    Type: String
  ApiKey: # プラットフォームアプリケーション生成に必要なAPIキー(サーバーキー)
    Type: String

Resources:
  CreatePlatformApplicationRole: # カスタムリソース生成時に実行されるLambdaに付与するロール
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: /
      Policies:
        - PolicyName: CreatePlatformApplicationPolicy # SNS操作用ポリシー
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: sns:*
                Resource: '*'

  CreatePlatformApplicationFunction: # カスタムリソース生成時に実行されるLambda
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${StackName}_CreatePlatformApplicationFunction
      Role: !GetAtt CreatePlatformApplicationRole.Arn
      CodeUri: create-platform-application/
      Layers:
        - !Ref CfnResponseArn

  # プラットフォームアプリケーションを生成するカスタムリソース
  CreatePlatformApplication:
    Type: Custom::CreatePlatformApplication
    Properties:
      ServiceToken: !GetAtt 'CreatePlatformApplicationFunction.Arn' # リソースを生成するLambda Arn
      PlatformApplicationName: !Sub ${StackName}_sample # プラットフォームアプリケーション名
      PlatformApplicationArn: !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:app/GCM/${StackName}_sample # プラットフォームアプリケーションArn
      ApiKey: !Ref ApiKey # プラットフォームアプリケーション生成に必要なAPIキー(サーバーキー)

Outputs:
  PlatformApplicationArn: # 生成したプラットフォームアプリケーションを他テンプレートで使用できるようにOutput
    Value: !Sub arn:aws:sns:${AWS::Region}:${AWS::AccountId}:app/GCM/${StackName}_sample

以下、カスタムリソース生成時に実行されるLambda

import json
import boto3
from cfnresponse import CfnResponse

def lambda_handler(event, context):
  try:
    sns = boto3.client('sns')

    if event['RequestType'] == 'Create': # リソース生成時
      # プラットフォームアプリケーションの生成
      response = sns.create_platform_application(
        Name = event['ResourceProperties']['PlatformApplicationName'],
        Platform = 'GCM',
        Attributes = {
          'PlatformPrincipal': '',
          'PlatformCredential': event['ResourceProperties']['ApiKey']
        }
      )
      CfnResponse.send(event, context, CfnResponse.SUCCESS, {})

    elif event['RequestType'] == 'Delete': # リソース削除時
      # プラットフォームアプリケーションの削除
      response = sns.delete_platform_application(
        PlatformApplicationArn = event['ResourceProperties']['PlatformApplicationArn']
      )
      CfnResponse.send(event, context, CfnResponse.SUCCESS, {})

    else:
      CfnResponse.send(event, context, CfnResponse.SUCCESS, {})

  except Exception as e:
    CfnResponse.send(event, context, CfnResponse.FAILED, {})

Lambdaによるアプリケーションエンドポイントの追加

以下、アプリケーションエンドポイントを追加するLambdaとそのテンプレート

Parameters:
  StackName:
    Type: String
  PlatformApplicationArn:
    Type: String

Resources:
  SetEndpointFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub '${StackName}_SetEndpointFunction'
      CodeUri: set-endpoint/
      Environment:
        Variables:
          PlatformApplicationArn: !Ref PlatformApplicationArn # プラットフォームアプリケーションArn
      Policies:
        - Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action: sns:*
            Resource: '*'
import os
import json
import boto3

platformApplicationArn = os.getenv('PlatformApplicationArn')
sns = boto3.client('sns')

def add_endpoint(token):
  # アプリケーションエンドポイントの追加
  # ※既存のtokenの場合はendpoint(Arn)返却のみされる
  endpoint = sns.create_platform_endpoint(
    PlatformApplicationArn = platformApplicationArn,
    Token = token
  )

プッシュ通知を行うLambda

以下、プッシュ通知のLambda実装は以下のようになる

import os
import boto3
import json

sns = boto3.client('sns')
platformApplicationArn = os.getenv('PlatformApplicationArn')

# デバイストークンから通知先のendpoint(Arn)を取得する
def get_targetEndpoint(deviceToken):
  try:
    # ※既存のtokenの場合はendpoint(Arn)返却のみされる
    endpoint = sns.create_platform_endpoint(
      PlatformApplicationArn = platformApplicationArn,
      Token = deviceToken
    )
  except:
    raise ValueError(f'get_targetEndpoint error')
  return endpoint['EndpointArn']

def publish_notification(deviceToken):
  try:
    payload = {
  	    'notification': {
        'body': 'message body',
        'title': 'message title',
        'click_action': 'FLUTTER_NOTIFICATION_CLICK',
        'android_channel_id': '1'
      },
      'data': {
        'key1': value1,
        'key2': value2
      }
    }
    message = json.dumps({
      "GCM": json.dumps(payload)
    })
    response = sns.publish(
      TargetArn = get_targetEndpoint(deviceToken),
      Message = message,
      MessageStructure = 'json'
    )
  except:
    raise ValueError(f'publish_notification error')

参考

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?