こんにちは、広野です。
本題だけ挨拶抜きで冒頭に話します。本記事では、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 側で必要な設定があるので、要注意です。私も実装しました。