homeIcon

【メール配信編】プロフィールサイトAWS移行メモ 〜インフラ初心者を添えて〜

インフラ
2025.09.13
2025.09.13

はじめに

この記事では前回に引き続き、AWS CDK を使用して API Gateway + Lambda + SES を用いたメール配信(お問い合わせ)機能を構築していきます。
アーキテクチャ図では、以下の部分を構築します。
クライアントからのリクエストが、API Gatewayによって受け取られ、次にLambda関数によって処理されます。その後、SESを経由してメールが送信されるシンプルな構成です。

画像1

これまでの内容は以下の記事に記載していますので、気になる方はぜひご覧ください。

【静的サイト編】プロフィールサイトAWS移行メモ 〜インフラ初心者を添えて〜

No description available.

miyazaki-profile.com

OGP Image

全体の構成

メール配信用の「mail-api」スタックを追加します。
内容は以下のとおりです。

スタック役割使用サービス
mail-apiメール配信(お問い合わせ機能)API Gateway, Lambda, SES

また、今回の実装で追加するファイルを示します。
前回の「静的サイト編」に引き続き、以下のファイルを新たに追加していきます。

My_profile-cdk/
...
├── function/
│   ...
│   └── lambda/
│       └── send-mail.ts
├── lib/
│   └── constructs/
│   │   ...
│   │   └── mail-api-construct/
│   │       └── contact-api-construct.ts
│   │       └── mailer-function-construct.ts
│   │       └── ses-domain-construct.ts
│   └── stacks/
│   │   ...
│   │   └── mail-api-stack.ts
│   ...
├── parameters/
│   ...
│   └── mail-parameter.ts
...

データフロー

APIのリクエストからメール送信までのデータフローをシーケンス図で示します。
今回は、リクエストボディに「名前(name)」「メールアドレス(address)」「件名(title)」「本文(message)」を指定すると、それに応じたメールを送信するAPIを作成します。
1回の問い合わせで、サイト運営者(私)と入力者への2つのメール配信を行います。

画像2

フローの内容

  1. クライアントは「名前(name)」「メールアドレス(address)」「件名(title)」「本文(message)」を入力し、API Gatewayにリクエストを送信します。
  2. Cloud Frontのビヘイビアにより、API Gatewayへとルーティングされます。
  3. API GatewayはリクエストをLambda関数に転送し、リクエスト内容を検証します。
  4. Lambda関数はSESにメール送信リクエストを送信します。
  5. SESはサイト運営者とクライアントにメールを配信します。
  6. SESはAPI Gateway & Lambdaに送信結果(MessageId)を返します。
  7. API Gateway & Lambdaはクライアントにレスポンス(ステータスコード、MessageId)を返します。

CDKの実装

前回と同様に、大まかな処理内容と主要サービスについて話します。
コード自体は以下のリポジトリに置いているので、気になった方は見てみてください。

GitHub - miyazaki-dev01/My_profile-infra: プロフィールサイト IaC(AWS CDK)

プロフィールサイト IaC(AWS CDK). Contribute to miyazaki-dev01/My_profile-infra development by creating an account on GitHub.

github.com

プロフィールサイト IaC(AWS CDK). Contribute to miyazaki-dev01/My_profile-infra development by creating an account on GitHub.

CloudFront ディストリビューションの編集

まずは、前回作成したCloudFront ディストリビューションを修正し、API Gatewayへのリクエストをルーティングします。
"api/*"のビヘイビアを追加します。

/lib/constructs/website-constructs/cdn-construct.ts
...
 
export class CdnConstruct extends Construct {
  readonly distribution: cf.Distribution;
 
