構成
プライベートサブネット内で起動したlambda関数から、インターネットを経由せずSESを呼び出そうと思います。
本記事では以下の構成でlambda関数からSESを呼び出します。

本文中コード.
github.com
lambdaからSESにアクセスする手段
プライベートサブネット内のlambdaからSESにメールを送信する場合、2通りのインフラ構成が考えれられます。
参考: [アップデート] Amazon SES が SMTP エンドポイントの VPC エンドポイントをサポートしました | DevelopersIO
また、lambdaようなアプリケーションからSESにリクエストを送る場合、SES API か SMTPインターフェイスの2通りの方法があります。
Set up email sending with Amazon SES - Amazon Simple Email Service
インターネット経由でSESにアクセス(SES API / SMTPインターフェイス)
1つ目はNAT GatewayとInternet Gatewayを設置し、インターネット経由でSESを呼び出す方法です。
この方法ではSES APIとSMTPインターフェイス両方の方法でSESにアクセスすることができます。
今回はインターネット接続を行わない方法を検討するので、この構成は採用しません。

VPC Endpoints経由でSESにアクセス(SMTPインターフェイス)
VPC Endpoints経由でプライベートサブネットのクライアントから直接SESへアクセスできます。
注意点として、VPC Endpointsを利用したSESへのアクセスはSMTPインターフェイスのみサポートされています。
AWSサポートに問い合わせてくれている方がいます。 
VPC内のLambdaからはSES向けのVPCエンドポイントを使用してもBoto3ではメールを送信できない #Python - Qiita
インターネットを経由しないため、今回はこの構成を採用します。

