ごきげんよう、@An_nAです!
この記事はハンズラボ Advent Calendar 2022 22日目の記事です。
前回の記事では、SESでのメール受信について概要をご紹介しました。
(まだ読んでないよ〜という方はぜひご一読ください)
今回の記事では、Serverless Frameworkを用いて実装をしていきますよ!
前提
この記事では扱わないこと
-
SESのバウンス対策
本記事では取り扱いませんが、SESを使用してメールを送信する場合はバウンス率の監視やログの保管を行いましょう。
参考:
どのようなものが Amazon SES でソフトバウンスとみなされますか? また、ソフトバウンスを監視するにはどうすればよいですか? -
今回の内容に特化した部分以外の説明は割愛しています。ゴメンナサイ
環境
- macOS Monterey
- Framework Core: 3.25.1
Plugin: 6.2.2
SDK: 4.3.2 - serverless-python-requirements インストール済み
- Route 53にドメイン登録済み&ホストゾーン作成済みのAWSアカウントを利用
(Route 53以外のDNSサービスをご利用の方は適宜読み替えてください。)
構成図
※メールデータの保存用DynamoDBやメール送信用SESは他の機能(本記事のスコープ外なのであえて描いていません)でも使用したいので、バージニア北部リージョンではなく東京リージョンに置きます。
ディレクトリ構成(必要部分だけ抜粋)
.
├── functions
│ ├── send_url.py
│ └── template
│ └── send_url.html
├── resource
│ ├── dynamodb.yml
│ ├── ses.yml
│ └── ses_us-east-1.yml
├── Pipfile
├── serverless.yml
└── serverless_us-east-1.yml
Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
jinja2 = "*"
boto3 = "*"
[dev-packages]
pytest = "*"
flake8 = "*"
autopep8 = "*"
[requires]
python_version = "3.9"
では、実装していきますよーーー!!!
東京リージョンのリソース作成
まずは東京リージョンのリソースを作っちゃいましょう。
デプロイ用ファイルを作成する
Serverless Frameworkでリソースを作成するために、必要なファイルを書いていきます。
DynamoDB用ファイル
uuidとメールアドレスを保存しておくDynamoDBを作成するためのファイルです。
メールアドレスをずっと保管しておくのは躊躇われたので、有効期限を設定しました。
Resources:
AdventCalendar2022EmailAddressTable:
# テーブル定義 --------------------------------------------------------------------
# uuid | String | uuid
# mail_address | String | メールアドレス
# expired_at_unix_time | Number | 有効期限(Unix エポック時間形式のタイムスタンプ・秒単位)
# -------------------------------------------------------------------------------
Type: AWS::DynamoDB::Table
Properties:
KeySchema:
-
AttributeName: uuid
KeyType: HASH
AttributeDefinitions:
-
AttributeName: uuid
AttributeType: 'S'
BillingMode: PAY_PER_REQUEST
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
SSESpecification:
SSEEnabled: true
SSEType: KMS
TableName: AdventCalendar2022EmailAddressTable
TimeToLiveSpecification:
AttributeName: expired_at_unix_time
Enabled: true
SES用ファイル
このファイルでは、メール送信に利用するドメインIDを作成します。
EmailIdentity
部分が利用するドメインです。
Resources:
# ドメインID
EmailIdentity:
Type: AWS::SES::EmailIdentity
Properties:
DkimAttributes:
SigningEnabled: true
EmailIdentity: advent.example.com
serverless.yml
言わずと知れたserverless.ymlファイル。
東京リージョン用なので、provider.region
は「ap-northeast-1」です。
service: advent2022
frameworkVersion: ">=3.18.2 <4.0.0"
plugins:
- serverless-python-requirements
custom:
defaultStage: dev
pythonRequirements:
usePipenv: true
layer: true
dockerizePip: true
dockerImage: public.ecr.aws/sam/build-python3.9:latest
provider:
name: aws
runtime: python3.9
region: ap-northeast-1
profile: ${opt:profile, ''}
stage: ${opt:stage, self:custom.defaultStage}
environment:
TZ: Asia/Tokyo
REGION: ${self:provider.region}
STAGE: ${self:provider.stage}
package:
patterns:
- '!node_modules/**'
- '!.vscode/**'
- '!.venv/**'
- '!.flake8'
- '!.gitignore'
- '!Pipfile'
- '!Pipfile.lock'
- '!README.md'
- '!package.json'
- '!package-lock.json'
functions:
resources:
- ${file(./resource/ses.yml)}
- ${file(./resource/dynamodb.yml)}
デプロイする
ファイルが作成できたら、さくっとデプロイしちゃいましょう!
$ sls deploy --stage dev --config serverless.yml
SESのドメインIDの認証
無事にリソースがデプロイされてSESのIDが作成できたら、ドメインの検証を行います。
今回は、Route 53のホストゾーンにCNAMEを登録しました。
ドメインの検証手順は以下の通りです。
SESをサンドボックスの外に出す(の代わりの作業を実施)
SESでメールを送る場合は、「サンドボックスの外に出す」という作業が必要です。
が、このサンドボックス外に出す作業が少し手間なので、今回は割愛します。
代わりに自分のメールアドレスを検証済みEメールアドレスにしておき、メールの送信先に利用できるようにしました。のちほど動作確認を行う際は、ここで登録したメールアドレスから空メールを送ります。
-
入力したEメールアドレス宛にAWSからメールが来るので、記載されたURLにアクセスする。
-
「検証に成功しました」と表示されればOK。
東京リージョンでの作業は以上です!
バージニア北部リージョンのリソース作成
次にバージニア北部リージョンのリソースを作成していきますよ〜。
デプロイ用ファイルを作成する
Serverless Frameworkでリソースを作成するために、必要なファイルを書いていきます。
SES用ファイル
メール受信用のSES関連リソースです。東京リージョン用とは別ファイルにします。
このファイルで作成するのは以下の4つ。
-
ドメインID
東京リージョンと構文は同じ。分かりやすいようサブドメインを変えています。 -
受信ルール
メール受信時にどのような動作をするかと、どのルールセットに含めるかを設定します。
複数のルールを作成して受信ルールセットにひとまとめにすることも可能です。
今回は、以下のルールを設定しました。
・register@advent-us-east-1.example.com
宛のメールを受信したら
・ arnがSendUrlLambdaFunction.Arn
なLambda関数を呼び出す -
受信ルールセット
ルールセット名を定義するだけ。 -
Lambdaを呼び出すための権限
Lambda呼び出しに必要な権限を付与します。
Resources:
# ドメインID
ReceivingEmailIdentity:
Type: AWS::SES::EmailIdentity
Properties:
DkimAttributes:
SigningEnabled: true
EmailIdentity: advent-us-east-1.example.com
# 受信ルールセット
ReceiptRuleSet:
Type: AWS::SES::ReceiptRuleSet
Properties:
RuleSetName: AdventCalendar2022ReceiptRuleSet
# 受信ルール
ReceiptRule:
Type: AWS::SES::ReceiptRule
DependsOn: SendUrlLambdaPermission
Properties:
Rule:
Actions:
- LambdaAction:
FunctionArn: !GetAtt SendUrlLambdaFunction.Arn
InvocationType: Event
Enabled: true
Name: AdventCalendar2022ReceiptRule
Recipients:
- register@advent-us-east-1.example.com
ScanEnabled: true
RuleSetName: !Ref ReceiptRuleSet
# Lambdaを呼び出すための権限を付与
SendUrlLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt SendUrlLambdaFunction.Arn
Action: lambda:InvokeFunction
Principal: ses.amazonaws.com
SourceArn: arn:aws:ses:${aws:region}:${aws:accountId}:receipt-rule-set/*
Lambda関数
受信ルールセットが呼び出すLambda関数と、関数内で使用するメール用のテンプレートです。
- 東京リージョンのDynamoDBに送信元のアドレスとUUID、有効期限を保存
- 東京リージョンのSESで送信元のアドレスにメールを送信
という流れにします。
メール用のテンプレートはこんな感じにしました。
必要最低限でまったくかわいくないので、実際に使う場合はデコってあげてください。
<p>
登録用URLは以下の通りです。
</p>
<p>
{{ web_url }}
</p>
Lambda関数はこんな感じ。pythonで書きました。
ログ出力や例外処理は書いていないので、適宜追加してください。
import boto3
import uuid
import datetime
from jinja2 import Environment, FileSystemLoader
ses = boto3.client('ses', region_name='ap-northeast-1')
dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1')
env = Environment(loader=FileSystemLoader(
'./functions/template/', encoding='utf8'))
def handler(event, context):
# 送信元(返信先)アドレスを抽出
to_address = event['Records'][0]['ses']['mail']['source']
# uuidを発行
temporary_uuid = str(uuid.uuid4())
# 有効期限を算出(現在時刻から10分後)
expired_at = datetime.datetime.now() + datetime.timedelta(minutes=10)
expired_at_unix_time = int(expired_at.timestamp())
# DynamoDBに書き込み
table_name = 'AdventCalendar2022EmailAddressTable'
item = {
'uuid': temporary_uuid,
'mail_address': to_address,
'expired_at_unix_time': expired_at_unix_time,
}
table = dynamodb.Table(table_name)
table.put_item(Item=item)
# メール送信準備
subject = '【AdventCalendar2022】登録URLのご案内'
web_url = f'https://xxx/input?code={temporary_uuid}'
dataset = {
'web_url': web_url,
}
html = env.get_template('send_url.html')
email_message = html.render(dataset)
# 送信先にメールを返信
response = ses.send_email(
Source='info@advent.example.com',
Destination={
'ToAddresses': [to_address],
'CcAddresses': [],
'BccAddresses': [],
},
Message={
'Subject': {
'Data': subject,
'Charset': 'UTF-8',
},
'Body': {
'Text': {'Data': email_message, 'Charset': 'UTF-8'},
'Html': {'Data': email_message, 'Charset': 'UTF-8'}
}
},
)
return response
このLambda関数のポイントはなんと言ってもココです。
ses = boto3.client('ses', region_name='ap-northeast-1')
dynamodb = boto3.resource('dynamodb', region_name='ap-northeast-1')
region_name='ap-northeast-1'
を指定することで、バージニア北部リージョンのLambdaから東京リージョンのSESやDynamoDBを操作することが可能なんですね〜。意外に簡単。
serverless_us-east-1.yml
言わずと知れたserverless.ymlファイル。
こちらのファイルはバージニア北部リージョン用なので、provider.region
を「us-east-1」にします。
iam
部でLambdaに適切な権限を付与するのも忘れずに!
「ses:SendEmail」と「dynamodb:PutItem」のResource
のリージョン部分が「ap-northeast-1」になっている点に要注目です。
service: advent2022
frameworkVersion: ">=3.18.2 <4.0.0"
plugins:
- serverless-python-requirements
custom:
defaultStage: dev
pythonRequirements:
usePipenv: true
layer: true
dockerizePip: true
dockerImage: public.ecr.aws/sam/build-python3.9:latest
provider:
name: aws
runtime: python3.9
region: us-east-1
profile: ${opt:profile, ''}
stage: ${opt:stage, self:custom.defaultStage}
environment:
TZ: Asia/Tokyo
REGION: ${self:provider.region}
STAGE: ${self:provider.stage}
iam:
role:
statements:
- Effect: Allow
Action:
- ses:SendEmail
Resource:
- arn:aws:ses:ap-northeast-1:${aws:accountId}:identity/*
- Effect: Allow
Action:
- dynamodb:PutItem
Resource:
- arn:aws:dynamodb:ap-northeast-1:${aws:accountId}:table/*
- Effect: Allow
Action:
- logs:DescribeLogGroups
- logs:DescribeSubscriptionFilters
- logs:PutSubscriptionFilter
Resource:
- arn:aws:logs:${aws:region}:${aws:accountId}:log-group:*
logRetentionInDays: 14
versionFunctions: false
package:
patterns:
- '!node_modules/**'
- '!.vscode/**'
- '!.venv/**'
- '!.flake8'
- '!.gitignore'
- '!Pipfile'
- '!Pipfile.lock'
- '!README.md'
- '!package.json'
- '!package-lock.json'
functions:
SendUrl:
handler: functions/send_url.handler
layers:
- !Ref PythonRequirementsLambdaLayer
resources:
- ${file(./resource/ses_us-east-1.yml)}
デプロイする
こちらもさくっとデプロイしちゃいます。
$ sls deploy --stage dev --config serverless_us-east-1.yml
SESのドメインIDの認証
バージニア北部リージョンのリソースがデプロイできたら、東京リージョンのときと同じくSESのドメインIDの検証をしてください。
サンドボックスの外には出さない
今回の構成ではバージニア北部リージョンのSESからはメールの送信を行わないので、サンドボックスの外に出す作業や送信先メールアドレスの検証作業は不要です。
MXレコードの登録
メール受信のため、DNSサーバーにMXレコードを登録します。
Route 53の場合は、以下の通り登録すればOKです。
設定箇所 | 登録内容 |
---|---|
レコード名 | SESに登録した受信用ドメイン (今回の例ならadvent-us-east-1.example.com) |
レコードタイプ | MX |
値 | 10 inbound-smtp.us-east-1.amazonaws.com |
参考:
Amazon SES による E メール受信のための MX レコードの公開
受信ルールセットをActiveにする
デプロイしただけでは受信ルールセットが有効化されないため、コンソールで有効化します。
受信ルールの確認
ルールセットの画面から以下の画像の通り順にクリックしていくことで受信ルールを確認できます。
受信者の条件
アクション
空メールを送ってみよう
さて、ここまでできたらリソースの準備はバッチリです。動作確認をしてみましょう!
PCのメーラーを起動して、空メールを送ります。
設定箇所 | 設定内容 |
---|---|
送信元 | 東京リージョンのSESに登録した「検証済みEメールアドレス」 |
送信先 | 受信ルールセットの「受信者の条件」に設定したアドレス (今回の例だと register@advent-us-east-1.example.com ) |
件名 | 不要 |
本文 | 不要 |
その後、受信ボックスに登録用URLが記載されたメールが届けば大成功です!
届きました!
(知っていたけど文面がシンプルすぎる...。)
伏字にしているので分かりづらいですが、東京リージョンのSESに設定したドメインからメールが届いています。
東京リージョンのDynamoDBにきちんとデータが保存されているか、様子を見てみましょう。
アイテムが保存できていました!
こちらも大成功です。
おわりに
ということで、空メールを送ったら登録用メールが返ってくるアレを無事実装できました。
どなたかの参考になれば幸いです。
明日、12/23の担当は@umiwatariさんです。
どうぞお楽しみに!