Skip to content

Commit

Permalink
feat(iot-actions): Add the action to put CloudWatch Logs (#17228)
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: 
- #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 committed Nov 2, 2021
1 parent 1e22189 commit a7c869e
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 0 deletions.
18 changes: 18 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/README.md
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
@@ -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
@@ -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
@@ -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
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
@@ -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',
],
},
},
},
],
},
});
});

0 comments on commit a7c869e

Please sign in to comment.