Skip to content

Commit

Permalink
feat(iot): add Action to capture CloudWatch metrics (aws#17503)
Browse files Browse the repository at this point in the history
I'm trying to implement aws-iot L2 Constructs.

This PR is one of steps after following PR: 
- aws#16681 (comment)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
yamatatsu authored and TikiTDO committed Feb 21, 2022
1 parent 561d1e3 commit 95fc82d
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 4 deletions.
25 changes: 25 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Currently supported are:
- Invoke a Lambda function
- Put objects to a S3 bucket
- Put logs to CloudWatch Logs
- Capture CloudWatch metrics
- Put records to Kinesis Data Firehose stream

## Invoke a Lambda function
Expand Down Expand Up @@ -123,6 +124,30 @@ new iot.TopicRule(this, 'TopicRule', {
});
```

## Capture CloudWatch metrics

The code snippet below creates an AWS IoT Rule that capture CloudWatch metrics
when it is triggered.

```ts
import * as iot from '@aws-cdk/aws-iot';
import * as actions from '@aws-cdk/aws-iot-actions';

const topicRule = new iot.TopicRule(this, 'TopicRule', {
sql: iot.IotSql.fromStringAsVer20160323(
"SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'",
),
actions: [
new actions.CloudWatchPutMetricAction({
metricName: '${topic(2)}',
metricNamespace: '${namespace}',
metricUnit: '${unit}',
metricValue: '${value}',
metricTimestamp: '${timestamp}',
}),
],
});
```

## Put records to Kinesis Data Firehose stream

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
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 metric.
*/
export interface CloudWatchPutMetricActionProps extends CommonActionProps {
/**
* The CloudWatch metric name.
*
* Supports substitution templates.
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
*/
readonly metricName: string;

/**
* The CloudWatch metric namespace name.
*
* Supports substitution templates.
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
*/
readonly metricNamespace: string;

/**
* A string that contains the timestamp, expressed in seconds in Unix epoch time.
*
* Supports substitution templates.
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
*
* @default - none -- Defaults to the current Unix epoch time.
*/
readonly metricTimestamp?: string;

/**
* The metric unit supported by CloudWatch.
*
* Supports substitution templates.
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
*/
readonly metricUnit: string;

/**
* A string that contains the CloudWatch metric value.
*
* Supports substitution templates.
* @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html
*/
readonly metricValue: string;
}

/**
* The action to capture an Amazon CloudWatch metric.
*/
export class CloudWatchPutMetricAction implements iot.IAction {
constructor(private readonly props: CloudWatchPutMetricActionProps) {
}

bind(rule: iot.ITopicRule): iot.ActionConfig {
const role = this.props.role ?? singletonActionRole(rule);
cloudwatch.Metric.grantPutMetricData(role);

return {
configuration: {
cloudwatchMetric: {
metricName: this.props.metricName,
metricNamespace: this.props.metricNamespace,
metricTimestamp: this.props.metricTimestamp,
metricUnit: this.props.metricUnit,
metricValue: this.props.metricValue,
roleArn: role.roleArn,
},
},
};
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iot-actions/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './cloudwatch-logs-action';
export * from './cloudwatch-put-metric-action';
export * from './common-action-props';
export * from './firehose-stream-action';
export * from './lambda-function-action';
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"jest": "^27.3.1"
},
"dependencies": {
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-iot": "0.0.0",
"@aws-cdk/aws-kinesisfirehose": "0.0.0",
Expand All @@ -92,6 +93,7 @@
},
"homepage": "https://github.com/aws/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-cloudwatch": "0.0.0",
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-iot": "0.0.0",
"@aws-cdk/aws-kinesisfirehose": "0.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { Template, Match } from '@aws-cdk/assertions';
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 metric action', () => {
// GIVEN
const stack = new cdk.Stack();
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'"),
});

// WHEN
topicRule.addAction(
new actions.CloudWatchPutMetricAction({
metricName: '${topic(2)}',
metricNamespace: '${namespace}',
metricUnit: '${unit}',
metricValue: '${value}',
}),
);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
{
CloudwatchMetric: {
MetricName: '${topic(2)}',
MetricNamespace: '${namespace}',
MetricUnit: '${unit}',
MetricValue: '${value}',
RoleArn: {
'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'],
},
},
},
],
},
});

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:PutMetricData',
Effect: 'Allow',
Resource: '*',
},
],
Version: '2012-10-17',
},
PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7',
Roles: [{ Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }],
});
});

test('can set timestamp', () => {
// GIVEN
const stack = new cdk.Stack();
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'"),
});

// WHEN
topicRule.addAction(
new actions.CloudWatchPutMetricAction({
metricName: '${topic(2)}',
metricNamespace: '${namespace}',
metricUnit: '${unit}',
metricValue: '${value}',
metricTimestamp: '${timestamp()}',
}),
);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
Match.objectLike({ CloudwatchMetric: { MetricTimestamp: '${timestamp()}' } }),
],
},
});
});

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, namespace, unit, value, timestamp FROM 'device/+/data'"),
});
const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest');

// WHEN
topicRule.addAction(
new actions.CloudWatchPutMetricAction({
metricName: '${topic(2)}',
metricNamespace: '${namespace}',
metricUnit: '${unit}',
metricValue: '${value}',
role,
}),
);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
Match.objectLike({ CloudwatchMetric: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }),
],
},
});

Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
PolicyName: 'MyRolePolicy64AB00A5',
Roles: ['ForTest'],
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/// !cdk-integ pragma:ignore-assets
import * as iot from '@aws-cdk/aws-iot';
import * as logs from '@aws-cdk/aws-logs';
import * as cdk from '@aws-cdk/core';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"Resources": {
"TopicRule40A4EA44": {
"Type": "AWS::IoT::TopicRule",
"Properties": {
"TopicRulePayload": {
"Actions": [
{
"CloudwatchMetric": {
"MetricName": "${topic(2)}",
"MetricNamespace": "${namespace}",
"MetricTimestamp": "${timestamp}",
"MetricUnit": "${unit}",
"MetricValue": "${value}",
"RoleArn": {
"Fn::GetAtt": [
"TopicRuleTopicRuleActionRole246C4F77",
"Arn"
]
}
}
}
],
"AwsIotSqlVersion": "2016-03-23",
"Sql": "SELECT topic(2) as device_id, namespace, unit, value, timestamp 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:PutMetricData",
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687",
"Roles": [
{
"Ref": "TopicRuleTopicRuleActionRole246C4F77"
}
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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, namespace, unit, value, timestamp FROM 'device/+/data'"),
});

topicRule.addAction(new actions.CloudWatchPutMetricAction({
metricName: '${topic(2)}',
metricNamespace: '${namespace}',
metricUnit: '${unit}',
metricValue: '${value}',
metricTimestamp: '${timestamp}',
}));
}
}

new TestStack(app, 'test-stack');
app.synth();
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/// !cdk-integ pragma:ignore-assets
import * as iot from '@aws-cdk/aws-iot';
import * as firehose from '@aws-cdk/aws-kinesisfirehose';
import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/// !cdk-integ pragma:ignore-assets
import * as iot from '@aws-cdk/aws-iot';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/// !cdk-integ pragma:ignore-assets
import * as iot from '@aws-cdk/aws-iot';
import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
Expand Down

0 comments on commit 95fc82d

Please sign in to comment.