はじめに
こんばんは、mirukyです。
今回は、これまでご紹介したAWS End User Messaging SMSをより実践的に使うべく、実際にアーキテクチャ設計から、各サービスとの連携、動作確認まで行いたいと思います。
目次
- アーキテクチャ全体図
- SNSトピック・SQS DLQ作成
- API Gateway作成
- DynamoDB設計・構築
- Lambda作成
- IAM権限付与
- 動作確認
1. アーキテクチャ全体図
流れとしては下記の通りです。
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 を開き、以下の設定で作成します。
| 設定項目 | 値 |
|---|---|
| タイプ | スタンダード |
| トピック名 | 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(デッドレターキュー)の作成
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 の作成

AWSマネジメントコンソールから API Gateway を開き、以下の設定で作成します。
| 設定項目 | 値 |
|---|---|
| APIタイプ | REST API |
| API名 | SmsSenderApi |
| 説明 | SMS送信用API |
| エンドポイントタイプ | リージョン |
3-2. リソースとメソッドの作成

まず、リソースを作成から、下記の通り入力します。
リソースパス:/
リソース名:sms

次に、/smsのリソースを作成から、下記の通り入力します。
リソースパス:/sms/
リソース名:send

次に、/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 |
| 自動生成 | 有効 |

左側のペインにあるAPIキーをクリックし、APIキーの作成をクリック
② 使用量プランの作成
| 設定項目 | 値 |
|---|---|
| プラン名 | SmsSenderUsagePlan |
| スロットリングレート | 10 リクエスト/秒 |
| スロットリングバースト | 5 |
| 日次クォータ | 100 リクエスト/日 |

左側のペインから使用量プランをクリックし、使用量プランを作成をクリック

左側のペインからAPIキーを選択し、先程作成したAPIキーの画面を開き、使用量プランに追加をクリック。先程作成した使用量プランを選び、保存する。
③ メソッドの設定(セクション5のLambdaをデプロイした後に行ってください)
作成した POST /sms/send メソッドの メソッドリクエスト で以下を設定します。
| 設定項目 | 値 |
|---|---|
| APIキーの必要性 | true |
3-4. APIのデプロイ
| 設定項目 | 値 |
|---|---|
| ステージ名 | dev |
デプロイ後、以下のようなエンドポイントURLが発行されます。
後で使用するので、控えておいてください。
https://{api-id}.execute-api.{region}.amazonaws.com/dev/sms/send
3-5. 使用量プラン画面で、使用量プランとステージを紐づける
3-6. ステージdevを再デプロイ
これで、正常にAPIキーとステージがひも付きました。
4. DynamoDB設計・構築
4-1. テーブル設計
本システムでは2つのDynamoDBテーブルを使用します。
テーブル①: SmsSendCount
同一電話番号への累計送信回数を管理するカウンターテーブルです。
UpdateItem の ADD 演算によるアトミックインクリメントで、同時リクエストでも正確にカウントできます。
| 属性名 | 型 | キー種別 | 説明 |
|---|---|---|---|
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 の作成
| 設定項目 | 値 |
|---|---|
| テーブル名 | SmsSendCount |
| パーティションキー |
phoneNumber(文字列) |
| ソートキー | なし |
| キャパシティモード | オンデマンド |
テーブル②: SmsSendHistory の作成
| 設定項目 | 値 |
|---|---|
| テーブル名 | SmsSendHistory |
| パーティションキー |
sessionId(文字列) |
| ソートキー |
sentAt(文字列) |
| キャパシティモード | オンデマンド |
作成後、以下の追加設定を行います。
TTL の有効化:

SmsSendHistoryの画面から、アクションを選び、TTLをオンにするをクリック
| 設定項目 | 値 |
|---|---|
| 対象テーブル | SmsSendHistory |
| TTL属性名 | ttl |
TTL(Time To Live)とは
指定した属性の Unix タイムスタンプを過ぎたアイテムを、DynamoDB が自動的に削除する機能です。本設計では90日後のタイムスタンプを格納し、古い送信履歴を自動的にクリーンアップします。
5. Lambda作成
5-1. 関数の作成
| 設定項目 | 値 |
|---|---|
| 関数名 | SmsSenderFunction |
| ランタイム | Python 3.14 |
| アーキテクチャ | arm64 |
| タイムアウト | 30秒 |
| メモリ | 256MB |

AWSマネジメントコンソールから Lambda を開き、上記の設定で作成します。

タイムアウトやメモリは、Lambda関数の設定タブの基本設定から設定します。
5-2. 環境変数の設定

※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(デッドレターキュー)の設定
Lambda のコンソールで 非同期呼び出し の設定から DLQ を指定します。
| 設定項目 | 値 |
|---|---|
| DLQリソース | SQS |
| SQSキュー |
SmsSender-DLQ(セクション2で作成済み) |
| 最大再試行回数 | 2 |
この時保存に失敗する場合は、LambdaがSQSにアクセスする権限が無いので、コンソール上のIAMロールから、このLambdaに紐づいているロールにSQSへの権限を追加してください。
5-4. SSM Parameter Store の準備
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_item の ADD 演算により、SmsSendCount が存在しない場合は自動的にアイテムが作成され、sendCount = 1 で初期化されます。
・ホワイトリスト不一致の場合も SmsSendHistory に REJECTED として記録することで、不正リクエストの監査証跡を残します。
・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 で入力します。
今回は便宜上分けて記載しますが、すべてのポリシーを全部盛りで記載していただいても構いません。その場合、下記のような表示になります。

最小権限の原則
各アクションは必要最小限のリソースに限定しています。{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"
}

SMS上で確認できました。
DynamoDBも確認してみましょう。
SmsSendCount(電話番号は削除しています)

2回テストしてみたのですが、無事にカウント2となっていますね。
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

当初の設計どおりの値が格納されていますね。
まとめ
本記事では、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_MODE を false に変更するだけで対応できる設計になっています。
こちらを参考に、ぜひ皆さんもEnd User Messaging SMSを使ってみてください。
次回の記事では、サンドボックス解除申請や、クォータ引き上げ申請を取り扱います。
では、またお会いしましょう。