VPC Endpoints経由でSTMPインターフェイスでメール送信する
Terraform
AWSリソースを作成するTerraformを用意しました
$ git clone https://github.com/nsakki55/lambda-ses-vpc-endpoints
$ cd lambda-ses-vpc-endpoints
AWSアカウントとSESで送るメールの値を環境変数に設定します。
$ export AWS_ACCOUNT_ID=<aws_account_id> $ export AWS_REGION=<aws_region> $ export DOMAIN=<domain> $ export FROM_ADDRESS=<from_address> $ export TO_ADDRESS=<to_address>
今回のインフラ構成をterraformで作成します。
$ cd terraform $ terraform init $ terraform apply -var="aws_account_id=$AWS_ACCOUNT_ID" -var="aws_region=$AWS_REGION" -var="domain=$DOMAIN" $ cd ..
lambda関数のDocker imageをビルドしECRにデプロイします
$ make build $ make push
SMTP認証情報の作成
SMTPインターフェイスでSESにアクセスする場合、SMTP認証情報を取得する必要があります。
AWSコンソールからSMTP認証ユーザーを作成する手順が公式ドキュメントで紹介されています。
Obtaining Amazon SES SMTP credentials - Amazon Simple Email Service.
IAMユーザーを作成し、SMTP認証に必要な2つの値をSecretManagerに保存しています。
- アクセスキー
- シークレットアクセスキーから作成したSMTPパスワード
シークレットアクセスキーとSMTPパスワードは異なる値ですので注意してください。
SMTP認証用のIAMユーザーに、SESでメールを送信するためのポリシーをアタッチしています。
resource "aws_iam_user" "smtp_user" { name = "smtp-user" } resource "aws_iam_access_key" "smtp_user_key" { user = aws_iam_user.smtp_user.name } resource "aws_secretsmanager_secret" "smtp_credential" { name = "smtp-credential" } resource "aws_secretsmanager_secret_version" "smtp_credentials" { secret_id = aws_secretsmanager_secret.smtp_credential.id secret_string = jsonencode({ username = aws_iam_access_key.smtp_user_key.id password = aws_iam_access_key.smtp_user_key.ses_smtp_password_v4 }) } resource "aws_iam_user_policy" "smtp_user_policy" { name = "smtp-user-policy" user = aws_iam_user.smtp_user.name policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "ses:SendEmail", "ses:SendRawEmail" ] Resource = "*" } ] }) }
SMTPインターフェイス用VPC Endpointsの作成
SMTPインターフェイスでSESにアクセスするための、VPC Endpoints、セキュリティグループを作成します。
com.amazonaws.ap-northeast-1.email-smtp がSES用のVPC Endpointsサービスです。
SMTPプロトコルで通信するために、587ポートのインバウンドルールをVPC Endpointsのセキュリティグループに設定します。
resource "aws_vpc" "main" { cidr_block = "10.1.0.0/16" enable_dns_support = true enable_dns_hostnames = true } resource "aws_subnet" "private" { vpc_id = aws_vpc.main.id availability_zone = "${var.aws_region}a" cidr_block = "10.1.10.0/24" } locals { aws_services = [ "com.amazonaws.${var.aws_region}.email-smtp", "com.amazonaws.${var.aws_region}.secretsmanager", ] } resource "aws_vpc_endpoint" "aws_services_interface_type" { for_each = toset(local.aws_services) vpc_id = aws_vpc.main.id service_name = each.value vpc_endpoint_type = "Interface" private_dns_enabled = true subnet_ids = [aws_subnet.private.id] security_group_ids = [aws_security_group.aws_services_interface_endpoints.id] tags = { Name = "Invoke SES via SMTP" } } resource "aws_security_group" "aws_services_interface_endpoints" { name = "aws-services-interface-endpoints-sg" vpc_id = aws_vpc.main.id } resource "aws_security_group" "invoke_ses" { name = "invoke-ses-sg" vpc_id = aws_vpc.main.id } resource "aws_vpc_security_group_egress_rule" "invoke_ses" { security_group_id = aws_security_group.invoke_ses.id cidr_ipv4 = "0.0.0.0/0" ip_protocol = "-1" from_port = -1 to_port = -1 } resource "aws_vpc_security_group_ingress_rule" "allows_access_to_interface_endpoints" { security_group_id = aws_security_group.aws_services_interface_endpoints.id referenced_security_group_id = aws_security_group.invoke_ses.id from_port = 443 to_port = 443 ip_protocol = "tcp" } resource "aws_vpc_security_group_ingress_rule" "allows_smtp_access_to_interface_endpoints" { security_group_id = aws_security_group.aws_services_interface_endpoints.id referenced_security_group_id = aws_security_group.invoke_ses.id from_port = 587 to_port = 587 ip_protocol = "tcp" }
SMTPインターフェイスでSESにアクセスするlambda関数の作成
SecretManagerからSMTP認証情報を取得し、SMTPインターフェイスでSESにアクセスする処理を記述したlambda関数です。
SMTPサーバーにはSESのVPC EndpointsのプライベートDNS名 email-smtp.ap-northeast-1.amazonaws.com を指定します。
import os import json import boto3 import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart def get_smtp_credential() -> tuple[str, str]: session = boto3.session.Session() client = session.client(service_name="secretsmanager", region_name="ap-northeast-1") response = client.get_secret_value(SecretId="smtp-credential") secret = json.loads(response["SecretString"]) smtp_username = secret["username"] smtp_password = secret["password"] return smtp_username, smtp_password def handler(event, context) -> None: # メールの作成 msg = MIMEMultipart() msg["From"] = os.environ['FROM_ADDRESS'] msg["To"] = os.environ['TO_ADDRESS'] msg["Subject"] = "test title" email_body = "This is test email via smtp." msg.attach(MIMEText(email_body, "plain")) # SMTPサーバーにメール送信 smtp_server = "email-smtp.ap-northeast-1.amazonaws.com" smtp_port = 587 smtp_username, smtp_password = get_smtp_credential() with smtplib.SMTP(smtp_server, smtp_port) as server: server.starttls() server.login(smtp_username, smtp_password) server.send_message(msg) return {"statusCode": 200, "body": json.dumps("Email sent successfully")}
動作確認
lambda関数をテスト実行すると、実行が成功します。

設定した送信元メールアドレスから、メールが届いてることを確認できます。
