電話番号取得不要で即 OTP 送信!AWS End User Messaging Notify を CDK で構築してみた
導入
背景
- 2026年3月のアップデートにより、AWS End User Messaging Notify が利用可能になりました。
- 従来、SMS で OTP(ワンタイムパスコード)を送信するには、電話番号の取得・キャリア登録・送信者 ID の審査といった準備に数日〜数週間を要していました。
- AWS End User Messaging Notify を使うと、電話番号の取得やキャリア登録なしに、数分以内に OTP 送信を開始できます。
- 本ブログでは、AWS End User Messaging Notify の概要を解説したうえで、AWS CDK を用いてインフラを構築し、実際に SMS が届くところまでを確認します。
本記事の目的
- AWS End User Messaging Notify を活用することで、電話番号取得の待ち時間ゼロで OTP 送信を実装できることを解説します。
対象読者
- AWS CDK の基本操作(スタックの作成・デプロイ)を把握している方を想定し、CDK 自体の基礎説明は割愛します。
AWS End User Messaging Notify とは
AWS End User Messaging Notify は、OTP(コード検証)メッセージを送信するためのテンプレート型メッセージング機能です。
最大の特徴は、利用者が電話番号を取得・管理する必要がない点にあります。
Standard SMS 送信との比較
| 項目 | Standard SMS | Notify |
|---|---|---|
| 電話番号の取得 | 必要 | 不要(AWS 管理) |
| キャリア登録 | お客様の責任 | AWS が管理 |
| 初回送信まで | 数日〜数週間 | 数分 |
| メッセージテンプレート | お客様が管理 | 事前承認済みテンプレートを使用 |
| 不正トラフィック対策(SMS Protect) | オプション | 標準で有効 |
| ID 管理・ルーティング | お客様が管理 | AWS が管理 |
AWS End User Messaging Notify は OTP 送信に特化したサービスです。カスタムメッセージ本文・マーケティング SMS・MMS など、柔軟な送信が必要な場合は Standard SMS を使用します。
ティア(Basic / Advanced)
AWS End User Messaging Notify には 2 つのサービスティアがあります。(以下は 2026年5月時点の情報です。)
| 項目 | Basic | Advanced |
|---|---|---|
| 利用開始 | 即時 | ティアアップグレード審査通過後 |
| 1日の送信上限 | 200 通 | 無制限 |
| TPS | 1 | 25 |
| 対応国 | 事前承認済み 30 か国 | すべての対応国 |
| 使用可能 ID 種別 | ロングコード・TFN・送信者 ID | ショートコードを含む全種別 |
本番運用では送信上限・TPS の観点から Advanced ティアの利用を推奨しますが、本記事の検証は即時利用できる Basic ティアで進めます。
CDK を用いた構築
前提条件
- AWS CDK v2 の実行環境が整っていること
手順
1. boto3 Lambda Layer を作成する
Lambda ランタイムに同梱されている boto3 は Notify API(2026/03 リリース)を含まないため、最新版の boto3 を Lambda Layer として事前に用意します。
mkdir python
pip3 install boto3 -t ./python
zip -r python.zip python
aws lambda publish-layer-version \
--layer-name python-boto3-layer \
--zip-file fileb://python.zip \
--compatible-runtimes python3.14
コマンドが成功すると Layer ARN が出力されます。次のステップで CDK スタックをデプロイする際に使用します。
2. 使用するテンプレート ID を確認する
Notify では事前承認済みテンプレートのみ使用できます。次のステップで CDK スタックや Lambda に設定する TemplateId と、テンプレートが要求する変数名を、以下のコマンドで確認します(SMS チャネル・Basic ティアで利用可能なテンプレートを絞り込んでいます)。
aws pinpoint-sms-voice-v2 describe-notify-templates \
--filters '[{"Name":"channels","Values":["SMS"]},{"Name":"tier-access","Values":["BASIC"]}]'
出力された TemplateId(本記事では notify-code-verification-japanese-001)を、後続の CDK スタックおよび Lambda 関数で使用します。
3. NotifyStack を作成する
2026年5月時点では、CloudFormation が NotifyConfiguration リソースをネイティブサポートしていないため、CDK の AwsCustomResource を用いて SDK 呼び出しを Custom Resource として実装します。
import { Stack, StackProps, RemovalPolicy, Duration } from "aws-cdk-lib";
import { Construct } from "constructs";
import {
AwsCustomResource,
AwsCustomResourcePolicy,
PhysicalResourceId,
PhysicalResourceIdReference,
} from "aws-cdk-lib/custom-resources";
import {
aws_iam as iam,
aws_lambda as lambda,
aws_logs as logs,
} from "aws-cdk-lib";
import * as path from "path";
export interface NotifyStackProps extends StackProps {
readonly boto3LayerArn: string;
}
export class NotifyStack extends Stack {
constructor(scope: Construct, id: string, props: NotifyStackProps) {
super(scope, id, props);
// ------------ Notify Configuration ---------------
// CloudFormation ネイティブサポート未対応のため Custom Resource で作成
const notifyConfig = new AwsCustomResource(this, "NotifyConfiguration", {
onCreate: {
service: "PinpointSMSVoiceV2",
action: "createNotifyConfiguration",
parameters: {
DisplayName: "MyOTPApp",
UseCase: "CODE_VERIFICATION",
EnabledChannels: ["SMS"],
},
physicalResourceId: PhysicalResourceId.fromResponse(
"NotifyConfigurationArn",
),
},
onDelete: {
service: "PinpointSMSVoiceV2",
action: "deleteNotifyConfiguration",
parameters: {
NotifyConfigurationId: new PhysicalResourceIdReference(),
},
},
policy: AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
actions: [
"sms-voice:CreateNotifyConfiguration",
"sms-voice:DeleteNotifyConfiguration",
],
resources: ["*"],
}),
]),
logRetention: logs.RetentionDays.ONE_WEEK,
// Notify API は 2026/03 リリースのため古いバンドル SDK には含まれていない
installLatestAwsSdk: true,
});
const notifyConfigArn = notifyConfig.getResponseField(
"NotifyConfigurationArn",
);
const notifyConfigId = notifyConfig.getResponseField(
"NotifyConfigurationId",
);
// ------------ Lambda Layer ---------------
// ランタイム同梱の boto3 は Notify API 未収録のため、最新版を含む Layer を ARN で参照(CDK外で事前作成)
const boto3Layer = lambda.LayerVersion.fromLayerVersionArn(
this,
"Boto3Layer",
props.boto3LayerArn,
);
// ------------ Lambda ---------------
const logGroup = new logs.LogGroup(this, "OtpLambdaLogGroup", {
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: RemovalPolicy.DESTROY,
});
const otpLambda = new lambda.Function(this, "OtpLambda", {
runtime: lambda.Runtime.PYTHON_3_14,
handler: "index.handler",
code: lambda.Code.fromAsset(
path.join(__dirname, "../../../assets/lambda/notify"),
),
environment: {
NOTIFY_CONFIGURATION_ID: notifyConfigId,
TEMPLATE_ID: "notify-code-verification-japanese-001",
},
timeout: Duration.seconds(30),
logGroup,
layers: [boto3Layer],
});
// Lambda に Notify 送信権限を付与
otpLambda.addToRolePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["sms-voice:SendNotifyTextMessage"],
resources: [notifyConfigArn],
}),
);
}
}
ポイント: Lambda ランタイムに同梱されている boto3 は Notify API(2026/03 リリース)を含まないため、最新版の boto3 を Lambda Layer として事前に用意し、
boto3LayerArnとして props 経由で渡します。AwsCustomResourceも同様の理由で最新版のJS SDKを利用するよう、installLatestAwsSdk: trueを明示しています。
4. スタックをデプロイする
上記スタックをデプロイすると、Notify Configuration と OTP 送信用 Lambda 関数が作成されます。
Python スクリプトによる OTP 送信
Lambda 関数のコード
CDK でデプロイした Lambda 関数 (assets/lambda/notify/index.py) は、イベントで受け取った電話番号へ OTP を送信します。
注意(実運用時):
ResolvedMessageBodyはデバッグ用途でレスポンスに含めていますが、SMS を受け取っていない相手に OTP が渡るリスクがあるため、実運用では返却しないでください。- 本記事は SMS 送信の仕組みに主眼を置いているため、生成した OTP の保存・検証フローは実装していません。コードの生成・保存・検証に関わる設計・実装の詳細はスコープ外としています。
- 実公開時には API エンドポイントに対するレート制限の実装も検討してください。
import os
import secrets
import string
import json
import boto3
client = boto3.client('pinpoint-sms-voice-v2')
NOTIFY_CONFIGURATION_ID = os.environ['NOTIFY_CONFIGURATION_ID']
TEMPLATE_ID = os.environ.get('TEMPLATE_ID', 'notify-code-verification-japanese-001')
def _generate_otp(length: int = 6) -> str:
return ''.join(secrets.choice(string.digits) for _ in range(length))
def handler(event: dict, context) -> dict:
phone_number = event.get('phone_number')
if not phone_number:
return {
'statusCode': 400,
'body': json.dumps({'error': 'phone_number is required'}),
}
otp = _generate_otp()
response = client.send_notify_text_message(
NotifyConfigurationId=NOTIFY_CONFIGURATION_ID,
DestinationPhoneNumber=phone_number,
TemplateId=TEMPLATE_ID,
TemplateVariables={'code': otp},
)
return {
'statusCode': 200,
'body': json.dumps({
'message_id': response['MessageId'],
'resolved_body': response.get('ResolvedMessageBody', ''),
}),
}
send_notify_text_message の主要なパラメータは以下の通りです。
| パラメータ | 説明 |
|---|---|
NotifyConfigurationId |
Notify Configuration の ID または ARN |
DestinationPhoneNumber |
送信先電話番号(E.164 形式:+819012345678) |
TemplateId |
使用するテンプレート ID |
TemplateVariables |
テンプレート変数(code に OTP 文字列を渡す) |
動作確認
Lambda から送信する
aws lambda invoke で Lambda を直接呼び出します。
aws lambda invoke \
--function-name <OtpLambdaArn> \
--payload '{"phone_number": "+819012345678"}' \
--cli-binary-format raw-in-base64-out \
response.json && cat response.json
成功すると"statusCode": 200を含むレスポンスが返ります。
SMS の受信を確認する
数秒〜10秒程度で、登録した電話番号に SMS が届きます。
MyOTPApp: 1回限りの認証コードは 891081 です。このメッセージは共有しないでください。Notify から送信されました。
DisplayName に設定したブランド名(ここでは MyOTPApp)が先頭に表示されているのが確認できます。
考察
Notify を使う利点
- 即時利用開始:電話番号の購入・キャリア登録が不要なため、今日試して今日 SMS を送れます。プロトタイプやハッカソンでの OTP 実装に最適です。
- SMS Protect が標準組み込み:AIT(人工的なトラフィック水増し)や SMS パンプ攻撃を検知・ブロックする仕組みが追加料金なしで有効です。
-
シンプルな API:
send_notify_text_messageの 1 呼び出しでメッセージを送信できます。テンプレートや送信者 ID の選択は AWS が自動で行います。
注意点・制約
- Basic ティアは 1 日 200 通・1 TPS:本番運用ではボトルネックになる可能性があります。Advanced ティア(1 日無制限・25 TPS)へのアップグレードには、ブランド検証(コンセント取得方法の提出)が必要です。
- テンプレートはカスタマイズ不可:事前承認済みテンプレートのみ使用できます。メッセージ本文を自由に編集したい場合は Standard SMS を使用します。
-
CDK ネイティブ未対応(2026年5月時点):CloudFormation リソースが未提供のため
AwsCustomResourceを使用しています。今後 L1 コンストラクトが追加された場合は移行を検討してください。
向いているユースケース
- ユーザー登録・ログイン時の SMS 二要素認証
- パスワードリセット時の本人確認
- スモールスタートの OTP 実装(後から Standard SMS へ移行も可能)
おわりに
AWS End User Messaging Notify を使うことで、電話番号取得ゼロ・キャリア登録ゼロで SMS OTP を実装できることが確認できました。
これまで「電話番号の取得に時間がかかるから OTP 実装は後回し」となっていたシナリオでも、Notify を使えばその日のうちに検証を開始できます。
Basic ティアの 1 日 200 通制限は本番用途には少ないですが、開発・検証フェーズには十分です。本番展開が近づいたタイミングで Advanced ティアへのアップグレード申請を並行して進める、というアプローチが現実的でしょう。
Discussion