From d6bf2189a608236ff512f5155d3bab01cb28706e Mon Sep 17 00:00:00 2001 From: Mingjie Shao Date: Wed, 8 Dec 2021 21:50:10 +0000 Subject: [PATCH] feat(iot): add Action to change a CloudWatch alarm --- packages/@aws-cdk/aws-iot-actions/README.md | 35 ++++ .../lib/cloudwatch-alarm-action.ts | 49 ++++++ .../@aws-cdk/aws-iot-actions/lib/index.ts | 1 + .../cloudwatch-alarm-action.test.ts | 156 ++++++++++++++++++ ...nteg.cloudwatch-alarm-action.expected.json | 92 +++++++++++ .../integ.cloudwatch-alarm-action.ts | 37 +++++ 6 files changed, 370 insertions(+) create mode 100644 packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-alarm-action.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-alarm-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-alarm-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-alarm-action.ts diff --git a/packages/@aws-cdk/aws-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md index bf2955757abe8..300282842d817 100644 --- a/packages/@aws-cdk/aws-iot-actions/README.md +++ b/packages/@aws-cdk/aws-iot-actions/README.md @@ -25,6 +25,7 @@ Currently supported are: - Put objects to a S3 bucket - Put logs to CloudWatch Logs - Capture CloudWatch metrics +- Change state for a CloudWatch alarm - Put records to Kinesis Data Firehose stream ## Invoke a Lambda function @@ -149,6 +150,40 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', { }); ``` +## Change state for a CloudWatch alarm + +The code snippet below creates an AWS IoT Rule that changes a CloudWatch alarm +when it is triggered. + +```ts +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iot from '@aws-cdk/aws-iot'; +import * as actions from '@aws-cdk/aws-iot-actions'; + +const metric = new cloudwatch.Metric({ + namespace: 'MyNamespace', + metricName: 'MyMetric', + dimensions: { MyDimension: 'MyDimensionValue' }, +}); + +const alarm = new cloudwatch.Alarm(this, 'MyAlarm', { + metric: metric, + threshold: 100, + evaluationPeriods: 3, + datapointsToAlarm: 2, +}); + +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + actions: [ + new actions.CloudWatchAlarmAction(alarm, { + stateReason: 'AWS Iot Rule action is triggered', + stateValue: 'ALARM', + }), + ] +}); +``` + ## Put records to Kinesis Data Firehose stream The code snippet below creates an AWS IoT Rule that put records to Put records diff --git a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-alarm-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-alarm-action.ts new file mode 100644 index 0000000000000..bb0aca454c0af --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-alarm-action.ts @@ -0,0 +1,49 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import { CommonActionProps } from './common-action-props'; +import { singletonActionRole } from './private/role'; + +/** + * Configuration properties of an action for CloudWatch alarm. + */ +export interface CloudWatchAlarmActionProps extends CommonActionProps { + /** + * The reason for the alarm change. + */ + readonly stateReason: string; + /** + * The value of the alarm state. + */ + readonly stateValue: string; +} + +/** + * The action to change a CloudWatch alarm state. + */ +export class CloudWatchAlarmAction implements iot.IAction { + constructor( + private readonly alarm: cloudwatch.IAlarm, + private readonly props: CloudWatchAlarmActionProps, + ) { + } + + bind(topicRule: iot.ITopicRule): iot.ActionConfig { + const role = this.props.role ?? singletonActionRole(topicRule); + role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['cloudwatch:SetAlarmState'], + resources: [this.alarm.alarmArn], + })); + + return { + configuration: { + cloudwatchAlarm: { + alarmName: this.alarm.alarmName, + roleArn: role.roleArn, + stateReason: this.props.stateReason, + stateValue: this.props.stateValue, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/index.ts b/packages/@aws-cdk/aws-iot-actions/lib/index.ts index 4ad9c1d2a1fb6..83f4295bb37d5 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/index.ts @@ -1,5 +1,6 @@ export * from './cloudwatch-logs-action'; export * from './cloudwatch-put-metric-action'; +export * from './cloudwatch-alarm-action'; export * from './common-action-props'; export * from './firehose-stream-action'; export * from './lambda-function-action'; diff --git a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-alarm-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-alarm-action.test.ts new file mode 100644 index 0000000000000..a1aa6403dd493 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-alarm-action.test.ts @@ -0,0 +1,156 @@ +import { Template, Match } from '@aws-cdk/assertions'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as cdk from '@aws-cdk/core'; +import { CloudWatchAlarmAction, CloudWatchAlarmActionProps } from '../../lib/cloudwatch-alarm-action'; + +test('Default cloudwatch alarm action', () => { + // Given + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, stateReason, stateValue FROM 'device/+/data'"), + }); + const alarmArn = 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm'; + const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', alarmArn); + const cloudWatchAlarmActionProps: CloudWatchAlarmActionProps = { + stateReason: '${stateReason}', + stateValue: '${stateValue}', + }; + + // When + topicRule.addAction(new CloudWatchAlarmAction(alarm, cloudWatchAlarmActionProps)); + + // Then + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchAlarm: { + AlarmName: 'MyAlarm', + RoleArn: { + 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'], + }, + StateReason: '${stateReason}', + StateValue: '${stateValue}', + }, + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'cloudwatch:SetAlarmState', + Effect: 'Allow', + Resource: alarmArn, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [{ Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }], + }); +}); + +test('can set stateReason', () => { + // Given + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, stateReason, stateValue FROM 'device/+/data'"), + }); + const alarmArn = 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm'; + const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', alarmArn); + const cloudWatchAlarmActionProps: CloudWatchAlarmActionProps = { + stateReason: 'Test SetAlarmState', + stateValue: '${stateValue}', + }; + + // When + topicRule.addAction(new CloudWatchAlarmAction(alarm, cloudWatchAlarmActionProps)); + + // Then + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ CloudwatchAlarm: { StateReason: 'Test SetAlarmState' } }), + ], + }, + }); +}); + +test('can set stateValue', () => { + // Given + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, stateReason, stateValue FROM 'device/+/data'"), + }); + const alarmArn = 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm'; + const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', alarmArn); + const cloudWatchAlarmActionProps: CloudWatchAlarmActionProps = { + stateReason: '${stateReason}', + stateValue: 'ALARM', + }; + + // When + topicRule.addAction(new CloudWatchAlarmAction(alarm, cloudWatchAlarmActionProps)); + + // Then + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ CloudwatchAlarm: { StateValue: 'ALARM' } }), + ], + }, + }); +}); + +test('can set role', () => { + // Given + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, stateReason, stateValue FROM 'device/+/data'"), + }); + const alarmArn = 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm'; + const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', alarmArn); + const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest'); + const cloudWatchAlarmActionProps: CloudWatchAlarmActionProps = { + stateReason: '${stateReason}', + stateValue: '${stateValue}', + role: role, + }; + + // When + topicRule.addAction(new CloudWatchAlarmAction(alarm, cloudWatchAlarmActionProps)); + + // Then + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + Match.objectLike({ CloudwatchAlarm: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }), + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyRolePolicy64AB00A5', + Roles: ['ForTest'], + }); +}); diff --git a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-alarm-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-alarm-action.expected.json new file mode 100644 index 0000000000000..b791f35f48ca4 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-alarm-action.expected.json @@ -0,0 +1,92 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "CloudwatchAlarm": { + "AlarmName": { + "Ref": "MyAlarm696658B6" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "StateReason": "Test reason", + "StateValue": "ALARM" + } + } + ], + "AwsIotSqlVersion": "2016-03-23", + "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" + } + } + }, + "TopicRuleTopicRuleActionRole246C4F77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "cloudwatch:SetAlarmState", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyAlarm696658B6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyAlarm696658B6": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "DatapointsToAlarm": 2, + "Dimensions": [ + { + "Name": "MyDimension", + "Value": "MyDimensionValue" + } + ], + "MetricName": "MyMetric", + "Namespace": "MyNamespace", + "Period": 300, + "Statistic": "Average", + "Threshold": 100 + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-alarm-action.ts b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-alarm-action.ts new file mode 100644 index 0000000000000..791f2ca789f27 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-alarm-action.ts @@ -0,0 +1,37 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iot from '@aws-cdk/aws-iot'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + + const metric = new cloudwatch.Metric({ + namespace: 'MyNamespace', + metricName: 'MyMetric', + dimensions: { MyDimension: 'MyDimensionValue' }, + }); + + const alarm = new cloudwatch.Alarm(this, 'MyAlarm', { + metric: metric, + threshold: 100, + evaluationPeriods: 3, + datapointsToAlarm: 2, + }); + + topicRule.addAction(new actions.CloudWatchAlarmAction(alarm, { + stateReason: 'Test reason', + stateValue: 'ALARM', + })); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); \ No newline at end of file