diff --git a/packages/@aws-cdk/aws-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md index bf2955757abe8..cc1844bbcc909 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,38 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', { }); ``` +## Change the state of an Amazon CloudWatch alarm + +The code snippet below creates an AWS IoT Rule that changes the state of an Amazon 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.CloudWatchSetAlarmStateAction(alarm, { + reason: 'AWS Iot Rule action is triggered', + alarmStateToSet: cloudwatch.AlarmState.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-set-alarm-state-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-set-alarm-state-action.ts new file mode 100644 index 0000000000000..07e42c04a2204 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-set-alarm-state-action.ts @@ -0,0 +1,52 @@ +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 CloudWatchSetAlarmStateActionProps extends CommonActionProps { + /** + * The reason for the alarm change. + * + * @default None + */ + readonly reason?: string; + + /** + * The value of the alarm state to set. + */ + readonly alarmStateToSet: cloudwatch.AlarmState; +} + +/** + * The action to change the state of an Amazon CloudWatch alarm. + */ +export class CloudWatchSetAlarmStateAction implements iot.IAction { + constructor( + private readonly alarm: cloudwatch.IAlarm, + private readonly props: CloudWatchSetAlarmStateActionProps, + ) { + } + + 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.reason ?? `Set state of '${this.alarm.alarmName}' to '${this.props.alarmStateToSet}'`, + stateValue: this.props.alarmStateToSet, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/index.ts b/packages/@aws-cdk/aws-iot-actions/lib/index.ts index 4ad9c1d2a1fb6..498f29b569fb8 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-set-alarm-state-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-set-alarm-state-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-set-alarm-state-action.test.ts new file mode 100644 index 0000000000000..6a89a7a175403 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/cloudwatch-set-alarm-state-action.test.ts @@ -0,0 +1,133 @@ +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 * as actions from '../../lib'; + +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 alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm'); + + // When + topicRule.addAction(new actions.CloudWatchSetAlarmStateAction(alarm, { + reason: 'Test reason', + alarmStateToSet: cloudwatch.AlarmState.ALARM, + })); + + // Then + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchAlarm: { + AlarmName: 'MyAlarm', + RoleArn: { + 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'], + }, + StateReason: 'Test reason', + StateValue: cloudwatch.AlarmState.ALARM, + }, + }, + ], + }, + }); + + 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: alarm.alarmArn, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [{ Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }], + }); +}); + +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'"), + }); + + // When + topicRule.addAction(new actions.CloudWatchSetAlarmStateAction( + cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm'), + { + reason: '${stateReason}', + alarmStateToSet: cloudwatch.AlarmState.ALARM, + role: iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest'), + }, + )); + + // 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'], + }); +}); + +test('set default reason', () => { + // 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 alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm'); + + // When + topicRule.addAction(new actions.CloudWatchSetAlarmStateAction(alarm, { + alarmStateToSet: cloudwatch.AlarmState.ALARM, + })); + + // Then + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchAlarm: { + AlarmName: 'MyAlarm', + RoleArn: { + 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'], + }, + StateReason: "Set state of 'MyAlarm' to 'ALARM'", + StateValue: cloudwatch.AlarmState.ALARM, + }, + }, + ], + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-set-alarm-state-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-set-alarm-state-action.expected.json new file mode 100644 index 0000000000000..750cac6d39bbf --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-set-alarm-state-action.expected.json @@ -0,0 +1,103 @@ +{ + "Resources": { + "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 + } + }, + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "CloudwatchAlarm": { + "AlarmName": { + "Ref": "MyAlarm696658B6" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "StateReason": { + "Fn::Join": [ + "", + [ + "Set state of '", + { + "Ref": "MyAlarm696658B6" + }, + "' to 'ALARM'" + ] + ] + }, + "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" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-set-alarm-state-action.ts b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-set-alarm-state-action.ts new file mode 100644 index 0000000000000..e9a6c1304f7d2 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-set-alarm-state-action.ts @@ -0,0 +1,34 @@ +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 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'"), + }); + + topicRule.addAction(new actions.CloudWatchSetAlarmStateAction(alarm, { + alarmStateToSet: cloudwatch.AlarmState.ALARM, + })); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); \ No newline at end of file