From 5ebdc41501b418454555c00b492e16866ae3be44 Mon Sep 17 00:00:00 2001 From: Hitendra Nishar Date: Fri, 29 Jan 2021 12:54:46 -0500 Subject: [PATCH] Update to version v1.84.0 --- .viperlightignore | 2 +- CHANGELOG.md | 7 + CONTRIBUTING.md | 8 +- DESIGN_GUIDELINES.md | 295 +++++++ source/lerna.json | 2 +- .../iot-lambda-dynamodb.test.js.snap | 1 + .../integ.iot-lambda-dynamodb.expected.json | 1 + .../test/iot-lambda-dynamodb.test.ts | 1 + .../lambda-dynamodb.test.js.snap | 1 + .../integ.add-secondary-index.expected.json | 1 + .../test/integ.no-arguments.expected.json | 1 + .../test/integ.set-billing-mode.expected.json | 1 + .../integ.use-existing-func.expected.json | 1 + .../test/lambda-dynamodb.test.ts | 5 +- .../aws-lambda-s3/README.md | 9 + .../aws-lambda-s3/lib/index.ts | 99 ++- .../aws-lambda-s3/package.json | 4 +- .../test/integ.deployFunction.expected.json | 4 +- .../integ.deployFunctionWithVpc.expected.json | 735 +++++++++++++++++ .../test/integ.deployFunctionWithVpc.ts | 37 + .../test/integ.existingFunction.expected.json | 4 +- .../integ.pre-existing-bucket.expected.json | 4 +- .../aws-lambda-s3/test/lambda-s3.test.ts | 211 ++++- .../aws-lambda-sns/README.md | 9 + .../aws-lambda-sns/lib/index.ts | 37 +- .../aws-lambda-sns/package.json | 2 + .../test/integ.deployFunction.expected.json | 4 +- .../integ.deployFunctionWithVpc.expected.json | 737 ++++++++++++++++++ .../test/integ.deployFunctionWithVpc.ts | 37 + .../test/integ.existingFunction.expected.json | 4 +- .../aws-lambda-sns/test/lambda-sns.test.ts | 211 ++++- .../aws-lambda-sqs/lib/index.ts | 3 +- .../serverless-backend-stack.test.js.snap | 1 + ...integ.002-backend-deployment.expected.json | 1 + 34 files changed, 2428 insertions(+), 52 deletions(-) create mode 100644 DESIGN_GUIDELINES.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunctionWithVpc.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunctionWithVpc.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunctionWithVpc.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunctionWithVpc.ts diff --git a/.viperlightignore b/.viperlightignore index 59b6a6bba..676f44ad8 100644 --- a/.viperlightignore +++ b/.viperlightignore @@ -9,7 +9,7 @@ source/patterns/@aws-solutions-constructs/aws-apigateway-lambda/test/integ.deplo source/patterns/@aws-solutions-constructs/aws-apigateway-lambda/test/integ.existingFunction.expected.json:196 source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway/test/integ.no-arguments.expected.json:196 CODE_OF_CONDUCT.md:4 -CONTRIBUTING.md:217 +CONTRIBUTING.md:223 source/patterns/@aws-solutions-constructs/core/test/step-function-helper.test.ts:118 source/patterns/@aws-solutions-constructs/aws-lambda-sqs/test/integ.deployFunction.expected.json:112 source/patterns/@aws-solutions-constructs/aws-lambda-sqs/test/integ.existingFunction.expected.json:112 diff --git a/CHANGELOG.md b/CHANGELOG.md index f0a6b7a93..326d4a40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## 1.84.0 (2021-01-29) + +### Changed +- Upgraded all patterns to CDK v1.84.0 +- Updated `aws-lambda-sns` and `aws-lambs-s3` to support for VPC +- Added [Design Guidelines](./DESIGN_GUIDELINES.md) + ## 1.83.0 (2021-01-21) ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4688a1971..36944bef1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,7 @@ If there isn't one already, open an issue describing what you intend to contribu advance, because sometimes, someone is already working in this space, so maybe it's worth collaborating with them instead of duplicating the efforts. -### Step 2: Design (optional) +### Step 2: Design In some cases, it is useful to seek for feedback by iterating on a design document. This is useful when you plan a big change or feature, or you want advice on what would be the best path forward. @@ -59,6 +59,12 @@ In such cases, use the GitHub issue description to collect **requirements** and Then, create a design document in markdown format under the `design/` directory and request feedback through a pull request. +If you are proposing a new Solutions Construct, the +best way to do this is create the full README.md document for the Construct in advance (defining all interfaces, +the minimal deployment scenario, the architecture diagram, etc.). This will give us all the information we +need to provide feedback and the document will live on as documentation (saving you that effort labor). Not all +groups of CDK L2 objects is a Solutions Construct - you will want to follow our [design guidelines](./DESIGN_GUIDELINES.md). + Once the design is finalized, you can re-purpose this PR for the implementation, or open a new PR to that end. diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md new file mode 100644 index 000000000..92ad61492 --- /dev/null +++ b/DESIGN_GUIDELINES.md @@ -0,0 +1,295 @@ +# DesignGuidelines + +# Solutions Constructs Design Guidelines + +One of the Solutions Constructs tenets states: + +> **Constructs are Consistent** - once customers are familiar with the use of Constructs, they can adopt additional constructs easily. Customers familiar with using the CDK can easily adopt Constructs. + +This document defines the ways in which Constructs are consistent. These rules have emerged as we built the original Solutions Constructs, and not every Solutions Construct adheres to every rule. We will go back and refactor non-compliant constructs over time, as we can do so without making breaking changes. When proposing a Solutions Construct, use this document to drive your design. + +## Overall Guidelines + +**Constructs Can Be Self Contained** +While passing an existing resource to Construct is essential to the ability to link Constructs together, it should never be required. If your Construct requires the client to create a resource to pass in as an argument to the constructor then you have probably not defined your Construct correctly or what you are designing is not a good fit for the Solutions Constructs library. + +**Constructs should be decomposed to their smallest architecture** +To make a Construct as flexible as possible, it should perform a single architectural task. For instance, a consistent Solutions Construct would not combine an Event rule with a Lambda function with an DynamoDB table because that would not be a single architectural task. These three services would be used to create 2 Solutions Constructs, an Event Rules with a Lambda function, and a Lambda function with a DynamoDB table. A client could use these constructs to perform the original task, but other constructs could use either construct in completely different workloads. While this rule often means a Construct deploys 2 services, there may be situations where the smallest architectural task requires 3 or more services. + +**Use CDK definitions to define a service or resource** +The construct should not create new classes or interfaces to describe services or resources. Although the new class may seem simpler now, as new capabilities are added to the construct the new class will acquire new properties – the ultimate result would be something equivalent to the CDK definition, but not compatible. The CDK definitions are well thought out and interact predictably with other CDK constructs, use them. If you want a client the ability to specify a few attributes of a ConstructProps without specifying every required value, then make the type of that attribute ConstructProps | any. This pattern exists several places in the Solutions Constructs library. + +Another practice that this rule prohibits is putting specific attributes of sub resources in your Solutions Constructs Props object. For instance - if your VPC needs an Internet Gateway, then the client should send VPC Props that create the Internet Gateway, don't create a property at in your Construct Props object of InternetGateway: true. + +**The client should have the option (but not requirement) to provide any props used within the construct** +When you supply a CDK defined props object to any CDK constructor within your construct (L1 or L2), your construct should be able to generate all the default values required to create that L1 or L2 construct. Alternatively, the client should be able to override any or all of those default props values to completely customize the construct. + +There are exceptions to this rule. The Lambda Function L2 construct, for instance, cannot specify a default value for the Lambda code – the client must provide this. Also, at times the workings of the construct require specific props values to work. In this situation, those values will override user provided values. In general, the props values sent to a CDK constructor are generated like this: + +``` +let actualProps = overrideProps(defaultProps, userProvidedProps); +actualProps = overrideProps(actualProps, constructRequiredProps) +``` + +Construct required props should be minimized and listed in the README.md file for the Construct. +There is a possibility that the client could specify some props values that are incompatible with default props values. That is the responsibility of the client – if they choose to not use the default implementation then they take responsibility for the configuration they specify. + +**The Minimal Deployable Pattern Definition should be minimal ** +While a client should have great flexibility to fully specify how a Construct is deployed, the minimal deployment should consist of a single new statement. At times things like table schema might require some additional code – but if your minimal deployable solution is more than a couple lines long or requires creating one or more services first then your construct is outside the patterns for the library. + +**No Business Logic** +The Construct should be restricted to deploying infrastructure. In general this means no schema and no code (eg – no implemented Lambda functions). We don’t rule out the idea that some future use case will require a Lambda function to accomplish an architectural need, but we have not seen that yet. Avoid including coded Lambda functions in your construct unless you’ve talked to us in advance to avoid disappointment. + +**Favor L2 CDK Constructs over L1** +L1 CDK constructs are merely thin code wrappers over the raw CloudFormation definitions – they bring little if any additional value to the table. L2 CDK constructs provide additional functionality, security and interoperability. Whenever possible, Solutions Constructs interfaces should speak in terms of L2 CDK constructs. If your definition includes L1 constructs it may not be rejected, but you will be asked to explain the reasons requiring their use.  + +## Naming + +The name of a Solutions Construct is composed by concatenating the names of the individual services or resources configured by the construct. When it is obvious what resource is being deployed by the service, use just the service name, such as SQS, SNS, DynamoDB, etc. When just the service name is ambiguous, append the specific resource type to the service name, such as SagemakerEndpoint (also do this for a different flavor of an already deployed service, such as DynamoDBStream). + +For the service name, separate the all lower-case service names by dashes and preface the whole name with “aws-“. For Example: + +     aws-s3-lambda +     aws-apigateway-sagemakerendpoint + +For the Typescript class name, use Pascal casing and concatenate the names separated by “To”. For Example: + +     S3ToLambda +     ApigatewayToSagemakerendpoint + +Once again, these rules became clear as we wrote all of the existing constructs and not all of the early constructs comply with these rules. + +# Service Usage in Constructs +It's important for consistency that services are implemented consistently across Solutions Constructs – that clients specify the same properties for a Lambda function in aws-s3-lambda and aws-sqs-lambda. This section specifies the require attributes on your Construct Props interface for each service currently in the library, as well as the reasons behind any exceptions. We are unlikely to approve a construct with additional attributes, although we may if the proposed new attribute is appropriate for us to implement across all constructs using that service. + +If you are creating a construct that uses a service for the first time, defining the appropriate interface is a key step and we will work with you. + +This version of the document also lists what Constructs currently violate these standards and whether making the object compliant would be a breaking change. + +Existing Inconsistencies would not be published, that’s for our internal use – only the Required Properties and Attributes on Props would be published as requirements (along with specific notes). +  + + +## S3 +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| existingBucketObj? | s3.Bucket | Either this or bucketProps must be provided | +| bucketProps? | s3.BucketProps | | +| s3EventTypes? | s3.EventType | Only required when construct responds to S3 events | +| s3EventFilters? | s3.NotificationKeyFilter |Only required when construct responds to S3 events | + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| s3Bucket | s3.Bucket || +| s3LoggingBucket | s3.Bucket || + + +## API Gateway +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| apiGatewayProps | api.RestApiProps | aws-cloudfront-apigateway is an exception (covered below) || +| allow*Name*Operation/ +*name*OperationTemplate | | Required in pairs for integration with DDB and SQS | + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| apiGateway | api.RestApi | | +|apiGatewayCloudwatchRole | iam.Role || +| apiGatewayLogGroup | logs.LogGroup || +| apiGatewayRol | iam.Role || +## IoT +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| iotEndpoint | string | When IoT is *downstream* (e.g. – aws-apigateway-iot) | +| iotTopicRuleProps | iot.CfnTopicRuleProps | When iot is *upstream* (eg – aws-iot-lambda) | + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| iotActionsRole | iam.Role | For upstream IoT| + +## Kinesis Streams +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| existingStreamObj? | kinesis.Stream | | +| kinesisStreamProps? | kinesis.StreamProps || + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| kinesisStream | kinesis.Stream || +| kinesisStreamRole | iam.Role | Only when Kinesis is upstream (because then the role is important to the construct) | + + +## Lambda +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| existingLambdaObj? | lambda.Function || +| lambdaFunctionProps? | lambda.FunctionProps || + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| lambdaFunction | lambda.Function || + +## SQS +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| queueProps? | sqs.QueueProps || +| existingQueueObj? | sqs.Qeue || +| deployDeadLetterQueue? | Boolean || +| deadLetterQueueProps? | sqs.QueueProps || +| maxReceiveCount | number || +| enableQueuePurging | boolean | This is only on 2 constructs, docs talk about a Lambda function role| +| encryptionKey? | kms.Key | Sending messages from an AWS service to an encrypted queue [requires a Customer Master key](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-key-management.html#compatibility-with-aws-services). Those constructs require these properties. | + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| sqsQueue | sqs.Queue || +| deadLetterQueue? | sqs.Queue || +| encryptionKey | kms.Key | Only for service to SQS constructs that require a non-default CMK. | + + +## CloudFront +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| cloudFrontDistributionProps? | cloudfront.CloudFront.WebDistributionProps || +| insertHttpSecurityHeaders? | boolean || + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| cloudFrontLoggingBucket? s3.Bucket || +| cloudFrontWebDistribution cloudfront.CloudrontWebDistribution || + + +## DynamoDB +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| dynamoTableProps? | dynamodb.TableProps || +| existingTableObj? | dynamodb.Table || +| tablePermissions? | string | Only where DynamoDB is a data store being accessed by the construct| +| dynamoEventSourceProps? | aws-lambda-event-sources.DynamoEventSourceProps | Only where DynamoDB is invoking other services (dynamodb streams) | + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| dynamoTable | dynamodb.Table || + +## Events Rules +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| eventRuleProps | events.RuleProps || + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| eventsRule | events.Rule || + +## Firehose +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| kinesisFirehoseProps? | aws-kinesisfirehose.CfnDeliveryStreamProps || + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| kinesisFirehose | kinesisfirehose.CfnDeliveryStream || +| kinesisFirehoseRole | iam.Role || +| kinesisFirehoseLogGroup | logs.LogGroup || + + +## SNS +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| existingTopicObj? | sns.Topic || +| topicProps? | sns.TopicProps || +| enableEncryptionWithCustomerManagedKey? | boolean | Sending messages from an AWS service to an encrypted Topic [requires a Customer Master key](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-key-management.html#compatibility-with-aws-services). Those constructs require these properties. | +| encryptionKey? | kms.Key | +| encryptionKeyProps? | kms.KeyProps | + + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| snsTopic | sns.Topic | | +| encryptionKey | kms.Key | Only required when AWS service is writing to the SNS topic (similar to SQS) | + + +## Step Functions +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| stateMachineProps | sfn.StateMachineProps || + + +**Required Construct Properties** +| Name | Type | Notes | +| --- | --- | --- | +| stateMachine | sfn.StateMachine || +| stateMachineLoggingGroup | logs.LogGroup || + + +## ElasticSearch +**Required Attributes on Props** + +| Name | Type | Notes | +| --- | --- | --- | +| esDomainProps? | elasticsearch.CfnDomainProps || +| domainName | string || + + + +**Required Construct Properties** + +| Name | Type | Notes | +| --- | --- | --- | +| elasticsearchDomain | elasticsearch.CfnDomain || +| elasticsearchDomainRole | iam.Role || + + diff --git a/source/lerna.json b/source/lerna.json index 5e1003492..e53d86bdf 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "1.83.0" + "version": "1.84.0" } diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/__snapshots__/iot-lambda-dynamodb.test.js.snap b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/__snapshots__/iot-lambda-dynamodb.test.js.snap index 249ad757e..6f11ba6f5 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/__snapshots__/iot-lambda-dynamodb.test.js.snap +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/test/__snapshots__/iot-lambda-dynamodb.test.js.snap @@ -219,6 +219,7 @@ Object { "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", 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 100ff7885..646a1369d 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 @@ -76,6 +76,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", 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 cad70b6b9..64e2a0691 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 @@ -226,6 +226,7 @@ test('check lambda function policy ', () => { "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/__snapshots__/lambda-dynamodb.test.js.snap b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/__snapshots__/lambda-dynamodb.test.js.snap index 7f69c1124..8ffabeaf1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/__snapshots__/lambda-dynamodb.test.js.snap +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/__snapshots__/lambda-dynamodb.test.js.snap @@ -204,6 +204,7 @@ Object { "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.add-secondary-index.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.add-secondary-index.expected.json index 27bfd8ecb..0576b4b03 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.add-secondary-index.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.add-secondary-index.expected.json @@ -76,6 +76,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.no-arguments.expected.json index 0f8faec03..f046bb0f4 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.no-arguments.expected.json @@ -76,6 +76,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.set-billing-mode.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.set-billing-mode.expected.json index 979119b61..d07544357 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.set-billing-mode.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.set-billing-mode.expected.json @@ -76,6 +76,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.use-existing-func.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.use-existing-func.expected.json index b6466a5e7..cac4aafcf 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.use-existing-func.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/integ.use-existing-func.expected.json @@ -76,6 +76,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/lambda-dynamodb.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/lambda-dynamodb.test.ts index b89c4c1ce..cbb988c18 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/lambda-dynamodb.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/test/lambda-dynamodb.test.ts @@ -192,6 +192,7 @@ test('check lambda function policy default table permissions', () => { "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", @@ -354,7 +355,8 @@ test('check lambda function policy ReadOnly table permissions', () => { "dynamodb:GetShardIterator", "dynamodb:Query", "dynamodb:GetItem", - "dynamodb:Scan" + "dynamodb:Scan", + "dynamodb:ConditionCheckItem" ], Effect: "Allow", Resource: [ @@ -461,6 +463,7 @@ test('check lambda function policy ReadWrite table permissions', () => { "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/README.md index 27d53f425..2812641c7 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/README.md @@ -60,6 +60,14 @@ _Parameters_ |existingBucketObj?|[`s3.IBucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.IBucket.html)|Existing instance of S3 Bucket object, if this is set then the bucketProps is ignored.| |bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|User provided props to override the default props for the S3 Bucket.| |bucketPermissions?|`string[]`|Optional bucket permissions to grant to the Lambda function. One or more of the following may be specified: `Delete`, `Put`, `Read`, `ReadWrite`, `Write`.| +|existingVpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html)|An optional, existing VPC into which this pattern should be deployed. When deployed in a VPC, the Lambda function will use ENIs in the VPC to access network resources and an Interface Endpoint will be created in the VPC for Amazon SQS. If an existing VPC is provided, the `deployVpc` property cannot be `true`. This uses `ec2.IVpc` to allow clients to supply VPCs that exist outside the stack using the [`ec2.Vpc.fromLookup()`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.Vpc.html#static-fromwbrlookupscope-id-options) method.| +|deployVpc?|`boolean`|Whether to create a new VPC based on `vpcProps` into which to deploy this pattern. Setting this to true will deploy the minimal, most private VPC to run the pattern: + +* One isolated subnet in each Availability Zone used by the CDK program +* `enableDnsHostnames` and `enableDnsSupport` will both be set to true + +If this property is `true` then `existingVpc` cannot be specified. Defaults to `false`.| +|vpcProps?|`ec2.VpcProps`|Optional user-provided properties to override the default properties for the new VPC. `enableDnsHostnames`, `enableDnsSupport`, `natGateways` and `subnetConfiguration` are set by the pattern, so any values for those properties supplied here will be overrriden. If `deployVpc` is not `true` then this property will be ignored.| ## Pattern Properties @@ -68,6 +76,7 @@ _Parameters_ |lambdaFunction|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| |s3Bucket?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of the S3 bucket created by the pattern.| |s3LoggingBucket?|[`s3.Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html)|Returns an instance of s3.Bucket created by the construct as the logging bucket for the primary bucket.| +|vpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html)|Returns an interface on the VPC used by the pattern (if any). This may be a VPC created by the pattern or the VPC supplied to the pattern constructor.| ## Default settings diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/lib/index.ts index 2a3baf2e6..aeda29d1f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/lib/index.ts @@ -14,6 +14,7 @@ // Imports import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as defaults from '@aws-solutions-constructs/core'; import { Construct } from '@aws-cdk/core'; @@ -21,37 +22,51 @@ import { Construct } from '@aws-cdk/core'; * @summary The properties for the LambdaToS3 class. */ export interface LambdaToS3Props { - /** - * Existing instance of Lambda Function object, if this is set then the lambdaFunctionProps is ignored. - * - * @default - None - */ - readonly existingLambdaObj?: lambda.Function, - /** - * User provided props to override the default props for the Lambda function. - * - * @default - Default properties are used. - */ - readonly lambdaFunctionProps?: lambda.FunctionProps - /** - * Existing instance of S3 Bucket object, if this is set then the bucketProps is ignored. - * - * @default - None - */ - readonly existingBucketObj?: s3.IBucket, - /** - * User provided props to override the default props for the S3 Bucket. - * - * @default - Default props are used - */ - readonly bucketProps?: s3.BucketProps - /** - * Optional bucket permissions to grant to the Lambda function. - * One or more of the following may be specified: "Delete", "Put", "Read", "ReadWrite", "Write". - * - * @default - Read/write access is given to the Lambda function if no value is specified. - */ - readonly bucketPermissions?: string[] + /** + * Existing instance of Lambda Function object, if this is set then the lambdaFunctionProps is ignored. + * + * @default - None + */ + readonly existingLambdaObj?: lambda.Function, + /** + * User provided props to override the default props for the Lambda function. + * + * @default - Default properties are used. + */ + readonly lambdaFunctionProps?: lambda.FunctionProps, + /** + * Existing instance of S3 Bucket object, if this is set then the bucketProps is ignored. + * + * @default - None + */ + readonly existingBucketObj?: s3.IBucket, + /** + * User provided props to override the default props for the S3 Bucket. + * + * @default - Default props are used + */ + readonly bucketProps?: s3.BucketProps, + /** + * Optional bucket permissions to grant to the Lambda function. + * One or more of the following may be specified: "Delete", "Put", "Read", "ReadWrite", "Write". + * + * @default - Read/write access is given to the Lambda function if no value is specified. + */ + readonly bucketPermissions?: 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; } /** @@ -61,6 +76,7 @@ export class LambdaToS3 extends Construct { public readonly lambdaFunction: lambda.Function; public readonly s3Bucket?: s3.Bucket; public readonly s3LoggingBucket?: s3.Bucket; + public readonly vpc?: ec2.IVpc; /** * @summary Constructs a new instance of the LambdaToS3 class. @@ -74,10 +90,29 @@ export class LambdaToS3 extends Construct { super(scope, id); let bucket: s3.IBucket; + if (props.deployVpc || props.existingVpc) { + if (props.deployVpc && props.existingVpc) { + throw new Error("More than 1 VPC specified in the properties"); + } + + this.vpc = defaults.buildVpc(scope, { + defaultVpcProps: defaults.DefaultIsolatedVpcProps(), + existingVpc: props.existingVpc, + userVpcProps: props.vpcProps, + constructVpcProps: { + enableDnsHostnames: true, + enableDnsSupport: true, + }, + }); + + defaults.AddAwsServiceEndpoint(scope, this.vpc, defaults.ServiceEndpointTypes.S3); + } + // Setup the Lambda function this.lambdaFunction = defaults.buildLambdaFunction(this, { existingLambdaObj: props.existingLambdaObj, - lambdaFunctionProps: props.lambdaFunctionProps + lambdaFunctionProps: props.lambdaFunctionProps, + vpc: this.vpc, }); // Setup S3 Bucket diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/package.json b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/package.json index 493922a23..3dfeed452 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/package.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/package.json @@ -55,6 +55,7 @@ "dependencies": { "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-solutions-constructs/core": "0.0.0", "constructs": "^3.2.0" @@ -74,6 +75,7 @@ "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-solutions-constructs/core": "0.0.0", - "constructs": "^3.2.0" + "constructs": "^3.2.0", + "@aws-cdk/aws-ec2": "0.0.0" } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunction.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunction.expected.json index 917875438..8ffee9563 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunction.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunction.expected.json @@ -164,14 +164,12 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "testlambdas3LambdaFunctionServiceRole72E20379", "Arn" ] }, - "Runtime": "nodejs10.x", "Environment": { "Variables": { "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", @@ -180,6 +178,8 @@ } } }, + "Handler": "index.handler", + "Runtime": "nodejs10.x", "TracingConfig": { "Mode": "Active" } diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunctionWithVpc.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunctionWithVpc.expected.json new file mode 100644 index 000000000..3b5a16ffe --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunctionWithVpc.expected.json @@ -0,0 +1,735 @@ +{ + "Description": "Integration Test for aws-lambda-s3", + "Resources": { + "testlambdas3LambdaFunctionServiceRole72E20379": { + "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" + } + ] + } + }, + "testlambdas3LambdaFunctionServiceRoleDefaultPolicyB6FC6493": { + "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": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testlambdas3S3Bucket179A52E6", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testlambdas3S3Bucket179A52E6", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdas3LambdaFunctionServiceRoleDefaultPolicyB6FC6493", + "Roles": [ + { + "Ref": "testlambdas3LambdaFunctionServiceRole72E20379" + } + ] + }, + "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." + } + ] + } + } + }, + "testlambdas3ReplaceDefaultSecurityGroup39AA8943": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test-lambda-s3/test-lambda-s3/ReplaceDefaultSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "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" + } + ] + } + } + }, + "testlambdas3LambdaFunction1B8788C9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "testlambdas3LambdaFunctionServiceRole72E20379", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": { + "Ref": "testlambdas3S3Bucket179A52E6" + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdas3ReplaceDefaultSecurityGroup39AA8943", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }, + "DependsOn": [ + "testlambdas3LambdaFunctionServiceRoleDefaultPolicyB6FC6493", + "testlambdas3LambdaFunctionServiceRole72E20379" + ], + "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 more tighter permissions." + } + ] + } + } + }, + "testlambdas3S3LoggingBucketD42FC73D": { + "Type": "AWS::S3::Bucket", + "Properties": { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket" + } + ] + } + } + }, + "testlambdas3S3LoggingBucketPolicyCEAFB213": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testlambdas3S3LoggingBucketD42FC73D" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testlambdas3S3LoggingBucketD42FC73D", + "Arn" + ] + }, + "/*" + ] + ] + }, + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + } + } + }, + "testlambdas3S3Bucket179A52E6": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + } + } + ] + }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionTransitions": [ + { + "StorageClass": "GLACIER", + "TransitionInDays": 90 + } + ], + "Status": "Enabled" + } + ] + }, + "LoggingConfiguration": { + "DestinationBucketName": { + "Ref": "testlambdas3S3LoggingBucketD42FC73D" + } + }, + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testlambdas3S3BucketPolicyE899B211": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testlambdas3S3Bucket179A52E6" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testlambdas3S3Bucket179A52E6", + "Arn" + ] + }, + "/*" + ] + ] + }, + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + } + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-s3/Vpc" + } + ] + } + }, + "VpcisolatedSubnet1SubnetE62B1B9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "test-lambda-s3/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableE442650B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-s3/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableAssociationD259E31A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + } + }, + "VpcisolatedSubnet2Subnet39217055": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "test-lambda-s3/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTable334F9764": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-s3/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTableAssociation25A4716F": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet2RouteTable334F9764" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + } + }, + "VpcisolatedSubnet3Subnet44F2537D": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "test-lambda-s3/Vpc/isolatedSubnet3" + } + ] + } + }, + "VpcisolatedSubnet3RouteTableA2F6BBC0": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-s3/Vpc/isolatedSubnet3" + } + ] + } + }, + "VpcisolatedSubnet3RouteTableAssociationDC010BEB": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet3RouteTableA2F6BBC0" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + } + }, + "VpcFlowLogIAMRole6A475D41": { + "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": "test-lambda-s3/Vpc" + } + ] + } + }, + "VpcFlowLogIAMRoleDefaultPolicy406FB995": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogLogGroup7B5C56B9", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VpcFlowLogIAMRoleDefaultPolicy406FB995", + "Roles": [ + { + "Ref": "VpcFlowLogIAMRole6A475D41" + } + ] + } + }, + "VpcFlowLogLogGroup7B5C56B9": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "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)" + } + ] + } + } + }, + "VpcFlowLog8FF33A73": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "Vpc8378EB38" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "VpcFlowLogLogGroup7B5C56B9" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-s3/Vpc" + } + ] + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + { + "Ref": "VpcisolatedSubnet2RouteTable334F9764" + }, + { + "Ref": "VpcisolatedSubnet3RouteTableA2F6BBC0" + } + ], + "VpcEndpointType": "Gateway" + } + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunctionWithVpc.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunctionWithVpc.ts new file mode 100644 index 000000000..1e7fec9f5 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.deployFunctionWithVpc.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2021 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. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToS3, LambdaToS3Props } from "../lib"; +import * as lambda from "@aws-cdk/aws-lambda"; + +// Setup +const app = new App(); +const stack = new Stack(app, "test-lambda-s3"); +stack.templateOptions.description = "Integration Test for aws-lambda-s3"; + +// Definitions +const props: LambdaToS3Props = { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + deployVpc: true, +}; + +new LambdaToS3(stack, "test-lambda-s3", props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.existingFunction.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.existingFunction.expected.json index 3e475472c..cd148f639 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.existingFunction.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.existingFunction.expected.json @@ -164,14 +164,12 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "LambdaFunctionServiceRole0C4CDE0B", "Arn" ] }, - "Runtime": "nodejs10.x", "Environment": { "Variables": { "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", @@ -180,6 +178,8 @@ } } }, + "Handler": "index.handler", + "Runtime": "nodejs10.x", "TracingConfig": { "Mode": "Active" } diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.pre-existing-bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.pre-existing-bucket.expected.json index bc7bce574..70b45df53 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.pre-existing-bucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/integ.pre-existing-bucket.expected.json @@ -168,20 +168,20 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "testlambdas3preexistingbucketLambdaFunctionServiceRole9AC7CED0", "Arn" ] }, - "Runtime": "nodejs10.x", "Environment": { "Variables": { "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", "S3_BUCKET_NAME": "cdktoolkit-stagingbucket-1cjqz1mn5psg3" } }, + "Handler": "index.handler", + "Runtime": "nodejs10.x", "TracingConfig": { "Mode": "Active" } diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/lambda-s3.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/lambda-s3.test.ts index 4eafc95dc..c1bebab52 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/lambda-s3.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/test/lambda-s3.test.ts @@ -14,6 +14,7 @@ // Imports import { Stack } from "@aws-cdk/core"; import * as lambda from "@aws-cdk/aws-lambda"; +import * as ec2 from "@aws-cdk/aws-ec2"; import { LambdaToS3 } from '../lib'; import { SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; @@ -199,4 +200,212 @@ test('Test the bucketProps override', () => { IndexDocument: 'index.main.html' } }); -}); \ No newline at end of file +}); + +// -------------------------------------------------------------- +// Test minimal deployment that deploys a VPC without vpcProps +// -------------------------------------------------------------- +test("Test minimal deployment that deploys a VPC without vpcProps", () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToS3(stack, "lambda-to-s3-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + deployVpc: true, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatos3stackReplaceDefaultSecurityGroup00762EE4", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B", + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055", + }, + ], + }, + }); + + 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 Stack(); + // Helper declaration + new LambdaToS3(stack, "lambda-to-s3-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + vpcProps: { + enableDnsHostnames: false, + enableDnsSupport: false, + cidr: "192.68.0.0/16", + }, + deployVpc: true, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatos3stackReplaceDefaultSecurityGroup00762EE4", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B", + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055", + }, + ], + }, + }); + + 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 Stack(); + + const testVpc = new ec2.Vpc(stack, "test-vpc", {}); + + // Helper declaration + new LambdaToS3(stack, "lambda-to-s3-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + existingVpc: testVpc, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatos3stackReplaceDefaultSecurityGroup00762EE4", + "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 VPCs +// +// 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 Stack(); + + const testLambdaFunction = new lambda.Function(stack, 'test-lamba', { + runtime: lambda.Runtime.NODEJS_10_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 LambdaToS3(stack, "lambda-to-s3-stack", { + existingLambdaObj: testLambdaFunction, + existingVpc: testVpc, + }); + }; + + // Assertion + expect(app).toThrowError(); + +}); + +// -------------------------------------------------------------- +// Test bad call with existingVpc and deployVpc +// -------------------------------------------------------------- +test("Test bad call with existingVpc and deployVpc", () => { + // Stack + const stack = new Stack(); + + const testVpc = new ec2.Vpc(stack, "test-vpc", {}); + + const app = () => { + // Helper declaration + new LambdaToS3(stack, "lambda-to-s3-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + existingVpc: testVpc, + deployVpc: true, + }); + }; + // Assertion + expect(app).toThrowError(); +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/README.md index 43537fe77..a2897373a 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/README.md @@ -59,6 +59,14 @@ _Parameters_ |lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|User provided props to override the default props for the Lambda function.| |existingTopicObj?|[`sns.Topic`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of SNS Topic object, if this is set then the topicProps is ignored.| |topicProps?|[`sns.TopicProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.TopicProps.html)|Optional user provided properties to override the default properties for the SNS topic.| +|existingVpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html)|An optional, existing VPC into which this pattern should be deployed. When deployed in a VPC, the Lambda function will use ENIs in the VPC to access network resources and an Interface Endpoint will be created in the VPC for Amazon SQS. If an existing VPC is provided, the `deployVpc` property cannot be `true`. This uses `ec2.IVpc` to allow clients to supply VPCs that exist outside the stack using the [`ec2.Vpc.fromLookup()`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.Vpc.html#static-fromwbrlookupscope-id-options) method.| +|deployVpc?|`boolean`|Whether to create a new VPC based on `vpcProps` into which to deploy this pattern. Setting this to true will deploy the minimal, most private VPC to run the pattern: + +* One isolated subnet in each Availability Zone used by the CDK program +* `enableDnsHostnames` and `enableDnsSupport` will both be set to true + +If this property is `true` then `existingVpc` cannot be specified. Defaults to `false`.| +|vpcProps?|`ec2.VpcProps`|Optional user-provided properties to override the default properties for the new VPC. `enableDnsHostnames`, `enableDnsSupport`, `natGateways` and `subnetConfiguration` are set by the pattern, so any values for those properties supplied here will be overrriden. If `deployVpc` is not `true` then this property will be ignored.| ## Pattern Properties @@ -66,6 +74,7 @@ _Parameters_ |:-------------|:----------------|-----------------| |lambdaFunction|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Returns an instance of the Lambda function created by the pattern.| |snsTopic|[`sns.Topic`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.Topic.html)|Returns an instance of the SNS topic created by the pattern.| +|vpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html)|Returns an interface on the VPC used by the pattern (if any). This may be a VPC created by the pattern or the VPC supplied to the pattern constructor.| ## Default settings diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/lib/index.ts index 4c74b3b75..baad51a0d 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/lib/index.ts @@ -16,6 +16,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as defaults from '@aws-solutions-constructs/core'; import { Construct } from '@aws-cdk/core'; +import * as ec2 from "@aws-cdk/aws-ec2"; /** * @summary The properties for the LambdaToSns class. @@ -45,6 +46,20 @@ export interface LambdaToSnsProps { * @default - Default properties are used. */ readonly topicProps?: sns.TopicProps + /** + * 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; } /** @@ -53,6 +68,7 @@ export interface LambdaToSnsProps { export class LambdaToSns extends Construct { public readonly lambdaFunction: lambda.Function; public readonly snsTopic: sns.Topic; + public readonly vpc?: ec2.IVpc; /** * @summary Constructs a new instance of the LambdaToSns class. @@ -65,10 +81,29 @@ export class LambdaToSns extends Construct { constructor(scope: Construct, id: string, props: LambdaToSnsProps) { super(scope, id); + if (props.deployVpc || props.existingVpc) { + if (props.deployVpc && props.existingVpc) { + throw new Error("More than 1 VPC specified in the properties"); + } + + this.vpc = defaults.buildVpc(scope, { + defaultVpcProps: defaults.DefaultIsolatedVpcProps(), + existingVpc: props.existingVpc, + userVpcProps: props.vpcProps, + constructVpcProps: { + enableDnsHostnames: true, + enableDnsSupport: true, + }, + }); + + defaults.AddAwsServiceEndpoint(scope, this.vpc, defaults.ServiceEndpointTypes.SNS); + } + // Setup the Lambda function this.lambdaFunction = defaults.buildLambdaFunction(this, { existingLambdaObj: props.existingLambdaObj, - lambdaFunctionProps: props.lambdaFunctionProps + lambdaFunctionProps: props.lambdaFunctionProps, + vpc: this.vpc, }); // Setup the SNS topic diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/package.json b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/package.json index d7a0c8c0d..387fdeb17 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/package.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/package.json @@ -57,6 +57,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-solutions-constructs/core": "0.0.0", "constructs": "^3.2.0" }, @@ -74,6 +75,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-solutions-constructs/core": "0.0.0", "constructs": "^3.2.0" diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunction.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunction.expected.json index 72dbd8885..2013fee4f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunction.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunction.expected.json @@ -138,14 +138,12 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "testlambdasnsLambdaFunctionServiceRole9C412F74", "Arn" ] }, - "Runtime": "nodejs10.x", "Environment": { "Variables": { "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", @@ -160,6 +158,8 @@ } } }, + "Handler": "index.handler", + "Runtime": "nodejs10.x", "TracingConfig": { "Mode": "Active" } diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunctionWithVpc.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunctionWithVpc.expected.json new file mode 100644 index 000000000..7e483b842 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunctionWithVpc.expected.json @@ -0,0 +1,737 @@ +{ + "Description": "Integration Test for aws-lambda-sns", + "Resources": { + "testlambdasnsLambdaFunctionServiceRole9C412F74": { + "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" + } + ] + } + }, + "testlambdasnsLambdaFunctionServiceRoleDefaultPolicyBB1D55CB": { + "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": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "testlambdasnsSnsTopic57DFED98" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testlambdasnsLambdaFunctionServiceRoleDefaultPolicyBB1D55CB", + "Roles": [ + { + "Ref": "testlambdasnsLambdaFunctionServiceRole9C412F74" + } + ] + }, + "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." + } + ] + } + } + }, + "testlambdasnsReplaceDefaultSecurityGroup1EB706DE": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test-lambda-sns/test-lambda-sns/ReplaceDefaultSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "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" + } + ] + } + } + }, + "testlambdasnsLambdaFunctionD8BC8ABA": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "testlambdasnsLambdaFunctionServiceRole9C412F74", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "SNS_TOPIC_NAME": { + "Fn::GetAtt": [ + "testlambdasnsSnsTopic57DFED98", + "TopicName" + ] + }, + "SNS_TOPIC_ARN": { + "Ref": "testlambdasnsSnsTopic57DFED98" + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testlambdasnsReplaceDefaultSecurityGroup1EB706DE", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }, + "DependsOn": [ + "testlambdasnsLambdaFunctionServiceRoleDefaultPolicyBB1D55CB", + "testlambdasnsLambdaFunctionServiceRole9C412F74" + ], + "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 more tighter permissions." + } + ] + } + } + }, + "testlambdasnsSnsTopic57DFED98": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":alias/aws/sns" + ] + ] + } + } + }, + "testlambdasnsSnsTopicPolicy4481ABC3": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "SNS:Publish", + "SNS:RemovePermission", + "SNS:SetTopicAttributes", + "SNS:DeleteTopic", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:Receive", + "SNS:AddPermission", + "SNS:Subscribe" + ], + "Condition": { + "StringEquals": { + "AWS:SourceOwner": { + "Ref": "AWS::AccountId" + } + } + }, + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Ref": "testlambdasnsSnsTopic57DFED98" + }, + "Sid": "TopicOwnerOnlyAccess" + }, + { + "Action": [ + "SNS:Publish", + "SNS:RemovePermission", + "SNS:SetTopicAttributes", + "SNS:DeleteTopic", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:Receive", + "SNS:AddPermission", + "SNS:Subscribe" + ], + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": "*", + "Resource": { + "Ref": "testlambdasnsSnsTopic57DFED98" + }, + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "testlambdasnsSnsTopic57DFED98" + } + ] + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-sns/Vpc" + } + ] + } + }, + "VpcisolatedSubnet1SubnetE62B1B9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "test-lambda-sns/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableE442650B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-sns/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableAssociationD259E31A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + } + }, + "VpcisolatedSubnet2Subnet39217055": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "test-lambda-sns/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTable334F9764": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-sns/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTableAssociation25A4716F": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet2RouteTable334F9764" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + } + }, + "VpcisolatedSubnet3Subnet44F2537D": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "test-lambda-sns/Vpc/isolatedSubnet3" + } + ] + } + }, + "VpcisolatedSubnet3RouteTableA2F6BBC0": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-sns/Vpc/isolatedSubnet3" + } + ] + } + }, + "VpcisolatedSubnet3RouteTableAssociationDC010BEB": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet3RouteTableA2F6BBC0" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + } + }, + "VpcFlowLogIAMRole6A475D41": { + "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": "test-lambda-sns/Vpc" + } + ] + } + }, + "VpcFlowLogIAMRoleDefaultPolicy406FB995": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogLogGroup7B5C56B9", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VpcFlowLogIAMRoleDefaultPolicy406FB995", + "Roles": [ + { + "Ref": "VpcFlowLogIAMRole6A475D41" + } + ] + } + }, + "VpcFlowLogLogGroup7B5C56B9": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "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)" + } + ] + } + } + }, + "VpcFlowLog8FF33A73": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "Vpc8378EB38" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "VpcFlowLogLogGroup7B5C56B9" + }, + "Tags": [ + { + "Key": "Name", + "Value": "test-lambda-sns/Vpc" + } + ] + } + }, + "VpcSNS5B664381": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".sns" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "ReplaceEndpointDefaultSecurityGroupCF0CCAF1", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ], + "VpcEndpointType": "Interface" + } + }, + "ReplaceEndpointDefaultSecurityGroupCF0CCAF1": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "test-lambda-sns/ReplaceEndpointDefaultSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": { + "Fn::GetAtt": [ + "Vpc8378EB38", + "CidrBlock" + ] + }, + "Description": { + "Fn::Join": [ + "", + [ + "from ", + { + "Fn::GetAtt": [ + "Vpc8378EB38", + "CidrBlock" + ] + }, + ":443" + ] + ] + }, + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "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" + } + ] + } + } + } + }, + "Parameters": { + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3Bucket749AC458": { + "Type": "String", + "Description": "S3 bucket for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420S3VersionKeyFF5CC16D": { + "Type": "String", + "Description": "S3 key for asset version \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + }, + "AssetParameters8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420ArtifactHashA71E92AD": { + "Type": "String", + "Description": "Artifact hash for asset \"8efd3dd9643a4d64a128ad582cab718a1e464bcc719bbbcf0e7b0481188a0420\"" + } + } +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunctionWithVpc.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunctionWithVpc.ts new file mode 100644 index 000000000..2909d7299 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.deployFunctionWithVpc.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2021 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. + */ + +// Imports +import { App, Stack } from "@aws-cdk/core"; +import { LambdaToSns, LambdaToSnsProps } from "../lib"; +import * as lambda from "@aws-cdk/aws-lambda"; + +// Setup +const app = new App(); +const stack = new Stack(app, "test-lambda-sns"); +stack.templateOptions.description = "Integration Test for aws-lambda-sns"; + +// Definitions +const props: LambdaToSnsProps = { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + deployVpc: true, +}; + +new LambdaToSns(stack, "test-lambda-sns", props); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.existingFunction.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.existingFunction.expected.json index 37b4336ea..9bfbe7c60 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.existingFunction.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/integ.existingFunction.expected.json @@ -138,14 +138,12 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "LambdaFunctionServiceRole0C4CDE0B", "Arn" ] }, - "Runtime": "nodejs10.x", "Environment": { "Variables": { "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", @@ -160,6 +158,8 @@ } } }, + "Handler": "index.handler", + "Runtime": "nodejs10.x", "TracingConfig": { "Mode": "Active" } diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/lambda-sns.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/lambda-sns.test.ts index f2d746c84..627525403 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/lambda-sns.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/test/lambda-sns.test.ts @@ -15,6 +15,7 @@ import { Stack } from "@aws-cdk/core"; import * as lambda from "@aws-cdk/aws-lambda"; import * as sns from "@aws-cdk/aws-sns"; +import * as ec2 from "@aws-cdk/aws-ec2"; import { LambdaToSns, LambdaToSnsProps } from '../lib'; import { SynthUtils, expect as expectCDK, haveResource } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; @@ -145,4 +146,212 @@ test('Test the properties', () => { // Assertion 2 const topic = pattern.snsTopic; expect(topic !== null); -}); \ No newline at end of file +}); + +// -------------------------------------------------------------- +// Test minimal deployment that deploys a VPC without vpcProps +// -------------------------------------------------------------- +test("Test minimal deployment that deploys a VPC without vpcProps", () => { + // Stack + const stack = new Stack(); + // Helper declaration + new LambdaToSns(stack, "lambda-to-sns-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + deployVpc: true, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatosnsstackReplaceDefaultSecurityGroup719F066F", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B", + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055", + }, + ], + }, + }); + + expect(stack).toHaveResource("AWS::EC2::VPC", { + EnableDnsHostnames: true, + EnableDnsSupport: true, + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + VpcEndpointType: "Interface", + }); + + 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 Stack(); + // Helper declaration + new LambdaToSns(stack, "lambda-to-sns-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + vpcProps: { + enableDnsHostnames: false, + enableDnsSupport: false, + cidr: "192.68.0.0/16", + }, + deployVpc: true, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatosnsstackReplaceDefaultSecurityGroup719F066F", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "VpcisolatedSubnet1SubnetE62B1B9B", + }, + { + Ref: "VpcisolatedSubnet2Subnet39217055", + }, + ], + }, + }); + + expect(stack).toHaveResource("AWS::EC2::VPC", { + CidrBlock: "192.68.0.0/16", + EnableDnsHostnames: true, + EnableDnsSupport: true, + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + VpcEndpointType: "Interface", + }); + + 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 Stack(); + + const testVpc = new ec2.Vpc(stack, "test-vpc", {}); + + // Helper declaration + new LambdaToSns(stack, "lambda-to-sns-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + existingVpc: testVpc, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + VpcConfig: { + SecurityGroupIds: [ + { + "Fn::GetAtt": [ + "lambdatosnsstackReplaceDefaultSecurityGroup719F066F", + "GroupId", + ], + }, + ], + SubnetIds: [ + { + Ref: "testvpcPrivateSubnet1Subnet865FB50A", + }, + { + Ref: "testvpcPrivateSubnet2Subnet23D3396F", + }, + ], + }, + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + VpcEndpointType: "Interface", + }); +}); + +// -------------------------------------------------------------- +// Test minimal deployment with an existing VPC and existing Lambda function not in a VPCs +// +// 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 Stack(); + + const testLambdaFunction = new lambda.Function(stack, 'test-lamba', { + runtime: lambda.Runtime.NODEJS_10_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 LambdaToSns(stack, "lambda-to-sns-stack", { + existingLambdaObj: testLambdaFunction, + existingVpc: testVpc, + }); + }; + + // Assertion + expect(app).toThrowError(); + +}); + +// -------------------------------------------------------------- +// Test bad call with existingVpc and deployVpc +// -------------------------------------------------------------- +test("Test bad call with existingVpc and deployVpc", () => { + // Stack + const stack = new Stack(); + + const testVpc = new ec2.Vpc(stack, "test-vpc", {}); + + const app = () => { + // Helper declaration + new LambdaToSns(stack, "lambda-to-sns-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_10_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + existingVpc: testVpc, + deployVpc: true, + }); + }; + // Assertion + expect(app).toThrowError(); +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sqs/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-sqs/lib/index.ts index 118c87f00..ab2885614 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sqs/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sqs/lib/index.ts @@ -17,7 +17,6 @@ import * as lambda from "@aws-cdk/aws-lambda"; import * as sqs from "@aws-cdk/aws-sqs"; import * as ec2 from "@aws-cdk/aws-ec2"; import { Construct } from "@aws-cdk/core"; -import { AddAwsServiceEndpoint } from "@aws-solutions-constructs/core"; /** * @summary The properties for the LambdaToSqs class. @@ -122,7 +121,7 @@ export class LambdaToSqs extends Construct { }, }); - AddAwsServiceEndpoint(scope, this.vpc, defaults.ServiceEndpointTypes.SQS); + defaults.AddAwsServiceEndpoint(scope, this.vpc, defaults.ServiceEndpointTypes.SQS); } // Setup the Lambda function diff --git a/source/use_cases/aws-serverless-web-app/test/__snapshots__/serverless-backend-stack.test.js.snap b/source/use_cases/aws-serverless-web-app/test/__snapshots__/serverless-backend-stack.test.js.snap index 7c8428d1b..a0929f356 100644 --- a/source/use_cases/aws-serverless-web-app/test/__snapshots__/serverless-backend-stack.test.js.snap +++ b/source/use_cases/aws-serverless-web-app/test/__snapshots__/serverless-backend-stack.test.js.snap @@ -314,6 +314,7 @@ Object { "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", diff --git a/source/use_cases/aws-serverless-web-app/test/integ.002-backend-deployment.expected.json b/source/use_cases/aws-serverless-web-app/test/integ.002-backend-deployment.expected.json index 5c4837a56..6ff3fa749 100644 --- a/source/use_cases/aws-serverless-web-app/test/integ.002-backend-deployment.expected.json +++ b/source/use_cases/aws-serverless-web-app/test/integ.002-backend-deployment.expected.json @@ -64,6 +64,7 @@ "dynamodb:Query", "dynamodb:GetItem", "dynamodb:Scan", + "dynamodb:ConditionCheckItem", "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem",