CloudWatch Alarm から通知される文章の UTC時刻を JST時刻に差し替えたい

CloudWatch Alarm からのSNS通知を一旦 Lambdaに噛ませます。LambdaでJST情報を付与して再度 SNS通知します。
2020.09.16

はじめに

img

上図のように CloudWatch Alarmから 通知先にSNSトピックを選択したときに送られるメールは以下のような文章です。

You are receiving this email because your Amazon CloudWatch Alarm "alarm-test-ec2-cpu" in the Asia Pacific (Tokyo) region has entered the ALARM state, because "Threshold Crossed: 1 out of the last 1 datapoints [50.0 (15/09/20 06:51:00)] was greater than the threshold (70.0) (minimum 1 datapoint for OK -> ALARM transition)." at "Tuesday 15 September, 2020 06:56:39 UTC".

View this alarm in the AWS Management Console:
(CloudWatch Alarmへのリンク)

Alarm Details:
- Name: alarm-test-ec2-cpu
- Description:  XXX
- State Change: OK -> ALARM
- Reason for State Change: Threshold Crossed: 1 out of the last 1 datapoints [50.0 (15/09/20 06:51:00)] was greater than the threshold (70.0) (minimum 1 datapoint for OK -> ALARM transition).
- **Timestamp:**  Tuesday 15 September, 2020 06:56:39 UTC
- AWS Account: XXXXXXXXXXXX
- Alarm Arn:  arn:aws:cloudwatch:ap-northeast-1:XXXXXXXXXXXX:alarm:alarm-test-ec2-cpu

(以下略)

Timestamp のタイムゾーンが UTCなので、 これをローカル(JST)にしたい です。

背景として、CloudWatch Alarm の通知を利用した運用をされている方は多いと思います。 メールが届いたときの、何かしらの初動・調査を行う際に 毎回 時刻の部分が UTCだと、 変換に手間がかかりますよね。

CloudWatchマネジメントコンソール上では ローカルタイムゾーンとして、 様々なグラフ・メトリクスを見ることができます。 しかし、CloudWatch Alarmからの通知文のタイムスタンプを JSTにするオプションはありません。

そこで、今回は Lambdaを介して JSTの時刻を付与したメッセージを送信する構成を作ってみます。 以下のような構成です。

img

構成

作成するリソースは以下の通り。先程の構成図通りです。

  • SNSトピック(via CW Alarm): CloudWatch Alarm の通知用。Lambda関数のトリガー
  • SNSトピック(via Lambda): Lambda関数からの通知用。メール送信
  • Lambda関数: CloudWatch Alarmからのメッセージの処理用

SNSトピックは2つ作成します。CloudWatch Alarm からの通知先と、Lambda関数からの通知先です。 SNSトピック(via Lambda) を購読(subscribe)しておきます。

Lambda関数の実行ロールの権限は以下の通り。 Lambdaの基本的な実行ポリシー ( AWSLambdaBasicExecutionRole )に sns:Publish を追加したものです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "sns:Publish"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

Lambda Function

Python3.7 で Lambda Functionを作成します。以下のようなコードを書いてみました。 タイムゾーンの変換に pytz を使用しています。

import json
import os
import boto3
from pytz import timezone
from dateutil import parser

SNS_TOPIC_ARN = os.environ['SNS_TOPIC_ARN']
client = boto3.client("sns")


def lambda_handler(event, context):
    subject = event["Records"][0]["Sns"]["Subject"]
    message = json.loads(event["Records"][0]["Sns"]["Message"])
    client.publish(
        TopicArn=SNS_TOPIC_ARN,
        Subject=subject,
        Message=parse(message)
    )


def parse(message):
    texts = []
    # parse UTC to JST
    jst_time = parser.parse(message["StateChangeTime"]
                            ).astimezone(timezone('Asia/Tokyo'))
    # Summary
    texts.append("{} state is now {}: {}".format(
        message["AlarmName"],
        message["NewStateValue"],
        message["NewStateReason"]
    ))
    texts.append("StateChangeTime(JST): {}".format(jst_time))
    # Details
    texts.append("### Details")
    for k, v in message.items():
        texts.append("- {}: {}".format(k, v))
    return "\n".join(texts)

環境変数に SNS_TOPIC_ARN を作成して、 SNSトピック(via Lambda)のARNを指定します。

※SNS通知が Lambdaに渡されるときのイベントフォーマットは以下参考になります。

SAMで構築

上記構成をAWS サーバーレスアプリケーションモデル (AWS SAM)を使って構築しました。 SAMのプロジェクト構成内容を記します。

プロジェクト

sam init で新規プロジェクトを作成します。

