RailsからSQS × Lambda × SESでメール送信を実装する
はじめに
はじめまして、教育系スタートアップのLX DESIGNでCTOしている工藤です。
私たちのアプリケーションではメール送信の際にRailsアプリケーションからSESを利用してメールを送信しています。その中で、ある施策の中で一度に数千件のメールを送信する必要がありました。その際に直面するのがAWS SESの送信レート制限です。
SESには1秒あたりの送信制限があり、通常のやり方で一度に大量のメールを送信しようとすると、SESの制限により送信完了までに数十秒かかってしまいます。また、他のメール送信がある処理も、この送信待ちに影響されるためアプリケーション全体のパフォーマンスが大幅に低下してしまい、ユーザー体験に大きな影響を与える可能性があります。
実装しているRailsアプリケーションでは、標準の delivery_method
の ses
を使ってAWS SESを直接呼び出し、メール送信を行っていました。この方法だと前述の通りSESの制限に引っかかり、パフォーマンス面で問題が生じます。AWSに送信レートをあげてもらって解決する方法もあるのですが、それでは根本的な解消にはならないため、スケーラビリティを意識したメール送信を行うように変更しました。
SQSとLambdaを使った解決策
この問題を解決するために、AWSのSQSとLambdaを利用してSESからメール送信することにしました。メールを直接SESに送信するのではなく、一度SQS(Simple Queue Service)にメール送信リクエストをキューに登録し、バックグラウンドでLambdaをトリガーしてSESからメールを送信する構成です。
このアプローチの利点は、Railsのアプリケーションがキューにリクエストを投げた瞬間に処理が完了するため、SESの送信を待ったレスポンスの遅延も発生しにくくなります。また、Lambdaがバッチ処理でSESの送信レートをうまく制御することで、SESの制限に引っかからずにスムーズに大量のメールを送信できます。
Rails側の変更を最小限にするために
Railsのコードにも大きな影響を与えたくなかったため、できるだけシンプルな改修で実装できるように意識しました。前述した通り、もともとはRailsの delivery_method
で標準の :ses
を呼び出していました。これをカスタムdelivery_methodを作成し、delivery_method
置き換えることで、コードの大幅な改修をがないように実装しました。
config.action_mailer.delivery_method = :ses
↓
config.action_mailer.delivery_method = :sqs_delivery
このように、Rails側のロジックはそのままにconfigファイルで delivery_method
を切り替えるだけで、SQS経由でのメール送信が可能になるようにしました。これにより既存のコードに大きな変更を加えることなく、スケーラブルなメール送信が実現できました。また、configファイルは各環境に存在するため、これまで通り本番環境と開発環境でメール送信方法を分けることも可能になります。
Rails側でのdelivery_methodの実装
カスタムdelivery_methodを利用できるように config/initializers/aws_sqs.rb
というファイルを以下のように実装しました
require 'aws-sdk-sqs'
require 'sqs_delivery'
ActionMailer::Base.add_delivery_method :sqs_delivery, SqsDelivery
Aws.config.update({
region: 'ap-northeast-1',
credentials: Aws::Credentials.new({access_key}, {secret_key})
})
実際のカスタム関数はapp/mailers/sqs_delivery.rb
というファイルを作成し、以下のように実装しています。
require 'aws-sdk-sqs'
class SqsDelivery
def initialize(settings)
@settings = settings
@sqs_client = Aws::SQS::Client.new(region: 'ap-northeast-1')
@queue_url = {作成したキューのURL}
end
def deliver!(mail)
body = {
to: mail.to,
from: mail.from,
subject: mail.subject,
html_body: mail.html_part.body.to_s,
text_body: mail.text_part.body.to_s
}.to_json
@sqs_client.send_message(
queue_url: @queue_url,
message_body: body,
message_group_id: {グループID},
message_deduplication_id: {重複排除ID},
)
end
end
ここで実装した SqsDelivery
を
ActionMailer::Base.add_delivery_method :sqs_delivery, SqsDelivery
で delivery_method
として追加し、呼び出せるようにしています。そのため以下のようにRailsのAction Mailerで定義した関数から deliver
メソッドを呼び出すだけでSQS経由のメール送信が可能となります。
UserMailer.with(user: @user).test_email.deliver
AWS Lambda側のSESへの送信
Railsの方が実装できたらそれを受け取ってSESに流すLambda関数を実装していきます。SQSで作成したキューを今回作成するLambdaのトリガーとして設定する必要やIAMの設定をする必要がありますが、その細かい説明は省略します。
Lambda関数については、Pythonで記述しています。以下が実際のコードです。
import json
import boto3
client = boto3.client('ses', region_name='ap-northeast-1')
def lambda_handler(event, context):
for record in event['Records']:
body = json.loads(record['body'])
response = client.send_email(
Destination={
'ToAddresses': body['to']
},
Message={
'Body': {
'Text': {
'Charset': 'UTF-8',
'Data': body['text_body']
},
'Html': {
'Charset': 'UTF-8',
'Data': body['html_body']
}
},
'Subject': {
'Charset': 'UTF-8',
'Data': body['subject'],
},
},
Source=body['from']
)
return {
'statusCode': 200,
'body': json.dumps("Email Sent Successfully. MessageId is: " + response['MessageId'])
}
このようにシンプルなものになります。SQSから受け取ったeventのRecordsの中にRailsからmessage_bodyで渡した値が配列で渡るので、この配列の中身をループして取り出して、boto3
のses clientに渡してあげるだけです。
まとめ
大量のメール送信を行う際のAWS SESの送信レートの制限による遅延をなくすため、SQSとLambdaを組み合わせて、パフォーマンスとスケーラビリティを向上させることができます。さらにRailsの場合、カスタムdelivery_method作成することで、コード上も影響範囲も最小限に抑えつつシンプルに移行し、システム全体のレスポンス向上ができました。
Discussion