こんにちは、広野です。
本題だけ挨拶抜きで冒頭に話します。本記事では、Amazon API Gateway と Amazon SES を Lambda 抜きで 連携する方法を紹介します。
はじめに
先月、AWS Summit Japan 2024 が開催されました。その中で、印象に残った AWS さんのセッションがありました。
「サーバーレス開発のベストプラクティス~より効果的に、より賢く使いこなすために~」
このセッションでは、サーバーレス開発のアーキテクチャ事例を数多く紹介してくれて非常に有用だったのですが、特に印象に残ったのは
「AWS サービス間をプロキシするだけの Lambda 使用はやめましょう」
というようなメッセージでした。
常々、私はサーバーレス開発において、コードやランタイムのメンテが必要になる AWS Lambda 関数が乱立することを負担に感じていました。また、昨今のアーキテクチャ時流では AWS Step Functions が AWS サービスの API を直接叩けるようになったり、Amazon EventBridge を活用したサービス間連携アーキテクチャの台頭があったりして、「とりあえず Lambda」から「脱 Lambda」に変わってきています。
そういう意味で、このメッセージには非常に共感を持つことができました。セッションでは Amazon API Gateway から 他の AWS サービスの連携も Lambda 関数抜きで行うアーキテクチャを紹介しており、やってみたくなりました。
私が題材に選んだのは、Amazon API Gateway と Amazon SES の直接連携です。現在主流のアーキテクチャでは、メールを送信する AWS Lambda 関数がメールのコンテンツを加工し、Amazon SES の API を叩く役割を持っています。しかしながら、加工と言っても文字列をちょこっといじるだけのようなもので、何らビジネスロジックがないケースが多いのではないかと思います。であれば、AWS Lambda 関数はいらんだろう!ということでチャレンジしたくなりました。
やりたいこと
本記事で紹介するアーキテクチャは、以前 jQuery でメール送信 API を叩くアーキテクチャを紹介した記事をベースにしています。その記事ではメール送信 API を Amazon API Gateway + AWS Lambda + Amazon SES で作成していたので、そこから Lambda を抜く!部分にフォーカスして執筆いたします。
ベースとなるアーキテクチャ、技術情報は以下リンクをご覧下さい。
前回記事の構成から
- Amazon API Gateway REST API は Amazon SES との直接連携をサポートしている。統合タイプの設定は AWS を選択する。
- 公開 WEB サイトの問い合わせフォームで使用するため、ユーザ認証はない。CORS はある。
- CORS でハマるのが嫌なので、AWS CloudFormation でデプロイする。
- WEB サイトから送信するデータは以下の JSON フォーマットとなっている。
{
"subj": "件名",
"name": "名前",
"email": "xxx@xxxxxxx.com",
"body": "問い合わせメッセージ本文"
}
- 従来、このデータを AWS Lambda 関数にパススルーして、文字列加工したデータを Amazon SES に渡していた。AWS Lambda 関数がやっていたことは Amazon API Gateway REST API の「統合リクエスト」内、「マッピングテンプレート」で肩代わりさせる。
- マッピングテンプレートは VTL (Velocity Template Language) で定義する。(AWS AppSync では多用するので AppSync 信者にはお馴染み)
- Amazon SES の API は SendEmail を使用する。
マッピングテンプレートをどう書くか
AWS Lambda 関数でやっていたことを Amazon API Gateway のマッピングテンプレートに肩代わりさせるので、そこが肝になります。イメージしてもらいやすくするため、AWS Lambda 関数と対比して紹介します。
AWS Lambda 関数
細かい違いはあれど、だいたい以下のような AWS Lambda 関数 (Python) で Amazon SES を呼び出すと思います。
import json
import datetime
import boto3
def datetimeconverter(o):
if isinstance(o, datetime.datetime):
return str(o)
def lambda_handler(event, context):
try:
# Define Variables
ses = boto3.client('ses',region_name='ap-northeast-1')
d = json.loads(event['body'])
print({"ReceivedData": d})
RECEIVERS = []
RECEIVERS.append('xxxxx@xxxxxxx.xxxx')
SENDER = 'xxxxxx@xxxxx.xxxxxx'
# Send Email
res = ses.send_email(
Destination={ 'ToAddresses': RECEIVERS },
Message={
'Body': {
'Text': {
'Charset': 'UTF-8',
'Data': 'NAME: ' + d['name'] + '\nMAILADDRESS: ' + d['email'] + '\n\nMESSAGE:\n' + d['body']
}
},
'Subject': {
'Charset': 'UTF-8',
'Data': d['subj']
}
},
Source=SENDER,
ReplyToAddresses=[d['email']]
)
except Exception as e:
print(e)
return {
'isBase64Encoded': False,
'statusCode': 200,
'body': str(e)
}
else:
return {
'isBase64Encoded': False,
'statusCode': 200,
'body': json.dumps(res, indent=4, default=datetimeconverter)
}
マッピングテンプレート
マッピングテンプレートだと、以下のようになります。
## API Gateway が受け取った引数を $inputRoot に格納する
#set($inputRoot = $input.path('$'))
## メール本文の文字列加工 改行を入れたければここでリアルに改行すればよい(かなり違和感を感じるw)
#set($tempbodya = "NAME: $inputRoot.name
MAILADDRESS: $inputRoot.email
MESSAGE:
$inputRoot.body")
## Windows 環境からだと改行コードが CRLF で来ることがあるので、LF に置換しておく
#set($tempbodyb = $tempbodya.replaceAll("\r\n", "\n"))
## メール本文は URL エンコードすること
#set($body = $util.urlEncode($tempbodyb))
## メールアドレスは URL エンコードすること
#set($destination = $util.urlEncode("xxxxx@xxxxx.xxx"))
#set($replyto = $util.urlEncode($inputRoot.email))
#set($source = $util.urlEncode("xxxxx@xxxxxx.xxxx"))
## 件名はエンコードしなくてよい、なぜか。エンコードするとエラーになる。改行が入っても通った。謎。
#set($subject = $inputRoot.subj)
## SES API 呼出はこのフォーマットで書く URL の続きに?付きでパラメータ指定するのと同じ形式
Action=SendEmail&Destination.ToAddresses.member.1=$destination&Message.Body.Text.Data=$body&Message.Subject.Data=$subject&ReplyToAddresses.member.1=$replyto&Source=$source
AWS Lambda 関数では、URL エンコードなんて一切意識していなかったと思います。AWS が提供してくれる SDK がいい感じにデータを加工してくれていたものと思います。VTL は低級言語?なのか SDK をロードできないですし、組み込み関数も少ないです。ただ URL エンコードする関数 ($util.urlEncode) は用意してくれているので助かりました。
URL エンコードが必要な理由は、マッピングテンプレートから Amazon SES SendEmail API を呼び出すときに API の URL に続けてパラメータを書いて渡すためです。(想像)
URL には使用してはいけない文字列、記号があり、UTF-8 の文字コードの文字列を URL で使用できる文字列、記号の範囲内におさまるようにエンコードする必要があります。それが URL エンコードです。
例えば、メールアドレスの @ は使えませんし、改行、日本語なんてもってのほかです。@ は %40 に、改行(\n) は %3A に置き換えられます。
メールの To だけでなく、Cc、Bcc ももちろん設定可能です。これらは複数指定できますが、仕様でそれぞれ 50 個までしか指定できません。To を複数指定するときは、Destination.ToAddress.member.1= のような記述を1始まりで50まで羅列させます。配列が使用できないので、このような書き方になります。AWS Lambda 関数では配列で SDK に渡していたので意識しなかった部分です。
VTL ではダブルクォーテーションとシングルクォーテーションで動きが異なるので、このサンプルコードにあるクォーテーションのまま使用することをお勧めします。
ここまで動く状態に到達するのに、情報が少なくて結構苦労しました。
他にも Amazon API Gateway の設定はあるのですが、メインはマッピングテンプレートでしたので、AWS CloudFormation テンプレートのところで補足いたします。
実際の動作
WEB サイトの問い合わせフォームから、問い合わせを送信します。
こんなメールが予め設定しておいた送信先メールアドレスに届きます。
AWS CloudFormation テンプレート
前回記事で紹介した AWS CloudFormation を修正したものを貼り付けます。一式、Amazon CloudFront や Amazon S3 なども含まれています。一応執筆時点で動いたことは確認済みです。
AWSTemplateFormatVersion: 2010-09-09
Description: The CloudFormation template that creates a static website hosting environment with a backend API for receiving emails from feedback form.
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
SiteName:
Type: String
Description: Your Site name. (Not any upper case characters)
Default: xxxxx
MaxLength: 40
MinLength: 1
DomainName:
Type: String
Description: Domain name for URL.
Default: example.com
MaxLength: 100
MinLength: 5
AllowedPattern: "([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}"
SubDomainName:
Type: String
Description: Sub domain name for URL.
Default: xxxxx
MaxLength: 20
MinLength: 1
FeedbackSenderEmail:
Type: String
Description: E-mail address that sends feedback emails.
Default: sender@example.com
MaxLength: 100
MinLength: 5
AllowedPattern: "[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}"
FeedbackReceiverEmail:
Type: String
Description: E-mail address that receives feedback emails.
Default: receiver@example.com
MaxLength: 100
MinLength: 5
AllowedPattern: "[a-zA-Z0-9_+-]+(.[a-zA-Z0-9_+-]+)*@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\\.)+[a-zA-Z]{2,}"
CertificateId:
Type: String
Description: ACM certificate ID. CloudFront only supports ACM certificates in us-east-1 region.
Default: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
MaxLength: 36
MinLength: 36
Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub website-${SiteName}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Cost
Value: !Sub website-${SiteName}
S3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "s3:GetObject"
Effect: Allow
Resource: !Sub "arn:aws:s3:::${S3Bucket}/*"
Principal:
Service: cloudfront.amazonaws.com
Condition:
StringEquals:
AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
DependsOn:
- S3Bucket
- CloudFrontDistribution
# ------------------------------------------------------------#
# CloudFront
# ------------------------------------------------------------#
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Comment: !Sub CloudFront distribution for website-${SiteName}
Aliases:
- !Sub ${SubDomainName}.${DomainName}
HttpVersion: http2
IPV6Enabled: true
PriceClass: PriceClass_200
DefaultCacheBehavior:
TargetOriginId: !Sub S3Origin-website-${SiteName}
ViewerProtocolPolicy: redirect-to-https
AllowedMethods:
- GET
- HEAD
- OPTIONS
CachedMethods:
- GET
- HEAD
CachePolicyId: !Ref CloudFrontCachePolicy
OriginRequestPolicyId: !Ref CloudFrontOriginRequestPolicy
ResponseHeadersPolicyId: !Ref CloudFrontResponseHeadersPolicy
Compress: true
SmoothStreaming: false
DefaultRootObject: index.html
CustomErrorResponses:
- ErrorCachingMinTTL: 10
ErrorCode: 403
ResponseCode: 404
ResponsePagePath: /404.html
Origins:
- Id: !Sub S3Origin-website-${SiteName}
DomainName: !Sub ${S3Bucket}.s3.${AWS::Region}.amazonaws.com
OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
S3OriginConfig:
OriginAccessIdentity: ""
ConnectionAttempts: 3
ConnectionTimeout: 10
ViewerCertificate:
AcmCertificateArn: !Sub "arn:aws:acm:us-east-1:${AWS::AccountId}:certificate/${CertificateId}"
MinimumProtocolVersion: TLSv1.2_2021
SslSupportMethod: sni-only
Tags:
- Key: Cost
Value: !Sub website-${SiteName}
DependsOn:
- CloudFrontCachePolicy
- CloudFrontOriginRequestPolicy
- CloudFrontResponseHeadersPolicy
- CloudFrontOriginAccessControl
- S3Bucket
CloudFrontOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Description: !Sub CloudFront OAC for website-${SiteName}
Name: !Sub OriginAccessControl-website-${SiteName}
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
CloudFrontCachePolicy:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Name: !Sub CachePolicy-website-${SiteName}
Comment: !Sub CloudFront Cache Policy for website-${SiteName}
DefaultTTL: 3600
MaxTTL: 86400
MinTTL: 60
ParametersInCacheKeyAndForwardedToOrigin:
CookiesConfig:
CookieBehavior: none
EnableAcceptEncodingBrotli: true
EnableAcceptEncodingGzip: true
HeadersConfig:
HeaderBehavior: whitelist
Headers:
- Access-Control-Request-Headers
- Access-Control-Request-Method
- Origin
QueryStringsConfig:
QueryStringBehavior: none
CloudFrontOriginRequestPolicy:
Type: AWS::CloudFront::OriginRequestPolicy
Properties:
OriginRequestPolicyConfig:
Name: !Sub OriginRequestPolicy-website-${SiteName}
Comment: !Sub CloudFront Origin Request Policy for website-${SiteName}
CookiesConfig:
CookieBehavior: none
HeadersConfig:
HeaderBehavior: whitelist
Headers:
- Access-Control-Request-Headers
- Access-Control-Request-Method
- Origin
QueryStringsConfig:
QueryStringBehavior: none
CloudFrontResponseHeadersPolicy:
Type: AWS::CloudFront::ResponseHeadersPolicy
Properties:
ResponseHeadersPolicyConfig:
Name: !Sub ResponseHeadersPolicy-website-${SiteName}
Comment: !Sub CloudFront Response Headers Policy for website-${SiteName}
SecurityHeadersConfig:
ContentSecurityPolicy:
ContentSecurityPolicy: !Sub >-
default-src 'self' *.${DomainName};
img-src 'self' *.${DomainName} data: blob:;
style-src 'self' *.${DomainName} fonts.googleapis.com;
font-src 'self' *.${DomainName} fonts.gstatic.com;
script-src 'self' *.${DomainName} 'unsafe-inline';
script-src-elem 'self' *.${DomainName};
connect-src 'self' *.${DomainName} *.amazonaws.com;
Override: true
ContentTypeOptions:
Override: true
FrameOptions:
FrameOption: DENY
Override: true
ReferrerPolicy:
Override: true
ReferrerPolicy: strict-origin-when-cross-origin
StrictTransportSecurity:
AccessControlMaxAgeSec: 31536000
IncludeSubdomains: true
Override: true
Preload: true
XSSProtection:
ModeBlock: true
Override: true
Protection: true
CustomHeadersConfig:
Items:
- Header: Cache-Control
Value: no-store
Override: true
# ------------------------------------------------------------#
# Route 53
# ------------------------------------------------------------#
Route53RecordIpv4:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Sub ${DomainName}.
Name: !Sub ${SubDomainName}.${DomainName}.
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt CloudFrontDistribution.DomainName
DependsOn:
- CloudFrontDistribution
Route53RecordIpv6:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Sub ${DomainName}.
Name: !Sub ${SubDomainName}.${DomainName}.
Type: AAAA
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt CloudFrontDistribution.DomainName
DependsOn:
- CloudFrontDistribution
# ------------------------------------------------------------#
# API Gateway
# ------------------------------------------------------------#
RestApiSendEmail:
Type: AWS::ApiGateway::RestApi
Properties:
Name: !Sub SendEmail-website-${SiteName}
Description: !Sub REST API to send user's feedback email for website-${SiteName}
EndpointConfiguration:
Types:
- REGIONAL
Tags:
- Key: Cost
Value: !Sub website-${SiteName}
RestApiDeploymentSendEmail:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref RestApiSendEmail
DependsOn:
- RestApiMethodSendEmailPost
- RestApiMethodSendEmailOptions
RestApiStageSendEmail:
Type: AWS::ApiGateway::Stage
Properties:
StageName: prod
Description: production stage
RestApiId: !Ref RestApiSendEmail
DeploymentId: !Ref RestApiDeploymentSendEmail
MethodSettings:
- ResourcePath: "/*"
HttpMethod: "*"
LoggingLevel: INFO
DataTraceEnabled : true
TracingEnabled: false
Tags:
- Key: Cost
Value: !Sub website-${SiteName}
RestApiResourceSendEmail:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref RestApiSendEmail
ParentId: !GetAtt RestApiSendEmail.RootResourceId
PathPart: sendemail
RestApiMethodSendEmailPost:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref RestApiSendEmail
ResourceId: !Ref RestApiResourceSendEmail
HttpMethod: POST
AuthorizationType: NONE
Integration:
Type: AWS
IntegrationHttpMethod: POST
Credentials: !GetAtt ApigSesInvocationRole.Arn
Uri: !Sub arn:aws:apigateway:${AWS::Region}:email:action/SendEmail
PassthroughBehavior: WHEN_NO_TEMPLATES
RequestParameters:
integration.request.header.Content-Type: "'application/x-www-form-urlencoded'"
RequestTemplates:
application/json: !Sub
- |-
#set($inputRoot = $input.path('$'))
#set($tempbodya = "NAME: $inputRoot.name
MAILADDRESS: $inputRoot.email
MESSAGE:
$inputRoot.body")
#set($tempbodyb = $tempbodya.replaceAll("\r\n", "\n"))
#set($body = $util.urlEncode($tempbodyb))
#set($destination = $util.urlEncode("${FeedbackReceiverEmail}"))
#set($replyto = $util.urlEncode($inputRoot.email))
#set($subject = $inputRoot.subj+"(${SubDomainName}.${DomainName})")
#set($source = $util.urlEncode("${FeedbackSenderEmail}"))
Action=SendEmail&Destination.ToAddresses.member.1=$destination&Message.Body.Text.Data=$body&Message.Subject.Data=$subject&ReplyToAddresses.member.1=$replyto&Source=$source
- {}
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
- StatusCode: 400
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
- StatusCode: 403
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
- StatusCode: 404
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
- StatusCode: 500
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
- StatusCode: 503
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
MethodResponses:
- StatusCode: 200
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
- StatusCode: 400
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
- StatusCode: 403
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
- StatusCode: 404
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
- StatusCode: 500
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
- StatusCode: 503
ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Origin: true
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
RestApiRequestModelSendEmail:
Type: AWS::ApiGateway::Model
Properties:
ContentType: application/json
RestApiId: !Ref RestApiSendEmail
Schema: |-
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "SendEmail",
"type": "object",
"properties": {
"subj": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"body": {
"type": "string"
}
},
"required": ["subj", "name", "email", "body"]
}
RestApiMethodSendEmailOptions:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref RestApiSendEmail
ResourceId: !Ref RestApiResourceSendEmail
HttpMethod: OPTIONS
AuthorizationType: NONE
Integration:
Type: MOCK
Credentials: !GetAtt ApigSesInvocationRole.Arn
IntegrationResponses:
- ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,Cache-Control'"
method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: !Sub "'https://${SubDomainName}.${DomainName}'"
ResponseTemplates:
application/json: ''
StatusCode: 200
PassthroughBehavior: WHEN_NO_MATCH
RequestTemplates:
application/json: '{"statusCode": 200}'
MethodResponses:
- ResponseModels:
application/json: Empty
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: true
method.response.header.Access-Control-Allow-Methods: true
method.response.header.Access-Control-Allow-Origin: true
StatusCode: 200
# ------------------------------------------------------------#
# API Gateway SES Invocation Role (IAM)
# ------------------------------------------------------------#
ApigSesInvocationRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ApigSesInvocationRole-website-${SiteName}
Description: This role allows API Gateways to invoke SES.
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action:
- sts:AssumeRole
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
Policies:
- PolicyName: !Sub ApigSesInvocationPolicy-website-${SiteName}
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- "ses:SendEmail"
Resource:
- "*"
# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
APIGatewayEndpointSendEmail:
Value: !Sub https://${RestApiSendEmail}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${RestApiStageSendEmail}/sendemail
SiteURL:
Value: !Sub https://${SubDomainName}.${DomainName}
Amazon API Gateway を CORS 対応にしているので、レスポンスのところで CORS 関連の設定が多くなっています。ヘッダー関連の設定はあまりいじらない方がよいと思います。
統合リクエストの設定で、HTTP ヘッダーに Content-Type: ‘application/x-www-form-urlencoded’ を設定しています。こうしないと動きませんでした。
このテンプレートが動作する前提はいろいろあるのですが (すいません) 前回記事から追加していることだけ。Amazon API Gateway のログを Amazon CloudWatch Logs に push するために必要な IAM ロールがアカウントに登録済みでないとエラーになります。それについては以下の記事をご確認ください。
まとめ
いかがでしたでしょうか?
正直に申しますと、意気込んでやってはみたものの、AWS Lambda 関数抜きの Amazon SES SendEmail API 呼出は絶対に流行らないと確信しました。VTL の使い勝手が Python に比べて悪すぎるのと、SendEmail 呼出だと文字列のエンコードというデリケートな部分があるためです。気を遣わなくて済む AWS Lambda の方が楽です。でも、せっかく作ったので私はこの方法を使おうと思います。他人にはお勧めしませんが。w
ですがこの例は、Amazon API Gateway から Amazon SES でない、他の AWS サービスへの直接連携の参考になるかもしれません。
本記事が皆様のお役に立てれば幸いです。
最後に補足です。Amazon SES からメール送信するときは、Gmail と Yahoo Mail が実装した送信者要件を満たさないとそれらにはメールが届かない恐れがあります。Amazon SES 側で必要な設定があるので、要注意です。私も実装しました。