Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(iot-actions): add the action to put CloudWatch Logs #17228

Merged
merged 7 commits into from
Nov 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,21 @@ new iot.TopicRule(this, 'TopicRule', {
actions: [new actions.LambdaFunctionAction(func)],
});
```

## Put logs to CloudWatch Logs

The code snippet below creates an AWS IoT Rule that put logs to CloudWatch Logs
when it is triggered.

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

const logGroup = new logs.LogGroup(this, 'MyLogGroup');

new iot.TopicRule(this, 'TopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"),
actions: [new actions.CloudWatchLogsAction(logGroup)],
});
```
49 changes: 49 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as iam from '@aws-cdk/aws-iam';
import * as iot from '@aws-cdk/aws-iot';
import * as logs from '@aws-cdk/aws-logs';
import { singletonActionRole } from './private/role';

/**
* Configuration properties of an action for CloudWatch Logs.
*/
export interface CloudWatchLogsActionProps {
/**
* The IAM role that allows access to the CloudWatch log group.
*
* @default a new role will be created
*/
readonly role?: iam.IRole;
}

/**
* The action to send data to Amazon CloudWatch Logs
*/
export class CloudWatchLogsAction implements iot.IAction {
private readonly role?: iam.IRole;

/**
* @param logGroup The CloudWatch log group to which the action sends data
* @param props Optional properties to not use default
*/
constructor(
private readonly logGroup: logs.ILogGroup,
props: CloudWatchLogsActionProps = {},
) {
this.role = props.role;
}

bind(rule: iot.ITopicRule): iot.ActionConfig {
const role = this.role ?? singletonActionRole(rule);
this.logGroup.grantWrite(role);
this.logGroup.grant(role, 'logs:DescribeLogStreams');

return {
configuration: {
cloudwatchLogs: {
logGroupName: this.logGroup.logGroupName,
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 +1,2 @@
export * from './cloudwatch-logs-action';
export * from './lambda-function-action';
27 changes: 27 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/lib/private/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as iam from '@aws-cdk/aws-iam';
import { IConstruct, PhysicalName } from '@aws-cdk/core';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';

/**
* Obtain the Role for the TopicRule
*
* If a role already exists, it will be returned. This ensures that if a rule have multiple
* actions, they will share a role.
* @internal
*/
export function singletonActionRole(scope: IConstruct): iam.IRole {
const id = 'TopicRuleActionRole';
const existing = scope.node.tryFindChild(id) as iam.IRole;
if (existing) {
return existing;
};

const role = new iam.Role(scope as Construct, id, {
roleName: PhysicalName.GENERATE_IF_NEEDED,
assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'),
});
return role;
}
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 @@ -82,6 +82,7 @@
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-iot": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/aws-logs": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
Expand All @@ -90,6 +91,7 @@
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-iot": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/aws-logs": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { Template } from '@aws-cdk/assertions';
import * as iam from '@aws-cdk/aws-iam';
import * as iot from '@aws-cdk/aws-iot';
import * as logs from '@aws-cdk/aws-logs';
import * as cdk from '@aws-cdk/core';
import * as actions from '../../lib';

test('Default cloudwatch logs action', () => {
// GIVEN
const stack = new cdk.Stack();
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"),
});
const logGroup = new logs.LogGroup(stack, 'MyLogGroup');

// WHEN
topicRule.addAction(
new actions.CloudWatchLogsAction(logGroup),
);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
{
CloudwatchLogs: {
LogGroupName: { Ref: 'MyLogGroup5C0DAD85' },
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: ['logs:CreateLogStream', 'logs:PutLogEvents'],
Effect: 'Allow',
Resource: {
'Fn::GetAtt': ['MyLogGroup5C0DAD85', 'Arn'],
},
},
{
Action: 'logs:DescribeLogStreams',
Effect: 'Allow',
Resource: {
'Fn::GetAtt': ['MyLogGroup5C0DAD85', 'Arn'],
},
},
],
Version: '2012-10-17',
},
PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7',
Roles: [
{ Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' },
],
});
});

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 FROM 'device/+/data'"),
});
const logGroup = new logs.LogGroup(stack, 'MyLogGroup');
const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest');

// WHEN
topicRule.addAction(
new actions.CloudWatchLogsAction(logGroup, {
role,
}),
);

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

test('The specified role is added a policy needed for sending data to logs', () => {
// GIVEN
const stack = new cdk.Stack();
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"),
});
const logGroup = new logs.LogGroup(stack, 'MyLogGroup');
const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest');

// WHEN
topicRule.addAction(
new actions.CloudWatchLogsAction(logGroup, {
role,
}),
);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: [
{
Action: ['logs:CreateLogStream', 'logs:PutLogEvents'],
Effect: 'Allow',
Resource: {
'Fn::GetAtt': ['MyLogGroup5C0DAD85', 'Arn'],
},
},
{
Action: 'logs:DescribeLogStreams',
Effect: 'Allow',
Resource: {
'Fn::GetAtt': ['MyLogGroup5C0DAD85', 'Arn'],
},
},
],
Version: '2012-10-17',
},
PolicyName: 'MyRolePolicy64AB00A5',
Roles: ['ForTest'],
});
});


test('When multiple actions are omitted role property, the actions use same one role', () => {
// GIVEN
// GIVEN
const stack = new cdk.Stack();
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"),
});
const logGroup1 = new logs.LogGroup(stack, 'MyLogGroup1');
const logGroup2 = new logs.LogGroup(stack, 'MyLogGroup2');

// WHEN
topicRule.addAction(new actions.CloudWatchLogsAction(logGroup1));
topicRule.addAction(new actions.CloudWatchLogsAction(logGroup2));

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
{
CloudwatchLogs: {
LogGroupName: { Ref: 'MyLogGroup14A6E382A' },
RoleArn: {
'Fn::GetAtt': [
'MyTopicRuleTopicRuleActionRoleCE2D05DA',
'Arn',
],
},
},
},
{
CloudwatchLogs: {
LogGroupName: { Ref: 'MyLogGroup279D6359D' },
RoleArn: {
'Fn::GetAtt': [
'MyTopicRuleTopicRuleActionRoleCE2D05DA',
'Arn',
],
},
},
},
],
},
});
});
Loading