sam init --runtime python3.7 --name cw-alarm-notification-with-jst

template.yaml

必要なリソースを記述した template.yaml は以下です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: change the UTC to JST, for SNS topics via CW Alarm

Parameters:
  EmailAddress:
    Type: String

Resources:
  # SNS topic (used by CW Alarm)
  SnsTopicFromCWAlarm:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: cw-alarm-topic
  # SNS topic (used by Lambda)
  SnsTopicFromLambda:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: cw-alarm-topic-jst-processed
  SnsSubscription:
    Type: AWS::SNS::Subscription
    Properties: 
      Protocol: email
      Endpoint: !Ref EmailAddress
      TopicArn: !Ref SnsTopicFromLambda
  # IAM Role
  IamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Policies:
        - PolicyName: "LambdaPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "sns:Publish"
                Resource: "*"
  # Function
  Function:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/
      Handler: app.lambda_handler
      Runtime: python3.7
      Environment:
        Variables:
          SNS_TOPIC_ARN: !Ref SnsTopicFromLambda
      Role: !GetAtt IamRole.Arn
      Events:
        EventBridgeRule:
          Type: SNS
          Properties:
            Topic: !Ref SnsTopicFromCWAlarm

src/app.py (Lambda関数)

前述のLambda関数のコードを src/app.py に格納します。

src/requirements.txt

必要なパッケージを記します。タイムゾーン指定に pytz を使用するため、記載します。

pytz

ビルド、デプロイ

sam build
sam deploy --guided

デプロイの設定は下記のとおりです。パラメータに指定した EmailAddress に送信先メールアドレス を記入します。

> sam deploy --guided

Configuring SAM deploy
======================

    Setting default arguments for 'sam deploy'
    =========================================
    Stack Name [cw-alarm-notification-with-jst]: cw-alarm-notification-with-jst
    AWS Region [ap-northeast-1]: ap-northeast-1
    Parameter EmailAddress []: sample@example.co.jp

確認

適当に作成した CloudWatch Alarm の通知先を作成した SNSトピック(via CloudWatch) に指定して、 アラーム状態にしてみました。

img

以下のような メールが届きます。

ec2-cpu-alarm-test state is now ALARM: 
Threshold Crossed: 1 out of the last 1 datapoints [50.0 (15/09/20 09:18:00)] was greater than the threshold (70.0) (minimum 1 datapoint for OK -> ALARM transition).
**StateChangeTime(JST): 2020-09-15 18:33:54.193000+09:00**
### Details
- AlarmName: ec2-cpu-alarm-test
- AlarmDescription: ec2 cpu alarm test
- AWSAccountId: XXXXXXXXXXXX
- NewStateValue: ALARM
- NewStateReason: Threshold Crossed: 1 out of the last 1 datapoints [50.0 (15/09/20 09:18:00)] was greater than the threshold (70.0) (minimum 1 datapoint for OK -> ALARM transition).
- StateChangeTime: 2020-09-15T09:33:54.193+0000
- Region: Asia Pacific (Tokyo)
- AlarmArn: arn:aws:cloudwatch:ap-northeast-1:XXXXXXXXXXXX:alarm:ec2-cpu-alarm-test
- OldStateValue: OK
- Trigger: {'MetricName': 'CPUUtilization', 'Namespace': 'AWS/EC2', 'StatisticType': 'Statistic', 'Statistic': 'AVERAGE', 'Unit': None, 'Dimensions': [{'value': 'i-01xxxx', 'name': 'InstanceId'}], 'Period': 300, 'EvaluationPeriods': 1, 'ComparisonOperator': 'GreaterThanThreshold', 'Threshold': 70.0, 'TreatMissingData': '- TreatMissingData:                    missing', 'EvaluateLowSampleCountPercentile': ''}

通知に JST時刻 StateChangeTime(JST): 2020-09-15 18:33:54.193000+09:00 を付与できました。 (それ以外は、Alarmの情報をそのまま出力しているだけです)

※ちなみにアラームのテストは AWS CLIから強制的に行うことが可能です。以下参考ください。

おわりに

以上、CloudWatch Alarm の情報に JST時刻を付与してメール通知してみました。

CloudWatch Alarm → SNS → メール の構成の場合、受信するメールの文は 良い感じに加工してくれるみたいですね。 ( You are receiving this email because your Amazon CloudWatch Alarm ... の部分)

Lambdaを介した今回の構成では、この加工は適用されません。 なので 「オリジナルの文章を 時刻だけJSTにして他はそのままに」みたいなことはできませんでした。

オリジナルの文章ほどリッチじゃありませんが、JST時間を付与できました。

少しでもどなたかのお役に立てば幸いです。

参考