そのメール、本当に届いてる?Amazon SESの運用で得た監視プラクティス

こんにちは、kintone.comのバックエンドエンジニアをしている@ueokandeです。 いきなりですが、メールって難しいですよね。 普段HTTPに慣れていると、メール周りのプロトコルの理解は難しく、トラブルにも見舞われることも少なくないです。 またメールプロトコルの性質上、メールを送った後にもトラブルは起こりがちです。 グローバル向けkintone.comはAWSで運用しており、メールの機能はAmazon Simple Email Service(SES)を利用しています。 この記事ではkintone.comのメール基盤の全貌と、Amazon SESを利用する上での運用プラクティスを紹介します。

kintone.comとメール

kintone.comでは、ユーザーの招待やkintone上の更新を知らせるためにメールを利用し、メール送信は重要な機能の1つです。 kintone.comは組織や会社ごとにプライベートな環境を提供し、管理者はその環境にアクセスするユーザーをメールで招待できます。 ユーザー数やkintone上のコミュニケーションが増えるにつれてメール送信数も大きくなり、それぞれのお客様環境ごとに送信するメールの数は異なります。

AWS上で運用しているkintone.comは、メールの送信にAmazon SESを採用しています。 Aumazon SESはフルマネージドのメール送信サービスで、メール送信だけでなくメトリクスやイベント通知などの機能も備わっています。 しかしフルマネージドといっても、無造作にメールを送ってよいというわけではありません。

存在しないメールアドレスに送ろうとすると、メールサーバーはバウンス(Bounce)メッセージを返します。 またメールがスパムメールと判定された場合、メールサーバーやユーザーが苦情(Complaint)を送ることもあります。 Amazonが保有するIPアドレスからのメール評価を良好に保つため、Amazon SESはBounceやComplaintの数が多くなるとメール送信をブロックすることがあります。 BounceまたはComplaintしたメールを再送しないようにするのは、Amazon SES利用者の責務です。

サービスの性質やビジネス要件によって、宛先のばらつきやメールを送る頻度は大きく異なります。 Amazon SESの利用者はそれぞれのアーキテクチャにフィットした方法で、メールの管理やモニタリングシステムを構築する必要があります。

kintone.comのメール配信基盤

現在のkintone.comのメール配信基盤は以下の構成です。

kintone.comのメール配信基盤
kintone.comのメール配信基盤

kintone.comのバックエンドサービスの多くは、Kubernetes(Amazon EKS)上にデプロイします。 kintone.com上のメール送信とBounce/Complaint再送防止の役割は、1つのサービスとしてデプロイしています(図中のMail Sender)。 Mail Senderはユーザーリクエスト(Web Pod)や定期バッチ(Batch Pod)から、HTTPリクエストでメールジョブを受け取ります。 Mail SenderはHTTPで受け取ったメールジョブをそのままAmazon SESに渡すのではなく、一度Amazon Simple Queue Service(SQS)にジョブをキューイングします (設計の経緯は過去記事マルチテナントを支えるkintoneのメール送信戦略をご覧ください)。

Mail Senderが受け取ったメールジョブをAmazon SESに送るまでのフローは以下の通りです(図中の赤丸数字)。

  1. Mail SenderはREST APIでメールジョブを受け取り、Amazon SQSにメールジョブをエンキューします。
  2. Amazon SQSに積まれたジョブをデキューして、メールの宛先が過去にBounce/Complaintされたアドレスかどうかをチェックします。
  3. 宛先のアドレスが送信可能なら、メールをAmazon SESに渡します。

またMail SenderはAmazon SESからのBounce/Complaintイベントを受け取り、再送しないよう記録します(図中の青丸数字)。

  1. Amazon SESはバウンスされたメッセージをSNS経由でAmazon SQSに通知します。
  2. Mail SenderはAmazon SQSからイベントをデキューして、DynamoDBに記録します。

たとえばお客様から「kintone.comからメールが届かない」というお問い合わせがあった時は、メール配信基盤のどこかでトラブルが発生していないか調査します。 「メールが遅れて届く」という問い合わせがあったときは、お客様の期待する時刻(リクエストがあった時刻)と送信時刻に乖離が無いか調査します。

