Amazon SESのメール送信をLambda Powertools PythonとAPI Gatewayで実現
Amazon SESのメール送信方法を紹介します。
Webサイトの登録依頼画面でユーザーが送信ボタンを押すとメールをAmazon SESで送信する設定を行ったのでそのメモです。
AWS SES、Lambda等々でググると参考記事がたくさん出てきますが、 今回はAWS Lambda Powertools Pythonで実装して少しハマったので記事にしました。
ちなみにWebの登録画面は以下です。
https://antena.itsys-tech.com/register
参考サイト
- AWS Lambda Powertools Python 入門 第 1 回
- AWS Lambda Powertools Python 入門 第 5 回 EventHandler Utility - REST API 編
→第1回はライブラリの導入方法、第5回はAPIの利用方法が記載されている。
その他の回で紹介されているが、Lambda PowertoolsはAPIの他に便利な機能(Tracer, Logger, Metrics)を使えるようなのでどこかで紹介したい。
→Amazon SESの設定方法。管理コンソールから行うがAWSのWebサイトは頻繁に更新されるので参考程度に。
対応概要
ゴール
- 以下の流れで送信先に設定したアドレスにメールを送信する。
Webサイト登録依頼画面で送信ボタンクリック → API Gateway → Lambda → Amazon SES → メール送信
※フロントエンド側の実装については割愛
作業概要
- Amazon SESの利用設定
→メールの送信先など設定を行う - pythonコード実装
- AWS SAMでLambda等の必要なリソース作成
1. Amazon SESの利用設定
ここでやることは以下3つの設定。
- 本稼働アクセスのリクエスト(任意)
- IDの作成(ドメイン)
- IDの作成(Eメールアドレス)
この辺の設定はやりたいことに応じて設定するのが良いです。
詳しいことは以下のサイトで解説されてますが、 セキュリティ等の理由で、事前にIDの作成で登録した送信先にのみメールの送信は可能です。 登録したメール以外にも送りたい場合は「本稼働アクセスのリクエスト」を行う必要があります。 今回は必要なかったのですが今後利用する可能性があるため申請しました。
以下、申請・ID登録の流れの画面ショット。
※一度却下されたがサポートページで詳細を返信して通った
本稼働アクセスのリクエスト
・Amazon SESのページを開きアカウントダッシュボードの本稼働へのアクセスをクリックする
・必要事項を入力してリクエスト
・却下メールが届くorz(申請して1~2時間後)
・サポートページで詳細を記入し返信
・無事承認される(半日以上は待った)
IDの作成
【前提】
・ドメインは本サイトのドメイン利用
・Route53でドメイン登録済み→別のドメインサービスを利用している場合はそっちで設定が必要
・Amazon SESの管理画面 > 検証済みID > IDの作成
・ドメインを選択してドメイン情報を入力
・作成したドメインのID情報が表示される。
AWSのRoute53でドメイン取得している場合は自動的に画面下のDNSレコードが登録されている。
レコードを登録するとステータスが検証保留→検証済みになる。
※ドメインサービスをAWS以外で使用している場合はそっちで登録必須
・しばらくするとステータスが検証済みになる
・次にEメールアドレスのID作成。
認証メールが指定したアドレスに届くのでメール内のリンクをクリックして認証完了させる。
2. pythonコード実装
- AWS SAMでリソースは作成。
- LambdaはDocker利用、実行コードはpython。
Lambda
srcディレクトリ配下に以下のファイルを置く。
※AWS SAMテンプレートからディレクトリ名を変更して作成
- app.py: メイン処理実行
- ses.py: メール送信処理
- Dockerfile: Docker定義
- requirements.txt: 使用ライブラリ記述
app.py
以下のコードのようにLambda Powertoolsを使うとルーティングが楽で見やすくなります。
POSTで送るデータのbodyもapp.current_event.json_body
で簡単に参照できます。
テスト環境からこのAPIを実行する場合はCORS設定を行う必要があるので注意。
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, content_types
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from aws_lambda_powertools.event_handler.api_gateway import CORSConfig, Response
from ses import send_mail
# [INFO] テスト時など必要に応じてコメントアウトを外してCORS設定を行う
# cors_config = CORSConfig(allow_origin="*", max_age=300)
# app = APIGatewayRestResolver(cors=cors_config)
app = APIGatewayRestResolver()
def lambda_handler(event, context):
"""Lambda function"""
return app.resolve(event, context)
@app.post('/api/send/register')
def send_mail_register():
FROM = "環境に合わせて変更 例)xxxx@xxxx"
TO = "環境に合わせて変更 例)yyyy@yyyy"
SUBJECT = "環境に合わせて変更"
try:
body_dict = app.current_event.json_body
body_text = "\n".join(["{0}: {1}".format(key, value) for (key, value) in body_dict.items()])
return send_mail(FROM, TO, SUBJECT, body_text)
except TypeError as e:
return {
'isBase64Encoded': False,
'statusCode': 500,
'headers': {},
'body': '{"message": "' + str(e) + '"}'
}
@app.not_found()
def handle_not_found_errors(ex: NotFoundError) -> Response:
return Response(
status_code=501,
content_type=content_types.TEXT_PLAIN,
body="Not Implemented Error. Rooting is not found.",
)
ses.py
import boto3
from botocore.exceptions import ClientError
def send_mail(from_address, to_address, subject, body):
""" Amazon SES send mail
:param from_address:
:param to_address:
:param subject:
:param body:
:return: send mail result
"""
CHARSET = "UTF-8"
client = boto3.client('ses')
# Try to send the email.
try:
# Provide the contents of the email.
response = client.send_email(
Destination={
'ToAddresses': [
to_address,
],
},
Message={
'Body': {
'Text': {
'Charset': CHARSET,
'Data': body,
},
},
'Subject': {
'Charset': CHARSET,
'Data': subject,
},
},
Source=from_address,
)
except ClientError as e:
return {
'isBase64Encoded': False,
'statusCode': 500,
'headers': {},
'body': '{"message": "' + e.response['Error']['Message'] + '"}',
}
else:
return {
'isBase64Encoded': False,
'statusCode': 200,
'headers': {},
'body': '{"message": "Email sent! Message ID:' + response['MessageId'] + '"}',
}
requirements.txt
boto3
aws-lambda-powertools
3. SAM template.yaml
以下のコードでリソースを作成。 PyCharmでデプロイ。
Lambda Powertoolsの場合はAPI Gatewayはプロキシ統合にする必要があるらしい。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
python3.9
SAM Template for sendMailForAntena
Globals:
Function:
Timeout: 60
Environment:
Variables:
TZ: Asia/Tokyo
Api:
OpenApiVersion: 3.0.2
# [info] CORS設定が必要な場合は以下コメントアウトを外す
# Cors:
# AllowOrigin: "'*'"
# AllowMethods: "'GET,POST,DELETE,OPTIONS'"
# AllowHeaders: "'X-Requested-With, Origin, X-Csrftoken, Content-Type, Accept'"
Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: SendMailRoleForAntena
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
# [info] SESのポリシー(AmazonSESFullAccess)を付与する
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- arn:aws:iam::aws:policy/AmazonSESFullAccess
ApiGateway:
Type: AWS::Serverless::Api
Properties:
Name: SendMailApiForAntena
StageName: prd
LambdaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: SendMailFunctionForAntena
Role: !GetAtt LambdaRole.Arn
Timeout: 60
MemorySize: 512
PackageType: Image
Architectures:
- x86_64
Events:
SendMailApi:
Type: Api
Properties:
RestApiId: !Ref ApiGateway
# [info] Lambda Powertoolsの場合はプロキシ統合形式になるため注意
Path: /api/{proxy+}
Method: ANY
Metadata:
Dockerfile: Dockerfile
DockerContext: ./src
DockerTag: python3.9-v1
FunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref LambdaFunction
Principal: apigateway.amazonaws.com
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${LambdaFunction}
# [info] ログの保持期間
RetentionInDays: 7
Outputs:
ApiGateway:
Description: "API Gateway endpoint URL for Prod stage for SendMailFunctionForAntena"
Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/prd/api/send/"
LambdaFunction:
Description: "Lambda Function ARN"
Value: !GetAtt LambdaFunction.Arn
LambdaRole:
Description: "Implicit IAM Role created for function"
Value: !GetAtt LambdaRole.Arn
最後に
Lambda Powertoolsすごく便利そう。
Share this post
Twitter
Facebook
Email