Skip to content

Commit eb3f223

Browse files
yamatatsuTikiTDO
authored andcommitted
feat(iot): allow setting Actions of TopicRule (aws#17110)
I'm trying to implement aws-iot L2 Constructs. This PR is the next step of aws#16681 refar: - aws#16681 (comment) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7c92494 commit eb3f223

16 files changed

+472
-23
lines changed

Diff for: packages/@aws-cdk/aws-iot-actions/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,33 @@
1818
This library contains integration classes to send data to any number of
1919
supported AWS Services. Instances of these classes should be passed to
2020
`TopicRule` defined in `@aws-cdk/aws-iot`.
21+
22+
Currently supported are:
23+
24+
- Invoke a Lambda function
25+
26+
## Invoke a Lambda function
27+
28+
The code snippet below creates an AWS IoT Rule that invoke a Lambda function
29+
when it is triggered.
30+
31+
```ts
32+
import * as iot from '@aws-cdk/aws-iot';
33+
import * as actions from '@aws-cdk/aws-iot-actions';
34+
import * as lambda from '@aws-cdk/aws-lambda';
35+
36+
const func = new lambda.Function(this, 'MyFunction', {
37+
runtime: lambda.Runtime.NODEJS_14_X,
38+
handler: 'index.handler',
39+
code: lambda.Code.fromInline(`
40+
exports.handler = (event) => {
41+
console.log("It is test for lambda action of AWS IoT Rule.", event);
42+
};`
43+
),
44+
});
45+
46+
new iot.TopicRule(this, 'TopicRule', {
47+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"),
48+
actions: [new actions.LambdaFunctionAction(func)],
49+
});
50+
```

Diff for: packages/@aws-cdk/aws-iot-actions/lib/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
// this is placeholder for monocdk
2-
export const dummy = true;
1+
export * from './lambda-function-action';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as iam from '@aws-cdk/aws-iam';
2+
import * as iot from '@aws-cdk/aws-iot';
3+
import * as lambda from '@aws-cdk/aws-lambda';
4+
5+
/**
6+
* The action to invoke an AWS Lambda function, passing in an MQTT message.
7+
*/
8+
export class LambdaFunctionAction implements iot.IAction {
9+
/**
10+
* @param func The lambda function to be invoked by this action
11+
*/
12+
constructor(private readonly func: lambda.IFunction) {}
13+
14+
bind(topicRule: iot.ITopicRule): iot.ActionConfig {
15+
this.func.addPermission('invokedByAwsIotRule', {
16+
action: 'lambda:InvokeFunction',
17+
principal: new iam.ServicePrincipal('iot.amazonaws.com'),
18+
sourceAccount: topicRule.env.account,
19+
sourceArn: topicRule.topicRuleArn,
20+
});
21+
22+
return {
23+
configuration: {
24+
lambda: {
25+
functionArn: this.func.functionArn,
26+
},
27+
},
28+
};
29+
}
30+
}

Diff for: packages/@aws-cdk/aws-iot-actions/package.json

+10
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,19 @@
7979
"jest": "^26.6.3"
8080
},
8181
"dependencies": {
82+
"@aws-cdk/aws-iam": "0.0.0",
83+
"@aws-cdk/aws-iot": "0.0.0",
84+
"@aws-cdk/aws-lambda": "0.0.0",
85+
"@aws-cdk/core": "0.0.0",
86+
"constructs": "^3.3.69"
8287
},
8388
"homepage": "https://github.com/aws/aws-cdk",
8489
"peerDependencies": {
90+
"@aws-cdk/aws-iam": "0.0.0",
91+
"@aws-cdk/aws-iot": "0.0.0",
92+
"@aws-cdk/aws-lambda": "0.0.0",
93+
"@aws-cdk/core": "0.0.0",
94+
"constructs": "^3.3.69"
8595
},
8696
"engines": {
8797
"node": ">= 10.13.0 <13 || >=13.7.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
{
2+
"Resources": {
3+
"MyFunctionServiceRole3C357FF2": {
4+
"Type": "AWS::IAM::Role",
5+
"Properties": {
6+
"AssumeRolePolicyDocument": {
7+
"Statement": [
8+
{
9+
"Action": "sts:AssumeRole",
10+
"Effect": "Allow",
11+
"Principal": {
12+
"Service": "lambda.amazonaws.com"
13+
}
14+
}
15+
],
16+
"Version": "2012-10-17"
17+
},
18+
"ManagedPolicyArns": [
19+
{
20+
"Fn::Join": [
21+
"",
22+
[
23+
"arn:",
24+
{
25+
"Ref": "AWS::Partition"
26+
},
27+
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
28+
]
29+
]
30+
}
31+
]
32+
}
33+
},
34+
"MyFunction3BAA72D1": {
35+
"Type": "AWS::Lambda::Function",
36+
"Properties": {
37+
"Code": {
38+
"ZipFile": "\n exports.handler = (event) => {\n console.log(\"It is test for lambda action of AWS IoT Rule.\", event);\n };\""
39+
},
40+
"Role": {
41+
"Fn::GetAtt": [
42+
"MyFunctionServiceRole3C357FF2",
43+
"Arn"
44+
]
45+
},
46+
"Handler": "index.handler",
47+
"Runtime": "nodejs14.x"
48+
},
49+
"DependsOn": [
50+
"MyFunctionServiceRole3C357FF2"
51+
]
52+
},
53+
"MyFunctioninvokedByAwsIotRule5581F304": {
54+
"Type": "AWS::Lambda::Permission",
55+
"Properties": {
56+
"Action": "lambda:InvokeFunction",
57+
"FunctionName": {
58+
"Fn::GetAtt": [
59+
"MyFunction3BAA72D1",
60+
"Arn"
61+
]
62+
},
63+
"Principal": "iot.amazonaws.com",
64+
"SourceAccount": {
65+
"Ref": "AWS::AccountId"
66+
},
67+
"SourceArn": {
68+
"Fn::GetAtt": [
69+
"TopicRule40A4EA44",
70+
"Arn"
71+
]
72+
}
73+
}
74+
},
75+
"TopicRule40A4EA44": {
76+
"Type": "AWS::IoT::TopicRule",
77+
"Properties": {
78+
"TopicRulePayload": {
79+
"Actions": [
80+
{
81+
"Lambda": {
82+
"FunctionArn": {
83+
"Fn::GetAtt": [
84+
"MyFunction3BAA72D1",
85+
"Arn"
86+
]
87+
}
88+
}
89+
}
90+
],
91+
"AwsIotSqlVersion": "2016-03-23",
92+
"Sql": "SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"
93+
}
94+
}
95+
}
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/// !cdk-integ pragma:ignore-assets
2+
import * as iot from '@aws-cdk/aws-iot';
3+
import * as lambda from '@aws-cdk/aws-lambda';
4+
import * as cdk from '@aws-cdk/core';
5+
import * as actions from '../../lib';
6+
7+
const app = new cdk.App();
8+
9+
class TestStack extends cdk.Stack {
10+
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
11+
super(scope, id, props);
12+
13+
const func = new lambda.Function(this, 'MyFunction', {
14+
runtime: lambda.Runtime.NODEJS_14_X,
15+
handler: 'index.handler',
16+
code: lambda.Code.fromInline(`
17+
exports.handler = (event) => {
18+
console.log("It is test for lambda action of AWS IoT Rule.", event);
19+
};"`,
20+
),
21+
});
22+
23+
new iot.TopicRule(this, 'TopicRule', {
24+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"),
25+
actions: [new actions.LambdaFunctionAction(func)],
26+
});
27+
}
28+
}
29+
30+
new TestStack(app, 'test-stack');
31+
app.synth();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Template } from '@aws-cdk/assertions';
2+
import * as iot from '@aws-cdk/aws-iot';
3+
import * as lambda from '@aws-cdk/aws-lambda';
4+
import * as cdk from '@aws-cdk/core';
5+
import * as actions from '../../lib';
6+
7+
test('create a topic rule with lambda action and a lambda permission to be invoked by the topic rule', () => {
8+
// GIVEN
9+
const stack = new cdk.Stack();
10+
const topicRule = new iot.TopicRule(stack, 'MyTopicRule', {
11+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"),
12+
});
13+
const func = new lambda.Function(stack, 'MyFunction', {
14+
runtime: lambda.Runtime.NODEJS_14_X,
15+
handler: 'index.handler',
16+
code: lambda.Code.fromInline('console.log("foo")'),
17+
});
18+
19+
// WHEN
20+
topicRule.addAction(new actions.LambdaFunctionAction(func));
21+
22+
// THEN
23+
Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', {
24+
TopicRulePayload: {
25+
Actions: [
26+
{
27+
Lambda: {
28+
FunctionArn: {
29+
'Fn::GetAtt': [
30+
'MyFunction3BAA72D1',
31+
'Arn',
32+
],
33+
},
34+
},
35+
},
36+
],
37+
},
38+
});
39+
40+
Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Permission', {
41+
Action: 'lambda:InvokeFunction',
42+
FunctionName: {
43+
'Fn::GetAtt': [
44+
'MyFunction3BAA72D1',
45+
'Arn',
46+
],
47+
},
48+
Principal: 'iot.amazonaws.com',
49+
SourceAccount: { Ref: 'AWS::AccountId' },
50+
SourceArn: {
51+
'Fn::GetAtt': [
52+
'MyTopicRule4EC2091C',
53+
'Arn',
54+
],
55+
},
56+
});
57+
});