ここからはkintone.comで導入している、Amazon SESを運用する上で役に立つ、以下の2つのプラクティスを紹介します。

  • 分散ロギング
  • Amazon SESからのイベントの集計

分散ロギング

MicroservicesやService-Oriented Architectureなどの、複数サービスが連携するアーキテクチャでは、各ホストで出力されるログをまとめて閲覧したくなります。 あるユーザーリクエストで発生したログと、そのリクエストがさらに別のサービスを呼び出すとき、それらのログは全て元のリクエストに関連します。 それらのログを時系列で並べると、そのリクエストが処理する内容やタイミングを知ることができます。 分散ロギングはそれを解決する1つのプラクティスで、分散ロギングでは以下の3つの情報をログに付与することが多いです。

  • Trace ID ... ユーザーリクエストやバッチごとに生成されるユニークなID。
  • Span ID ... 1つのサービスやスレッドごとに生成されるユニークなID。
  • Parent ID ... サービスの呼び出し元やスレッドの作成元のSpan ID。

本記事ではこれらを相関IDと呼ぶことにします。 各サービスが別のサービスを呼び出すときや、スレッドを作成するときにこれらの値を伝搬することで、各サービスのログは一貫した相関IDを持ちます。 例えばKibana上で同じTrace IDのログを検索することで、ユーザーリクエストやバッチに関連する複数ホストのログを閲覧できます。

HTTP

他のサービスにHTTPリクエストで処理を依頼するとき、相関IDをHTTPリクエストのヘッダーに付与することで、呼び出されたサービスに相関IDを伝搬できます。 kintone.comのバックエンドサービスの多くはKotlinとSpring Bootで記述されており、Spring Cloud Sleuthを使って相関IDの受け渡しをしています。 Amazon Elastic Load Balancerは、バックエンドサービスにリクエストを流すときに、以下のようなHTTPヘッダを付与します。

X-Amzn-Trace-Id: Root=1-67891234-abcdef012345678912345678

このヘッダーのRootの値をTrace IDとして扱い、kintone.comではすべてのユーザーリクエストに紐づくログを検索できます。 そしてログ出力時にはこれらの値をJSONログとして出力し、Trace ID、Span ID、Parentをそれぞれフィールドに載せます。

{"logged_at":"2021-11-19T08:48:07.522Z","severity":"INFO","message":"Mail sent!","trace_id":"67891234abcdef012345678912345678","parent_id":"0123456789abcdef","span_id":"0011223344556677"}

Amazon SQS

Mail SenderがAmazon SQSにエンキューとデキューをするとき、それぞれのスレッドは別になり、Podも別の可能性があります。 エンキュー・デキューで同じ相関IDを持つには、エンキュー時の相関IDをデキューするスレッドに渡す必要があります。 これを実現するために、Amazon SQSのメッセージにメタデータとして相関IDを付与します。 こうするとエンキューとデキューの前後で一貫した相関IDを持ち、Amazon SESに送るとき元となったユーザーリクエストを紐づけることができます。

Amazon SQSのAttributeは、メッセージに任意の追加情報を付与できます。 Mail SendはAmazon SQSにメッセージをエンキューするときAttributeに相関IDを乗せて、デキューするときにAttributeの値から処理しているのスレッドに相関IDを設定します。

Amazon SES

Amazon SESからのイベントにも相関IDがあると、どのお客様操作によってバウンスされたか調査するのに役に立ちます。 メールにもHTTPと同じくヘッダーがありますが、Amazon SESにはメールのメッセージにタグを付与できます。 Amazon SESにメールを送信するときに、タグに相関IDを付与します。 Amazon SESが送るBounce/Complaintイベントにも送信時に付与したタグが載っているので、それを受け取ったMail Senderは相関IDを受け継いで処理を続行できます。

