💨

電話番号取得不要で即 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 パンプ攻撃を検知・ブロックする仕組みが追加料金なしで有効です。
  • シンプルな APIsend_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 ティアへのアップグレード申請を並行して進める、というアプローチが現実的でしょう。

参考

Accenture Japan (有志)

Discussion