Diff for: packages/@aws-cdk/aws-iot/README.md

+27-8
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,35 @@ import * as iot from '@aws-cdk/aws-iot';
4040

4141
## `TopicRule`
4242

43-
The `TopicRule` construct defined Rules that give your devices the ability to
44-
interact with AWS services.
45-
46-
For example, to define a rule:
43+
Create a topic rule that give your devices the ability to interact with AWS services.
44+
You can create a topic rule with an action that invoke the Lambda action as following:
4745

4846
```ts
49-
new iot.TopicRule(stack, 'MyTopicRule', {
50-
topicRuleName: 'MyRuleName', // optional property
51-
sql: iot.IotSql.fromStringAsVer20160323(
52-
"SELECT topic(2) as device_id, temperature FROM 'device/+/data'",
47+
import * as iot from '@aws-cdk/aws-iot';
48+
import * as actions from '@aws-cdk/aws-iot-actions';
49+
import * as lambda from '@aws-cdk/aws-lambda';
50+
51+
const func = new lambda.Function(this, 'MyFunction', {
52+
runtime: lambda.Runtime.NODEJS_14_X,
53+
handler: 'index.handler',
54+
code: lambda.Code.fromInline(`
55+
exports.handler = (event) => {
56+
console.log("It is test for lambda action of AWS IoT Rule.", event);
57+
};`
5358
),
5459
});
60+
61+
new iot.TopicRule(this, 'TopicRule', {
62+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'"),
63+
actions: [new actions.LambdaFunctionAction(func)],
64+
});
65+
```
66+
67+
Or, you can add an action after constructing the `TopicRule` instance as following:
68+
69+
```ts
70+
const topicRule = new iot.TopicRule(this, 'TopicRule', {
71+
sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'"),
72+
});
73+
topicRule.addAction(new actions.LambdaFunctionAction(func))
5574
```

Diff for: packages/@aws-cdk/aws-iot/lib/action.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { CfnTopicRule } from './iot.generated';
2+
import { ITopicRule } from './topic-rule';
3+
4+
/**
5+
* An abstract action for TopicRule.
6+
*/
7+
export interface IAction {
8+
/**
9+
* Returns the topic rule action specification.
10+
*
11+
* @param topicRule The TopicRule that would trigger this action.
12+
*/
13+
bind(topicRule: ITopicRule): ActionConfig;
14+
}
15+
16+
/**
17+
* Properties for an topic rule action
18+
*/
19+
export interface ActionConfig {
20+
/**
21+
* The configuration for this action.
22+
*/
23+
readonly configuration: CfnTopicRule.ActionProperty;
24+
}

Diff for: packages/@aws-cdk/aws-iot/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './action';
12
export * from './iot-sql';
23
export * from './topic-rule';
34

Diff for: packages/@aws-cdk/aws-iot/lib/iot-sql.ts

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ export abstract class IotSql {
5656
public abstract bind(scope: Construct): IotSqlConfig;
5757
}
5858

59-
6059
class IotSqlImpl extends IotSql {
6160
constructor(private readonly version: string, private readonly sql: string) {
6261
super();

0 commit comments

Comments
 (0)