diff --git a/packages/@aws-cdk/aws-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md index 1c098d25eb3de..67d9b2924dd53 100644 --- a/packages/@aws-cdk/aws-iot-actions/README.md +++ b/packages/@aws-cdk/aws-iot-actions/README.md @@ -18,3 +18,33 @@ 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 func = 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); + };` + ), +}); + +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.LambdaFunctionAction(func)], +}); +``` diff --git a/packages/@aws-cdk/aws-iot-actions/lib/index.ts b/packages/@aws-cdk/aws-iot-actions/lib/index.ts index 3e4b0ef0a73d4..751863744ffe2 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/index.ts @@ -1,2 +1 @@ -// this is placeholder for monocdk -export const dummy = true; +export * from './lambda-function-action'; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/lambda-function-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/lambda-function-action.ts new file mode 100644 index 0000000000000..8296e112e8be5 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/lambda-function-action.ts @@ -0,0 +1,30 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; + +/** + * The action to invoke an AWS Lambda function, passing in an MQTT message. + */ +export class LambdaFunctionAction implements iot.IAction { + /** + * @param func The lambda function to be invoked by this action + */ + constructor(private readonly func: lambda.IFunction) {} + + bind(topicRule: iot.ITopicRule): iot.ActionConfig { + this.func.addPermission('invokedByAwsIotRule', { + action: 'lambda:InvokeFunction', + principal: new iam.ServicePrincipal('iot.amazonaws.com'), + sourceAccount: topicRule.env.account, + sourceArn: topicRule.topicRuleArn, + }); + + return { + configuration: { + lambda: { + functionArn: this.func.functionArn, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/package.json b/packages/@aws-cdk/aws-iot-actions/package.json index 76878637e0430..3b66b73a1562e 100644 --- a/packages/@aws-cdk/aws-iot-actions/package.json +++ b/packages/@aws-cdk/aws-iot-actions/package.json @@ -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" diff --git a/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-function-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-function-action.expected.json new file mode 100644 index 0000000000000..345ead052c921 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-function-action.expected.json @@ -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": "\n exports.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'" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-function-action.ts b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-function-action.ts new file mode 100644 index 0000000000000..58a7773afec03 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-function-action.ts @@ -0,0 +1,31 @@ +/// !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 func = 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); + };"`, + ), + }); + + 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.LambdaFunctionAction(func)], + }); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda-function-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda-function-action.test.ts new file mode 100644 index 0000000000000..76263f5fa5e5c --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda-function-action.test.ts @@ -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 func = 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.LambdaFunctionAction(func)); + + // 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', + ], + }, + }); +}); diff --git a/packages/@aws-cdk/aws-iot/README.md b/packages/@aws-cdk/aws-iot/README.md index bbde9aae8a21d..6a9640629891a 100644 --- a/packages/@aws-cdk/aws-iot/README.md +++ b/packages/@aws-cdk/aws-iot/README.md @@ -40,16 +40,35 @@ 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. - -For example, to define a rule: +Create a topic rule that give your devices the ability to interact with AWS services. +You can create a topic rule with an action that invoke the Lambda action 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'", +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 func = 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); + };` ), }); + +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'"), + actions: [new actions.LambdaFunctionAction(func)], +}); +``` + +Or, you can add an action after constructing the `TopicRule` instance as following: + +```ts +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.LambdaFunctionAction(func)) ``` diff --git a/packages/@aws-cdk/aws-iot/lib/action.ts b/packages/@aws-cdk/aws-iot/lib/action.ts new file mode 100644 index 0000000000000..f22daf6194b1c --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/action.ts @@ -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 topicRule The TopicRule that would trigger this action. + */ + bind(topicRule: ITopicRule): ActionConfig; +} + +/** + * Properties for an topic rule action + */ +export interface ActionConfig { + /** + * The configuration for this action. + */ + readonly configuration: CfnTopicRule.ActionProperty; +} diff --git a/packages/@aws-cdk/aws-iot/lib/index.ts b/packages/@aws-cdk/aws-iot/lib/index.ts index 18b6f2e03aaeb..f2e82a6c755b2 100644 --- a/packages/@aws-cdk/aws-iot/lib/index.ts +++ b/packages/@aws-cdk/aws-iot/lib/index.ts @@ -1,3 +1,4 @@ +export * from './action'; export * from './iot-sql'; export * from './topic-rule'; diff --git a/packages/@aws-cdk/aws-iot/lib/iot-sql.ts b/packages/@aws-cdk/aws-iot/lib/iot-sql.ts index c673552743364..7014778cc94ab 100644 --- a/packages/@aws-cdk/aws-iot/lib/iot-sql.ts +++ b/packages/@aws-cdk/aws-iot/lib/iot-sql.ts @@ -56,7 +56,6 @@ export abstract class IotSql { public abstract bind(scope: Construct): IotSqlConfig; } - class IotSqlImpl extends IotSql { constructor(private readonly version: string, private readonly sql: string) { super(); diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index 17f121eb29ab3..a8cd21fe2bd96 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -1,5 +1,6 @@ -import { ArnFormat, Resource, Stack, IResource } from '@aws-cdk/core'; +import { ArnFormat, Resource, Stack, IResource, Lazy } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { IAction } from './action'; import { IotSql } from './iot-sql'; import { CfnTopicRule } from './iot.generated'; @@ -28,11 +29,18 @@ export interface ITopicRule extends IResource { */ export interface TopicRuleProps { /** - * The name of the rule. + * The name of the topic rule. * @default None */ readonly topicRuleName?: string; + /** + * The actions associated with the topic rule. + * + * @default No actions will be perform + */ + readonly actions?: IAction[]; + /** * A simplified SQL syntax to filter messages received on an MQTT topic and push the data elsewhere. * @@ -69,17 +77,19 @@ export class TopicRule extends Resource implements ITopicRule { } /** - * Arn of this rule + * Arn of this topic rule * @attribute */ public readonly topicRuleArn: string; /** - * Name of this rule + * Name of this topic rule * @attribute */ public readonly topicRuleName: string; + private readonly actions: CfnTopicRule.ActionProperty[] = []; + constructor(scope: Construct, id: string, props: TopicRuleProps) { super(scope, id, { physicalName: props.topicRuleName, @@ -90,7 +100,7 @@ export class TopicRule extends Resource implements ITopicRule { const resource = new CfnTopicRule(this, 'Resource', { ruleName: this.physicalName, topicRulePayload: { - actions: [], + actions: Lazy.any({ produce: () => this.actions }), awsIotSqlVersion: sqlConfig.awsIotSqlVersion, sql: sqlConfig.sql, }, @@ -102,5 +112,28 @@ export class TopicRule extends Resource implements ITopicRule { resourceName: this.physicalName, }); this.topicRuleName = this.getResourceNameAttribute(resource.ref); + + props.actions?.forEach(action => { + this.addAction(action); + }); + } + + /** + * Add a action to the topic rule. + * + * @param action the action to associate with the topic rule. + */ + public addAction(action: IAction): void { + const { configuration } = action.bind(this); + + const keys = Object.keys(configuration); + if (keys.length === 0) { + throw new Error('An action property cannot be an empty object.'); + } + if (keys.length > 1) { + throw new Error(`An action property cannot have multiple keys, received: ${keys}`); + } + + this.actions.push(configuration); } } diff --git a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.expected.json b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.expected.json index cf4be2735229e..9daad98410825 100644 --- a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.expected.json +++ b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.expected.json @@ -4,7 +4,13 @@ "Type": "AWS::IoT::TopicRule", "Properties": { "TopicRulePayload": { - "Actions": [], + "Actions": [ + { + "Http": { + "Url": "https://example.com" + } + } + ], "AwsIotSqlVersion": "2015-10-08", "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" } diff --git a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts index a06edc3c3f5e1..0f4bab54a9d2a 100644 --- a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts @@ -10,6 +10,15 @@ class TestStack extends cdk.Stack { new iot.TopicRule(this, 'TopicRule', { sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id FROM 'device/+/data'"), + actions: [ + { + bind: () => ({ + configuration: { + http: { url: 'https://example.com' }, + }, + }), + }, + ], }); } } diff --git a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts index 1dec8c3065a86..66246e860dddb 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -19,14 +19,14 @@ test('Default property', () => { test('can get topic rule name', () => { const stack = new cdk.Stack(); - const rule = new iot.TopicRule(stack, 'MyTopicRule', { + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), }); new cdk.CfnResource(stack, 'Res', { type: 'Test::Resource', properties: { - TopicRuleName: rule.topicRuleName, + TopicRuleName: topicRule.topicRuleName, }, }); @@ -37,14 +37,14 @@ test('can get topic rule name', () => { test('can get topic rule arn', () => { const stack = new cdk.Stack(); - const rule = new iot.TopicRule(stack, 'MyTopicRule', { + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), }); new cdk.CfnResource(stack, 'Res', { type: 'Test::Resource', properties: { - TopicRuleArn: rule.topicRuleArn, + TopicRuleArn: topicRule.topicRuleArn, }, }); @@ -100,16 +100,119 @@ test.each([ }).toThrow('IoT SQL string cannot be empty'); }); +test('can set actions', () => { + const stack = new cdk.Stack(); + + const action1: iot.IAction = { + bind: () => ({ + configuration: { + http: { url: 'http://example.com' }, + }, + }), + }; + const action2: iot.IAction = { + bind: () => ({ + configuration: { + lambda: { functionArn: 'test-functionArn' }, + }, + }), + }; + + new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), + actions: [action1, action2], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { Http: { Url: 'http://example.com' } }, + { Lambda: { FunctionArn: 'test-functionArn' } }, + ], + Sql: "SELECT topic(2) as device_id, temperature FROM 'device/+/data'", + }, + }); +}); + +test('can add an action', () => { + const stack = new cdk.Stack(); + + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), + }); + topicRule.addAction({ + bind: () => ({ + configuration: { + http: { url: 'http://example.com' }, + }, + }), + }); + topicRule.addAction({ + bind: () => ({ + configuration: { + lambda: { functionArn: 'test-functionArn' }, + }, + }), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { Http: { Url: 'http://example.com' } }, + { Lambda: { FunctionArn: 'test-functionArn' } }, + ], + Sql: "SELECT topic(2) as device_id, temperature FROM 'device/+/data'", + }, + }); +}); + +test('cannot add an action as empty object', () => { + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), + }); + + const emptyKeysAction: iot.IAction = { + bind: () => ({ + configuration: {}, + }), + }; + + expect(() => { + topicRule.addAction(emptyKeysAction); + }).toThrow('An action property cannot be an empty object.'); +}); + +test('cannot add an action that have multiple keys', () => { + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), + }); + + const multipleKeysAction: iot.IAction = { + bind: () => ({ + configuration: { + http: { url: 'http://example.com' }, + lambda: { functionArn: 'test-functionArn' }, + }, + }), + }; + + expect(() => { + topicRule.addAction(multipleKeysAction); + }).toThrow('An action property cannot have multiple keys, received: http,lambda'); +}); + test('can import a TopicRule by ARN', () => { const stack = new cdk.Stack(); - const topicRuleArn = 'arn:aws:iot:ap-northeast-1:123456789012:rule/my-rule-name'; + const topicRuleArn = 'arn:aws:iot:ap-northeast-1:123456789012:rule/my-topic-rule-name'; const topicRule = iot.TopicRule.fromTopicRuleArn(stack, 'TopicRuleFromArn', topicRuleArn); expect(topicRule).toMatchObject({ topicRuleArn, - topicRuleName: 'my-rule-name', + topicRuleName: 'my-topic-rule-name', }); }); diff --git a/tools/@aws-cdk/pkglint/lib/rules.ts b/tools/@aws-cdk/pkglint/lib/rules.ts index e18fcb1086fdb..eda6436884d77 100644 --- a/tools/@aws-cdk/pkglint/lib/rules.ts +++ b/tools/@aws-cdk/pkglint/lib/rules.ts @@ -1653,6 +1653,7 @@ export class NoExperimentalDependents extends ValidationRule { ['@aws-cdk/aws-apigatewayv2-authorizers', ['@aws-cdk/aws-apigatewayv2']], ['@aws-cdk/aws-events-targets', ['@aws-cdk/aws-kinesisfirehose']], ['@aws-cdk/aws-kinesisfirehose-destinations', ['@aws-cdk/aws-kinesisfirehose']], + ['@aws-cdk/aws-iot-actions', ['@aws-cdk/aws-iot']], ]); private readonly excludedModules = ['@aws-cdk/cloudformation-include'];