Amazon SNSのサブスクリプションフィルターとLambdaでRDSイベントを監視しよう

SNSのサブスクリプションフィルターポリシーとLambdaを利用してRDSのイベント通知を取得する方法を作成しました。
2021.12.17

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。クラスメソッドのスジェです。
今回RDSインスタンスの一部のイベントを外してすべてのイベントについてアラートを発生させる作業が必要でした。
簡単な内容ですがSNSサブスクリプションフィルターを設定しながら勘ちがいした内容もあったので、記事として残します。

本記事で使うサービス

  • Amazon SNS
    • ユーザーやサービスを対象にメッセージを送るために利用するサービスです。
    • 本記事ではSNSを利用しましたが、EventBridgeなどを利用しても同じ機能ができます。
  • RDS
    • RDSインスタンスのイベントを監視するために[イベントトサブスクリプション]機能を利用します。
  • Lambda
    • SNSに届いたメッセージのパーシングやattributeを設定するために利用します。

設定しよう

作業順番

  1. SNSのトピック作成
  2. Lambda 作成
  3. SNS サブスクリプション・サブスクリプションフィルター設定
  4. RDS イベントサブスクリプション作成

SNSのトピック作成

まず、SNSのトピックを作成します。
RDSのイベントサブスクリプション用のトピック1個とLambdaからのメッセージを受信用の1個を作成します。
トピックを作成する際、名前以外のオプションは必要によって設定してください。
本記事では sns_for_lambda, sns_for_email という名前でトピックを作成しました。

なぜ2個とトピックを作成するのか?

私が最初に考えた順番は

  • SNS トピック作成
  • SNS サブスクリプション・フィルター作成
  • RDS イベントサブスクリプション作成

で、Lambdaなしで1個とトピックで実装できると思いました。
このような構成で設定した後、RDSを再起動してみると下記のようなメールが届いていました。

Event Source : db-instance

Identifier Link: https://console.aws.amazon.com/rds/home?region=ap-northeast-1#dbinstance:id=rds-test

SourceId: rds-test

Notification time : 2021-12-14 07:37:33.597

Message : DB instance restarted

Event ID : http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html#RDS-EVENT-0006

ちゃんとメールが届いたので「この内容をもとにフィルターを設定すればオッケーだろ」と思い、フィルターを設定してテストしたところ、メールが届きませんでした。 原因を把握するためにCloudWatchのメトリクスを調べてみるとSNSの [NumberOfNotificationsFilteredOut-NoMessageAttributes] が上がっていました。
このメトリクスの意味はメッセージがフィルターのattributeが含まれていないので、結果的にサブスクリプションフィルターポリシーと一致せずフィルタリングされたすべてのメッセージのことです。

SNSに届くメッセージの内容を確認してみると下記の通りでした。

{
      "Records": [
        {
          "EventVersion": "1.0",
          "EventSubscriptionArn": "arn:aws:sns:us-east-2:123456789012:rds-lambda:21be56ed-a058-49f5-8c98-aedd2564c486",
          "EventSource": "aws:sns",
          "Sns": {
            "SignatureVersion": "1",
            "Timestamp": "2019-01-02T12:45:07.000Z",
            "Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==",
            "SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem",
            "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
            "Message": "{\"Event Source\":\"db-instance\",\"Event Time\":\"2019-01-02 12:45:06.000\",\"Identifier Link\":\"https://console.aws.amazon.com/rds/home?region=eu-west-1#dbinstance:id=dbinstanceid\",\"Source ID\":\"dbinstanceid\",\"Event ID\":\"http://docs.amazonwebservices.com/AmazonRDS/latest/UserGuide/USER_Events.html#RDS-EVENT-0002\",\"Event Message\":\"Finished DB Instance backup\"}",
            "MessageAttributes": {},
            "Type": "Notification",
            "UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486",
            "TopicArn":"arn:aws:sns:us-east-2:123456789012:sns-lambda",
            "Subject": "RDS Notification Message"
          }
        }
      ]
}

つまりフィルタリングがちゃんとできてなかった理由はサブスクリプションフィルターポリシーがメッセージの属性(attribute)を基準にフィルタリングするからです。
一致する属性があると、Amazon SNS は受信者にメッセージを送信します。それ以外の場合、Amazon SNS はメッセージを送信することなく、受信者をスキップします。 SNSに受信されるメッセージを見ると"Message" 属性も文字(string)になっているし、外部の属性でもありません。

そういうことでRDSのイベントをフィルタリングするためにはLambdaを経由してメッセージを加工する必要がありました。

