diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/lib/index.ts index 9885df31e..7426ed2a2 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/lib/index.ts @@ -13,6 +13,7 @@ import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as iot from 'aws-cdk-lib/aws-iot'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import { IotToLambda } from '@aws-solutions-constructs/aws-iot-lambda'; import { LambdaToDynamoDB } from '@aws-solutions-constructs/aws-lambda-dynamodb'; @@ -54,13 +55,34 @@ export interface IotToLambdaToDynamoDBProps { * * @default - Read/write access is given to the Lambda function if no value is specified. */ - readonly tablePermissions?: string + readonly tablePermissions?: string, + /** + * Optional Name for the Lambda function environment variable set to the name of the DynamoDB table. + * + * @default - DDB_TABLE_NAME + */ + readonly tableEnvironmentVariableName?: string; + /** + * An existing VPC for the construct to use (construct will NOT create a new VPC in this case) + */ + readonly existingVpc?: ec2.IVpc; + /** + * Properties to override default properties if deployVpc is true + */ + readonly vpcProps?: ec2.VpcProps; + /** + * Whether to deploy a new VPC + * + * @default - false + */ + readonly deployVpc?: boolean; } export class IotToLambdaToDynamoDB extends Construct { public readonly iotTopicRule: iot.CfnTopicRule; public readonly lambdaFunction: lambda.Function; public readonly dynamoTable: dynamodb.Table; + public readonly vpc?: ec2.IVpc; /** * @summary Constructs a new instance of the IotToLambdaToDynamoDB class. @@ -80,17 +102,26 @@ export class IotToLambdaToDynamoDB extends Construct { defaults.CheckListValues(['All', 'Read', 'ReadWrite', 'Write'], [props.tablePermissions], 'table permission'); } - // Setup the IotToLambda - const iotToLambda = new IotToLambda(this, 'IotToLambda', props); - this.iotTopicRule = iotToLambda.iotTopicRule; - this.lambdaFunction = iotToLambda.lambdaFunction; - // Setup the LambdaToDynamoDB const lambdaToDynamoDB = new LambdaToDynamoDB(this, 'LambdaToDynamoDB', { tablePermissions: props.tablePermissions, - existingLambdaObj: this.lambdaFunction, - dynamoTableProps: props.dynamoTableProps + existingLambdaObj: props.existingLambdaObj, + lambdaFunctionProps: props.lambdaFunctionProps, + dynamoTableProps: props.dynamoTableProps, + tableEnvironmentVariableName: props.tableEnvironmentVariableName, + existingVpc: props.existingVpc, + deployVpc: props.deployVpc, + vpcProps: props.vpcProps, }); this.dynamoTable = lambdaToDynamoDB.dynamoTable; + this.vpc = lambdaToDynamoDB.vpc; + + // Setup the IotToLambda + const iotToLambda = new IotToLambda(this, 'IotToLambda', { + existingLambdaObj: lambdaToDynamoDB.lambdaFunction, + iotTopicRuleProps: props.iotTopicRuleProps + }); + this.iotTopicRule = iotToLambda.iotTopicRule; + this.lambdaFunction = iotToLambda.lambdaFunction; } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.expected.json index b8dccfbac..cd978c2a2 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.iot-lambda-dynamodb.expected.json @@ -1,6 +1,6 @@ { "Resources": { - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionServiceRoleC57F7FDA": { + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -55,7 +55,7 @@ ] } }, - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionServiceRoleDefaultPolicyB43AD823": { + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRoleDefaultPolicy2B35234F": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -99,10 +99,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "testiotlambdadynamodbstackIotToLambdaLambdaFunctionServiceRoleDefaultPolicyB43AD823", + "PolicyName": "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRoleDefaultPolicy2B35234F", "Roles": [ { - "Ref": "testiotlambdadynamodbstackIotToLambdaLambdaFunctionServiceRoleC57F7FDA" + "Ref": "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05" } ] }, @@ -117,7 +117,7 @@ } } }, - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionDFEAF894": { + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunction5165A7EE": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { @@ -128,7 +128,7 @@ }, "Role": { "Fn::GetAtt": [ - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionServiceRoleC57F7FDA", + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05", "Arn" ] }, @@ -147,8 +147,8 @@ } }, "DependsOn": [ - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionServiceRoleDefaultPolicyB43AD823", - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionServiceRoleC57F7FDA" + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRoleDefaultPolicy2B35234F", + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05" ], "Metadata": { "cfn_nag": { @@ -169,13 +169,13 @@ } } }, - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionAwsIotLambdaInvokePermission1CF07890C": { + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionAwsIotLambdaInvokePermission13FCFED39": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "FunctionName": { "Fn::GetAtt": [ - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionDFEAF894", + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunction5165A7EE", "Arn" ] }, @@ -188,28 +188,6 @@ } } }, - "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB": { - "Type": "AWS::IoT::TopicRule", - "Properties": { - "TopicRulePayload": { - "Actions": [ - { - "Lambda": { - "FunctionArn": { - "Fn::GetAtt": [ - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionDFEAF894", - "Arn" - ] - } - } - } - ], - "Description": "Processing of DTC messages from the AWS Connected Vehicle Solution.", - "RuleDisabled": false, - "Sql": "SELECT * FROM 'connectedcar/dtc/#'" - } - } - }, "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733": { "Type": "AWS::DynamoDB::Table", "Properties": { @@ -235,6 +213,28 @@ }, "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" + }, + "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunction5165A7EE", + "Arn" + ] + } + } + } + ], + "Description": "Processing of DTC messages from the AWS Connected Vehicle Solution.", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/dtc/#'" + } + } } }, "Parameters": { diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.with-vpc.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.with-vpc.expected.json new file mode 100644 index 000000000..b27565283 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.with-vpc.expected.json @@ -0,0 +1,654 @@ +{ + "Resources": { + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRolePolicy" + } + ] + } + }, + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRoleDefaultPolicy2B35234F": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:AssignPrivateIpAddresses", + "ec2:UnassignPrivateIpAddresses" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRoleDefaultPolicy2B35234F", + "Roles": [ + { + "Ref": "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC." + } + ] + } + } + }, + "testiotlambdadynamodbstackLambdaToDynamoDBReplaceDefaultSecurityGroupsecuritygroup7D851D3B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "with-vpc/test-iot-lambda-dynamodb-stack/LambdaToDynamoDB/ReplaceDefaultSecurityGroup-security-group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W5", + "reason": "Egress of 0.0.0.0/0 is default and generally considered OK" + }, + { + "id": "W40", + "reason": "Egress IPProtocol of -1 is default and generally considered OK" + } + ] + } + } + }, + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunction5165A7EE": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "42a35bbf0dec9ef0ac5b0dde87e71a1b8929e8d2d178dd09ccfb2c928ec0198c.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "CUSTOM_TABLE_NAME": { + "Ref": "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733" + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaToDynamoDBReplaceDefaultSecurityGroupsecuritygroup7D851D3B", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet1Subnet3AB7ADA5" + }, + { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet2SubnetBDEE1FAE" + }, + { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet3Subnet5D41F483" + } + ] + } + }, + "DependsOn": [ + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRoleDefaultPolicy2B35234F", + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05", + "testiotlambdadynamodbstackVpcisolatedSubnet1RouteTableAssociationFAA18521", + "testiotlambdadynamodbstackVpcisolatedSubnet2RouteTableAssociation80ECEB84", + "testiotlambdadynamodbstackVpcisolatedSubnet3RouteTableAssociationF06E774F" + ], + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W58", + "reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions." + }, + { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + }, + { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients" + } + ] + } + } + }, + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionAwsIotLambdaInvokePermission13FCFED39": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunction5165A7EE", + "Arn" + ] + }, + "Principal": "iot.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB", + "Arn" + ] + } + }, + "DependsOn": [ + "testiotlambdadynamodbstackVpcisolatedSubnet1RouteTableAssociationFAA18521", + "testiotlambdadynamodbstackVpcisolatedSubnet2RouteTableAssociation80ECEB84", + "testiotlambdadynamodbstackVpcisolatedSubnet3RouteTableAssociationF06E774F" + ] + }, + "testiotlambdadynamodbstackLambdaToDynamoDBDynamoTableE17E5733": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingMode": "PAY_PER_REQUEST", + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "SSESpecification": { + "SSEEnabled": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testiotlambdadynamodbstackVpc1986A4BB": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc" + } + ] + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet1Subnet3AB7ADA5": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.0.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc/isolatedSubnet1" + } + ] + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet1RouteTableE28AAAB5": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + }, + "Tags": [ + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc/isolatedSubnet1" + } + ] + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet1RouteTableAssociationFAA18521": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet1RouteTableE28AAAB5" + }, + "SubnetId": { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet1Subnet3AB7ADA5" + } + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet2SubnetBDEE1FAE": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.64.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc/isolatedSubnet2" + } + ] + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet2RouteTableAF607A65": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + }, + "Tags": [ + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc/isolatedSubnet2" + } + ] + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet2RouteTableAssociation80ECEB84": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet2RouteTableAF607A65" + }, + "SubnetId": { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet2SubnetBDEE1FAE" + } + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet3Subnet5D41F483": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.128.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc/isolatedSubnet3" + } + ] + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet3RouteTableE56B664A": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + }, + "Tags": [ + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc/isolatedSubnet3" + } + ] + } + }, + "testiotlambdadynamodbstackVpcisolatedSubnet3RouteTableAssociationF06E774F": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet3RouteTableE56B664A" + }, + "SubnetId": { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet3Subnet5D41F483" + } + } + }, + "testiotlambdadynamodbstackVpcFlowLogIAMRole84CD262B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "vpc-flow-logs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc" + } + ] + } + }, + "testiotlambdadynamodbstackVpcFlowLogIAMRoleDefaultPolicy8C209270": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackVpcFlowLogLogGroup0BA54CDB", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackVpcFlowLogIAMRole84CD262B", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testiotlambdadynamodbstackVpcFlowLogIAMRoleDefaultPolicy8C209270", + "Roles": [ + { + "Ref": "testiotlambdadynamodbstackVpcFlowLogIAMRole84CD262B" + } + ] + } + }, + "testiotlambdadynamodbstackVpcFlowLogLogGroup0BA54CDB": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731, + "Tags": [ + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "testiotlambdadynamodbstackVpcFlowLogC88B17DB": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + }, + "ResourceType": "VPC", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackVpcFlowLogIAMRole84CD262B", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "testiotlambdadynamodbstackVpcFlowLogLogGroup0BA54CDB" + }, + "Tags": [ + { + "Key": "Name", + "Value": "with-vpc/test-iot-lambda-dynamodb-stack/Vpc" + } + ], + "TrafficType": "ALL" + } + }, + "testiotlambdadynamodbstackVpcDDBD215AB1B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + "VpcId": { + "Ref": "testiotlambdadynamodbstackVpc1986A4BB" + }, + "RouteTableIds": [ + { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet1RouteTableE28AAAB5" + }, + { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet2RouteTableAF607A65" + }, + { + "Ref": "testiotlambdadynamodbstackVpcisolatedSubnet3RouteTableE56B664A" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "testiotlambdadynamodbstackIotToLambdaIotTopic74F5E3BB": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunction5165A7EE", + "Arn" + ] + } + } + } + ], + "Description": "Processing of DTC messages from the AWS Connected Vehicle Solution.", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'connectedcar/dtc/#'" + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.with-vpc.ts b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.with-vpc.ts new file mode 100644 index 000000000..0bf8cd067 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/integ.with-vpc.ts @@ -0,0 +1,43 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +/// !cdk-integ * +import { App, Stack } from "aws-cdk-lib"; +import { IotToLambdaToDynamoDB, IotToLambdaToDynamoDBProps } from "../lib"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); + +const props: IotToLambdaToDynamoDBProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + }, + deployVpc: true, + tableEnvironmentVariableName: 'CUSTOM_TABLE_NAME', +}; + +new IotToLambdaToDynamoDB(stack, 'test-iot-lambda-dynamodb-stack', props); + +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/iot-lambda-dynamodb.test.ts b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/iot-lambda-dynamodb.test.ts index fe5d815aa..bbdf989be 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/iot-lambda-dynamodb.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/iot-lambda-dynamodb.test.ts @@ -13,6 +13,7 @@ import { IotToLambdaToDynamoDB, IotToLambdaToDynamoDBProps } from "../lib"; import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as cdk from "aws-cdk-lib"; import '@aws-cdk/assert/jest'; @@ -45,7 +46,7 @@ test('check lambda function properties', () => { Handler: "index.handler", Role: { "Fn::GetAtt": [ - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionServiceRoleC57F7FDA", + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunctionServiceRole31915E05", "Arn" ] }, @@ -70,7 +71,7 @@ test('check lambda function permission', () => { Action: "lambda:InvokeFunction", FunctionName: { "Fn::GetAtt": [ - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionDFEAF894", + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunction5165A7EE", "Arn" ] }, @@ -155,7 +156,7 @@ test('check iot topic rule properties', () => { Lambda: { FunctionArn: { "Fn::GetAtt": [ - "testiotlambdadynamodbstackIotToLambdaLambdaFunctionDFEAF894", + "testiotlambdadynamodbstackLambdaToDynamoDBLambdaFunction5165A7EE", "Arn" ] } @@ -249,11 +250,29 @@ test('check lambda function policy ', () => { test('check properties', () => { const stack = new cdk.Stack(); - const construct: IotToLambdaToDynamoDB = deployStack(stack); + const props: IotToLambdaToDynamoDBProps = { + deployVpc: true, + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }; + + const construct = new IotToLambdaToDynamoDB(stack, 'test-iot-lambda-dynamodb-stack', props); - expect(construct.lambdaFunction !== null); - expect(construct.dynamoTable !== null); - expect(construct.iotTopicRule !== null); + expect(construct.lambdaFunction).toBeDefined(); + expect(construct.dynamoTable).toBeDefined(); + expect(construct.iotTopicRule).toBeDefined(); + expect(construct.vpc).toBeDefined(); }); test('check exception for Missing existingObj from props for deploy = false', () => { @@ -303,4 +322,287 @@ test('Check incorrect table permission', () => { // Assertion expect(app).toThrowError(/Invalid table permission submitted - Reed/); -}); \ No newline at end of file +}); + +test('check lambda function custom environment variable', () => { + const stack = new cdk.Stack(); + const props: IotToLambdaToDynamoDBProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, + tableEnvironmentVariableName: 'CUSTOM_DYNAMODB_TABLE', + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }; + + new IotToLambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', props); + + expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Handler: 'index.handler', + Runtime: 'nodejs14.x', + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1', + CUSTOM_DYNAMODB_TABLE: { + Ref: 'testlambdadynamodbstackLambdaToDynamoDBDynamoTable7E730A23' + } + } + } + }); +}); + +// -------------------------------------------------------------- +// Test minimal deployment that deploys a VPC without vpcProps +// -------------------------------------------------------------- +test("Test minimal deployment that deploys a VPC without vpcProps", () => { + // Stack + const stack = new cdk.Stack(); + // Helper declaration + new IotToLambdaToDynamoDB(stack, "lambda-to-dynamodb-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + deployVpc: true, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatodynamodbstackLambdaToDynamoDBReplaceDefaultSecurityGroupsecuritygroup04A024BF", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "lambdatodynamodbstackVpcisolatedSubnet1Subnet90CC3593", + }, + { + Ref: "lambdatodynamodbstackVpcisolatedSubnet2Subnet4693DAE3", + }, + ], + }, + }); + + expect(stack).toHaveResource("AWS::EC2::VPC", { + EnableDnsHostnames: true, + EnableDnsSupport: true, + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + VpcEndpointType: "Gateway", + }); + + expect(stack).toCountResources("AWS::EC2::Subnet", 2); + expect(stack).toCountResources("AWS::EC2::InternetGateway", 0); +}); + +// -------------------------------------------------------------- +// Test minimal deployment that deploys a VPC w/vpcProps +// -------------------------------------------------------------- +test("Test minimal deployment that deploys a VPC w/vpcProps", () => { + // Stack + const stack = new cdk.Stack(); + // Helper declaration + new IotToLambdaToDynamoDB(stack, "lambda-to-dynamodb-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + vpcProps: { + enableDnsHostnames: false, + enableDnsSupport: false, + ipAddresses: ec2.IpAddresses.cidr("192.68.0.0/16"), + }, + deployVpc: true, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatodynamodbstackLambdaToDynamoDBReplaceDefaultSecurityGroupsecuritygroup04A024BF", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "lambdatodynamodbstackVpcisolatedSubnet1Subnet90CC3593", + }, + { + Ref: "lambdatodynamodbstackVpcisolatedSubnet2Subnet4693DAE3", + }, + ], + }, + }); + + expect(stack).toHaveResource("AWS::EC2::VPC", { + CidrBlock: "192.68.0.0/16", + EnableDnsHostnames: true, + EnableDnsSupport: true, + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + VpcEndpointType: "Gateway", + }); + + expect(stack).toCountResources("AWS::EC2::Subnet", 2); + expect(stack).toCountResources("AWS::EC2::InternetGateway", 0); +}); + +// -------------------------------------------------------------- +// Test minimal deployment with an existing VPC +// -------------------------------------------------------------- +test("Test minimal deployment with an existing VPC", () => { + // Stack + const stack = new cdk.Stack(); + + const testVpc = new ec2.Vpc(stack, "test-vpc", {}); + + // Helper declaration + new IotToLambdaToDynamoDB(stack, "lambda-to-dynamodb-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + existingVpc: testVpc, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatodynamodbstackLambdaToDynamoDBReplaceDefaultSecurityGroupsecuritygroup04A024BF", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "testvpcPrivateSubnet1Subnet865FB50A", + }, + { + Ref: "testvpcPrivateSubnet2Subnet23D3396F", + }, + ], + }, + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + VpcEndpointType: "Gateway", + }); +}); + +// -------------------------------------------------------------- +// Test minimal deployment with an existing VPC and existing Lambda function not in a VPC +// +// buildLambdaFunction should throw an error if the Lambda function is not +// attached to a VPC +// -------------------------------------------------------------- +test("Test minimal deployment with an existing VPC and existing Lambda function not in a VPC", () => { + // Stack + const stack = new cdk.Stack(); + + const testLambdaFunction = new lambda.Function(stack, 'test-lambda', { + runtime: lambda.Runtime.NODEJS_14_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }); + + const testVpc = new ec2.Vpc(stack, "test-vpc", {}); + + // Helper declaration + const app = () => { + // Helper declaration + new IotToLambdaToDynamoDB(stack, "lambda-to-dynamodb-stack", { + existingLambdaObj: testLambdaFunction, + existingVpc: testVpc, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }); + }; + + // Assertion + expect(app).toThrowError(); + +}); + +// -------------------------------------------------------------- +// Test bad call with existingVpc and deployVpc +// -------------------------------------------------------------- +test("Test bad call with existingVpc and deployVpc", () => { + // Stack + const stack = new cdk.Stack(); + + const testVpc = new ec2.Vpc(stack, "test-vpc", {}); + + const app = () => { + // Helper declaration + new IotToLambdaToDynamoDB(stack, "lambda-to-dynamodb-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + existingVpc: testVpc, + deployVpc: true, + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql: "SELECT * FROM 'connectedcar/dtc/#'", + actions: [] + } + } + }); + }; + // Assertion + expect(app).toThrowError(); +});