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): allow setting Actions of TopicRule #17110

Merged
merged 11 commits into from
Oct 28, 2021
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,32 @@
This library contains integration classes to send data to any number of
supported AWS Services. Instances of these classes should be passed to
`TopicRule` defined in `@aws-cdk/aws-iot`.

Currently supported are:

- Invoke a Lambda function

## Invoke a Lambda function

The code snippet below creates an AWS IoT Rule that invoke a Lambda function
when it is triggered.

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

const lambdaFn = new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = (event) => {
console.log("It is test for lambda action of AWS IoT Rule.", event)
}`),
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
});

new iot.TopicRule(this, 'TopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"),
actions: [new actions.LambdaAction(lambdaFn)],
});
```
3 changes: 1 addition & 2 deletions packages/@aws-cdk/aws-iot-actions/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
// this is placeholder for monocdk
export const dummy = true;
export * from './lambda-action';
32 changes: 32 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/lib/lambda-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as iam from '@aws-cdk/aws-iam';
import * as iot from '@aws-cdk/aws-iot';
import * as lambda from '@aws-cdk/aws-lambda';
import { Stack } from '@aws-cdk/core';


yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
/**
* The action to invoke an AWS Lambda function, passing in an MQTT message.
*/
export class LambdaAction implements iot.IAction {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this to LambdaFunctionAction.

/**
* @param lambdaFn The lambda function to be invoked by this action
*/
constructor(private readonly lambdaFn: lambda.IFunction) {}
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved

bind(rule: iot.ITopicRule): iot.ActionConfig {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bind(rule: iot.ITopicRule): iot.ActionConfig {
bind(topicRule: iot.ITopicRule): iot.ActionConfig {

this.lambdaFn.addPermission('invokedByAwsIotRule', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use function.grantInvoke() instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skinny85 Thank you for your review!

In my understanding, using addPermission() is better. Because a lambda resource policy created by using grantInvoke() is more lax than using addPermission().

As described in this document, lambda resource policy fields sourceAccount and sourceArn are required (or recommended?). But functions.grantInvoke() cannot add sourceAccount and sourceArn as this code.

For confirmation, I tried following code instead of this.func.addPermission():

this.func.grantInvoke({ grantPrincipal: new iam.ServicePrincipal('iot.amazonaws.com') });

Then there is template difference as following:

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, fair enough 🙂.

action: 'lambda:InvokeFunction',
principal: new iam.ServicePrincipal('iot.amazonaws.com'),
sourceAccount: Stack.of(rule).account,
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
sourceArn: rule.topicRuleArn,
});

return {
configuration: {
lambda: {
functionArn: this.lambdaFn.functionArn,
},
},
};
}
}
10 changes: 10 additions & 0 deletions packages/@aws-cdk/aws-iot-actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,19 @@
"jest": "^26.6.3"
},
"dependencies": {
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-iot": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
"homepage": "https://github.com/aws/aws-cdk",
"peerDependencies": {
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-iot": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/core": "0.0.0",
"constructs": "^3.3.69"
},
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"Resources": {
"MyFunctionServiceRole3C357FF2": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
}
},
"MyFunction3BAA72D1": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile": "\nexports.handler = (event) => {\n console.log(\"It is test for lambda action of AWS IoT Rule.\", event)\n}"
},
"Role": {
"Fn::GetAtt": [
"MyFunctionServiceRole3C357FF2",
"Arn"
]
},
"Handler": "index.handler",
"Runtime": "nodejs14.x"
},
"DependsOn": [
"MyFunctionServiceRole3C357FF2"
]
},
"MyFunctioninvokedByAwsIotRule5581F304": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"MyFunction3BAA72D1",
"Arn"
]
},
"Principal": "iot.amazonaws.com",
"SourceAccount": {
"Ref": "AWS::AccountId"
},
"SourceArn": {
"Fn::GetAtt": [
"TopicRule40A4EA44",
"Arn"
]
}
}
},
"TopicRule40A4EA44": {
"Type": "AWS::IoT::TopicRule",
"Properties": {
"TopicRulePayload": {
"Actions": [
{
"Lambda": {
"FunctionArn": {
"Fn::GetAtt": [
"MyFunction3BAA72D1",
"Arn"
]
}
}
}
],
"AwsIotSqlVersion": "2016-03-23",
"Sql": "SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// !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';
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 lambdaFn = new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = (event) => {
console.log("It is test for lambda action of AWS IoT Rule.", event)
}`),
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
});

new iot.TopicRule(this, 'TopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"),
actions: [new actions.LambdaAction(lambdaFn)],
});
}
}

new TestStack(app, 'test-stack');
app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Template } from '@aws-cdk/assertions';
import * as iot from '@aws-cdk/aws-iot';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import * as actions from '../../lib';

test('create a topic rule with lambda action and a lambda permission to be invoked by the topic rule', () => {
// 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 lambdaFn = new lambda.Function(stack, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromInline('console.log("foo")'),
});

// WHEN
topicRule.addAction(new actions.LambdaAction(lambdaFn));

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
TopicRulePayload: {
Actions: [
{
Lambda: {
FunctionArn: {
'Fn::GetAtt': [
'MyFunction3BAA72D1',
'Arn',
],
},
},
},
],
},
});

Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', {
Action: 'lambda:InvokeFunction',
FunctionName: {
'Fn::GetAtt': [
'MyFunction3BAA72D1',
'Arn',
],
},
Principal: 'iot.amazonaws.com',
SourceAccount: { Ref: 'AWS::AccountId' },
SourceArn: {
'Fn::GetAtt': [
'MyTopicRule4EC2091C',
'Arn',
],
},
});
});
34 changes: 26 additions & 8 deletions packages/@aws-cdk/aws-iot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,34 @@ import * as iot from '@aws-cdk/aws-iot';

## `TopicRule`

The `TopicRule` construct defined Rules that give your devices the ability to
interact with AWS services.
Create a rule that give your devices the ability to interact with AWS services.
You can create a rule with an action that invoke the Lambda action as following:

For example, to define a rule:
```ts
import * as iot from '@aws-cdk/aws-iot';
import * as actions from '@aws-cdk/aws-iot-actions';
import * as lambda from '@aws-cdk/aws-lambda';

const lambdaFn = new lambda.Function(this, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`
exports.handler = (event) => {
console.log("It is test for lambda action of AWS IoT Rule.", event)
}`),
yamatatsu marked this conversation as resolved.
Show resolved Hide resolved
});

new iot.TopicRule(this, 'TopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'"),
actions: [new actions.LambdaAction(lambdaFn)],
});
```

Or, you can add an action after constructing the `TopicRule` instance as following:

```ts
new iot.TopicRule(stack, 'MyTopicRule', {
topicRuleName: 'MyRuleName', // optional property
sql: iot.IotSql.fromStringAsVer20160323(
"SELECT topic(2) as device_id, temperature FROM 'device/+/data'",
),
const topicRule = new iot.TopicRule(this, 'TopicRule', {
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'"),
});
topicRule.addAction(new actions.LambdaAction(lambdaFn))
```
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-iot/lib/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CfnTopicRule } from './iot.generated';
import { ITopicRule } from './topic-rule';

/**
* An abstract action for TopicRule.
*/
export interface IAction {
/**
* Returns the topic rule action specification.
*
* @param rule The TopicRule that would trigger this action.
*/
bind(rule: ITopicRule): ActionConfig;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bind(rule: ITopicRule): ActionConfig;
bind(topicRule: ITopicRule): ActionConfig;

}

/**
* Properties for an topic rule action
*/
export interface ActionConfig {
/**
* The configuration for this action.
*/
readonly configuration: CfnTopicRule.ActionProperty;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iot/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './action';
export * from './iot-sql';
export * from './topic-rule';

Expand Down
Loading