概要
cdkでsnsのtopicとsubscriptionを作ってlambdaからpublishしたメモで作成したメール送付Lambdaを複合アラームに繋いでみる。
コード
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から作成した。
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を作成した。
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の明示が必要なことに注意。
省略するとメトリックアラームのみ返却される
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);
}
参考
cdk CompositeAlarm
Amazon CloudWatch Metrics & Alarms for typical Serverless application using AWS CDK: Part 2
CloudWatchの複合アラームを仕組みを確認しつつ使ってみた
CloudWatch複合アラームでELBの5XXをいい感じに検知しようとしたらうまくいかなかった話