こんにちは、普段PR TIMES STORY(以下STORY)の開発リーダーをしている岩下(@iwashi623)です。
今回はSQS とLambdaを使って、AWS Fargate上で動作しているLaravelからメールを送信する基盤を作成したことについて書いていきます。
なぜ作るのか
2/21にPR TIMES Webクリッピングの正式版がリリースされました。

正式版をリリースするにあたって、ユーザーにメールを送信したいという要件が生じました。PR TIMES WebクリッピングはFargate上にデプロイされていて、アプリケーションのFWはLaravelを使用しています。
当初はセキュリティグループの25番ポートを開放して、アプリケーションから直接メールを送信しようとしていたのですが、Fargateの仕様を調べていくと、AWSのドキュメントには25番ポートについての記載がありません。また、メールを送信したいアプリケーションがPR TIMES Webクリッピング以外にも今後発生してくる可能性もありました。
なので、PR TIMES Webクリッピングにメールを送信する機能を新規で追加する形ではなく、メールを送信する責務だけを持つ別アプリケーションを作成して、その別アプリケーションを使って今回の要件を満たそうと思いました。
アプリケーションの設計
今回作成したアプリケーションのインフラ設計は以下のようになっています。

アプリケーションの使い方は、
- Fagate上の各アプリケーションから、メールの情報(宛先・本文・etc..)の情報を入れたメッセージをSQS defaultへエンキューする。
- LambdaはSQS defaultからEventを取得して、キューが入っているときにアプリケーションを実行する。
- アプリケーションの中では、とってきたSQSのメッセージ情報をもとにSendGridのメール送信用Web APIを叩く。
- 各ユーザーにメールが届く。
というのが基本の流れです。
また、SendGridのWebAPIを使用するためには、アカウントごとに作成できるAPI Keyが必要なのですが、こちらはSystem Manager Prameter Storeに暗号化して保存されています。
アプリケーションのコードは以下のような感じです。
自分で作っておいてなんですが、Lambdaのなかでは特に難しいことはしていないことがわかると思います。
今回はエンキューする段階でSendGridのWeb API実行時にそのまま使えるJSONをメッセージに加えているので、メール送信側のLambdaはメッセージの内容をリクエストボディに入れてAPIを叩いているだけです。
Prameter StoreのKeyをとってくるときに、少しだけ手こずりましたが、結果以下のDocumentとほぼ同じ方法で落ち着きました。
開発Tips
SendGridとは
SendGridは全世界で利用されているメール配信サービスです。
クラウドサービスのためアカウントを作成するだけで即日メールを送信でき、面倒でコストのかかるメールサーバの構築は不要です。
https://sendgrid.kke.co.jp/about/
メールを送信するためのクラウドサービスで、Web APIやSMTP APIを使って簡単にメールを送信することができます。Web APIを使ってメールを送信する際は、以下のようなJSON形式のメッセージをリクエストボディに付与します。
{ "personalizations": [
{
"to": [
{
"email": "recipient_address@example.com"
}
],
"subject": "こんにちは!"
}
],
"from": {
"email": "from_address@example.com"
},
"content": [
{
"type": "text/plain",
"value": "テキストメールです!"
}
]
}
また、Domain Authenticationの設定をすることで、独自ドメインからのメールの送信の際も送信ドメイン認証の設定が完了します。
ハードバウンスのメールアドレスへの配信停止などもSendGrid側で行ってくれるので、メーリングリストの使われていないメールアドレスへメールを送信し続けることによるレピュテーションの低下なども心配しなくて良くなります。
AWSリソースの設定について
SQS
SQSの設定値として注意したいのは、可視性タイムアウトの値です。
可視性タイムアウトはAmazon SQSがメッセージを返した時点で開始します。タイムアウト時間内に、コンシューマーはメッセージを処理して削除します。ただし、コンシューマーでメッセージを削除する前に障害が発生して、
[DeleteMessage])
アクションが呼び出されないまま可視性タイムアウトの期限が切れると、そのメッセージは他のコンシューマーに見えるようになり、再度受信されます。メッセージを一度だけ受信する必要がある場合、コンシューマーは可視性タイムアウトの時間内にメッセージを削除する必要があります。すべてのAmazon SQSキューの可視性タイムアウトはデフォルトで 30秒に設定されています。この設定はキュー全体で変更できます。通常、可視性タイムアウトは、アプリケーションがキューのメッセージを処理して削除するまでの最大所要時間に設定します。メッセージを受信したら、キュー全体のタイムアウトを変更しなくても、返されるメッセージに特別な可視性タイムアウトを設定することもできます。詳細については、「タイムリーな方法でのメッセージの処理」セクションのベストプラクティスを参照してください。
メッセージの処理にかかる時間がわからない場合は、ハートビートあなたの消費者プロセスのために:初期可視性タイムアウト (2 分など) を特定し、コンシューマがメッセージで作業している限り、可視性タイムアウトを 1 分ごとに 2 分延長します。
https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html
平たく言うと、「同じメッセージを複数のコンシューマーが処理しないように設定する値」です。
こちらの適切な値はコンシューマー側のワークロードにより異なるのですが、今回のアプリケーションではデフォルトの30秒のままにしています。設定値を0にすると複数のLambdaが起動しているときに同じメールを複数送信してまう可能性がありますし、長過ぎると何らかの原因でLambdaが途中で失敗したときにリトライの実行開始が遅くなります。
Lambda
Lambdaの設定値として注意したいのは、バッチサイズと同時実行数です。
バッチサイズは1実行あたりに読み込める最大のSQSメッセージ数です。デフォルトが10で最大値は標準キューは10000、FiFOキューは10です。今回のアプリケーションでは1を設定しています。理由はメールの送信成功・失敗の扱いを個別に行いたいからです。例えば1実行の中で送信に成功したメールと失敗したメールが混ざっていたとき、関数全体としては失敗扱いになります(失敗したメールがあった時は異常終了させないと、LambdaからDeleteMessage
が走ってメッセージがSQSに残らないため)。その際、同時に実行された成功したメッセージまで失敗したことになってSQSに残ることになります。結果として、リトライしたときに最初の実行で成功したメールも含めて送信することになってしまいます。
こちらは今回のケースだと都合が悪いため、Lambdaの負荷や料金が気になり始めるまではバッチサイズは1にしておくことにしました。
同時実行数はそのままで、同時に実行されるLambdaの数です。前述した可視性タイムアウトを設定してあれば、複数のLambdaが同時に起動しても問題ないため、任意の数を指定すれば良いと思います。今回のアプリケーションでは空白にして同時実行されないようになっていますが、こちらも負荷が気になり始めたら引き上げる予定です。
注意点としては、何も申請をしていないと1アカウントの1リージョンあたりのLambdaの最大実効数は1000となっていることです。
こちらは全てのLambda含めての数となりますので、他に重要なワークロードを担っているLambdaがある場合、そちらがスケールできなくなる恐れがあります。なので、制限の引き上げ申請や同時実行数の調整を行うことをおすすめします。
エラー・異常系の処理について
今回のアプリケーションでは、エラーやエラー通知に関しては以下のように扱っています。
- Lambda上で何かしらのエラーがでて、関数が異常終了した場合、SQSにキューが残る。
- Lambdaアプリケーションでエラーが出て場合は、Lambdaの「Errors」メトリクスをもとにCloudWatch Alermが発火する。
- 5回を超えて関数が異常終了するキューに関しては、「SQS deadletter」に移動する。
- Deadletterにキューが入った場合、SQSの「ApproximateNumberOfMessagesVisible」メトリクスをもとにCloudWatch Alermが発火する。
- Lambdaのスロットリング発生時やメモリ使用率80%超えの際にもCloudWatch Alermが発火する。
AWSのコンソール上エラーが出るだけでは誰も検知できないので、SNSとChatbotを使って任意のSlackチャンネルに通知を飛ばせるようにしました。ただ、こちらの組み合わせではメッセージがSlackに送られてくるだけで、メンションが付きません。

