Skip to content

Commit d6bf218

Browse files
committed
feat(iot): add Action to change a CloudWatch alarm
1 parent cd3f24e commit d6bf218

File tree

6 files changed

+370
-0
lines changed

6 files changed

+370
-0
lines changed

Diff for: packages/@aws-cdk/aws-iot-actions/README.md

+35
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,40 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', {
149150
});
150151
```
151152

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

154189
The code snippet below creates an AWS IoT Rule that put records to Put records
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 CloudWatchAlarmActionProps extends CommonActionProps {
11+
/**
12+
* The reason for the alarm change.
13+
*/
14+
readonly stateReason: string;
15+
/**
16+
* The value of the alarm state.
17+
*/
18+
readonly stateValue: string;
19+
}
20+
21+
/**
22+
* The action to change a CloudWatch alarm state.
23+
*/
24+
export class CloudWatchAlarmAction implements iot.IAction {
25+
constructor(
26+
private readonly alarm: cloudwatch.IAlarm,
27+
private readonly props: CloudWatchAlarmActionProps,
28+
) {
29+
}
30+
31+
bind(topicRule: iot.ITopicRule): iot.ActionConfig {
32+
const role = this.props.role ?? singletonActionRole(topicRule);
33+
role.addToPrincipalPolicy(new iam.PolicyStatement({
34+
actions: ['cloudwatch:SetAlarmState'],
35+
resources: [this.alarm.alarmArn],
36+
}));
37+
38+
return {
39+
configuration: {
40+
cloudwatchAlarm: {
41+
alarmName: this.alarm.alarmName,
42+
roleArn: role.roleArn,
43+
stateReason: this.props.stateReason,
44+
stateValue: this.props.stateValue,
45+
},
46+
},
47+
};
48+
}
49+
}

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

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

0 commit comments

Comments
 (0)