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

これまでの内容は以下の記事に記載していますので、気になる方はぜひご覧ください。
【静的サイト編】プロフィールサイトAWS移行メモ 〜インフラ初心者を添えて〜
No description available.
miyazaki-profile.com

全体の構成
メール配信用の「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つのメール配信を行います。

フローの内容
- クライアントは「名前(name)」「メールアドレス(address)」「件名(title)」「本文(message)」を入力し、API Gatewayにリクエストを送信します。
- Cloud Frontのビヘイビアにより、API Gatewayへとルーティングされます。
- API GatewayはリクエストをLambda関数に転送し、リクエスト内容を検証します。
- Lambda関数はSESにメール送信リクエストを送信します。
- SESはサイト運営者とクライアントにメールを配信します。
- SESはAPI Gateway & Lambdaに送信結果(MessageId)を返します。
- 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
CloudFront ディストリビューションの編集
まずは、前回作成したCloudFront ディストリビューションを修正し、API Gatewayへのリクエストをルーティングします。
"api/*"
のビヘイビアを追加します。
...
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 一発で自動化しています。
具体的には、以下の処理を行っています。
- Route 53 のホストゾーンを参照。
- SES のドメインアイデンティティを作成。
- SPF を自動有効化。(SPFとは、メールの改ざんやなりすましを検知するための送信ドメイン認証技術のこと。)
- 独自 MAIL FROM ドメインを設定。
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

Lambdaの設定
続いて、LambdaのCDKを実装していきます。
内容としては、問い合わせメールを送る Lambda 関数(Node.js 20) を CDK で作成し、最小権限で SES 送信できるようにしています。
- 差出人メールアドレスの組み立て。(
noreply@example.com
) - CloudWatch Logs(監査・トラブルシュート用)のロググループを、保持期間1ヶ月で作成。
- Lambda 関数の作成。(送信処理の本体)
- SES送信に必要な最小権限を設定。
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の設定を行って実装は完了です。
処理内容とコードを以下に記載します。
- CloudWatch Logs(監査・トラブルシュート用)のロググループを、保持期間1ヶ月で作成。
- Regional タイプの REST API を作成する。(
allowedOrigins
があれば CORS のプリフライト(OPTIONS)を自動応答で有効化。) - API Gateway が CloudWatch に書き込むための IAM ロールを作成し、API アカウント設定(Region 単位)にロール ARN を登録する。
- ステージ(
CfnStage
)がロール設定後に有効になるよう依存関係を張る。 - 入力 JSON の簡易スキーマ(name/email/title/message)Model を作成し、ボディのみ検証する
RequestValidator
によりリクエストボディの検証を行う。 - ルート直下に
/api/contact
を定義し、POST
を Lambda プロキシ統合でハンドラに接続する。
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
デプロイ
では前回と同様に、実装したメール配信機能もデプロイしていきたいと思います。
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

この制限は、SESのコンソールからAWSに対して本番稼働アクセス申請をすることで解除できるので、最後にこれを行っていきます。
本番稼働アクセスの申請
まず、SESのコンソール画面を開きます。
左側に表示されるナビゲーションバーを開くと表示される「アカウントダッシュボード」の画面に移ります。
こちらの「Amazon SESアカウントは以下のサンドボックスにあります アジアパシフィック (東京)」の部分に「設定を始めるページを表示」のボタンがありますので、こちらを押下します。

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

本稼働アクセスのリクエスト画面では、各項目で設定値を入力します。
各項目の説明は以下の通りです。
項目 | 内容 |
---|---|
メールタイプ | マーケティング or トランザクション |
webサイトurl | メール配信を行うにあたって参考となるウェブサイトのURL |
その他の連絡先 | 申請に関するお知らせを受け取るメールアドレス |
希望言語 | English or Japanese |

これらの項目の入力ができましたら、「了解」のチェックを入れ、リクエスト画面下部の「リクエストの送信」ボタンを押下します。
SESの画面に「本番稼働リクエストを確認中です」の表示が出たらOKです。
「その他の連絡先」に追加したメールアドレスにも、正常にリクエストができた旨が送信されているかと思います。
ユースケースの送信
リクエストをすると以下のようなメッセージが送られてきます。
これに返信すると、正式にサンドボックス解除の申請が完了するので、具体的なユースケースについて記載して返信します。
返信は「AWS サポート」から行います。

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

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

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

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

▼ サイト運営者

無事にメール配信が行えていることを確認できました!
まとめ
今回は、前回に引き続きAWS CDK を使用して API Gateway + Lambda + SES を用いたメール配信(お問い合わせ)機能を構築しました。
資格勉強で名前だけ知っていたサービスを実際に使用してみて、解像度が上がったように感じます。
次回は、これまで作成してきた環境に対して CI/CD を構築していきますので、併せてご覧ください!
参考
【AWS SES, Lambda, API Gateway】設計編:サーバーレスでメール配信APIを作る
No description available.
zenn.dev

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

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

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