Amazon CloudWatch Alarmのアラーム発生時にSNS Topic経由で起動したLambda内で直近のデータポイントの値を取得する

2022.06.04

こんにちは、CX事業本部 IoT事業部の若槻です。

Amazon CloudWatch Alarmのアラーム発生時にSNS Topic経由で起動したLambda内で、アラームが発生したメトリクスの直近のデータポイント値を取得したいことがありました。

アラーム発生時に発行されるイベントメッセージには様々な情報が含まれるため、

  • そもそもイベントメッセージ内に直近のデータポイント値は含まれているのか?
  • 含まれている場合のデータポイント値の取り出し方は?

という観点で今回は検証を行ってみました。

やってみた

実装

AWS CDKで下記構成の実装を行います。

下記はCDK Stackのコードです。

lib/process-stack.ts

import { Construct } from 'constructs';
import {
  aws_lambda_nodejs,
  aws_sns,
  aws_cloudwatch,
  aws_cloudwatch_actions,
  Duration,
  Stack,
  StackProps,
  aws_lambda_event_sources,
} from 'aws-cdk-lib';

export class ProcessStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    //CloudWatch Alarm
    const sampleAlarm = new aws_cloudwatch.Alarm(this, 'sampleAlarm', {
      alarmName: 'sampleAlarm',
      metric: new aws_cloudwatch.Metric({
        namespace: 'StateMachinePublish',
        metricName: 'temperature',
        statistic: aws_cloudwatch.Statistic.MAXIMUM,
        period: Duration.minutes(1),
      }),
      evaluationPeriods: 1,
      threshold: 5,
      comparisonOperator:
        aws_cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
    });

    //SNS Topic
    const cloudwatchAlarmNotifyTopic = new aws_sns.Topic(
      this,
      'cloudwatchAlarmNotifyTopic'
    );

    //Alarmアクション追加
    sampleAlarm.addAlarmAction(
      new aws_cloudwatch_actions.SnsAction(cloudwatchAlarmNotifyTopic)
    );

    //Lambda関数
    const sampleFunc = new aws_lambda_nodejs.NodejsFunction(
      this,
      'sampleFunc',
      {
        functionName: 'sampleFunc',
        entry: 'src/lambda/handler.ts',
      }
    );

    //Lambdaイベントソース追加
    sampleFunc.addEventSource(
      new aws_lambda_event_sources.SnsEventSource(cloudwatchAlarmNotifyTopic)
    );
  }
}

下記はLambdaコードです。この中でアラーム発生時に発行されるイベントメッセージをパースし、コンソール出力しています。

src/lambda/handler.ts

interface Event {
  Records: { Sns: { Message: string } }[];
}

export const handler = (event: Event): void => {
  const message = JSON.parse(event.Records[0].Sns.Message);
  console.log(message);
};

CDK Deployして実装をデプロイします。

アラームを発生させてデータポイントが含まれたメッセージを取得してみる

CloudWatch metricのデータポイントの値として1をPutしてアラームを発生させます。

metric.json

[
  {
    "MetricName": "temperature",
    "Value": 1,
    "Unit": "Count"
  }
]
$ NAME_SPACE=sampleNameSpace
$ aws cloudwatch put-metric-data \
  --namespace $NAME_SPACE \
  --metric-data file://metric.json

$ ALARM_NAME=sampleAlarm
$ aws cloudwatch set-alarm-state \
  --alarm-name $alarmName \
  --state-value "ALARM" \
  --state-reason "test"

アラームが発生しました。

するとLambdaが起動し、CloudWatch Logsには次のログが出力されていました。このうちNewStateReasonフィールドに直近のデータポイントの情報(1 datapoint [1.0 (04/06/22 15:04:00)])が入っているのが確認できます。

