Skip to content

Commit ec4187c

Browse files
authoredNov 24, 2021
feat(iot): add Action to capture CloudWatch metrics (#17503)
I'm trying to implement aws-iot L2 Constructs. This PR is one of steps after following PR: - #16681 (comment) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent f2bf322 commit ec4187c

11 files changed

+333
-4
lines changed
 

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

+25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Currently supported are:
2424
- Invoke a Lambda function
2525
- Put objects to a S3 bucket
2626
- Put logs to CloudWatch Logs
27+
- Capture CloudWatch metrics
2728
- Put records to Kinesis Data Firehose stream
2829

2930
## Invoke a Lambda function
@@ -123,6 +124,30 @@ new iot.TopicRule(this, 'TopicRule', {
123124
});
124125
```
125126

127+
## Capture CloudWatch metrics
128+
129+
The code snippet below creates an AWS IoT Rule that capture CloudWatch metrics
130+
when it is triggered.
131+
132+
```ts
133+
import * as iot from '@aws-cdk/aws-iot';
134+
import * as actions from '@aws-cdk/aws-iot-actions';
135+
136+
const topicRule = new iot.TopicRule(this, 'TopicRule', {
137+
sql: iot.IotSql.fromStringAsVer20160323(
138+
"SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'",
139+
),
140+
actions: [
141+
new actions.CloudWatchPutMetricAction({
142+
metricName: '${topic(2)}',
143+
metricNamespace: '${namespace}',
144+
metricUnit: '${unit}',
145+
metricValue: '${value}',
146+
metricTimestamp: '${timestamp}',
147+
}),
148+
],
149+
});
150+
```
126151

127152
## Put records to Kinesis Data Firehose stream
128153

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
2+
import * as iot from '@aws-cdk/aws-iot';
3+
import { CommonActionProps } from './common-action-props';
4+
import { singletonActionRole } from './private/role';
5+
6+
/**
7+
* Configuration properties of an action for CloudWatch metric.
8+
*/
9+
export interface CloudWatchPutMetricActionProps extends CommonActionProps {
10+
/**
11+
* The CloudWatch metric name.
12+
*
13+
* Supports substitution templates.
14+
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
15+
*/
16+
readonly metricName: string;
17+
18+
/**
19+
* The CloudWatch metric namespace name.
20+
*
21+
* Supports substitution templates.
22+
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
23+
*/
24+
readonly metricNamespace: string;
25+
26+
/**
27+
* A string that contains the timestamp, expressed in seconds in Unix epoch time.
28+
*
29+
* Supports substitution templates.
30+
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
31+
*
32+
* @default - none -- Defaults to the current Unix epoch time.
33+
*/
34+
readonly metricTimestamp?: string;
35+
36+
/**
37+
* The metric unit supported by CloudWatch.
38+
*
39+
* Supports substitution templates.
40+
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
41+
*/
42+
readonly metricUnit: string;
43+
44+
/**
45+
* A string that contains the CloudWatch metric value.
46+
*
47+
* Supports substitution templates.
48+
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
49+
*/
50+
readonly metricValue: string;
51+
}
52+
53+
/**
54+
* The action to capture an Amazon CloudWatch metric.
55+
*/
56+
export class CloudWatchPutMetricAction implements iot.IAction {
57+
constructor(private readonly props: CloudWatchPutMetricActionProps) {
58+
}
59+
60+
bind(rule: iot.ITopicRule): iot.ActionConfig {
61+
const role = this.props.role ?? singletonActionRole(rule);
62+
cloudwatch.Metric.grantPutMetricData(role);
63+
64+
return {
65+
configuration: {
66+
cloudwatchMetric: {
67+
metricName: this.props.metricName,
68+
metricNamespace: this.props.metricNamespace,
69+
metricTimestamp: this.props.metricTimestamp,
70+
metricUnit: this.props.metricUnit,
71+
metricValue: this.props.metricValue,
72+
roleArn: role.roleArn,
73+
},
74+
},
75+
};
76+
}
77+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './cloudwatch-logs-action';
2+
export * from './cloudwatch-put-metric-action';
23
export * from './common-action-props';
34
export * from './firehose-stream-action';
45
export * from './lambda-function-action';

‎packages/@aws-cdk/aws-iot-actions/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"jest": "^27.3.1"
8181
},
8282
"dependencies": {
83+
"@aws-cdk/aws-cloudwatch": "0.0.0",
8384
"@aws-cdk/aws-iam": "0.0.0",
8485
"@aws-cdk/aws-iot": "0.0.0",
8586
"@aws-cdk/aws-kinesisfirehose": "0.0.0",
@@ -92,6 +93,7 @@
9293
},
9394
"homepage": "https://github.com/aws/aws-cdk",
9495
"peerDependencies": {
96+
"@aws-cdk/aws-cloudwatch": "0.0.0",
9597
"@aws-cdk/aws-iam": "0.0.0",
9698
"@aws-cdk/aws-iot": "0.0.0",
9799
"@aws-cdk/aws-kinesisfirehose": "0.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { Template, Match } from '@aws-cdk/assertions';
2+
import * as iam from '@aws-cdk/aws-iam';
3+
import * as iot from '@aws-cdk/aws-iot';
4+
import * as cdk from '@aws-cdk/core';
5+
import * as actions from '../../lib';
6+
7+
test('Default cloudwatch metric action', () => {
8+
// GIVEN
9+
const stack = new cdk.Stack();
10+
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
11+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'"),
12+
});
13+
14+
// WHEN
15+
topicRule.addAction(
16+
new actions.CloudWatchPutMetricAction({
17+
metricName: '${topic(2)}',
18+
metricNamespace: '${namespace}',
19+
metricUnit: '${unit}',
20+
metricValue: '${value}',
21+
}),
22+
);
23+
24+
// THEN
25+
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
26+
TopicRulePayload: {
27+
Actions: [
28+
{
29+
CloudwatchMetric: {
30+
MetricName: '${topic(2)}',
31+
MetricNamespace: '${namespace}',
32+
MetricUnit: '${unit}',
33+
MetricValue: '${value}',
34+
RoleArn: {
35+
'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'],
36+
},
37+
},
38+
},
39+
],
40+
},
41+
});
42+
43+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', {
44+
AssumeRolePolicyDocument: {
45+
Statement: [
46+
{
47+
Action: 'sts:AssumeRole',
48+
Effect: 'Allow',
49+
Principal: {
50+
Service: 'iot.amazonaws.com',
51+
},
52+
},
53+
],
54+
Version: '2012-10-17',
55+
},
56+
});
57+
58+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
59+
PolicyDocument: {
60+
Statement: [
61+
{
62+
Action: 'cloudwatch:PutMetricData',
63+
Effect: 'Allow',
64+
Resource: '*',
65+
},
66+
],
67+
Version: '2012-10-17',
68+
},
69+
PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7',
70+
Roles: [{ Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }],
71+
});
72+
});
73+
74+
test('can set timestamp', () => {
75+
// GIVEN
76+
const stack = new cdk.Stack();
77+
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
78+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'"),
79+
});
80+
81+
// WHEN
82+
topicRule.addAction(
83+
new actions.CloudWatchPutMetricAction({
84+
metricName: '${topic(2)}',
85+
metricNamespace: '${namespace}',
86+
metricUnit: '${unit}',
87+
metricValue: '${value}',
88+
metricTimestamp: '${timestamp()}',
89+
}),
90+
);
91+
92+
// THEN
93+
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
94+
TopicRulePayload: {
95+
Actions: [
96+
Match.objectLike({ CloudwatchMetric: { MetricTimestamp: '${timestamp()}' } }),
97+
],
98+
},
99+
});
100+
});
101+
102+
test('can set role', () => {
103+
// GIVEN
104+
const stack = new cdk.Stack();
105+
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
106+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'"),
107+
});
108+
const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest');
109+
110+
// WHEN
111+
topicRule.addAction(
112+
new actions.CloudWatchPutMetricAction({
113+
metricName: '${topic(2)}',
114+
metricNamespace: '${namespace}',
115+
metricUnit: '${unit}',
116+
metricValue: '${value}',
117+
role,
118+
}),
119+
);
120+
121+
// THEN
122+
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
123+
TopicRulePayload: {
124+
Actions: [
125+
Match.objectLike({ CloudwatchMetric: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }),
126+
],
127+
},
128+
});
129+
130+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
131+
PolicyName: 'MyRolePolicy64AB00A5',
132+
Roles: ['ForTest'],
133+
});
134+
});

‎packages/@aws-cdk/aws-iot-actions/test/cloudwatch/integ.cloudwatch-logs-action.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// !cdk-integ pragma:ignore-assets
21
import * as iot from '@aws-cdk/aws-iot';
32
import * as logs from '@aws-cdk/aws-logs';
43
import * as cdk from '@aws-cdk/core';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"Resources": {
3+
"TopicRule40A4EA44": {
4+
"Type": "AWS::IoT::TopicRule",
5+
"Properties": {
6+
"TopicRulePayload": {
7+
"Actions": [
8+
{
9+
"CloudwatchMetric": {
10+
"MetricName": "${topic(2)}",
11+
"MetricNamespace": "${namespace}",
12+
"MetricTimestamp": "${timestamp}",
13+
"MetricUnit": "${unit}",
14+
"MetricValue": "${value}",
15+
"RoleArn": {
16+
"Fn::GetAtt": [
17+
"TopicRuleTopicRuleActionRole246C4F77",
18+
"Arn"
19+
]
20+
}
21+
}
22+
}
23+
],
24+
"AwsIotSqlVersion": "2016-03-23",
25+
"Sql": "SELECT topic(2) as device_id, namespace, unit, value, timestamp 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:PutMetricData",
53+
"Effect": "Allow",
54+
"Resource": "*"
55+
}
56+
],
57+
"Version": "2012-10-17"
58+
},
59+
"PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687",
60+
"Roles": [
61+
{
62+
"Ref": "TopicRuleTopicRuleActionRole246C4F77"
63+
}
64+
]
65+
}
66+
}
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as iot from '@aws-cdk/aws-iot';
2+
import * as cdk from '@aws-cdk/core';
3+
import * as actions from '../../lib';
4+
5+
const app = new cdk.App();
6+
7+
class TestStack extends cdk.Stack {
8+
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
9+
super(scope, id, props);
10+
11+
const topicRule = new iot.TopicRule(this, 'TopicRule', {
12+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'"),
13+
});
14+
15+
topicRule.addAction(new actions.CloudWatchPutMetricAction({
16+
metricName: '${topic(2)}',
17+
metricNamespace: '${namespace}',
18+
metricUnit: '${unit}',
19+
metricValue: '${value}',
20+
metricTimestamp: '${timestamp}',
21+
}));
22+
}
23+
}
24+
25+
new TestStack(app, 'test-stack');
26+
app.synth();

‎packages/@aws-cdk/aws-iot-actions/test/kinesis-firehose/integ.firehose-stream-action.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// !cdk-integ pragma:ignore-assets
21
import * as iot from '@aws-cdk/aws-iot';
32
import * as firehose from '@aws-cdk/aws-kinesisfirehose';
43
import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations';

‎packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-function-action.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// !cdk-integ pragma:ignore-assets
21
import * as iot from '@aws-cdk/aws-iot';
32
import * as lambda from '@aws-cdk/aws-lambda';
43
import * as cdk from '@aws-cdk/core';

‎packages/@aws-cdk/aws-iot-actions/test/s3/integ.s3-put-object-action.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// !cdk-integ pragma:ignore-assets
21
import * as iot from '@aws-cdk/aws-iot';
32
import * as s3 from '@aws-cdk/aws-s3';
43
import * as cdk from '@aws-cdk/core';

0 commit comments

Comments
 (0)
Please sign in to comment.