HTTPリクエスト、Amazon SQSのメッセージ、Amazon SESのメッセージそれぞれに相関IDを付与するとことで、メール処理全体で一貫した相関IDを付与できるようになりました。 kintone.comでメールの遅延や障害があったとき、いち早くトラブルの原因を特定できるようになります。 これに加えてkintone.comでは、次節で説明するAmazon SES上のイベントを集計することでお客様ごとの統計情報を確認できます。

Amazon SES上のイベントを集計する

最初に述べた通り、お客様環境ごとにメールを送る量は大きく異なります。 kintone.com全体で安定してメールを送信するために、お客様環境ごとのメールの送信数やBounce/Complaintの件数を知る必要があります。 そこでAmazon SESからのイベントを集計可能なログとして残しておくことで、 お客様環境ごとの送信件数をや月ごとのBounce/Complaintの件数を確認できるようになります。

Amazon SESはBounce/Complaint意外にもいくつかのイベントを送信します。 kintone.comでは主要な以下のイベントを受け取り、集計可能な形式で保存します。

  • Send ... Amazon SESがメールを受け取った
  • Reject ... Amazon SESがメールを拒否した
  • Bounce ... メールサーバーがバウンスメッセージを返した
  • Complaint ... メールサーバーやメールの受信者がAmazonに苦情を送った
  • Delivery ... Amazon SESはメールを配送できた
  • DeliveryDelay ... Amazon SESはメールを配送できたが遅延した

それらの情報をJSONログとして出力し、CloudWatch Logsに保存しておくとCloudWatch Logs Insightで解析可能になります。 もちろんAmazon SESのイベントは、送信時刻やイベントの種類だけでなく、Amazon SES送信時のタグも渡されるので、先程説明した相関IDもログに付与できます。 このAmazon SESからのイベントはAmazon SNSで受け取り、AWS LambdaがCloudWatch Logsに保存します。

Amazon SESのイベントを受け取りLambdaで記録する
Amazon SESのイベントを受け取りLambdaで記録する

AWS Lambdaは以下ようなログをCloudWatch Logsに保存します。

{"logged_at":"2021-11-18T00:11:14.233Z","event_type":"Bounce","bounce_type":"Permanent","bounce_sub_type":"General","mail_timestamp":"2021-11-18T00:11:13.055Z","message_id":"0101010101010101-00110011-0000-1111-2222-333344445555-666666","trace_id":"67891234","parent_id":"0123456789abcdef"}

CloudWatch Logs Insightでクエリを書いてこのログを集計すると、Amazon SES上で発生したイベントの統計情報がわかります。 たとえば日ごとのメール送信数、Bounce数、Complaint数を取得するには以下のクエリになります。

stats count(*) by event_type, bin(1d) as date | sort date

結果は以下のようになります(開発環境での実行結果です)。

event_type date count(*)
Send 2021-11-17 00:00:00.000 11
Delivery 2021-11-17 00:00:00.000 9
Reject 2021-11-17 00:00:00.000 1
Bounce 2021-11-17 00:00:00.000 1
Complaint 2021-11-17 00:00:00.000 1
Send 2021-11-18 00:00:00.000 6
Delivery 2021-11-18 00:00:00.000 4
Reject 2021-11-18 00:00:00.000 1
Bounce 2021-11-18 00:00:00.000 1
Complaint 2021-11-18 00:00:00.000 1

これから

kintone.comをローンチしてから現在まで、メールに関するトライブルが何度か発生し、そのたびにメール配信基盤の改良を続けてきました。 この記事ではその過程で得た知見と、Amazon SESを利用する上でのプラクティスを紹介しました。 これらの対策は問い合わせがあった時に役立つだけでなく、データを元に将来起こりうるトラブルの予見もできます。

kintone.comのバックエンドサービスはAWSを利用し、kintoneに密に関係するバックエンドサービスは自分たちで開発しています。 自分のチームは引き続き、kintone.comの安定運用を目指して、サービスの開発・運用や技術的なチャレンジに取り組んでいく予定です。 kintone.comではバックエンドサービスの仲間を募集しています。 この記事を読んで少しでも興味が湧いた方はぜひご連絡ください。 We are hiring!

cybozu.co.jp