{
  AlarmName: 'sampleAlarm',
  AlarmDescription: null,
  AWSAccountId: 'xxxxxxxxxxxx',
  AlarmConfigurationUpdatedTimestamp: '2022-06-04T15:01:50.499+0000',
  NewStateValue: 'ALARM',
  NewStateReason: 'Threshold Crossed: 1 datapoint [1.0 (04/06/22 15:04:00)] was less than or equal to the threshold (5.0).',
  StateChangeTime: '2022-06-04T15:05:18.979+0000',
  Region: 'Asia Pacific (Tokyo)',
  AlarmArn: 'arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:sampleAlarm',
  OldStateValue: 'INSUFFICIENT_DATA',
  OKActions: [],
  AlarmActions: [
    'arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:ProcessStack-cloudwatchAlarmNotifyTopic85FD23D4-VAGGJ15VXMNK'
  ],
  InsufficientDataActions: [],
  Trigger: {
    MetricName: 'temperature',
    Namespace: 'sampleNameSpace',
    StatisticType: 'Statistic',
    Statistic: 'MAXIMUM',
    Unit: null,
    Dimensions: [],
    Period: 60,
    EvaluationPeriods: 1,
    ComparisonOperator: 'LessThanOrEqualToThreshold',
    Threshold: 5,
    TreatMissingData: '',
    EvaluateLowSampleCountPercentile: ''
  }
}

上記は単一のデータポイントが評価対象の場合です。では評価期間を変更して、複数のデータポイントを評価対象とした場合はどうなるでしょうか。

CloudWatch Alarmの評価期間を1から3に変更します。CDK Deployして変更を反映します。

lib/process-stack.ts

    //CloudWatch Alarm
    const sampleAlarm = new aws_cloudwatch.Alarm(this, 'sampleAlarm', {
      alarmName: 'sampleAlarm',
      metric: new aws_cloudwatch.Metric({
        namespace: 'sampleNameSpace',
        metricName: 'temperature',
        statistic: aws_cloudwatch.Statistic.MAXIMUM,
        period: Duration.minutes(1),
      }),
      evaluationPeriods: 3,
      threshold: 5,
      comparisonOperator:
        aws_cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
    });

複数のデータポイントをPutし、アラームを発生させます。

すると次は2 datapoints [2.0 (04/06/22 15:41:00), 4.0 (04/06/22 15:39:00)]と2つのデータポイントの値が取得できています。前方のデータポイントが直近のもののようですね。

{
  AlarmName: 'sampleAlarm',
  AlarmDescription: null,
  AWSAccountId: 'xxxxxxxxxxxx',
  AlarmConfigurationUpdatedTimestamp: '2022-06-04T15:33:59.042+0000',
  NewStateValue: 'ALARM',
  NewStateReason: 'Threshold Crossed: 2 datapoints [2.0 (04/06/22 15:41:00), 4.0 (04/06/22 15:39:00)] were less than or equal to the threshold (5.0).',
  StateChangeTime: '2022-06-04T15:42:08.298+0000',
  Region: 'Asia Pacific (Tokyo)',
  AlarmArn: 'arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:sampleAlarm',
  OldStateValue: 'INSUFFICIENT_DATA',
  OKActions: [],
  AlarmActions: [
    'arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:ProcessStack-cloudwatchAlarmNotifyTopic85FD23D4-VAGGJ15VXMNK'
  ],
  InsufficientDataActions: [],
  Trigger: {
    MetricName: 'temperature',
    Namespace: 'sampleNameSpace',
    StatisticType: 'Statistic',
    Statistic: 'MAXIMUM',
    Unit: null,
    Dimensions: [],
    Period: 60,
    EvaluationPeriods: 3,
    ComparisonOperator: 'LessThanOrEqualToThreshold',
    Threshold: 5,
    TreatMissingData: '',
    EvaluateLowSampleCountPercentile: ''
  }
}

NewStateReasonからデータポイント値を取り出す

newStateReasonはString型のため文字列処理をして直近のデータポイント値を取り出す必要があります。ちょっと無理やりですが次のようにすれば、含まれているデータポイントが1つまたは複数のいずれの場合でも直近のものを取り出せました。

> newStateReason="Threshold Crossed: 1 datapoint [1.0 (04/06/22 15:04:00)] was less than or equal to the threshold (5.0)."
> Number(
    newStateReason
      .split(']')[0]
      .split('[')[1]
      .split(',')[0]
      .split('(')[0]
      .trim()
  );
1

> newStateReason="Threshold Crossed: 2 datapoints [2.0 (04/06/22 15:41:00), 4.0 (04/06/22 15:39:00)] were less than or equal to the threshold (5.0)."
> Number(
    newStateReason
      .split(']')[0]
      .split('[')[1]
      .split(',')[0]
      .split('(')[0]
      .trim()
  );
2

参考

以上