この問題は、CloudWatchで発火するトピックをサブスクライブするLambdaを作って、そのLambda内で
- メッセージにメンションを付与した新SNSトピックをPublishする
- 直接Slack APIを叩いて送信する
などで解消できそうなので、随時改修予定です。
また、現状のLambdaアプリケーションは決まったフォーマットのメッセージをそのままリクエストボディに入れてSendGridのAPIを叩くことを想定して作られています。なので複数のアプリケーションがこちらのメール送信基盤を使うと、同じフォーマットのメッセージをSQS送ることになります。その際、仮に送信に失敗してdeadletterにメッセージが入った時、メッセージのフォーマットにアプリケーションごとの差異がないため、どのアプリケーションが送信したメッセージなのか調査が難しいです。また、メールの種類によっては再送が必須な場合もあると思います。
このあたりも、実際に複数のアプリケーションがこちらの基盤を使うようになったときにアップデートをする予定なので、改修次第本ブログで発信していければと思っています。
最後に
今回作ったサービス(機能?)は、完全に他のアプリケーションから切り離された別サービスでしたが、AWSやSendGridなどのクラウドサービスを組み合わせることで、1スプリント(弊社では二週間)ほどで設計と実装を終えることができました。
これからもクラウドサービスをガンガン使ってアプリケーションをアップデートしていきたいです。
また、上記のような新規アプリケーションの設計やクラウドサービスを利用した挑戦に興味のある方は、カジュアル面談でお話できると嬉しいです。