3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS End User Messaging SMS #3】実務に沿ったアーキテクチャを作成し、実践してみる

3
Last updated at Posted at 2026-02-23

はじめに

こんばんは、mirukyです。
今回は、これまでご紹介したAWS End User Messaging SMSをより実践的に使うべく、実際にアーキテクチャ設計から、各サービスとの連携、動作確認まで行いたいと思います。

目次

  1. アーキテクチャ全体図
  2. SNSトピック・SQS DLQ作成
  3. API Gateway作成
  4. DynamoDB設計・構築
  5. Lambda作成
  6. IAM権限付与
  7. 動作確認

1. アーキテクチャ全体図

image.png

流れとしては下記の通りです。

End User Messaging SMSがサンドボックス環境にあるため、確認済みの電話番号にしか送れないことを加味しています。

①フロントエンドからクライアントがアクセスする。この時に電話番号とメッセージ内容を入力

②API GatewayがリクエストをLambdaに転送

③LambdaがSSMと通信を行い、Parameter Storeからホワイトリスト(確認済み電話番号リスト)と送信元ID(Sender ID)を取得

④Lambdaは受け取った電話番号とホワイトリストを照合し、合致すれば次の処理へ進む(不一致の場合は400エラーを返却)

⑤End User Messaging SMSからSMSを送信

⑥送信回数(同一電話番号への)・送信履歴をDynamoDBに格納

⑦CloudWatch Logsにログを出力

もし⑤のSMS送信が失敗した時、下記の処理を行います。
・送信内容をSQSのDLQに格納
・SNSトピックへの通知(運用担当者へメールでアラート)

2. SNSトピック・SQS DLQ作成

Lambda やアラートの設定で ARN が必要になるため、先に SNS トピックと SQS キューを作成しておきます。

2-1. SNSトピックの作成

SMS送信失敗時に運用担当者へメールでアラート通知するためのトピックです。
AWSマネジメントコンソールから Amazon SNS を開き、以下の設定で作成します。

① トピックの作成
スクリーンショット 2026-02-23 18.57.22.png

設定項目
タイプ スタンダード
トピック名 SmsAlertTopic
表示名 SMS送信アラート

その他の設定はデフォルトのままで問題ありません。「トピックの作成」をクリックします。

② サブスクリプションの作成

作成した SmsAlertTopic の画面から「サブスクリプションの作成」をクリックし、以下を入力します。

