Skip to content

Commit

Permalink
feat(iot): add Action to change a CloudWatch alarm
Browse files Browse the repository at this point in the history
  • Loading branch information
jerry-shao committed Dec 14, 2021
1 parent cd3f24e commit d6bf218
Show file tree
Hide file tree
Showing 6 changed files with 370 additions and 0 deletions.
35 changes: 35 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -149,6 +150,40 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', {
});
```

## Change state for a CloudWatch alarm

The code snippet below creates an AWS IoT Rule that changes a 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.CloudWatchAlarmAction(alarm, {
stateReason: 'AWS Iot Rule action is triggered',
stateValue: 'ALARM',
}),
]
});
```

## Put records to Kinesis Data Firehose stream

The code snippet below creates an AWS IoT Rule that put records to Put records
Expand Down
49 changes: 49 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-alarm-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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 CloudWatchAlarmActionProps extends CommonActionProps {
/**
* The reason for the alarm change.
*/
readonly stateReason: string;
/**
* The value of the alarm state.
*/
readonly stateValue: string;
}

/**
* The action to change a CloudWatch alarm state.
*/
export class CloudWatchAlarmAction implements iot.IAction {
constructor(
private readonly alarm: cloudwatch.IAlarm,
private readonly props: CloudWatchAlarmActionProps,
) {
}

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.stateReason,
stateValue: this.props.stateValue,
},
},
};
}
}
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,5 +1,6 @@
export * from './cloudwatch-logs-action';
export * from './cloudwatch-put-metric-action';
export * from './cloudwatch-alarm-action';
export * from './common-action-props';
export * from './firehose-stream-action';
export * from './lambda-function-action';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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 { CloudWatchAlarmAction, CloudWatchAlarmActionProps } from '../../lib/cloudwatch-alarm-action';

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 alarmArn = 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm';
const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', alarmArn);
const cloudWatchAlarmActionProps: CloudWatchAlarmActionProps = {
stateReason: '${stateReason}',
stateValue: '${stateValue}',
};

// When
topicRule.addAction(new CloudWatchAlarmAction(alarm, cloudWatchAlarmActionProps));

// Then
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
{
CloudwatchAlarm: {
AlarmName: 'MyAlarm',
RoleArn: {
'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'],
},
StateReason: '${stateReason}',
StateValue: '${stateValue}',
},
},
],
},
});

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

test('can set stateReason', () => {
// 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 alarmArn = 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm';
const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', alarmArn);
const cloudWatchAlarmActionProps: CloudWatchAlarmActionProps = {
stateReason: 'Test SetAlarmState',
stateValue: '${stateValue}',
};

// When
topicRule.addAction(new CloudWatchAlarmAction(alarm, cloudWatchAlarmActionProps));

// Then
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
Match.objectLike({ CloudwatchAlarm: { StateReason: 'Test SetAlarmState' } }),
],
},
});
});

test('can set stateValue', () => {
// 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 alarmArn = 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm';
const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', alarmArn);
const cloudWatchAlarmActionProps: CloudWatchAlarmActionProps = {
stateReason: '${stateReason}',
stateValue: 'ALARM',
};

// When
topicRule.addAction(new CloudWatchAlarmAction(alarm, cloudWatchAlarmActionProps));

// Then
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
Match.objectLike({ CloudwatchAlarm: { StateValue: 'ALARM' } }),
],
},
});
});

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'"),
});
const alarmArn = 'arn:aws:cloudwatch:us-east-1:123456789012:alarm:MyAlarm';
const alarm = cloudwatch.Alarm.fromAlarmArn(stack, 'MyAlarm', alarmArn);
const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest');
const cloudWatchAlarmActionProps: CloudWatchAlarmActionProps = {
stateReason: '${stateReason}',
stateValue: '${stateValue}',
role: role,
};

// When
topicRule.addAction(new CloudWatchAlarmAction(alarm, cloudWatchAlarmActionProps));

// 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'],
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"Resources": {
"TopicRule40A4EA44": {
"Type": "AWS::IoT::TopicRule",
"Properties": {
"TopicRulePayload": {
"Actions": [
{
"CloudwatchAlarm": {
"AlarmName": {
"Ref": "MyAlarm696658B6"
},
"RoleArn": {
"Fn::GetAtt": [
"TopicRuleTopicRuleActionRole246C4F77",
"Arn"
]
},
"StateReason": "Test reason",
"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"
}
]
}
},
"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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 topicRule = new iot.TopicRule(this, 'TopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"),
});

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,
});

topicRule.addAction(new actions.CloudWatchAlarmAction(alarm, {
stateReason: 'Test reason',
stateValue: 'ALARM',
}));
}
}

new TestStack(app, 'test-stack');
app.synth();

0 comments on commit d6bf218

Please sign in to comment.