AWS SESの送信クォータを監視する

AWSのSESには、送信クォータや送信レートという、アカウントごとに一定期間に送信できるメール数の上限が定められています。

送信クォータ
24時間のうちに何通のメールを送信できるか。上限を超えた場合には、送信クォータの上限に達した旨のエラーとなりメールが送信されなくなる(再送処理等はない)。

送信レート
SESが1秒間に受け入れ可能なメールの最大数です。この値を瞬間的に超えることは問題ありませんが、制限を超えた状態が長時間続く場合、送信クォータ同様メールの送信ができなくなる恐れがあります。

送信クォータの上限に達しているかどうかは、CloudWatchアラームなどでリアルタイムに監視するメトリクスが用意されておらず、SESのコンソールやAPIを通じて把握する必要があります。若干の面倒臭さが漂ってきますね。

送信レートの監視をする場合には秒間単位での監視になることや、上限を超えても直ちにペナルティが無いなどのはっきりとしない状況から、より明確なラインで確実な配信不能が訪れる送信クォータの監視をするのが良さそうです。

今回は、送信クォータの60%を消費した場合に、Slackに通知が来るようなLambda関数を紹介します。
※このLambda関数は、送信数に問題がない間は実行ログを吐き出す以外沈黙します。

Lambda関数

大まかな処理の流れは下記のとおりです。

  1. 送信クォータのステータスを取得
  2. 直近24時間の送信数 / 24時間での最大送信数 * 100 [%] を算出
  3. 60%以上消費していた場合、Slackへ通知(60%未満なら何もしない)
    1. 投稿メッセージを作成
    2. WebhookでSlackへPOST

Slackへの投稿はなるべく簡素にし、現在はメタデータなども含めないように(コメントアウト箇所)しています。

import boto3
import json
import urllib.request


def build_message_block(response, send_quota_usage):
    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": "SES Send quota use " + str(int(send_quota_usage)) + "%"
            }
        },
        {
            "type": "divider"
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*SentLast24Hours (usage):* " + str(f"{int(response['SentLast24Hours']):,}")
                }
            ]
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*Max24HourSend (limt):*   " + str(f"{int(response['Max24HourSend']):,}")
                }
            ]
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": "*MaxSendRate:* " + str(f"{int(response['MaxSendRate']):,}")
                }
            ]
        }
    ]
    
    # if response['ResponseMetadata']:
    #     meta_data_json = json.dumps(response['ResponseMetadata'])
    #     blocks += [
    #         {
    #             "type": "section",
    #             "text": {
    #                 "type": "mrkdwn",
    #                 "text": "*ResponseMetadata:*\n```" + meta_data_json + "```"
    #             }
    #         }
    #     ]
    
    return blocks


def post_slack(blocks, message=None):
    send_data = {
        "channel": "XXXXXXXXXXX",   # SESの通知用channel
        "username": "SES Monitor",
        "icon_emoji": ":chart_with_upwards_trend:",
        "text": message,
        "blocks": blocks
    }
    send_text = "payload=" + json.dumps(send_data)
    
    print('post_slack() send_text: ', send_text)
    
    request = urllib.request.Request(
        "(SlackのWebhook URL)",
        data=send_text.encode('utf-8'),
        method="POST"
    )
    
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode('utf-8')


def get_ses_send_quota():
    ses = boto3.client('ses')
    response = ses.get_send_quota()
    send_quota_usage = response['SentLast24Hours'] / response['Max24HourSend'] * 100
    
    if send_quota_usage >= 60:
        message_body = build_message_block(response, send_quota_usage)
        notice_message = 'SES send quota usage: ' + str(send_quota_usage) + '%'
        print(message_body)
        print(notice_message)
        post_slack(message_body, notice_message)


def lambda_handler(event, context):
    get_ses_send_quota()
    
    return

Slackへの投稿内容をBlockで作成している都合上、どうしてもその箇所が長大になってしまっています……

Lambda関数に付与する権限

デフォルトのCloudWatchログ関係の他に、「AmazonSESReadOnlyAccess」を雑に付与してありました。もうちょっと精査しても良かったと思う。

実行方法

Lambda関数のトリガーに「EventBridge (CloudWatch Events)」を指定し、cronで定期実行しています。

送信されるメッセージの例

しきい値を下げてテストしたものがこちら。
※緩和申請済みのため送信クォータの上限が高いです。

これで、知らぬ間に送信クォータを使い果たしてメール配信不能に陥るような事故をケアすることができるようになりました。
緩和申請が間に合えば配信不能には陥りませんし、送信クォータの利用率がわからない状態で日々運用するよりも幾分精神衛生に良いと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です