設定項目
プロトコル Eメール
エンドポイント 通知を受け取るメールアドレス(例: ops-team@example.com

サブスクリプション作成後、入力したメールアドレスに確認メールが届きます。メール内の 「Confirm subscription」 リンクを必ずクリックしてください。確認が完了するまで通知は配信されません。

作成後、トピックの ARN(例: arn:aws:sns:ap-northeast-1:{account-id}:SmsAlertTopic)を控えておきます。Lambda の環境変数 SNS_TOPIC_ARN に設定します。

2-2. SQS DLQ(デッドレターキュー)の作成

スクリーンショット 2026-02-23 19.00.30.png

Lambda の非同期呼び出しが失敗した際にメッセージを退避するためのキューです。
AWSマネジメントコンソールから Amazon SQS を開き、以下の設定で作成します。

設定項目
タイプ 標準
キュー名 SmsSender-DLQ
可視性タイムアウト 30秒
メッセージ保持期間 14日間
最大メッセージサイズ 256 KB

その他の設定はデフォルトのままで問題ありません。「キューの作成」をクリックします。

なぜDLQ(デッドレターキュー)が必要なのか
Lambda の非同期呼び出しが最大再試行回数を超えて失敗した場合、リクエスト内容がそのまま失われてしまいます。DLQ を設定しておくことで、失敗したメッセージを退避し、後から原因調査や再処理を行うことができます。

作成後、キューの ARN(例: arn:aws:sqs:ap-northeast-1:{account-id}:SmsSender-DLQ)を控えておきます。Lambda の DLQ 設定で使用します。

3. API Gateway作成

3-1. API の作成

スクリーンショット 2026-02-23 17.48.35.png
AWSマネジメントコンソールから API Gateway を開き、以下の設定で作成します。

設定項目
APIタイプ REST API
API名 SmsSenderApi
説明 SMS送信用API
エンドポイントタイプ リージョン

3-2. リソースとメソッドの作成

スクリーンショット 2026-02-23 17.54.33.png
まず、リソースを作成から、下記の通り入力します。
リソースパス:/
リソース名:sms

スクリーンショット 2026-02-23 17.54.47.png
次に、/smsのリソースを作成から、下記の通り入力します。
リソースパス:/sms/
リソース名:send

スクリーンショット 2026-02-23 17.55.40.png
次に、/sms/sendのメソッドを作成をクリックし、下記の通り選択します。

メソッドタイプ:POST
統合タイプ:Lambda関数
Lambdaプロキシ統合:有効
Lambda関数:後ほど作成するLambdaのARNを選択します。今は一旦保留とします。

最終的に、下記の表のようになります。

設定項目
リソースパス /sms/send
HTTPメソッド POST
統合タイプ Lambda関数
Lambda関数 SmsSenderFunction(セクション5で作成)
Lambdaプロキシ統合 有効

Lambdaプロキシ統合を有効にする理由

プロキシ統合を有効にすると、HTTPリクエストの全情報(ヘッダー、ボディ、クエリパラメータ等)がそのまま Lambda の event に渡されます。Lambda 側でレスポンスの形式も自由に制御できるため、柔軟な実装が可能です。

3-3. APIキー認証の設定

不正なリクエストを防ぐため、APIキー認証を設定します。

① APIキーの作成

設定項目
名前 SmsSenderApiKey
自動生成 有効

スクリーンショット 2026-02-23 18.01.32.png
左側のペインにあるAPIキーをクリックし、APIキーの作成をクリック

スクリーンショット 2026-02-23 18.02.20.png
上記の表の通り入力して、保存をクリック

② 使用量プランの作成

設定項目
プラン名 SmsSenderUsagePlan
スロットリングレート 10 リクエスト/秒
スロットリングバースト 5
日次クォータ 100 リクエスト/日

スクリーンショット 2026-02-23 18.03.57.png
左側のペインから使用量プランをクリックし、使用量プランを作成をクリック

スクリーンショット 2026-02-23 18.05.24.png
上記の表の通り選択、入力し、使用量プランを作成をクリック

スクリーンショット 2026-02-23 18.07.43.png
左側のペインからAPIキーを選択し、先程作成したAPIキーの画面を開き、使用量プランに追加をクリック。先程作成した使用量プランを選び、保存する。

③ メソッドの設定(セクション5のLambdaをデプロイした後に行ってください)

作成した POST /sms/send メソッドの メソッドリクエスト で以下を設定します。

設定項目
APIキーの必要性 true

3-4. APIのデプロイ

スクリーンショット 2026-02-23 18.34.03.png

設定項目
ステージ名 dev

デプロイ後、以下のようなエンドポイントURLが発行されます。
後で使用するので、控えておいてください。

https://{api-id}.execute-api.{region}.amazonaws.com/dev/sms/send

3-5. 使用量プラン画面で、使用量プランとステージを紐づける

スクリーンショット 2026-02-23 19.15.54.png

3-6. ステージdevを再デプロイ

これで、正常にAPIキーとステージがひも付きました。

4. DynamoDB設計・構築

4-1. テーブル設計

本システムでは2つのDynamoDBテーブルを使用します。

テーブル①: SmsSendCount

同一電話番号への累計送信回数を管理するカウンターテーブルです。
UpdateItemADD 演算によるアトミックインクリメントで、同時リクエストでも正確にカウントできます。

属性名 キー種別 説明
phoneNumber S (String) Partition Key 送信先電話番号(E.164形式: +819012345678
sendCount N (Number) 累計送信回数

格納データイメージ:

phoneNumber sendCount
+819012345678 14
+819098765432 3

テーブル②: SmsSendHistory

送信ごとの詳細な履歴を記録するテーブルです。

属性名 キー種別 説明
sessionId S (String) Partition Key セッションID(UUID v4)
sentAt S (String) Sort Key 送信日時(JST: 2026-02-23T14:30:00+09:00
phoneNumber S (String) 送信先電話番号(E.164形式)
messageBody S (String) 送信メッセージ内容
sendStatus S (String) 送信ステータス
messageId S (String) End User Messaging SMS が返すメッセージID
errorMessage S (String) 失敗時のエラーメッセージ(成功時は空文字)
lambdaRequestId S (String) Lambda の実行リクエストID
ttl N (Number) TTL用Unixタイムスタンプ

sendStatus の取りうる値:

ステータス 説明
SUCCESS SMS送信成功
FAILED SMS送信失敗(API エラー)
REJECTED ホワイトリスト不一致で送信拒否

格納データイメージ:

sessionId sentAt phoneNumber messageBody sendStatus messageId errorMessage lambdaRequestId ttl
550e8400-... 2026-02-23T14:30:00+09:00 +819012345678 認証コード: 123456 SUCCESS msg-abc123 req-xyz789 1748188200
660f9500-... 2026-02-23T15:00:00+09:00 +819099999999 テスト送信 REJECTED Destination not in sandbox whitelist req-def456 1748190000

4-2. テーブル構築(マネジメントコンソール)

AWSマネジメントコンソールから DynamoDB を開き、2つのテーブルを作成します。
DynamoDBはスキーマレスなので、パーティションキー、ソートキー以外の事前作成は必要ありません。

テーブル①: SmsSendCount の作成

スクリーンショット 2026-02-23 18.36.18.png
コンソール上で、DynamoDBを作成します。

設定項目
テーブル名 SmsSendCount
パーティションキー phoneNumber(文字列)
ソートキー なし
キャパシティモード オンデマンド

テーブル②: SmsSendHistory の作成

スクリーンショット 2026-02-23 18.37.06.png
同じく、コンソール上でDynamoDBを作成します。

設定項目
テーブル名 SmsSendHistory
パーティションキー sessionId(文字列)
ソートキー sentAt(文字列)
キャパシティモード オンデマンド

作成後、以下の追加設定を行います。

TTL の有効化:
スクリーンショット 2026-02-23 18.38.26.png
SmsSendHistoryの画面から、アクションを選び、TTLをオンにするをクリック

スクリーンショット 2026-02-23 18.38.56.png
ttl属性名:ttlとして保存。

設定項目
対象テーブル SmsSendHistory
TTL属性名 ttl

TTL(Time To Live)とは
指定した属性の Unix タイムスタンプを過ぎたアイテムを、DynamoDB が自動的に削除する機能です。本設計では90日後のタイムスタンプを格納し、古い送信履歴を自動的にクリーンアップします。

5. Lambda作成

5-1. 関数の作成

下記の通り作成します。
スクリーンショット 2026-02-23 18.18.04.png

設定項目
関数名 SmsSenderFunction
ランタイム Python 3.14
アーキテクチャ arm64
タイムアウト 30秒
メモリ 256MB

スクリーンショット 2026-02-23 18.09.49.png
AWSマネジメントコンソールから Lambda を開き、上記の設定で作成します。

スクリーンショット 2026-02-23 18.14.52.png
タイムアウトやメモリは、Lambda関数の設定タブの基本設定から設定します。

5-2. 環境変数の設定

スクリーンショット 2026-02-23 18.13.19.png
※SNSトピックのARNはスクリーンショットのために別のものにしています。

キー 説明
SANDBOX_MODE true サンドボックスモードフラグ
SSM_WHITELIST_PATH /sms/sandbox/whitelist ホワイトリストのSSMパス
SSM_SENDER_ID_PATH /sms/sender/senderId 送信元ID(Sender ID)のSSMパス
COUNT_TABLE_NAME SmsSendCount カウントテーブル名
HISTORY_TABLE_NAME SmsSendHistory 履歴テーブル名
SNS_TOPIC_ARN arn:aws:sns:ap-northeast-1:xxxx:SmsAlertTopic アラート通知先(セクション2で作成したトピックのARN)
TZ Asia/Tokyo タイムゾーン

5-3. DLQ(デッドレターキュー)の設定

スクリーンショット 2026-02-23 18.19.01.png

Lambda のコンソールで 非同期呼び出し の設定から DLQ を指定します。

設定項目
DLQリソース SQS
SQSキュー SmsSender-DLQ(セクション2で作成済み)
最大再試行回数 2

この時保存に失敗する場合は、LambdaがSQSにアクセスする権限が無いので、コンソール上のIAMロールから、このLambdaに紐づいているロールにSQSへの権限を追加してください。

5-4. SSM Parameter Store の準備

スクリーンショット 2026-02-23 19.02.38.png

Lambda から参照するパラメータを事前に作成しておきます。
Systems Manager → Parameter Store から以下を作成します。

パラメータ名 タイプ 説明
/sms/sandbox/whitelist StringList +819012345678,+819098765432(ご自分の確認済み電話番号を+81形式で格納してください) 検証済み電話番号リスト
/sms/sender/senderId String TESTSMS 送信元ID(Sender ID)

送信元ID(Sender ID)とは
前回の記事でも触れていますが、SMSの送信者名として受信側に表示される英数字の識別子です(例: TESTSMS)。電話番号(例: +12065550123)とは異なり、受信者がSMSを受け取った際に送信元として表示される名前です。日本向けSMSでは Sender ID がサポートされています。

5-5. Lambda関数コード

import json
import os
import uuid
import time
import re
import logging
from datetime import datetime, timezone, timedelta

import boto3
from botocore.exceptions import ClientError

# ロガー設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# AWSクライアント初期化
ssm = boto3.client("ssm")
sms = boto3.client("pinpoint-sms-voice-v2")
dynamodb = boto3.resource("dynamodb")
sns = boto3.client("sns")

# 環境変数
SANDBOX_MODE = os.environ.get("SANDBOX_MODE", "true") == "true"
SSM_WHITELIST_PATH = os.environ["SSM_WHITELIST_PATH"]
SSM_SENDER_ID_PATH = os.environ["SSM_SENDER_ID_PATH"]
COUNT_TABLE_NAME = os.environ["COUNT_TABLE_NAME"]
HISTORY_TABLE_NAME = os.environ["HISTORY_TABLE_NAME"]
SNS_TOPIC_ARN = os.environ["SNS_TOPIC_ARN"]

# DynamoDBテーブル
count_table = dynamodb.Table(COUNT_TABLE_NAME)
history_table = dynamodb.Table(HISTORY_TABLE_NAME)

# JST タイムゾーン
JST = timezone(timedelta(hours=9))

# TTL: 90日後
TTL_DAYS = 90


def lambda_handler(event, context):
    """メインハンドラー"""
    logger.info(f"Received event: {json.dumps(event)}")

    lambda_request_id = context.aws_request_id

    try:
        # ① リクエストボディのパース
        body = json.loads(event.get("body", "{}"))
        phone_number = body.get("phoneNumber", "")
        message = body.get("message", "")

        # ② バリデーション
        validation_error = validate_request(phone_number, message)
        if validation_error:
            logger.warning(f"Validation failed: {validation_error}")
            return build_response(400, {"error": validation_error})

        # ③ SSM Parameter Store から送信元ID(Sender ID)を取得
        sender_id = get_ssm_parameter(SSM_SENDER_ID_PATH)

        # ④ サンドボックス ホワイトリスト検証
        if SANDBOX_MODE:
            whitelist = get_ssm_parameter_list(SSM_WHITELIST_PATH)
            if phone_number not in whitelist:
                logger.warning(
                    f"Phone number {phone_number} not in sandbox whitelist"
                )
                session_id = str(uuid.uuid4())
                sent_at = datetime.now(JST).isoformat()

                # REJECTED として履歴に記録
                put_history(
                    session_id=session_id,
                    sent_at=sent_at,
                    phone_number=phone_number,
                    message_body=message,
                    send_status="REJECTED",
                    message_id="",
                    error_message="Destination not in sandbox whitelist",
                    lambda_request_id=lambda_request_id,
                )

                return build_response(
                    400,
                    {
                        "error": "Phone number is not in the sandbox whitelist",
                        "sessionId": session_id,
                        "sendStatus": "REJECTED",
                    },
                )

        # ⑤ セッションID生成・送信日時記録
        session_id = str(uuid.uuid4())
        sent_at = datetime.now(JST).isoformat()

        # ⑥ SMS送信(送信元IDを指定)
        send_status = "SUCCESS"
        message_id = ""
        error_message = ""

        try:
            response = sms.send_text_message(
                DestinationPhoneNumber=phone_number,
                OriginationIdentity=sender_id,
                MessageBody=message,
                MessageType="TRANSACTIONAL",
            )
            message_id = response.get("MessageId", "")
            logger.info(
                f"SMS sent successfully. MessageId: {message_id}"
            )

        except ClientError as e:
            send_status = "FAILED"
            error_message = str(e)
            logger.error(f"SMS send failed: {error_message}")

            # SNS通知(送信失敗アラート)
            publish_alert(phone_number, error_message, lambda_request_id)

        # ⑦ DynamoDB書き込み
        # カウントテーブル更新(アトミックインクリメント)
        update_send_count(phone_number)

        # 履歴テーブル書き込み
        put_history(
            session_id=session_id,
            sent_at=sent_at,
            phone_number=phone_number,
            message_body=message,
            send_status=send_status,
            message_id=message_id,
            error_message=error_message,
            lambda_request_id=lambda_request_id,
        )

        logger.info(
            f"Processing complete. sessionId={session_id}, "
            f"status={send_status}"
        )

        # ⑧ レスポンス返却
        status_code = 200 if send_status == "SUCCESS" else 502
        return build_response(
            status_code,
            {
                "sessionId": session_id,
                "sendStatus": send_status,
                "messageId": message_id,
            },
        )

    except Exception as e:
        logger.error(f"Unexpected error: {str(e)}", exc_info=True)
        return build_response(
            500,
            {
                "error": "Internal server error",
                "lambdaRequestId": lambda_request_id,
            },
        )


def validate_request(phone_number: str, message: str) -> str | None:
    """リクエストのバリデーション"""
    if not phone_number:
        return "phoneNumber is required"
    if not message:
        return "message is required"
    # E.164形式チェック
    if not re.match(r"^\+[1-9]\d{1,14}$", phone_number):
        return "phoneNumber must be in E.164 format (e.g., +819012345678)"
    if len(message) > 160:
        return "message must be 160 characters or less"
    return None


def get_ssm_parameter(name: str) -> str:
    """SSM Parameter Store からパラメータを取得"""
    response = ssm.get_parameter(Name=name, WithDecryption=True)
    return response["Parameter"]["Value"]


def get_ssm_parameter_list(name: str) -> list[str]:
    """SSM Parameter Store から StringList を取得"""
    value = get_ssm_parameter(name)
    return [v.strip() for v in value.split(",")]


def update_send_count(phone_number: str):
    """SmsSendCount テーブルのカウントをインクリメント"""
    count_table.update_item(
        Key={"phoneNumber": phone_number},
        UpdateExpression="ADD sendCount :inc",
        ExpressionAttributeValues={":inc": 1},
    )
    logger.info(f"SendCount incremented for {phone_number}")


def put_history(
    session_id: str,
    sent_at: str,
    phone_number: str,
    message_body: str,
    send_status: str,
    message_id: str,
    error_message: str,
    lambda_request_id: str,
):
    """SmsSendHistory テーブルに履歴を書き込み"""
    ttl_value = int(time.time()) + (TTL_DAYS * 24 * 60 * 60)

    history_table.put_item(
        Item={
            "sessionId": session_id,
            "sentAt": sent_at,
            "phoneNumber": phone_number,
            "messageBody": message_body,
            "sendStatus": send_status,
            "messageId": message_id,
            "errorMessage": error_message,
            "lambdaRequestId": lambda_request_id,
            "ttl": ttl_value,
        }
    )
    logger.info(f"History recorded. sessionId={session_id}")


def publish_alert(phone_number: str, error_message: str, request_id: str):
    """SNSトピックにアラートを発行"""
    sns.publish(
        TopicArn=SNS_TOPIC_ARN,
        Subject="[ALERT] SMS送信失敗",
        Message=(
            f"SMS送信に失敗しました。\n\n"
            f"送信先: {phone_number}\n"
            f"エラー: {error_message}\n"
            f"Lambda RequestId: {request_id}\n"
            f"発生時刻: {datetime.now(JST).isoformat()}"
        ),
    )
    logger.info("Alert published to SNS topic")


def build_response(status_code: int, body: dict) -> dict:
    """API Gateway用レスポンスを構築"""
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
        },
        "body": json.dumps(body, ensure_ascii=False),
    }

コードのポイント
update_itemADD 演算により、SmsSendCount が存在しない場合は自動的にアイテムが作成され、sendCount = 1 で初期化されます。

・ホワイトリスト不一致の場合も SmsSendHistoryREJECTED として記録することで、不正リクエストの監査証跡を残します。

・SMS送信の API クライアントは pinpoint-sms-voice-v2 を使用します(End User Messaging SMS の SDK 名称)。

OriginationIdentity には送信元ID(Sender ID: TESTSMS)を指定します。電話番号ではなく、受信者のSMS画面に表示される送信者名です。

6. IAM権限付与

6-1. Lambda実行ロールへのポリシーアタッチ

Lambda関数に紐づく IAM ロールに、以下のカスタムポリシーをアタッチします。
マネジメントコンソールの IAM → ポリシー → ポリシーの作成 から JSON で入力します。
今回は便宜上分けて記載しますが、すべてのポリシーを全部盛りで記載していただいても構いません。その場合、下記のような表示になります。
スクリーンショット 2026-02-23 18.31.11.png

最小権限の原則
各アクションは必要最小限のリソースに限定しています。{account-id}{region} はご自身の環境に置き換えてください。

ポリシー①: DynamoDB アクセス

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DynamoDBSmsSendCount",
            "Effect": "Allow",
            "Action": [
                "dynamodb:UpdateItem",
                "dynamodb:GetItem"
            ],
            "Resource": "arn:aws:dynamodb:{region}:{account-id}:table/SmsSendCount"
        },
        {
            "Sid": "DynamoDBSmsSendHistory",
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem"
            ],
            "Resource": "arn:aws:dynamodb:{region}:{account-id}:table/SmsSendHistory"
        },
        {
            "Sid": "DynamoDBSmsSendHistoryGSI",
            "Effect": "Allow",
            "Action": [
                "dynamodb:Query"
            ],
            "Resource": "arn:aws:dynamodb:{region}:{account-id}:table/SmsSendHistory/index/*"
        }
    ]
}