  constructor(scope: Construct, id: string, props: CdnConstructProps) {
    super(scope, id);
 
    ...
 
    // API Gateway へのオリジン(Contact用)
    const apiOrigin = new origins.HttpOrigin( 
      props.apiOriginForContact.domainName, 
      { 
        originPath: props.apiOriginForContact.originPath, 
        protocolPolicy: cf.OriginProtocolPolicy.HTTPS_ONLY, // API GW は HTTPS
      } 
    ); 
 
    // API の Preflight に必要なヘッダだけを転送するポリシー
    const apiCorsOriginRequestPolicy = new cf.OriginRequestPolicy( 
      this, 
      "ApiCorsReqPolicy", 
      { 
        comment: "Forward CORS preflight headers to API", 
        headerBehavior: cf.OriginRequestHeaderBehavior.allowList( 
          "Origin", 
          "Access-Control-Request-Method", 
          "Access-Control-Request-Headers"
        ), 
        queryStringBehavior: cf.OriginRequestQueryStringBehavior.all(), 
        cookieBehavior: cf.OriginRequestCookieBehavior.none(), 
      } 
    ); 
 
    // CloudFront Distribution
    this.distribution = new cf.Distribution(this, "Distribution", {
      defaultBehavior: {
        origin: origins.S3BucketOrigin.withOriginAccessControl(            // OACでS3を私的アクセス
          props.contentBucket
        ),
        viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,   // HTTP→HTTPS
        responseHeadersPolicy: headers,                                    // セキュリティヘッダ
        functionAssociations: rewriteFn                                    // ディレクトリ補完
          ? [
              {
                eventType: cf.FunctionEventType.VIEWER_REQUEST,
                function: rewriteFn,
              },
            ]
          : undefined,
        compress: true,                                                    // 自動圧縮
        cachePolicy: defaultCache,                                         // 上記キャッシュ方針
        allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,          // 読み取り系のみ許可
      },
      additionalBehaviors: { 
        "api/*": { 
          origin: apiOrigin, 
          viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, // HTTPS 強制
          allowedMethods: cf.AllowedMethods.ALLOW_ALL,                     // POST/OPTIONS 含む
          cachePolicy: cf.CachePolicy.CACHING_DISABLED,                    // フォームはキャッシュしない
          originRequestPolicy: apiCorsOriginRequestPolicy,                 // ヘッダ/クッキー/クエリを転送(安全策)
          responseHeadersPolicy: headers,                                  // 同じセキュリティヘッダ
          compress: false,                                                 // API は圧縮不要
        }, 
      }, 
      defaultRootObject: "index.html",                                     // ルート(/)はindex.html
      certificate: props.certificate,                                      // TLS終端
      domainNames: [props.domainName],                                     // ALTN(CNAME)
      errorResponses: [                                                    // 403/404は404.htmlへ
        {
          httpStatus: 404,
          responseHttpStatus: 404,
          responsePagePath: "/404.html",
          ttl: Duration.minutes(5),
        },
        {
          httpStatus: 403,
          responseHttpStatus: 404,
          responsePagePath: "/404.html",
          ttl: Duration.minutes(5),
        },
      ],
      priceClass: cf.PriceClass.PRICE_CLASS_200,                           // コスト/性能バランス
      minimumProtocolVersion: cf.SecurityPolicyProtocol.TLS_V1_2_2021,     // TLS最低バージョン
      httpVersion: cf.HttpVersion.HTTP3,                                   // HTTP/3対応
    });
 
    ...
  }
}

SESの設定

次に、SESの設定をします。
以下の処理は、元々コンソールで手作業になる 「SES のドメイン登録 → DKIM 有効化 → MAIL FROM 設定 → DNS レコード作成」 の一連を、CDK 一発で自動化しています。
具体的には、以下の処理を行っています。

  1. Route 53 のホストゾーンを参照。
  2. SES のドメインアイデンティティを作成。
  3. SPF を自動有効化。(SPFとは、メールの改ざんやなりすましを検知するための送信ドメイン認証技術のこと。)
  4. 独自 MAIL FROM ドメインを設定。
/lib/constructs/mail-api-construct/ses-domain-construct.ts
import { Construct } from "constructs";
import * as r53 from "aws-cdk-lib/aws-route53";
import * as ses from "aws-cdk-lib/aws-ses";
import { SesParams } from "@/parameters/mail-parameter";
import { HostedZoneRef } from "@/lib/stacks/dns-stack";
 
export interface SesDomainConstructProps extends SesParams {
  hostedZoneRef: HostedZoneRef;
}
 
export class SesDomainConstruct extends Construct {
  public readonly domainName: string;
 
