AWS LambdaとAmazon SNSで引数に指定した電話番号にSMSを送信する(AWS CDK)前編

2021.10.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部 IoT事業部の若槻です。

今回は、AWS LambdaとAmazon SNSで引数に指定した電話番号にSMSを送信する(AWS CDK)の前編です。

※前編となる本エントリでは、Anazon SNSへのトピックとサブスクリプションの作成が必要な前提の内容となっていますが、後に不要ということが分かりました。後編では両者を作成しない方法でSMSを送信する方法を紹介しているので合わせてご参照ください。

なぜ作りたいのか

テーブルなどで管理しているユーザーの連絡先情報(メールアドレス、電話番号など)宛にLambdaを使用してメッセージを送信したい場合、EmailであればAmazon SESを使用してメールアドレス宛に送信すれば良いのですが、SMSの場合はそうはいきません。AWSでSMSを送信する方法はAmazon SNSになりますが、Amazon SNSの場合は事前に送信先のサブスクリプションが作られている必要があり(冒頭の通り実際は不要)、またSMSを送信するためだけにAmazon Connectの環境を立ち上げるのは荷が重いです。

そこで思いついたのが、予め作成されたSNSトピックに対してLambda関数が下記の処理を行う構成です。Lambdaの引数に指定された電話番号に対するSMSのサブスクリプションを都度作成/削除しようという作戦です。

  1. SNSサブスクリプション(送信先電話番号のSMS)の作成
  2. SNSトピックへのイベントの発行
  3. SNSサブスクリプション(送信先電話番号のSMS)の削除

作ってみた

CDKコード

SMSのサブスクリプションを登録するSNSトピックと、SNSトピックへの発行を行うLambda関数を作っています。

lib/aws-cdk-app-stack.ts

import * as cdk from '@aws-cdk/core';
import * as lambda from '@aws-cdk/aws-lambda';
import * as lambdaNodejs from '@aws-cdk/aws-lambda-nodejs';
import * as sns from '@aws-cdk/aws-sns';
import * as iam from '@aws-cdk/aws-iam';

export class AwsCdkAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const notificationTopic = new sns.Topic(this, 'notification-topic', {
      topicName: 'notification-topic',
    });

    const sendSmsFunction = new lambdaNodejs.NodejsFunction(
      this,
      'sendSmsFunction',
      {
        functionName: 'send-sms-function',
        entry: './src/lambda/handlers/send-sms-handler.ts',
        handler: 'handler',
        runtime: lambda.Runtime.NODEJS_14_X,
        environment: {
          TOPIC_ARN: notificationTopic.topicArn,
        },
      }
    );

    const snsTopicPolicy = new iam.PolicyStatement({
      actions: ['sns:*'],
      resources: ['*'],
    });

    sendSmsFunction.addToRolePolicy(snsTopicPolicy);
  }
}

Lambdaコード

src/lambda/handlers/send-sms-handler.ts

import * as AWS from 'aws-sdk';
import { v4 as uuidv4 } from 'uuid';

const sns = new AWS.SNS({ apiVersion: '2010-03-31' });
const TOPIC_ARN = process.env.TOPIC_ARN!;

interface Event {
  phoneNumber: string;
}

export const handler = async (event: Event) => {
  const sendId = uuidv4();

  //SNSサブスクリプションの作成
  const subscribeParams = {
    TopicArn: TOPIC_ARN,
    Protocol: 'sms',
    Endpoint: event.phoneNumber,
    Attributes: { FilterPolicy: `{"sendId": ["${sendId}"]}` },
  };
  const subscription = await sns.subscribe(subscribeParams).promise();

  //SNSトピックへのイベントの発行
  const publischParams = {
    Message: 'message',
    Subject: 'subject',
    TopicArn: TOPIC_ARN,
    MessageAttributes: {
      sendId: { DataType: 'String', StringValue: sendId },
    },
  };
  await sns.publish(publischParams).promise();

  //SNSサブスクリプションの削除
  const unsubscribeParams = { SubscriptionArn: subscription.SubscriptionArn! };
  await sns.unsubscribe(unsubscribeParams).promise();
};
  • SNSトピックに作成するサブスクリプションでは、毎回生成するsendId(UUID)の属性を持つフィルターポリシーを指定し、イベント発行時にそのsendIdを指定することにより、Lambdaは自分の実行により作成したサブスクリプションに対してのみイベントを発行できるようにしています。これにより異なるLambdaのランタイム間でサブスクリプションが重複して発行されるのを回避しています。ただしこれはuuidのライブラリを使わなくてもイベントに応じたRequest Idなどを流用しても良いかもしれません。

動作確認

作成したLambdaをイベントを指定して実行します。

{
  "phoneNumber": "+81XXXXXXXXXX"
}

すると指定した番号にSMSが届きました。

参考

以上