ポリシー②: End User Messaging SMS

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "EndUserMessagingSMS",
            "Effect": "Allow",
            "Action": [
                "sms-voice:SendTextMessage"
            ],
            "Resource": "*"
        }
    ]
}

ポリシー③: SSM Parameter Store

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "SSMParameterAccess",
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameter"
            ],
            "Resource": "arn:aws:ssm:{region}:{account-id}:parameter/sms/*"
        }
    ]
}

ポリシー④: SNS アラート通知

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "SNSPublishAlert",
            "Effect": "Allow",
            "Action": [
                "sns:Publish"
            ],
            "Resource": "arn:aws:sns:{region}:{account-id}:SmsAlertTopic"
        }
    ]
}

ポリシー⑤: CloudWatch Logs

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "CloudWatchLogs",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:{region}:{account-id}:log-group:/aws/lambda/SmsSenderFunction:*"
        }
    ]
}

ポリシー⑥: SQS DLQ

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "SQSDLQAccess",
            "Effect": "Allow",
            "Action": [
                "sqs:SendMessage"
            ],
            "Resource": "arn:aws:sqs:{region}:{account-id}:SmsSender-DLQ"
        }
    ]
}

6-2. 権限の全体像まとめ

対象サービス 許可アクション 対象リソース
DynamoDB UpdateItem, GetItem SmsSendCount
DynamoDB PutItem SmsSendHistory
DynamoDB Query SmsSendHistory/index/*
End User Messaging SMS SendTextMessage *
SSM Parameter Store GetParameter /sms/*
SNS Publish SmsAlertTopic
CloudWatch Logs CreateLogGroup, CreateLogStream, PutLogEvents /aws/lambda/SmsSenderFunction
SQS SendMessage SmsSender-DLQ

6-3. API Gateway → Lambda の呼び出し権限

API Gateway が Lambda を呼び出すためのリソースベースポリシーは、マネジメントコンソールで API Gateway の統合設定時に自動的に追加されます。手動でのポリシー設定は不要です。

7. 動作確認

7-1. 正常系テスト(ホワイトリスト内の番号)

APIステージURLの後半の/sms/sendがコンソール上では抜けているので、お忘れなきようお願いします。

curl -X POST \
  https://{api-id}.execute-api.ap-northeast-1.amazonaws.com/dev/sms/send \
  -H "Content-Type: application/json" \
  -H "x-api-key: {your-api-key}" \
  -d '{
    "phoneNumber": "+819012345678",
    "message": "テストメッセージです"
  }'

期待されるレスポンス:

{
  "sessionId": "550e8400-e29b-41d4-a716-446655440000",
  "sendStatus": "SUCCESS",
  "messageId": "msg-abc123def456"
}

スクリーンショット 2026-02-23 19.26.53.png
SMS上で確認できました。
DynamoDBも確認してみましょう。

SmsSendCount(電話番号は削除しています)
スクリーンショット 2026-02-23 19.32.56.png
2回テストしてみたのですが、無事にカウント2となっていますね。

SmsSendHistory(電話番号は削除しています)
スクリーンショット 2026-02-23 19.31.06.png

7-2. 異常系テスト(ホワイトリスト外の番号)

curl -X POST \
  https://{api-id}.execute-api.ap-northeast-1.amazonaws.com/dev/sms/send \
  -H "Content-Type: application/json" \
  -H "x-api-key: {your-api-key}" \
  -d '{
    "phoneNumber": "+819099999999",
    "message": "このメッセージは送信されません"
  }'

期待されるレスポンス:

{
  "error": "Phone number is not in the sandbox whitelist",
  "sessionId": "660f9500-abcd-1234-efgh-567890abcdef",
  "sendStatus": "REJECTED"
}

DynamoDBを確認してみましょう。
SmsSendHistory
スクリーンショット 2026-02-23 19.36.21.png
当初の設計どおりの値が格納されていますね。

まとめ

本記事では、AWS End User Messaging SMS を使ったSMS送信システムを以下の構成で構築しました。

コンポーネント 役割
API Gateway REST APIエンドポイント + APIキー認証
Lambda メイン処理(バリデーション → 送信 → 記録)
End User Messaging SMS SMS送信(サンドボックス環境)
DynamoDB × 2 送信回数カウント + 送信履歴管理
SSM Parameter Store ホワイトリスト・送信元IDの一元管理
CloudWatch Logs 実行ログ
SNS 送信失敗時のアラート通知
SQS (DLQ) Lambda失敗時のメッセージ退避

サンドボックス環境の制約をアプリケーションレベルで安全にハンドリングしており、本番移行時は 環境変数 SANDBOX_MODEfalse に変更するだけで対応できる設計になっています。

こちらを参考に、ぜひ皆さんもEnd User Messaging SMSを使ってみてください。
次回の記事では、サンドボックス解除申請や、クォータ引き上げ申請を取り扱います。
では、またお会いしましょう。

3
5
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
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?