Lambda 作成

続いて受信したメッセージをパーシングし、SNSに送るLambdaを作成します。
コードは下記の通りです。作成後、トリガーも忘れずに設定してください。

import json
import boto3

def lambda_handler(event, context):
    sns = boto3.client("sns")
    
    topic_arn = "arn:aws:sns:ap-northeast-1:{アカウントID}:sns_for_email"
    
    subject = "RDS EVENT ALERT"
    origin_message = event['Records'][0]['Sns']['Message']
    message = json.loads(origin_message)
    event_message = message['Event Message']
    
    sns.publish(
        TopicArn=topic_arn,
        Subject=subject,
        Message=origin_message,
        MessageAttributes={
            "event_source": {
                "DataType": "String",
                "StringValue": message['Event Source']
            },
            "event_time": {
                "DataType": "String",
                "StringValue": message['Event Time'],
            },
            "identifier_link": {
                "DataType": "String",
                "StringValue": message['Identifier Link']
            },
            "source_id": {
                "DataType": "String",
                "StringValue": message['Source ID']
            },
            "event_id": {
                "DataType": "String",
                "StringValue": message['Event ID']
            },
            "event_message": {
                "DataType": "String",
                "StringValue": message['Event Message']
            }
    })

トリガーまで設定が終わったら、最後にLambdaのIAMポリシーにsns_for_emailについてwriteポリシーを追加します。

サブスクリプション作成

まず、サブスクリプションフィルターを作成する前に簡単に見てみると
サブスクリプションフィルターポリシーを使うとキーバリューのJSONオブジェクトを設定してメッセージの属性をもとにフィルタリングが可能です。
単純に文字や数字バリューの一致だけではなく接頭辞(prefix)条件や属性を一つも含めていないフィルタリング(anything-but)などの条件も可能です。
詳細な内容は Amazon SNS サブスクリプションフィルターポリシーの 公式ドキュメントをご参照ください。

ではサブスクリプションを作成ます。 EMAILをサブスクリプションターゲットに指定すると下記のようなメールが届きます。[Confirm subscription] をクリックして、承認します。

帆記事ではメールをターゲットにしましたが、webhookを利用すると多様な対象を受信先として設定できます。
設定方法は下記のリンクをご参照ください。

  • [ウェブフックを使用して Amazon SNS メッセージを Amazon Chime、Slack、または Microsoft Teams に発行する方法を教えてください。]https://aws.amazon.com/jp/premiumsupport/knowledge-center/sns-lambda-webhooks-chime-slack-teams/?nc1=h_ls)

承認まで終わったら sns_for_lambda のメッセージの発行で上記の __なぜ2個とトピックを作成するのか?_ 段階の "Record" が含まれたメッセージを発行してメールがちゃんと届くのかを確認しましょう。
おそらく下記のようなメールが届くはずです。

{"Event Source":"db-instance","Event Time":"2021-12-16 12:15:32.494","Identifier Link":"https://console.aws.amazon.com/rds/home?region=ap-northeast-1#dbinstance:id=rds-test","Source ID":"rds-test","Source ARN":"arn:aws:rds:ap-northeast-1:{アカウントID}:db:rds-test","Event ID":"http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html#RDS-EVENT-0004","Event Message":"DB instance shutdown"}

続いてサブスクリプションフィルターポリシーを作成します。
本時期では簡単にRDSのREBOOTとSHUTDOWN以外のすべてのイベントを受信するフィルターを作成します。

RDSのイベント一覧は公式ドキュメントをご参照ください。
ただしドキュメントの "Description" と実際に届く"Message"は違いますので要注意です。別で調べてみましょう。

anything-but 条件でフィルターを作成後、保存します。

{
  "event_message": [
    {
      "anything-but": [
        "DB instance shutdown",
        "DB instance restarted"
      ]
    }
  ]
}

文字列でフィルタを設定するとORロジックで処理されますので、二つのイベントがフィルタリングされます。

RDSのイベントサブスクリプション作成

最後にRDSのダッシュボードのイベントサブスクリプションでサブスクリプションを作成します。
ARNは sns_for_lambda を指定します。
設定後、RDSの停止・起動してみるとメールが届きますが、再起動をしてみるとメールが届きません。

最後に

本時ではRDSのイベントを対象にしましたが、Amazon SNSに直接、または他サービス経由でメッセージを送れるサービスは全部このように設定できます。

お読みいただきありがとうございます。
誤字脱字、内容フィードバックはいつもありがとうございます。must01940 gmailでお願いします。