  constructor(scope: Construct, id: string, props: SesDomainConstructProps) {
    super(scope, id);
 
    // Route53: 同一ゾーンを参照
    const zone = r53.HostedZone.fromHostedZoneAttributes(this, "ImportedZone", {
      hostedZoneId: props.hostedZoneRef.hostedZoneId,
      zoneName: props.hostedZoneRef.zoneName,
    });
 
    // SES ドメインアイデンティティを作成(DKIM & MAIL FROM 自動設定)
    new ses.EmailIdentity(this, "SesDomainIdentity", {
      identity: ses.Identity.publicHostedZone(zone),
      mailFromDomain: `${props.mailFromSubdomain}.${zone.zoneName}`,
    });
 
    // ゾーン名を公開
    this.domainName = zone.zoneName;
  }
}
 

また、SPFに関しては以下の記事で詳しく解説されていますので、詳細が気になる方はご覧ください。

[SES入門] SPFを理解しよう! - Qiita

この記事について 皆さんはAWSのSESというサービスを利用していますか!? コミュニケーションの手段として、EメールからSlackなどのツールが代替されてEメールはオワコンでは?ということも囁かれていますが、まだまだEメールは色々なところに使われており、滅びる気配があり...

qiita.com

OGP Image

Lambdaの設定

続いて、LambdaのCDKを実装していきます。
内容としては、問い合わせメールを送る Lambda 関数(Node.js 20) を CDK で作成し、最小権限で SES 送信できるようにしています。

  1. 差出人メールアドレスの組み立て。(noreply@example.com
  2. CloudWatch Logs(監査・トラブルシュート用)のロググループを、保持期間1ヶ月で作成。
  3. Lambda 関数の作成。(送信処理の本体)
  4. SES送信に必要な最小権限を設定。
/lib/constructs/mail-api-construct/mailer-function-construct.ts
import { Duration, RemovalPolicy } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as lambdaNode from "aws-cdk-lib/aws-lambda-nodejs";
import * as logs from "aws-cdk-lib/aws-logs";
import * as iam from "aws-cdk-lib/aws-iam";
import { LambdaParams } from "@/parameters/mail-parameter";
 
export interface MailerFunctionConstructProps extends LambdaParams {
  domainName: string;
}
 
export interface MailerFunctionConstructOutputs {
  fn: lambdaNode.NodejsFunction;
}
 
export class MailerFunctionConstruct extends Construct {
  public readonly function: MailerFunctionConstructOutputs;
 
  constructor(
    scope: Construct,
    id: string,
    props: MailerFunctionConstructProps
  ) {
    super(scope, id);
 
    // 差出人メールアドレス
    const fromAddress = `${props.fromLocalPart}@${props.domainName}`;
 
    // 事前にロググループを作成
    const sendMailFnLogs = new logs.LogGroup(this, "SendMailFnLogs", {
      retention: logs.RetentionDays.ONE_MONTH, // ログ保持1ヶ月
      removalPolicy: RemovalPolicy.DESTROY,    // Destroy 方針
    });
 
    // 送信用のLambda関数を作成
    const fn = new lambdaNode.NodejsFunction(this, "SendMailFn", {
      entry: require.resolve("@/functions/lambda/send-mail.ts"), // Lambdaエントリ
      runtime: lambda.Runtime.NODEJS_20_X,                       // Node.js 20
      memorySize: 256,                                           // メモリ(256MB)
      timeout: Duration.seconds(10),                             // タイムアウト(10秒)
      bundling: { target: "node20" },                            // esbuildターゲット
      logGroup: sendMailFnLogs,
      environment: {
        FROM_EMAIL: fromAddress,                                 // 差出人
        TO_EMAIL  : props.fixedToGmail,                          // 宛先
        ALLOWED_ORIGINS: props.allowedOrigins.join(","),         // 許可するオリジン(カンマ区切り)
      }
    });
 
    // SES送信の最小権限(Fromを固定するConditionで縛る)
    fn.addToRolePolicy(
      new iam.PolicyStatement({
        actions: ["ses:SendEmail"],         // 送信に必要な権限
        resources: ["*"],                   // SESの仕様上"*"で可
        conditions: {
          StringEquals: {
            "ses:FromAddress": fromAddress, // 差出人を固定
          },
        },  
      })
    );
 
    this.function = { fn };
  }
}

Lambda関数

Lambda関数では、フォーム入力(name/email/title/message)を Lambda 実行時に検証し、不正リクエストを 400 で早期に弾くため、バリデーションにzodを使用します。
そのため、以下のコマンドでインストールします。

$ npm i zod

また、関数(メール送信処理)についてはコード量が多いのでここでは記載しませんが、大まかな処理については次のようになります。

  • API Gateway 経由の問い合わせAPI(POST)を受け、最小限のCORSと入力バリデーション(zod)を行い、SES でメールを2通送る。

具体的なコードは、こちらよりご覧ください。

API Gatewayの設定

最後に、API Gatewayの設定を行って実装は完了です。
処理内容とコードを以下に記載します。

  1. CloudWatch Logs(監査・トラブルシュート用)のロググループを、保持期間1ヶ月で作成。
  2. Regional タイプの REST API を作成する。(allowedOrigins があれば CORS のプリフライト(OPTIONS)を自動応答で有効化。)
  3. API Gateway が CloudWatch に書き込むための IAM ロールを作成し、API アカウント設定(Region 単位)にロール ARN を登録する。
  4. ステージ(CfnStage)がロール設定後に有効になるよう依存関係を張る。
  5. 入力 JSON の簡易スキーマ(name/email/title/message)Model を作成し、ボディのみ検証する RequestValidator によりリクエストボディの検証を行う。
  6. ルート直下に /api/contact を定義し、POSTLambda プロキシ統合でハンドラに接続する。
/lib/constructs/mail-api-construct/contact-api-construct.ts
import { Construct } from "constructs";
import * as logs from "aws-cdk-lib/aws-logs";
import * as apigw from "aws-cdk-lib/aws-apigateway";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
import { Stack } from "aws-cdk-lib";
import { ApiGatewayParams } from "@/parameters/mail-parameter";
 
export interface ContactApiConstructProps extends ApiGatewayParams {
  handler: lambda.IFunction;
}
 
export interface ContactApiConstructOutputs {
  restApi: apigw.RestApi;
  apiDomainForCf: string;
  stageName: string;
}
 
export class ContactApiConstruct extends Construct {
  public readonly outputs: ContactApiConstructOutputs;
 
  constructor(scope: Construct, id: string, props: ContactApiConstructProps) {
    super(scope, id);
 
    const stageName = props.stageName ?? "prod";
 
    // アクセスログ用 LogGroup
    const accessLogs = new logs.LogGroup(this, "ApiAccessLogs", {
      retention: logs.RetentionDays.ONE_MONTH, // ログ保持期間は既定で 1ヶ月
    });
 
    // REST API(Regional)
    const api = new apigw.RestApi(this, "MailApi", {
      restApiName: "MailApi",
      description: "Contact form API (API Gateway → Lambda → SES)",
      endpointConfiguration: { types: [apigw.EndpointType.REGIONAL] },      // エンドポイント種別:Regional(CF から接続)
      deployOptions: {
        stageName,                                                          // ステージ名("prod")
        accessLogDestination: new apigw.LogGroupLogDestination(accessLogs), // アクセスログの出力先
        accessLogFormat: apigw.AccessLogFormat.jsonWithStandardFields({
          caller: true,                                                     // 呼び出し元(認証情報など)
          httpMethod: true,                                                 // HTTP メソッド(POST 等)
          ip: true,                                                         // クライアント IP
          protocol: true,                                                   // プロトコル(HTTP/1.1 等)
          requestTime: true,                                                // リクエスト時刻
          resourcePath: true,                                               // リソースパス(/contact)
          responseLength: true,                                             // レスポンスサイズ
          status: true,                                                     // ステータスコード
          user: true,                                                       // ユーザー識別(あれば)
        }),
        loggingLevel: apigw.MethodLoggingLevel.INFO,                        // メソッドログの詳細度(INFO:適度)
        metricsEnabled: true,                                               // CloudWatch メトリクス有効化(可観測性)
        dataTraceEnabled: false,                                            // リクエスト/レスポンス本文の生記録は無効(個人情報保護)
        throttlingBurstLimit: props.throttle.burst,                         // バースト制限(瞬間的な同時呼び出し数の上限)
        throttlingRateLimit: props.throttle.rate,                           // 1秒あたりの平均レート上限
      },
      ...(props.allowedOrigins && props.allowedOrigins.length > 0           // CORS を設定したいときだけオプションを付与
        ? {
            defaultCorsPreflightOptions: {                                  // 自動で OPTIONS 応答(プリフライト)を有効化
              allowOrigins: props.allowedOrigins,                           // 許可するオリジン
              allowMethods: ["POST", "OPTIONS"],                            // 使うメソッドだけ許可
              allowHeaders: ["Content-Type", "X-Requested-With"],           // 必要なヘッダだけ許可
            },
          }
        : {}),                                                              // CloudFront リバースプロキシのみで同一オリジンなら CORS 省略可
    });
 
    // (A) API Gateway → CloudWatch Logs 出力用の IAM ロール
    //     - 信頼ポリシー: apigateway.amazonaws.com
    //     - 付与権限   : AmazonAPIGatewayPushToCloudWatchLogs(AWS管理ポリシー)
    const apiGwCloudWatchRole =
      (this.node.tryFindChild("ApiGwCloudWatchRole") as iam.Role) ??
      new iam.Role(this, "ApiGwCloudWatchRole", {
        assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
        managedPolicies: [
          iam.ManagedPolicy.fromAwsManagedPolicyName(
            "service-role/AmazonAPIGatewayPushToCloudWatchLogs"
          ),
        ],
      });
 
    // (B) アカウント設定(Region 単位で一意)にロール ARN を登録
    const apiGwAccount =
      (this.node.tryFindChild("ApiGwAccount") as apigw.CfnAccount) ??
      new apigw.CfnAccount(this, "ApiGwAccount", {
        cloudWatchRoleArn: apiGwCloudWatchRole.roleArn,
      });
 
    // (C) この設定が効いてから Stage を作るよう、Stage → Account に依存関係を張る
    const cfnStage = api.deploymentStage.node.defaultChild as apigw.CfnStage;
    cfnStage.addDependency(apiGwAccount);
 
    // 入力 JSON の簡易スキーマ(形式はLambda側で厳格確認)
    const requestModel = new apigw.Model(this, "ContactRequestModel", {
      restApi: api,
      contentType: "application/json",
      modelName: "ContactRequest",
      schema: {
        schema: apigw.JsonSchemaVersion.DRAFT4,                                          // スキーマバージョン
        title: "ContactRequest",                                                         // スキーマタイトル
        type: apigw.JsonSchemaType.OBJECT,                                               // オブジェクト形式の期待
        required: ["name", "email", "title", "message"],                                 // 必須4項目(フォーム仕様)
        properties: {
          name:    { type: apigw.JsonSchemaType.STRING, minLength: 1, maxLength: 100 },  // 名前:1〜100文字
          email:   { type: apigw.JsonSchemaType.STRING, minLength: 3, maxLength: 100 },  // メール:3〜100文字
          title:   { type: apigw.JsonSchemaType.STRING, minLength: 1, maxLength: 300 },  // タイトル:1〜300文字
          message: { type: apigw.JsonSchemaType.STRING, minLength: 1, maxLength: 5000 }, // 本文:1〜5000文字
        },
      },
    });
 
    // リクエストボディ検証器
    const bodyValidator = new apigw.RequestValidator(this, "BodyValidator", {
      restApi: api,
      validateRequestBody: true,                 // ボディのスキーマ検証をオン
      validateRequestParameters: false,          // クエリ/パスの検証は不要
      requestValidatorName: "BodyOnlyValidator",
    });
 
    // /contact(POST) → Lambda プロキシ統合
    const apiRoot = api.root.addResource("api");    // ルート直下に /api リソースを作成
    const contact = apiRoot.addResource("contact"); // /api/contact リソースを作成
    contact.addMethod(
      "POST",
      new apigw.LambdaIntegration(props.handler, { proxy: true }), // Lambda プロキシ統合(イベントそのまま渡す)
      {
        requestModels: { "application/json": requestModel },       // 上で定義したスキーマを適用
        requestValidator: bodyValidator,                           // 入力バリデーションを有効化
        apiKeyRequired: false,                                     // API キー不要(必要になれば後から有効化も可)
      }
    );
 
    // CloudFront のオリジンに使うドメイン名を生成
    const apiDomainForCf = `${api.restApiId}.execute-api.${Stack.of(this).region}.amazonaws.com`;
 
    this.outputs = { restApi: api, apiDomainForCf, stageName };
  }
}

アプリ側の実装

アプリ側も従来のnodemailerを用いたメール配信から、今回の構成に対応するように修正します。
fetchを使用し、Next.jsアプリからAPI Gateway経由でLambda関数を呼び出し、SESによるメール送信を実現します。
具体的なコードは以下にあります。

My_profile/my_profile/src/hooks/useContactForm.ts at main · miyazaki-dev01/My_profile

プロフィールサイト(React[Next.js]/TypeScript). Contribute to miyazaki-dev01/My_profile development by creating an account on GitHub.

github.com

プロフィールサイト(React[Next.js]/TypeScript). Contribute to miyazaki-dev01/My_profile development by creating an account on GitHub.

デプロイ

では前回と同様に、実装したメール配信機能もデプロイしていきたいと思います。

CDK プロジェクトでの対応

ライブラリのインストール

Lambda, SES を導入するにあたり、必要になる以下の3つのライブラリをインストールします。

  • @aws-sdk/client-sesv2:AWS SDK v3 の SESv2 クライアント。Lambda などから メール送信(SendEmail) を行うために使用。
  • @types/aws-lambda:TypeScript で Lambda を書くときの 型定義パッケージ。
  • esbuild:超高速のバンドラ/トランスパイラ。aws-lambda-nodejs(CDK)の NodejsFunction が内部で使用し、TS→JS 変換や最小バンドルを行います。
$ npm i @aws-sdk/client-sesv2
$ npm i -D @types/aws-lambda
$ npm i -D esbuild

テンプレートの生成

追加したmail-apiスタックに対して synthesize(合成)を行い、 AWS CloudFormation テンプレートを生成します。

$ cdk synth

差分を確認する

以下のコマンドにより、前回のデプロイからの差分を表示します。
追加したmail-apiスタックの内容が差分として表示されていれば問題ないです。

$ cdk diff MyProfileCdkStage/*

デプロイ

では、mail-apiスタックをデプロイしていきます。
以下のコマンドでは、以前に作成したスタックも同時にデプロイしていますが、特に変更はしていないため「宣言的アプローチ」によりリソースの再作成などは行われず、mail-apiスタックのみが新たにデプロイされる形になります。

$ cdk deploy MyProfileCdkStage/DnsStack \
             MyProfileCdkStage/EdgeCertStack \
             MyProfileCdkStage/MailApiStack \
             MyProfileCdkStage/WebsiteStack \
             --concurrency 1

これで静的サイトに加えて、メール配信のためのインフラも構築されました。

Next.js プロジェクトでの対応

Next.js プロジェクトの方でも修正を行ったので、以前と同様の手順でS3にファイルをアップロードします。
まず、以下のコマンドでビルドします。

$ npm run build

そして、./outディレクトリに出力されたファイルを、以下のコマンドで S3 にアップロードします。
BUCKET_NAMEには、作成したS3バケット名を入れます。

$ aws s3 sync ./out s3://BUCKET_NAME --delete

これで、メール配信を行う準備が整いました。

SESのサンドボックスを解除する

使用を始めたばかりのSESアカウントには、Eメールサービスの不正利用を防止する目的で、SESの各機能に対して一定の制限が適用されています。
具体的には、SESアカウントはサンドボックスという環境に配置されており、「認証済みのメールアドレスに対してのみメール送信ができる」、「24時間あたりで送信可能なメッセージ数の上限が200通」など、メール配信に関するさまざまな機能が制限されています。

本稼働アクセスのリクエスト (Amazon SES サンドボックスからの移行) - Amazon Simple Email Service

Amazon SES サンドボックスからアカウントを移行して本稼働アクセスのリクエストを行い、送信クォータを引き上げます。

docs.aws.amazon.com

no-image

この制限は、SESのコンソールからAWSに対して本番稼働アクセス申請をすることで解除できるので、最後にこれを行っていきます。

本番稼働アクセスの申請

まず、SESのコンソール画面を開きます。
左側に表示されるナビゲーションバーを開くと表示される「アカウントダッシュボード」の画面に移ります。
こちらの「Amazon SESアカウントは以下のサンドボックスにあります アジアパシフィック (東京)」の部分に「設定を始めるページを表示」のボタンがありますので、こちらを押下します。

画像3

すると、以下のセットアップ画面に遷移するので、「本稼働アクセスのリクエスト」を押下します。

画像4

本稼働アクセスのリクエスト画面では、各項目で設定値を入力します。
各項目の説明は以下の通りです。

項目内容
メールタイプマーケティング or トランザクション
webサイトurlメール配信を行うにあたって参考となるウェブサイトのURL
その他の連絡先申請に関するお知らせを受け取るメールアドレス
希望言語English or Japanese
画像5

これらの項目の入力ができましたら、「了解」のチェックを入れ、リクエスト画面下部の「リクエストの送信」ボタンを押下します。
SESの画面に「本番稼働リクエストを確認中です」の表示が出たらOKです。
「その他の連絡先」に追加したメールアドレスにも、正常にリクエストができた旨が送信されているかと思います。

ユースケースの送信

リクエストをすると以下のようなメッセージが送られてきます。
これに返信すると、正式にサンドボックス解除の申請が完了するので、具体的なユースケースについて記載して返信します。
返信は「AWS サポート」から行います。

画像6

申請が承認されると

サンドボックス解除の申請を行うと、AWSのサポートチームが24時間以内に最初のレスポンスを送信します。
今回の場合だと、申請をしてから22時間後くらいに以下のメールが届きました。
無事にSESのアカウントがサンドボックスから移動されたとのことです。

画像7

SESのコンソール画面を確認すると、AWSサポートからのメール通り、SESアカウントがサンドボックス環境から本番稼働環境に移動し、制限が解除されていることが確認できます。

画像8

動作確認

SESのサンドボックス解除申請も無事に承認されたので、実際にメールが正常に配信されるかテストしてみます。
アプリのお問い合わせフォームに以下の情報を入力して、送信します。

画像9

少し待つと、以下のメールが送られてきました。

▼ 入力者

画像10

▼ サイト運営者

画像11

無事にメール配信が行えていることを確認できました!

まとめ

今回は、前回に引き続きAWS CDK を使用して API Gateway + Lambda + SES を用いたメール配信(お問い合わせ)機能を構築しました。
資格勉強で名前だけ知っていたサービスを実際に使用してみて、解像度が上がったように感じます。
次回は、これまで作成してきた環境に対して CI/CD を構築していきますので、併せてご覧ください!

参考

【AWS SES, Lambda, API Gateway】設計編:サーバーレスでメール配信APIを作る

No description available.

zenn.dev

OGP Image

【AWS SES, Lambda, API Gateway】実装編:サーバーレスでメール配信APIを作る

No description available.

zenn.dev

OGP Image

Handling Contact Forms Submissions With a Custom REST API using AWS SES, API Gateway, and Lambda | Coner Murphy

Learn how to build a REST API to handle contact form submissions using AWS SES, Lambda, and API Gateway via the AWS CDK. And, how to test it using Postman.

conermurphy.com

OGP Image

Amazon SESの本番稼働アクセスを申請してみた | DevelopersIO

Amazon SESのアカウントには、最初はSESの各機能に対して一定の制限が適用されています。この制限は、SESのコンソールからAWSに対して本番稼働アクセス申請を行うことで解除できます。この手順をまとめる機会がありましたので、今回はそちらを紹介します。

dev.classmethod.jp

OGP Image
Share