バックエンドエンジニアの宮澤です。
弊社のANDPADでは多くのWebサービスと同じようにメールでの通知機能を持っています。 今回はRailsと複数のメール配信サービスを組み合わせて、メール配信機能を冗長化した事例を紹介します。
ANDPADで導入しているメール配信サービス
ANDPADでは2つのメール配信サービスを利用しています
通常はMailgunを主系として利用し、SendgridはMailgunでの送信失敗時に備えて待機する構成になっていて、Mailgunで障害が発生した場合でも自動的にSendgird経由でメール配信できるようになっています。
また、サブにSendgridを採用している理由にRFCに準拠しないメールアドレスへの対策もあります。 ANDPADでは個人の携帯キャリアメールで登録しているアカウントも多く、キャリアメールの過去の経緯からRFCに準拠しないアドレスも少数ですが登録されています。 MailgunではRFCに準拠しないメールアドレスは容赦なく弾く仕様となっているのですが、Sendgridは日本でのサービス開始時にそういったメールアドレスへの送信にも対応しています。
Railsでメール配信を冗長化する方法
では、Railsで2つのメール配信サービスをメイン/サブとして切り替えて利用する方法を紹介します。
Mailgunでのエラーをキャッチする
まずは主系であるMailgunで配信できなかったことを検知する仕組みです。
Mailgunでのメール送信には mailgun-ruby
のGemを利用しています。
github.com
mailgun-rubyではMailgun APIからメール送信リクエストを弾かれると Mailgun::CommunicationError
を投げるようになっているので、この例外をキャッチしてエラーハンドリングします。
私の入社時にはエラーハンドリングをこのように記述していました。
# サンプル begin NotifyMailer.send_receive_message(@message).deliver rescue Mailgun::CommunicationError => e # Mailgunで配信失敗したときの処理 . . . end
これではエラーハンドリングを各所のメール送信処理ごとに記述する必要があり、実際にこの記述が漏れて一部のメール送信を伴う機能では実装漏れも発見されました。
また、この方法ではメール送信処理を #deliver_later
でエンキューする場合はキャッチすることができません(ANDPADでは #deliver
をコールする処理自体をsidekiqジョブで実行することで、非同期実行でもエラーをキャッチしていましたが、非常にややこしい書き方になっていた)
そういった実装漏れや非同期でのメール送信ができない制限をクリアするためにも、メール送信基盤として Mailgun::CommunicationError
のエラーハンドリングのリファクタリングを始めました。
調査したところ、Rails5.1からはActionMailerでもrescue_from
が実装されたのを知り、これを使って実装することにしました。
# /app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base rescue_from Mailgun::CommunicationError, with: :resend_by_smtp # Mailgunで送信新失敗したメールをSendgridで再送する def resend_by_smtp(e) # Mailgunで配信失敗したときの処理 . . . end end
メール送信の基底クラスとなるApplicatonMailerクラスでrescue_from
でのエラーハンドリングを記述することで、これを継承する各メール送信処理のクラスでもエラーキャッチできるようになって実装漏れが防げます。
また、この方法ではエラーキャッチをMailerクラスの呼び出し元ではなく、Mailerクラス内部に持つので #deliver_later
でジョブとして実行する場合でもエラーをキャッチすることができます。
注意点として、Railsでは config.action_mailer.raise_delivery_errors
をtrueにしておかないとメール配信での失敗時のエラーを握りつぶしてしまいます。
外部サービスでは配信失敗しているはずなのに例外をキャッチできない場合はこの値を確認してみましょう(自分はこの設定が原因で2時間ほど無駄にしました)
Sendgrid経由でのメール再送
次にエラーキャッチしたあとにSendgrid経由に切り替えてメール送信する方法を解説します。
これにはいくつかやり方があると思うのですが、ANDPADではSendgridの設定を config.action_mailer.smtp_settings
に設定して、エラーハンドリング中に切り替えるようにしています。
# /config/environments/xxxx.rb # Mailgunの接続情報 config.action_mailer.delivery_method = :mailgun config.action_mailer.mailgun_settings = { api_key: `xxxxxxxx`, domain: Settings.domain, } # Sendgridの接続情報 config.action_mailer.smtp_settings = { domain: Settings.domain, address: "smtp.sendgrid.net", port: 587, authentication: :plain, user_name: 'apikey', password: `xxxxxxxx` }
# /app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base rescue_from Mailgun::CommunicationError, with: :resend_by_smtp # Mailgunで送信新失敗したメールをSendgridで再送する def resend_by_smtp(e) # NOTE: Mail::Messageインスタンス message = mail # 送信設定をconfig/environments/xxxx.rbで設定したsmtp_settingsに上書きする message.delivery_method(:smtp, Rails.configuration.action_mailer.smtp_settings) # Sendgrid経由でメール再送する message.deliver end end
これでメール配信サービスの冗長化ができました。
通常はメインのMailgunでメール配信して、Mailgunに障害が発生しても自動的にSendgrid経由で再送します。 実際にMailgun側の障害があったときにも、Sendgrid側で再送されてANDPADでの障害を防いだ場面が何度かありました。
RFC非準拠のメールアドレスへSendgrid経由でメール送信する
冒頭で書いたようにSendgridをサブで利用しているのには、Mailgunで弾かれてしまうRFC違反のメールアドレスへの配信という目的もあります。
RFC違反で弾かれた場合でも同様に Mailgun::CommunicationError
が投げられるので、その対策もしておきます。
SendgridではローカルパートをダブルクォーテーションでくくることでRFCに準拠していないメールアドレスへも送信が可能です。
# /app/mailers/application_mailer.rb class ApplicationMailer < ActionMailer::Base rescue_from Mailgun::CommunicationError, with: :resend_by_smtp # Mailgunで送信新失敗したメールをSendgridで再送する def resend_by_smtp(e) # NOTE: Mail::Messageインスタンス message = mail # 送信設定をconfig/environments/xxxx.rbで設定したsmtp_settingsに上書きする message.delivery_method(:smtp, Rails.configuration.action_mailer.smtp_settings) # RFC非準拠のアドレスはローカルパートをダブルクォーテーションで囲う message.to = message.to.map do |to| address = Mail::Address.new(to) %("#{address.local}"@#{address.domain}) end # Sendgrid経由でメール再送する message.deliver end end
Sengridが障害で止まった場合にはRFCに準拠しないメールアドレスへの配信は止まってしまうのですが、そういったメールは少量なので一定期間はSidekiqがプールしてSendgridが復活次第に再送できる仕組みになっています。
おまけ
deviseでのメール送信にも適用する
ユーザー認証にdeviseを利用しているプロダクトでは、deviseの機能で登録完了メールやメール認証をしている場合もあります。
deviseのデフォルト設定ではメール送信を行うDevise::MailerクラスはActionMailer::Baseを直接継承しているので今回の実装が適用されないのですが、deviseの設定 parent_mailer
で ApplicationMailerを継承させることができます。
# config/initializers/devise.rb # Configure the parent class responsible to send e-mails. config.parent_mailer = 'ApplicationMailer'
おわりに
アンドパッドでは様々に工夫をこらして開発しています!
興味のある方、下記サイトからお話しに来てください!
engineer.andpad.co.jp