aws cdkで複合アラームを作成してみたメモ

## 概要
[cdkでsnsのtopicとsubscriptionを作ってlambdaからpublishしたメモ](https://qiita.com/hibohiboo/items/da71e09c5d729f8aacbc)で作成したメール送付Lambdaを[複合アラーム]()に繋いでみる。

[前回との差分](https://github.com/hibohiboo/aws-cdk-v2/commit/5ed0b59229e6a8cc07eb34737bdcae9675369375)

## コード

```ts:cdk/lib/AlarmSNSStack.ts
import { Aspects, CfnOutput, Duration, Stack, StackProps, Tag } from "aws-cdk-lib";
import { Alarm, AlarmProps, AlarmRule, AlarmState, ComparisonOperator, CompositeAlarm, Metric, MetricProps, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
import { Rule } from "aws-cdk-lib/aws-events";
import { LambdaFunction } from "aws-cdk-lib/aws-events-targets";
import { Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Topic } from "aws-cdk-lib/aws-sns";
import { EmailSubscription } from "aws-cdk-lib/aws-sns-subscriptions";
import { Construct } from "constructs";

interface AlarmSNSStackProps extends StackProps { emailAdress: string }

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

    // ↓↓ 前回と同じ部分
    const topicName = 'topic'
    const snsTopic = new Topic(this, topicName, { displayName: topicName, topicName, fifo: false });
    snsTopic.addSubscription(new EmailSubscription(props.emailAdress));
    const mailSenderLambda = new NodejsFunction(this, 'publishLambda', {
      runtime: Runtime.NODEJS_18_X,
      entry: `../src/handler/eventBridge/sendMail.ts`,
      environment: { snsTopic: topicName },
    })
    snsTopic.grantPublish(mailSenderLambda)
    new CfnOutput(this, `sns-confirm-subscription`, {
      value: `aws sns confirm-subscription --topic-arn ${snsTopic.topicArn} --authenticate-on-unsubscribe true --region ${props.env?.region} --profile produser --token xxxCopyAndPasteTokenfromMailxxx`,
    })
    new CfnOutput(this, `sns-list`, {
      value: `aws sns list-subscriptions-by-topic --topic-arn ${snsTopic.topicArn} --profile produser`,
    })
    // ↑↑ 前回と同じ部分

    const alarmProp: Omit<AlarmProps, 'metric'> = {
      threshold: 5, // データポイントのメトリックのvalueのしきい値
      comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
      datapointsToAlarm: 2, // x分以内のyデータポイント: y
      evaluationPeriods: 3, // x分以内のyデータポイント: x
      treatMissingData: TreatMissingData.NOT_BREACHING,
      actionsEnabled: false,
    } as const;
    const metricProps: MetricProps = {
      namespace: 'Test',
      metricName: 'TestCount',
      period: Duration.minutes(1),
      statistic: 'Sum'
    } as const;
    const alarmRules = ['test1', 'test2'].map(alarmName => {
      const metric = new Metric({ ...metricProps, dimensionsMap: { Target: alarmName } });
      const alarm = new Alarm(this, alarmName, { ...alarmProp, alarmName, metric });
      return AlarmRule.fromAlarm(alarm, AlarmState.ALARM)
    });
    const compositeAlarmName = 'composite-alarm';
    const compositeAlarm = new CompositeAlarm(this, compositeAlarmName, { compositeAlarmName, alarmRule: AlarmRule.anyOf(...alarmRules) });
    const rule = new Rule(this, 'composit-rule', { eventPattern: { resources: [compositeAlarm.alarmArn], detail: { state: { value: ['ALARM', 'OK'] } } } });
    rule.addTarget(new LambdaFunction(mailSenderLambda))

    Aspects.of(this).add(new Tag('Stack', 'AlarmSNSStack'));
  }
}
```

前回はただのHandlerだったが、今回は`EventBridgeHandler`となる。
型の2つ目は自作する必要があったので、取得した[Event](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/cloudwatch-and-eventbridge.html)から作成した。

```ts:src/domain/eventBridge/types.ts
import { EventBridgeHandler } from "aws-lambda"

export type AlarmStateChangeHandler = EventBridgeHandler<"CloudWatch Alarm State Change", Detail, void>;

type StateValue = 'OK' | 'ALARM' | 'INSUFFICIENT';

interface Detail {
  alarmName: string
  state: State
  previousState: PreviousState
  configuration: Configuration
}

interface State {
  value: StateValue
  reason: string //  'arn:aws:cloudwatch:ap-northeast-1:0000000000:alarm:test2 transitioned to OK at Thursday 22 June, 2023 14:26:01 UTC',
  reasonData: string // '{"triggeringAlarms":[{"arn":"arn:aws:cloudwatch:ap-northeast-1:0000000000:alarm:test2","state":{"value":"OK","timestamp":"2023-06-22T14:26:01.928+0000"}},{"arn":"arn:aws:cloudwatch:ap-northeast-1:0000000000:alarm:test1","state":{"value":"OK","timestamp":"2023-06-22T14:25:30.174+0000"}}]}'
  timestamp: string // yyyy-MM-dd'T'HH:mm:ss.SSS+0000
}

interface PreviousState {
  value: StateValue
  reason: string
  reasonData: string
  timestamp: string
}

interface Configuration {
  alarmRule: string // '(ALARM("arn:aws:cloudwatch:ap-northeast-1:0000000000:alarm:test1") OR ALARM("arn:aws:cloudwatch:ap-northeast-1:0000000000:alarm:test2"))'
}
```

## アラームの確認
状態を確認するLambdaを作成した。

```diff:cdk/lib/alarm-sns-stack.ts
+    const describeLambda = new NodejsFunction(this, 'describeLambda', {
+      runtime: Runtime.NODEJS_18_X,
+      entry: `../src/handler/invoke/describeAlarm.ts`,
+      environment: { compositeAlarmName },
+      initialPolicy: [new PolicyStatement({ actions: ['cloudwatch:DescribeAlarms'], resources: ['*'] })]
+    });
```

複合アラームを取得するときは`AlarmTypes`の明示が必要なことに注意。
[省略するとメトリックアラームのみ返却される](https://docs.aws.amazon.com/cli/latest/reference/cloudwatch/describe-alarms.html#options)


```ts:src/handler/invoke/describeAlarm.ts
import { CloudWatchClient, DescribeAlarmsCommand } from "@aws-sdk/client-cloudwatch";
import { Handler } from "aws-lambda";

export const handler: Handler = async (event, context) => {
  const envList = ['AWS_REGION', 'compositeAlarmName'] as const;
  envList.forEach(k => { if (!process.env[k]) throw new Error(`${k} environment required`) });
  const processEnv = process.env as Record<typeof envList[number], string>;

  const client = new CloudWatchClient({ region: processEnv.AWS_REGION });
  const compositeAlarmCommand = new DescribeAlarmsCommand({
    AlarmNames: [processEnv.compositeAlarmName],
    AlarmTypes: ['CompositeAlarm']
  })
  const compositeResult = await client.send(compositeAlarmCommand);
  console.log('composite', compositeResult);

  const alarmCommand = new DescribeAlarmsCommand({
    AlarmNamePrefix: 'test'
  });
  const alarmResult = await client.send(alarmCommand);
  console.log('alarms', alarmResult);
}
```

[この時点のソース](https://github.com/hibohiboo/aws-cdk-v2/blob/04d1f8ec478749f7408aa66c817469bce3096d02/cdk/lib/alarm-sns-stack.ts)

## 参考
[cdk CompositeAlarm ](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.CompositeAlarm.html)
[Amazon CloudWatch Metrics & Alarms for typical Serverless application using AWS CDK: Part 2](https://klika-tech.com/blog/2023/04/12/amazon-cloudwatch-metrics-alarms-for-typical-serverless-application-using-aws-cdk-part-2/)
[CloudWatchの複合アラームを仕組みを確認しつつ使ってみた](https://dev.classmethod.jp/articles/i-tried-using-cloudwatchs-composite-alarm-while-checking-how-it-works/)
[CloudWatch複合アラームでELBの5XXをいい感じに検知しようとしたらうまくいかなかった話](https://go-to-k.hatenablog.com/entry/2022/02/21/075000)