Skip to content

Commit de2369c

Browse files
authored
feat(iot): add Action to set a CloudWatch alarm (#18021)
Adding IoT Rule action for CloudWatch alarm. Fixes #17705 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 002202f commit de2369c

6 files changed

+356
-0
lines changed

packages/@aws-cdk/aws-iot-actions/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Currently supported are:
2525
- Put objects to a S3 bucket
2626
- Put logs to CloudWatch Logs
2727
- Capture CloudWatch metrics
28+
- Change state for a CloudWatch alarm
2829
- Put records to Kinesis Data Firehose stream
2930

3031
## Invoke a Lambda function
@@ -149,6 +150,38 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', {
149150
});
150151
```
151152

153+
## Change the state of an Amazon CloudWatch alarm
154+
155+
The code snippet below creates an AWS IoT Rule that changes the state of an Amazon CloudWatch alarm when it is triggered:
156+
157+
```ts
158+
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
159+
import * as iot from '@aws-cdk/aws-iot';
160+
import * as actions from '@aws-cdk/aws-iot-actions';
161+
162+
const metric = new cloudwatch.Metric({
163+
namespace: 'MyNamespace',
164+
metricName: 'MyMetric',
165+
dimensions: { MyDimension: 'MyDimensionValue' },
166+
});
167+
const alarm = new cloudwatch.Alarm(this, 'MyAlarm', {
168+
metric: metric,
169+
threshold: 100,
170+
evaluationPeriods: 3,
171+
datapointsToAlarm: 2,
172+
});
173+
174+
const topicRule = new iot.TopicRule(this, 'TopicRule', {
175+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"),
176+
actions: [
177+
new actions.CloudWatchSetAlarmStateAction(alarm, {
178+
reason: 'AWS Iot Rule action is triggered',
179+
alarmStateToSet: cloudwatch.AlarmState.ALARM,
180+
}),
181+
],
182+
});
183+
```
184+
152185
## Put records to Kinesis Data Firehose stream
153186

154187
The code snippet below creates an AWS IoT Rule that put records to Put records
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
2+
import * as iam from '@aws-cdk/aws-iam';
3+
import * as iot from '@aws-cdk/aws-iot';
4+
import { CommonActionProps } from './common-action-props';
5+
import { singletonActionRole } from './private/role';
6+
7+
/**
8+
* Configuration properties of an action for CloudWatch alarm.
9+
*/
10+
export interface CloudWatchSetAlarmStateActionProps extends CommonActionProps {
11+
/**
12+
* The reason for the alarm change.
13+
*
14+
* @default None
15+
*/
16+
readonly reason?: string;
17+
18+
/**
19+
* The value of the alarm state to set.
20+
*/
21+
readonly alarmStateToSet: cloudwatch.AlarmState;
22+
}
23+
24+
/**
25+
* The action to change the state of an Amazon CloudWatch alarm.
26+
*/
27+
export class CloudWatchSetAlarmStateAction implements iot.IAction {
28+
constructor(
29+
private readonly alarm: cloudwatch.IAlarm,
30+
private readonly props: CloudWatchSetAlarmStateActionProps,
31+
) {
32+
}
33+
34+
bind(topicRule: iot.ITopicRule): iot.ActionConfig {
35+
const role = this.props.role ?? singletonActionRole(topicRule);
36+
role.addToPrincipalPolicy(new iam.PolicyStatement({
37+
actions: ['cloudwatch:SetAlarmState'],
38+
resources: [this.alarm.alarmArn],
39+
}));
40+
41+
return {
42+
configuration: {
43+
cloudwatchAlarm: {
44+
alarmName: this.alarm.alarmName,
45+
roleArn: role.roleArn,
46+
stateReason: this.props.reason ?? `Set state of '${this.alarm.alarmName}' to '${this.props.alarmStateToSet}'`,
47+
stateValue: this.props.alarmStateToSet,
48+
},
49+
},
50+
};
51+
}
52+
}

packages/@aws-cdk/aws-iot-actions/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './cloudwatch-logs-action';
22
export * from './cloudwatch-put-metric-action';
3+
export * from './cloudwatch-set-alarm-state-action';
34
export * from './common-action-props';
45
export * from './firehose-stream-action';
56
export * from './lambda-function-action';
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Template, Match } from '@aws-cdk/assertions';
2+
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
3+
import * as iam from '@aws-cdk/aws-iam';
4+
import * as iot from '@aws-cdk/aws-iot';
5+
import * as cdk from '@aws-cdk/core';
6+
import * as actions from '../../lib';
7+
8+
test('Default cloudwatch alarm action', () => {
9+
// Given
10+
const stack = new cdk.Stack();
11+
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
12+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, stateReason, stateValue FROM 'device/+/data'"),
13+
});
14+
const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm');
15+
16+
// When
17+
topicRule.addAction(new actions.CloudWatchSetAlarmStateAction(alarm, {
18+
reason: 'Test reason',
19+
alarmStateToSet: cloudwatch.AlarmState.ALARM,
20+
}));
21+
22+
// Then
23+
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
24+
TopicRulePayload: {
25+
Actions: [
26+
{
27+
CloudwatchAlarm: {
28+
AlarmName: 'MyAlarm',
29+
RoleArn: {
30+
'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'],
31+
},
32+
StateReason: 'Test reason',
33+
StateValue: cloudwatch.AlarmState.ALARM,
34+
},
35+
},
36+
],
37+
},
38+
});
39+
40+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', {
41+
AssumeRolePolicyDocument: {
42+
Statement: [
43+
{
44+
Action: 'sts:AssumeRole',
45+
Effect: 'Allow',
46+
Principal: {
47+
Service: 'iot.amazonaws.com',
48+
},
49+
},
50+
],
51+
Version: '2012-10-17',
52+
},
53+
});
54+
55+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
56+
PolicyDocument: {
57+
Statement: [
58+
{
59+
Action: 'cloudwatch:SetAlarmState',
60+
Effect: 'Allow',
61+
Resource: alarm.alarmArn,
62+
},
63+
],
64+
Version: '2012-10-17',
65+
},
66+
PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7',
67+
Roles: [{ Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }],
68+
});
69+
});
70+
71+
test('can set role', () => {
72+
// Given
73+
const stack = new cdk.Stack();
74+
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
75+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, stateReason, stateValue FROM 'device/+/data'"),
76+
});
77+
78+
// When
79+
topicRule.addAction(new actions.CloudWatchSetAlarmStateAction(
80+
cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm'),
81+
{
82+
reason: '${stateReason}',
83+
alarmStateToSet: cloudwatch.AlarmState.ALARM,
84+
role: iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest'),
85+
},
86+
));
87+
88+
// Then
89+
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
90+
TopicRulePayload: {
91+
Actions: [
92+
Match.objectLike({ CloudwatchAlarm: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }),
93+
],
94+
},
95+
});
96+
97+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
98+
PolicyName: 'MyRolePolicy64AB00A5',
99+
Roles: ['ForTest'],
100+
});
101+
});
102+
103+
test('set default reason', () => {
104+
// Given
105+
const stack = new cdk.Stack();
106+
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
107+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, stateReason, stateValue FROM 'device/+/data'"),
108+
});
109+
const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm');
110+
111+
// When
112+
topicRule.addAction(new actions.CloudWatchSetAlarmStateAction(alarm, {
113+
alarmStateToSet: cloudwatch.AlarmState.ALARM,
114+
}));
115+
116+
// Then
117+
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
118+
TopicRulePayload: {
119+
Actions: [
120+
{
121+
CloudwatchAlarm: {
122+
AlarmName: 'MyAlarm',
123+
RoleArn: {
124+
'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'],
125+
},
126+
StateReason: "Set state of 'MyAlarm' to 'ALARM'",
127+
StateValue: cloudwatch.AlarmState.ALARM,
128+
},
129+
},
130+
],
131+
},
132+
});
133+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"Resources": {
3+
"MyAlarm696658B6": {
4+
"Type": "AWS::CloudWatch::Alarm",
5+
"Properties": {
6+
"ComparisonOperator": "GreaterThanOrEqualToThreshold",
7+
"EvaluationPeriods": 3,
8+
"DatapointsToAlarm": 2,
9+
"Dimensions": [
10+
{
11+
"Name": "MyDimension",
12+
"Value": "MyDimensionValue"
13+
}
14+
],
15+
"MetricName": "MyMetric",
16+
"Namespace": "MyNamespace",
17+
"Period": 300,
18+
"Statistic": "Average",
19+
"Threshold": 100
20+
}
21+
},
22+
"TopicRule40A4EA44": {
23+
"Type": "AWS::IoT::TopicRule",
24+
"Properties": {
25+
"TopicRulePayload": {
26+
"Actions": [
27+
{
28+
"CloudwatchAlarm": {
29+
"AlarmName": {
30+
"Ref": "MyAlarm696658B6"
31+
},
32+
"RoleArn": {
33+
"Fn::GetAtt": [
34+
"TopicRuleTopicRuleActionRole246C4F77",
35+
"Arn"
36+
]
37+
},
38+
"StateReason": {
39+
"Fn::Join": [
40+
"",
41+
[
42+
"Set state of '",
43+
{
44+
"Ref": "MyAlarm696658B6"
45+
},
46+
"' to 'ALARM'"
47+
]
48+
]
49+
},
50+
"StateValue": "ALARM"
51+
}
52+
}
53+
],
54+
"AwsIotSqlVersion": "2016-03-23",
55+
"Sql": "SELECT topic(2) as device_id FROM 'device/+/data'"
56+
}
57+
}
58+
},
59+
"TopicRuleTopicRuleActionRole246C4F77": {
60+
"Type": "AWS::IAM::Role",
61+
"Properties": {
62+
"AssumeRolePolicyDocument": {
63+
"Statement": [
64+
{
65+
"Action": "sts:AssumeRole",
66+
"Effect": "Allow",
67+
"Principal": {
68+
"Service": "iot.amazonaws.com"
69+
}
70+
}
71+
],
72+
"Version": "2012-10-17"
73+
}
74+
}
75+
},
76+
"TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687": {
77+
"Type": "AWS::IAM::Policy",
78+
"Properties": {
79+
"PolicyDocument": {
80+
"Statement": [
81+
{
82+
"Action": "cloudwatch:SetAlarmState",
83+
"Effect": "Allow",
84+
"Resource": {
85+
"Fn::GetAtt": [
86+
"MyAlarm696658B6",
87+
"Arn"
88+
]
89+
}
90+
}
91+
],
92+
"Version": "2012-10-17"
93+
},
94+
"PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687",
95+
"Roles": [
96+
{
97+
"Ref": "TopicRuleTopicRuleActionRole246C4F77"
98+
}
99+
]
100+
}
101+
}
102+
}
103+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
2+
import * as iot from '@aws-cdk/aws-iot';
3+
import * as cdk from '@aws-cdk/core';
4+
import * as actions from '../../lib';
5+
6+
const app = new cdk.App();
7+
8+
class TestStack extends cdk.Stack {
9+
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
10+
super(scope, id, props);
11+
12+
const metric = new cloudwatch.Metric({
13+
namespace: 'MyNamespace',
14+
metricName: 'MyMetric',
15+
dimensions: { MyDimension: 'MyDimensionValue' },
16+
});
17+
const alarm = new cloudwatch.Alarm(this, 'MyAlarm', {
18+
metric: metric,
19+
threshold: 100,
20+
evaluationPeriods: 3,
21+
datapointsToAlarm: 2,
22+
});
23+
const topicRule = new iot.TopicRule(this, 'TopicRule', {
24+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"),
25+
});
26+
27+
topicRule.addAction(new actions.CloudWatchSetAlarmStateAction(alarm, {
28+
alarmStateToSet: cloudwatch.AlarmState.ALARM,
29+
}));
30+
}
31+
}
32+
33+
new TestStack(app, 'test-stack');
34+
app.synth();

0 commit comments

Comments
 (0)