From 2ecd9dd935b731d2e4705ed9c146efcad0961fd8 Mon Sep 17 00:00:00 2001 From: mickychetta <45010053+mickychetta@users.noreply.github.com> Date: Thu, 31 Mar 2022 13:49:33 -0700 Subject: [PATCH 01/34] feat(README.md): add python and java minimal deployment (#582) * added python min deployment and removed initializers * fixed merge conflict * changed to v1 imports in deprecated constructs * added python min dep in new constructs * added java minimal deployment code * fixed java builder prop defintion * fixed indentation * fixed indentation * updated python minimal deployments * updated java minimal deployments * updated to correct prop in route53-apigateway * updated python minimal deployments * cast to type Any in python example * updated java minimal deployments * updated java minimal deployments * updated typescript minimal deployments * added viperlight ignore to README with fake account numbers * added viperlight ignore to README with fake account numbers * added v1 deprecated comments in v1 constructs * changed asset name in aws-lambda-sqs-lambda --- .viperlightignore | 14 ++ .../aws-alb-fargate/README.md | 103 +++++++++--- .../aws-alb-lambda/README.md | 130 ++++++++++---- .../aws-apigateway-dynamodb/README.md | 29 +++- .../aws-apigateway-iot/README.md | 32 ++-- .../aws-apigateway-kinesisstreams/README.md | 32 ++-- .../aws-apigateway-lambda/README.md | 59 +++++-- .../README.md | 94 ++++++++--- .../aws-apigateway-sqs/README.md | 29 +++- .../README.md | 59 +++++-- .../aws-cloudfront-apigateway/README.md | 100 ++++++++--- .../aws-cloudfront-mediastore/README.md | 29 +++- .../aws-cloudfront-s3/README.md | 29 +++- .../aws-cognito-apigateway-lambda/README.md | 137 +++++++++++++-- .../README.md | 71 ++++++-- .../aws-dynamodb-stream-lambda/README.md | 65 +++++-- .../README.md | 70 ++++++-- .../aws-dynamodbstreams-lambda/README.md | 53 ++++-- .../README.md | 52 ++++-- .../aws-eventbridge-kinesisstreams/README.md | 62 ++++--- .../aws-eventbridge-lambda/README.md | 85 +++++++--- .../aws-eventbridge-sns/README.md | 90 +++++++--- .../aws-eventbridge-sqs/README.md | 80 +++++++-- .../aws-eventbridge-stepfunctions/README.md | 79 ++++++--- .../README.md | 53 ++++-- .../aws-events-rule-kinesisstreams/README.md | 59 +++++-- .../aws-events-rule-lambda/README.md | 81 ++++++--- .../aws-events-rule-sns/README.md | 87 +++++++--- .../aws-events-rule-sqs/README.md | 88 +++++++--- .../aws-events-rule-step-function/README.md | 79 +++++++-- .../aws-fargate-s3/README.md | 39 ++++- .../aws-fargate-sns/README.md | 53 +++--- .../aws-fargate-sqs/README.md | 37 +++- .../aws-iot-kinesisfirehose-s3/README.md | 60 +++++-- .../aws-iot-kinesisstreams/README.md | 78 ++++++--- .../aws-iot-lambda-dynamodb/README.md | 103 +++++++++--- .../aws-iot-lambda/README.md | 101 ++++++++--- .../aws-iot-s3/README.md | 78 ++++++--- .../aws-iot-sqs/README.md | 76 ++++++--- .../README.md | 159 +++++++++++++----- .../aws-kinesisfirehose-s3/README.md | 31 ++-- .../aws-kinesisstreams-gluejob/README.md | 15 +- .../README.md | 29 +++- .../aws-kinesisstreams-lambda/README.md | 81 ++++++--- .../aws-lambda-dynamodb/README.md | 68 +++++--- .../aws-lambda-elasticsearch-kibana/README.md | 79 ++++++--- .../aws-lambda-eventbridge/README.md | 61 +++++-- .../aws-lambda-s3/README.md | 52 ++++-- .../aws-lambda-sagemakerendpoint/README.md | 83 +++++++-- .../aws-lambda-secretsmanager/README.md | 72 +++++--- .../aws-lambda-sns/README.md | 61 +++++-- .../aws-lambda-sqs-lambda/README.md | 66 ++++++-- .../aws-lambda-sqs/README.md | 61 +++++-- .../aws-lambda-ssmstringparameter/README.md | 80 ++++++--- .../aws-lambda-step-function/README.md | 76 +++++++-- .../aws-lambda-stepfunctions/README.md | 73 ++++++-- .../aws-route53-alb/README.md | 67 ++++++-- .../aws-route53-apigateway/README.md | 100 +++++++++-- .../aws-s3-lambda/README.md | 57 +++++-- .../aws-s3-sqs/README.md | 32 ++-- .../aws-s3-step-function/README.md | 57 +++++-- .../aws-s3-stepfunctions/README.md | 50 ++++-- .../aws-sns-lambda/README.md | 61 +++++-- .../aws-sns-sqs/README.md | 56 ++++-- .../aws-sqs-lambda/README.md | 57 +++++-- .../aws-wafwebacl-alb/README.md | 83 ++++++--- .../aws-wafwebacl-apigateway/README.md | 80 +++++++-- .../aws-wafwebacl-cloudfront/README.md | 43 ++++- 68 files changed, 3430 insertions(+), 1115 deletions(-) diff --git a/.viperlightignore b/.viperlightignore index c168d5aea..3f9309e54 100644 --- a/.viperlightignore +++ b/.viperlightignore @@ -145,6 +145,16 @@ source/patterns/@aws-solutions-constructs/aws-alb-lambda/README.md:35 source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/alb-lambda.test.ts:27 source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/alb-lambda.test.ts:680 # These are references to the us-east-1 ELBV2 account (publicly known) +source/patterns/@aws-solutions-constructs/aws-route53-alb/test/integ.deployPrivateApi.expected.json:196 +source/patterns/@aws-solutions-constructs/aws-route53-alb/test/integ.deployPrivateApiExistingZone.expected.json:853 +source/patterns/@aws-solutions-constructs/aws-route53-alb/test/integ.deployPublicApiNewAlb.expected.json:191 +source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/integ.privateApiNewResources.expected.json:199 +source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/integ.publicApiNewResources.expected.json:202 +source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/integ.twoTargets.expected.json:202 +source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/integ.privateApiExistingResources.expected.json:1067 +source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/integ.publicApiExistingResources.expected.json:1067 +source/patterns/@aws-solutions-constructs/aws-alb-fargate/test/integ.all-new-two-targets.expected.json:1034 +source/patterns/@aws-solutions-constructs/aws-alb-fargate/test/integ.all-new-public-http.expected.json:1007 source/patterns/@aws-solutions-constructs/aws-route53-alb/test/integ.deployPrivateApi.expected.json:242 source/patterns/@aws-solutions-constructs/aws-route53-alb/test/integ.deployPrivateApiExistingZone.expected.json:899 source/patterns/@aws-solutions-constructs/aws-route53-alb/test/integ.deployPublicApiNewAlb.expected.json:237 @@ -155,3 +165,7 @@ source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/integ.privateApiEx source/patterns/@aws-solutions-constructs/aws-alb-lambda/test/integ.publicApiExistingResources.expected.json:1113 source/patterns/@aws-solutions-constructs/aws-alb-fargate/test/integ.all-new-two-targets.expected.json:1081 source/patterns/@aws-solutions-constructs/aws-alb-fargate/test/integ.all-new-public-http.expected.json:1054 +source/patterns/@aws-solutions-constructs/aws-alb-fargate/README.md:78 +source/patterns/@aws-solutions-constructs/aws-alb-lambda/README.md:84 +source/patterns/@aws-solutions-constructs/aws-route53-alb/README.md:59 +source/patterns/@aws-solutions-constructs/aws-wafwebacl-alb/README.md:68 diff --git a/source/patterns/@aws-solutions-constructs/aws-alb-fargate/README.md b/source/patterns/@aws-solutions-constructs/aws-alb-fargate/README.md index 9ec754c03..8b979c0a0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-alb-fargate/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-alb-fargate/README.md @@ -24,41 +24,100 @@ This AWS Solutions Construct implements an an Application Load Balancer to an AWS Fargate service -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript - import { AlbToFargate, AlbToFargateProps } from '@aws-solutions-constructs/aws-alb-fargate'; - - // Obtain a pre-existing certificate from your account - const certificate = acm.Certificate.fromCertificateArn( - scope, - 'existing-cert', - "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" - ); - - const props: AlbToFargateProps = { +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { AlbToFargate, AlbToFargateProps } from '@aws-solutions-constructs/aws-alb-fargate'; +import * as acm from 'aws-cdk-lib/aws-certificatemanager'; + +const certificate = acm.Certificate.fromCertificateArn( + this, + 'existing-cert', + "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" +); + +const constructProps: AlbToFargateProps = { ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", ecrImageVersion: "latest", listenerProps: { - certificates: [ certificate ] + certificates: [certificate] }, publicApi: true - }; +}; - new AlbToFargate(stack, 'new-construct', props); +// Note - all alb constructs turn on ELB logging by default, so require that an environment including account +// and region be provided when creating the stack +// +// new MyStack(app, 'id', {env: {account: '123456789012', region: 'us-east-1' }}); +new AlbToFargate(this, 'new-construct', constructProps); ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_alb_fargate import AlbToFargate, AlbToFargateProps +from aws_cdk import ( + aws_certificatemanager as acm, + aws_elasticloadbalancingv2 as alb, + Stack +) +from constructs import Construct + +# Obtain a pre-existing certificate from your account +certificate = acm.Certificate.from_certificate_arn( + self, + 'existing-cert', + "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" + ) + +# Note - all alb constructs turn on ELB logging by default, so require that an environment including account +# and region be provided when creating the stack +# +# MyStack(app, 'id', env=cdk.Environment(account='679431688440', region='us-east-1')) +AlbToFargate(self, 'new-construct', + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", + ecr_image_version="latest", + listener_props=alb.BaseApplicationListenerProps( + certificates=[certificate], + ), + public_api=True) -``` text -new AlbToFargate(scope: Construct, id: string, props: AlbToFargateProps); ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`AlbToFargateProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.elasticloadbalancingv2.*; +import software.amazon.awsconstructs.services.albfargate.*; + +// The code that defines your stack goes here +// Obtain a pre-existing certificate from your account +ListenerCertificate listenerCertificate = ListenerCertificate + .fromArn("arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012"); + +// Note - all alb constructs turn on ELB logging by default, so require that an environment including account +// and region be provided when creating the stack +// +// new MyStack(app, "id", StackProps.builder() +// .env(Environment.builder() +// .account("123456789012") +// .region("us-east-1") +// .build()); +new AlbToFargate(this, "AlbToFargatePattern", new AlbToFargateProps.Builder() + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") + .ecrImageVersion("latest") + .listenerProps(new BaseApplicationListenerProps.Builder() + .certificates(List.of(listenerCertificate)) + .build()) + .publicApi(true) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-alb-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-alb-lambda/README.md index 4a18c6c8c..98b723ca7 100644 --- a/source/patterns/@aws-solutions-constructs/aws-alb-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-alb-lambda/README.md @@ -24,42 +24,112 @@ This AWS Solutions Construct implements an an Application Load Balancer to an AWS Lambda function -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript - - // Obtain a pre-existing certificate from your account - const certificate = acm.Certificate.fromCertificateArn( - scope, - 'existing-cert', - "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" - ); - const props: AlbToLambdaProps = { - lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' - }, - listenerProps: { - certificates: [ certificate ] - }, - publicApi: true - }; - new AlbToLambda(stack, 'new-construct', props); - +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { AlbToLambda, AlbToLambdaProps } from '@aws-solutions-constructs/aws-alb-lambda'; +import * as acm from 'aws-cdk-lib/aws-certificatemanager'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +// Obtain a pre-existing certificate from your account +const certificate = acm.Certificate.fromCertificateArn( + this, + 'existing-cert', + "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" +); + +const constructProps: AlbToLambdaProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, + listenerProps: { + certificates: [certificate] + }, + publicApi: true +}; + +// Note - all alb constructs turn on ELB logging by default, so require that an environment including account +// and region be provided when creating the stack +// +// new MyStack(app, 'id', {env: {account: '123456789012', region: 'us-east-1' }}); +new AlbToLambda(this, 'new-construct', constructProps); ``` -## Initializer - -``` text -new AlbToLambda(scope: Construct, id: string, props: AlbToLambdaProps); +Python +``` python +from aws_solutions_constructs.aws_alb_lambda import AlbToLambda, AlbToLambdaProps +from aws_cdk import ( + aws_certificatemanager as acm, + aws_lambda as _lambda, + aws_elasticloadbalancingv2 as alb, + Stack +) +from constructs import Construct + +# Obtain a pre-existing certificate from your account +certificate = acm.Certificate.from_certificate_arn( + self, + 'existing-cert', + "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" +) + +# Note - all alb constructs turn on ELB logging by default, so require that an environment including account +# and region be provided when creating the stack +# +# MyStack(app, 'id', env=cdk.Environment(account='679431688440', region='us-east-1')) +AlbToLambda(self, 'new-construct', + lambda_function_props=_lambda.FunctionProps( + runtime=_lambda.Runtime.PYTHON_3_7, + code=_lambda.Code.from_asset('lambda'), + handler='index.handler', + ), + listener_props=alb.BaseApplicationListenerProps( + certificates=[certificate] + ), + public_api=True) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`AlbToLambdaProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.elasticloadbalancingv2.*; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.alblambda.*; + +// Obtain a pre-existing certificate from your account +ListenerCertificate listenerCertificate = ListenerCertificate + .fromArn("arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012"); + +// Note - all alb constructs turn on ELB logging by default, so require that an environment including account +// and region be provided when creating the stack +// +// new MyStack(app, "id", StackProps.builder() +// .env(Environment.builder() +// .account("123456789012") +// .region("us-east-1") +// .build()); +new AlbToLambda(this, "AlbToLambdaPattern", new AlbToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .listenerProps(new BaseApplicationListenerProps.Builder() + .certificates(List.of(listenerCertificate)) + .build()) + .publicApi(true) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md index e555d3768..237ba4334 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/README.md @@ -26,26 +26,37 @@ ## Overview This AWS Solutions Construct implements an Amazon API Gateway REST API connected to Amazon DynamoDB table. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition in +Typescript: ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { ApiGatewayToDynamoDBProps, ApiGatewayToDynamoDB } from "@aws-solutions-constructs/aws-apigateway-dynamodb"; new ApiGatewayToDynamoDB(this, 'test-api-gateway-dynamodb-default', {}); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_apigateway_dynamodb import ApiGatewayToDynamoDB +from aws_cdk import Stack +from constructs import Construct -``` text -new ApiGatewayToDynamoDB(scope: Construct, id: string, props: ApiGatewayToDynamoDBProps); +ApiGatewayToDynamoDB(self, 'test-api-gateway-dynamodb-default') ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.apigatewaydynamodb.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`ApiGatewayToDynamoDBProps`](#pattern-construct-props) +new ApiGatewayToDynamoDB(this, "test-api-gateway-dynamodb-default", new ApiGatewayToDynamoDBProps.Builder() + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/README.md index c28786e40..655f457ab 100755 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-iot/README.md @@ -30,29 +30,41 @@ This construct creates a scalable HTTPS proxy between API Gateway and AWS IoT. T This implementation enables write-only messages to be published on given MQTT topics, and also supports shadow updates of HTTPS devices to allowed things in the device registry. It does not involve Lambda functions for proxying messages, and instead relies on direct API Gateway to AWS IoT integration which supports both JSON messages as well as binary messages. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { ApiGatewayToIot } from '@aws-solutions-constructs/aws-apigateway-iot'; new ApiGatewayToIot(this, 'ApiGatewayToIotPattern', { iotEndpoint: 'a1234567890123-ats' }); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_apigateway_iot import ApiGatewayToIot +from aws_cdk import Stack +from constructs import Construct -``` text -new ApiGatewayToIot(scope: Construct, id: string, props: ApiGatewayToIotProps); +ApiGatewayToIot(self, 'ApiGatewayToIotPattern', + iot_endpoint='a1234567890123-ats' +) ``` +Java +``` java +import software.constructs.Construct; -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`ApiGatewayToIotProps`](#pattern-construct-props) +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.apigatewayiot.*; +new ApiGatewayToIot(this, "ApiGatewayToIotPattern", new ApiGatewayToIotProps.Builder() + .iotEndpoint("a1234567890123-ats") + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md index a3dd78c13..ecd0ca06c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/README.md @@ -22,26 +22,38 @@ This AWS Solutions Construct implements an Amazon API Gateway connected to an Amazon Kinesis Data Stream pattern. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: -``` javascript +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { ApiGatewayToKinesisStreams, ApiGatewayToKinesisStreamsProps } from '@aws-solutions-constructs/aws-apigateway-kinesisstreams'; new ApiGatewayToKinesisStreams(this, 'test-apigw-kinesis', {}); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_apigateway_kinesisstreams import ApiGatewayToKinesisStreams +from aws_cdk import Stack +from constructs import Construct -``` text -new ApiGatewayToKinesisStreams(scope: Construct, id: string, props: ApiGatewayToKinesisStreamsProps); +ApiGatewayToKinesisStreams(self, 'test-apigw-kinesis') ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.apigatewaykinesisstreams.*; + +new ApiGatewayToKinesisStreams(this, "test-apigw-kinesis", new ApiGatewayToKinesisStreamsProps.Builder() + .build()); +``` -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`ApiGatewayToKinesisStreamsProps`](#pattern-construct-props) ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-lambda/README.md index de9a77233..0ff4e1c86 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-lambda/README.md @@ -22,32 +22,61 @@ This AWS Solutions Construct implements an Amazon API Gateway REST API connected to an AWS Lambda function pattern. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { ApiGatewayToLambda } from '@aws-solutions-constructs/aws-apigateway-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new ApiGatewayToLambda(this, 'ApiGatewayToLambdaPattern', { - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) - } + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) + } }); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_apigateway_lambda import ApiGatewayToLambda +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +ApiGatewayToLambda(self, 'ApiGatewayToLambdaPattern', + lambda_function_props=_lambda.FunctionProps( + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler', + code=_lambda.Code.from_asset('lambda') + ) + ) -``` text -new ApiGatewayToLambda(scope: Construct, id: string, props: ApiGatewayToLambdaProps); ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`ApiGatewayToLambdaProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.apigatewaylambda.*; + +new ApiGatewayToLambda(this, "ApiGatewayToLambdaPattern", new ApiGatewayToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/README.md index fd86c4979..526e30937 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/README.md @@ -26,43 +26,93 @@ This AWS Solutions Construct implements an Amazon API Gateway connected to an Amazon SageMaker endpoint pattern. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: -``` javascript +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { ApiGatewayToSageMakerEndpoint, ApiGatewayToSageMakerEndpointProps } from '@aws-solutions-constructs/aws-apigateway-sagemakerendpoint'; // Below is an example VTL (Velocity Template Language) mapping template for mapping the Api GET request to the Sagemaker POST request -const requestTemplate = -`{ +const requestTemplate = ` +{ "instances": [ -#set( $user_id = $input.params("user_id") ) -#set( $items = $input.params("items") ) -#foreach( $item in $items.split(",") ) - {"in0": [$user_id], "in1": [$item]}#if( $foreach.hasNext ),#end - $esc.newline -#end + # set( $user_id = $input.params("user_id") ) + # set( $items = $input.params("items") ) + # foreach( $item in $items.split(",") ) + # if( $foreach.hasNext ),#end + {"in0": [$user_id], "in1": [$item]} + $esc.newline + # end ] -}`; +}` // Replace 'my-endpoint' with your Sagemaker Inference Endpoint new ApiGatewayToSageMakerEndpoint(this, 'test-apigw-sagemakerendpoint', { - endpointName: 'my-endpoint', - resourcePath: '{user_id}', - requestMappingTemplate: requestTemplate + endpointName: 'my-endpoint', + resourcePath: '{user_id}', + requestMappingTemplate: requestTemplate }); ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_apigateway_sagemakerendpoint import ApiGatewayToSageMakerEndpoint +from aws_cdk import Stack +from constructs import Construct -``` text -new ApiGatewayToSageMakerEndpoint(scope: Construct, id: string, props: ApiGatewayToSageMakerEndpointProps); +# Below is an example VTL (Velocity Template Language) mapping template for mapping the Api GET request to the Sagemaker POST request +request_template = """ +{ + "instances": [ + # set( $user_id = $input.params("user_id") ) + # set( $items = $input.params("items") ) + # foreach( $item in $items.split(",") ) + # if( $foreach.hasNext ),#end + {"in0": [$user_id], "in1": [$item]} + $esc.newline + # end + ] +}""" + +# Replace 'my-endpoint' with your Sagemaker Inference Endpoint +ApiGatewayToSageMakerEndpoint(self, 'test-apigw-sagemakerendpoint', + endpoint_name='my-endpoint', + resource_path='{user_id}', + request_mapping_template=request_template + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`ApiGatewayToSageMakerEndpointProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.apigatewaysagemakerendpoint.*; + +// Create an example VTL (Velocity Template Language) mapping template for mapping the Api GET request to the Sagemaker POST request +final String requestTemplate = "{" + + "\"instances\": [" + + "# set( $user_id = $input.params(\"user_id\") )" + + "# set( $items = $input.params(\"items\") )" + + "# foreach( $item in $items.split(\",\") )" + + "# if( $foreach.hasNext ),#end" + + "{\"in0\": [$user_id], \"in1\": [$item]}" + + " $esc.newline" + + "# end" + + "]" + + "}"; + +// Replace ""my-endpoint"" with your Sagemaker Inference Endpoint +new ApiGatewayToSageMakerEndpoint(this, "ApiGatewayToSageMakerEndpointPattern", + new ApiGatewayToSageMakerEndpointProps.Builder() + .endpointName("my-endpoint") + .resourcePath("{user_id}") + .requestMappingTemplate(requestTemplate) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md index c7c88eacb..5b6ddd7c2 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/README.md @@ -22,26 +22,37 @@ This AWS Solutions Construct implements an Amazon API Gateway connected to an Amazon SQS queue pattern. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { ApiGatewayToSqs, ApiGatewayToSqsProps } from "@aws-solutions-constructs/aws-apigateway-sqs"; new ApiGatewayToSqs(this, 'ApiGatewayToSqsPattern', {}); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_apigateway_sqs import ApiGatewayToSqs +from aws_cdk import Stack +from constructs import Construct -``` text -new ApiGatewayToSqs(scope: Construct, id: string, props: ApiGatewayToSqsProps); +ApiGatewayToSqs(self, 'ApiGatewayToSqsPattern') ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.apigatewaysqs.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`ApiGatewayToSqsProps`](#pattern-construct-props) +new ApiGatewayToSqs(this, "ApiGatewayToSqsPattern", new ApiGatewayToSqsProps.Builder() + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/README.md index ee9640d3e..08112642c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/README.md @@ -20,31 +20,62 @@ This AWS Solutions Construct implements an AWS CloudFront fronting an Amazon API Gateway Lambda backed REST API. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { CloudFrontToApiGatewayToLambda } from '@aws-solutions-constructs/aws-cloudfront-apigateway-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new CloudFrontToApiGatewayToLambda(this, 'test-cloudfront-apigateway-lambda', { - lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' - } + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + } }); ``` -## Initializer - -``` text -new CloudFrontToApiGatewayToLambda(scope: Construct, id: string, props: CloudFrontToApiGatewayToLambdaProps); +Python +``` python +from aws_solutions_constructs.aws_cloudfront_apigateway_lambda import CloudFrontToApiGatewayToLambda +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +CloudFrontToApiGatewayToLambda(self, 'test-cloudfront-apigateway-lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.cloudfrontapigatewaylambda.*; + +new CloudFrontToApiGatewayToLambda(this, "test-cloudfront-apigateway-lambda", + new CloudFrontToApiGatewayToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`CloudFrontToApiGatewayToLambdaProps`](#pattern-construct-props) ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway/README.md b/source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway/README.md index a16d7678c..c4b0dfb1b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway/README.md @@ -20,50 +20,100 @@ This AWS Solutions Construct implements an AWS CloudFront fronting an Amazon API Gateway REST API. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: -``` javascript -import * as api from '@aws-cdk/aws-apigateway'; -import * as lambda from "@aws-cdk/aws-lambda"; +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { CloudFrontToApiGateway } from '@aws-solutions-constructs/aws-cloudfront-apigateway'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as api from 'aws-cdk-lib/aws-apigateway'; const lambdaProps: lambda.FunctionProps = { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' }; const lambdafunction = new lambda.Function(this, 'LambdaFunction', lambdaProps); const apiGatewayProps: api.LambdaRestApiProps = { - handler: lambdafunction, - endpointConfiguration: { - types: [api.EndpointType.REGIONAL] - }, - defaultMethodOptions: { - authorizationType: api.AuthorizationType.NONE - } + handler: lambdafunction, + endpointConfiguration: { + types: [api.EndpointType.REGIONAL] + }, + defaultMethodOptions: { + authorizationType: api.AuthorizationType.NONE + } }; const apiGateway = new api.LambdaRestApi(this, 'LambdaRestApi', apiGatewayProps); new CloudFrontToApiGateway(this, 'test-cloudfront-apigateway', { - existingApiGatewayObj: apiGateway + existingApiGatewayObj: apiGateway }); - ``` -## Initializer - -``` text -new CloudFrontToApiGateway(scope: Construct, id: string, props: CloudFrontToApiGatewayProps); +Python +``` python +from aws_solutions_constructs.aws_cloudfront_apigateway import CloudFrontToApiGateway +from aws_cdk import ( + aws_lambda as _lambda, + aws_apigateway as api, + Stack +) +from constructs import Construct + +lambda_function = _lambda.Function(self, 'LambdaFunction', + code=_lambda.Code.from_asset( + 'lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler') + +api_gateway = api.LambdaRestApi(self, 'LambdaRestApi', + handler=lambda_function, + endpoint_configuration=api.EndpointConfiguration( + types=[api.EndpointType.REGIONAL] + ), + default_method_options=api.MethodOptions( + authorization_type=api.AuthorizationType.NONE + )) + +CloudFrontToApiGateway(self, 'test-cloudfront-apigateway', + existing_api_gateway_obj=api_gateway + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`CloudFrontToApiGatewayProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.apigateway.*; +import software.amazon.awsconstructs.services.cloudfrontapigateway.*; + +final Function lambdaFunction = Function.Builder.create(this, "IndexHandler") + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build(); + +final LambdaRestApi apiGateway = LambdaRestApi.Builder.create(this, "myapi") + .handler(lambdaFunction) + .endpointConfiguration(new EndpointConfiguration.Builder() + .types(List.of(EndpointType.REGIONAL)) + .build()) + .build(); + +new CloudFrontToApiGateway(this, "test-cloudfront-apigateway", new CloudFrontToApiGatewayProps.Builder() + .existingApiGatewayObj(apiGateway) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-cloudfront-mediastore/README.md b/source/patterns/@aws-solutions-constructs/aws-cloudfront-mediastore/README.md index 6456ce764..70de0638d 100644 --- a/source/patterns/@aws-solutions-constructs/aws-cloudfront-mediastore/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-cloudfront-mediastore/README.md @@ -22,26 +22,37 @@ ## Overview This AWS Solutions Construct implements an Amazon CloudFront distribution to an AWS Elemental MediaStore container. -Here is a minimal deployable pattern definition in TypeScript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { CloudFrontToMediaStore } from '@aws-solutions-constructs/aws-cloudfront-mediastore'; new CloudFrontToMediaStore(this, 'test-cloudfront-mediastore-default', {}); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_cloudfront_mediastore import CloudFrontToMediaStore +from aws_cdk import Stack +from constructs import Construct -``` text -new CloudFrontToMediaStore(scope: Construct, id: string, props: CloudFrontToMediaStoreProps); +CloudFrontToMediaStore(self, 'test-cloudfront-mediastore-default') ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.cloudfrontmediastore.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`CloudFrontToMediaStoreProps`](#pattern-construct-props) +new CloudFrontToMediaStore(this, "test-cloudfront-mediastore-default", new CloudFrontToMediaStoreProps.Builder() + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-cloudfront-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-cloudfront-s3/README.md index df32a0149..d4c9bbbea 100644 --- a/source/patterns/@aws-solutions-constructs/aws-cloudfront-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-cloudfront-s3/README.md @@ -20,26 +20,37 @@ This AWS Solutions Construct implements an AWS CloudFront fronting an AWS S3 Bucket. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3'; new CloudFrontToS3(this, 'test-cloudfront-s3', {}); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_cloudfront_s3 import CloudFrontToS3 +from aws_cdk import Stack +from constructs import Construct -``` text -new CloudFrontToS3(scope: Construct, id: string, props: CloudFrontToS3Props); +CloudFrontToS3(self, 'test-cloudfront-s3') ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.cloudfronts3.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`CloudFrontToS3Props`](#pattern-construct-props) +new CloudFrontToS3(this, "test-cloudfront-s3", new CloudFrontToS3Props.Builder() + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-cognito-apigateway-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-cognito-apigateway-lambda/README.md index e72d3931b..de4d94ef6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-cognito-apigateway-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-cognito-apigateway-lambda/README.md @@ -24,33 +24,80 @@ This AWS Solutions Construct implements an Amazon Cognito securing an Amazon API Gateway Lambda backed REST APIs pattern. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { CognitoToApiGatewayToLambda } from '@aws-solutions-constructs/aws-cognito-apigateway-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new CognitoToApiGatewayToLambda(this, 'test-cognito-apigateway-lambda', { - lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' - } + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + } }); ``` -If you are defining resources and methods on your API (e.g. proxy = false), then you must call addAuthorizers() after the API is fully defined to ensure every method is protected. Here is an example in Typescript: +Python +``` python +from aws_solutions_constructs.aws_cognito_apigateway_lambda import CognitoToApiGatewayToLambda +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +CognitoToApiGatewayToLambda(self, 'test-cognito-apigateway-lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.cognitoapigatewaylambda.*; + +new CognitoToApiGatewayToLambda(this, "test-cognito-apigateway-lambda", + new CognitoToApiGatewayToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` + +If you are defining resources and methods on your API (e.g. proxy = false), then you must call addAuthorizers() after the API is fully defined to ensure every method is protected. Here is an example: + +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { CognitoToApiGatewayToLambda } from '@aws-solutions-constructs/aws-cognito-apigateway-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; const construct = new CognitoToApiGatewayToLambda(this, 'test-cognito-apigateway-lambda', { lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), + code: lambda.Code.fromAsset(`lambda`), runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler' }, apiGatewayProps: { - proxy: false + proxy: false } }); @@ -61,17 +108,73 @@ resource.addMethod('POST'); construct.addAuthorizers(); ``` -## Initializer - -``` text -new CognitoToApiGatewayToLambda(scope: Construct, id: string, props: CognitoToApiGatewayToLambdaProps); +Python +``` python +from aws_solutions_constructs.aws_cognito_apigateway_lambda import CognitoToApiGatewayToLambda +from aws_cdk import ( + aws_lambda as _lambda, + aws_apigateway as api, + Stack +) +from constructs import Construct +from typing import Any + +# Overriding LambdaRestApiProps with type Any +gateway_props = dict[Any, Any] + +construct = CognitoToApiGatewayToLambda(self, 'test-cognito-apigateway-lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset( + 'lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + api_gateway_props=gateway_props( + proxy=False + ) + ) + +resource = construct.api_gateway.root.add_resource('foobar') +resource.add_method('POST') + +# Mandatory to call this method to Apply the Cognito Authorizers on all API methods +construct.add_authorizers() ``` -_Parameters_ +``` java +import software.constructs.Construct; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import software.amazon.awscdk.*; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.apigateway.IResource; +import software.amazon.awsconstructs.services.cognitoapigatewaylambda.*; + +// Overriding LambdaRestApiProps with type Any +Map> gatewayProps = new HashMap>(); +gatewayProps.put("proxy", Optional.of(false)); + +final CognitoToApiGatewayToLambda construct = new CognitoToApiGatewayToLambda(this, + "test-cognito-apigateway-lambda", + new CognitoToApiGatewayToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .apiGatewayProps(gatewayProps) + .build()); + +final IResource resource = construct.getApiGateway().getRoot().addResource("foobar"); +resource.addMethod("POST"); -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`CognitoToApiGatewayToLambdaProps`](#pattern-construct-props) +// Mandatory to call this method to Apply the Cognito Authorizers on all API methods +construct.addAuthorizers(); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda-elasticsearch-kibana/README.md b/source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda-elasticsearch-kibana/README.md index 751f159e9..574713cc8 100644 --- a/source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda-elasticsearch-kibana/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda-elasticsearch-kibana/README.md @@ -22,37 +22,74 @@ This AWS Solutions Construct implements Amazon DynamoDB table with stream, AWS Lambda function and Amazon Elasticsearch Service with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +// aws-dynamodb-stream-lambda-elasticsearch-kibana has been deprecated for CDK V2 in favor of aws-dynamodbstreams-lambda-elasticsearch-kibana. +// This sample uses the CDK V1 syntax +import * as cdk from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; import { DynamoDBStreamToLambdaToElasticSearchAndKibana, DynamoDBStreamToLambdaToElasticSearchAndKibanaProps } from '@aws-solutions-constructs/aws-dynamodb-stream-lambda-elasticsearch-kibana'; -import { Aws } from "@aws-cdk/core"; -const props: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps = { +const constructProps: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps = { lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' }, domainName: 'test-domain', // TODO: Ensure the Cognito domain name is globally unique - cognitoDomainName: 'globallyuniquedomain' + Aws.ACCOUNT_ID; + cognitoDomainName: 'globallyuniquedomain' + cdk.Aws.ACCOUNT_ID }; -new DynamoDBStreamToLambdaToElasticSearchAndKibana(this, 'test-dynamodb-stream-lambda-elasticsearch-kibana', props); +new DynamoDBStreamToLambdaToElasticSearchAndKibana(this, 'test', constructProps); ``` -## Initializer - -``` text -new DynamoDBStreamToLambdaToElasticSearchAndKibana(scope: Construct, id: string, props: DynamoDBStreamToLambdaToElasticSearchAndKibanaProps); +Python +``` python +# aws-dynamodb-stream-lambda-elasticsearch-kibana has been deprecated for CDK V2 in favor of aws-dynamodbstreams-lambda-elasticsearch-kibana. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_dynamodb_stream_lambda_elasticsearch_kibana import DynamoDBStreamToLambdaToElasticSearchAndKibana, DynamoDBStreamToLambdaToElasticSearchAndKibanaProps +from aws_cdk import ( + aws_lambda as _lambda, + core +) + +DynamoDBStreamToLambdaToElasticSearchAndKibana( + self, 'test', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + domain_name='test-domain', + # TODO: Ensure the Cognito domain name is globally unique + cognito_domain_name='globallyuniquedomain' + core.Aws.ACCOUNT_ID) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`DynamoDBStreamToLambdaToElasticSearchAndKibanaProps`](#pattern-construct-props) +Java +``` java +// aws-dynamodb-stream-lambda-elasticsearch-kibana has been deprecated for CDK V2 in favor of aws-dynamodbstreams-lambda-elasticsearch-kibana. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.dynamodbstreamlambdaelasticsearchkibana.*; + +new DynamoDBStreamToLambdaToElasticSearchAndKibana(this, "test", + new DynamoDBStreamToLambdaToElasticSearchAndKibanaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .domainName("test-domain") + .cognitoDomainName("globallyuniquedomain" + Aws.ACCOUNT_ID) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda/README.md index 8c78eaf78..a61f5c89f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda/README.md @@ -21,32 +21,65 @@ This AWS Solutions Construct implements a pattern Amazon DynamoDB table with stream to invoke the AWS Lambda function with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import { DynamoDBStreamToLambdaProps, DynamoDBStreamToLambda} from '@aws-solutions-constructs/aws-dynamodb-stream-lambda'; +// aws-dynamodb-stream-lambda has been deprecated for CDK V2 in favor of aws-dynamodbstreams-lambda. +// This sample uses the CDK V1 syntax +import * as cdk from '@aws-cdk/core'; +import { DynamoDBStreamToLambda } from '@aws-solutions-constructs/aws-dynamodb-stream-lambda'; +import * as lambda from '@aws-cdk/aws-lambda'; new DynamoDBStreamToLambda(this, 'test-dynamodb-stream-lambda', { - lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' - }, + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, }); - ``` -## Initializer +Python +``` python +# aws-dynamodb-stream-lambda has been deprecated for CDK V2 in favor of aws-dynamodbstreams-lambda. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_dynamodb_stream_lambda import DynamoDBStreamToLambda +from aws_cdk import ( + aws_lambda as _lambda, + core, +) + +DynamoDBStreamToLambda(self, 'test-dynamodb-stream-lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) -``` text -new DynamoDBStreamToLambda(scope: Construct, id: string, props: DynamoDBStreamToLambdaProps); ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`DynamoDBStreamToLambdaProps`](#pattern-construct-props) +Java +``` java +// aws-dynamodb-stream-lambda has been deprecated for CDK V2 in favor of aws-dynamodbstreams-lambda. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.dynamodbstreamlambda.*; + +new DynamoDBStreamToLambda(this, "test-dynamodbstreams-lambda", + new DynamoDBStreamToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda-elasticsearch-kibana/README.md b/source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda-elasticsearch-kibana/README.md index 82bc85882..538b96d2c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda-elasticsearch-kibana/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda-elasticsearch-kibana/README.md @@ -24,37 +24,73 @@ This AWS Solutions Construct implements Amazon DynamoDB table with stream, AWS Lambda function and Amazon Elasticsearch Service with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps, Aws } from 'aws-cdk-lib'; import { DynamoDBStreamsToLambdaToElasticSearchAndKibana, DynamoDBStreamsToLambdaToElasticSearchAndKibanaProps } from '@aws-solutions-constructs/aws-dynamodbstreams-lambda-elasticsearch-kibana'; -import { Aws } from "@aws-cdk/core"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; -const props: DynamoDBStreamsToLambdaToElasticSearchAndKibanaProps = { +const constructProps: DynamoDBStreamsToLambdaToElasticSearchAndKibanaProps = { lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' }, domainName: 'test-domain', // TODO: Ensure the Cognito domain name is globally unique - cognitoDomainName: 'globallyuniquedomain' + Aws.ACCOUNT_ID; + cognitoDomainName: 'globallyuniquedomain' + Aws.ACCOUNT_ID }; -new DynamoDBStreamsToLambdaToElasticSearchAndKibana(this, 'test-dynamodbstreams-lambda-elasticsearch-kibana', props); +new DynamoDBStreamsToLambdaToElasticSearchAndKibana(this, 'test-dynamodbstreams-lambda-elasticsearch-kibana', constructProps); ``` -## Initializer - -``` text -new DynamoDBStreamsToLambdaToElasticSearchAndKibana(scope: Construct, id: string, props: DynamoDBStreamsToLambdaToElasticSearchAndKibanaProps); +Python +``` Python +from aws_solutions_constructs.aws_dynamodbstreams_lambda_elasticsearch_kibana import DynamoDBStreamsToLambdaToElasticSearchAndKibana, DynamoDBStreamsToLambdaToElasticSearchAndKibanaProps +from aws_cdk import ( + Stack, + aws_lambda as _lambda, + Aws, +) +from constructs import Construct + +DynamoDBStreamsToLambdaToElasticSearchAndKibana( + self, 'test-dynamodbstreams-lambda-elasticsearch-kibana', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + domain_name='test-domain', + # TODO: Ensure the Cognito domain name is globally unique + cognito_domain_name='globallyuniquedomain' + Aws.ACCOUNT_ID) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`DynamoDBStreamsToLambdaToElasticSearchAndKibanaProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Aws; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.dynamodbstreamslambdaelasticsearchkibana.*; + +new DynamoDBStreamsToLambdaToElasticSearchAndKibana(this, "test-dynamodb-stream-lambda-elasticsearch-kibana", + new DynamoDBStreamsToLambdaToElasticSearchAndKibanaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .domainName("test-domain") + .cognitoDomainName("globallyuniquedomain" + Aws.ACCOUNT_ID) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda/README.md index 19ffad291..daf24e552 100644 --- a/source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda/README.md @@ -24,32 +24,61 @@ This AWS Solutions Construct implements a pattern Amazon DynamoDB table with stream to invoke the AWS Lambda function with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { DynamoDBStreamsToLambdaProps, DynamoDBStreamsToLambda} from '@aws-solutions-constructs/aws-dynamodbstreams-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new DynamoDBStreamsToLambda(this, 'test-dynamodbstreams-lambda', { lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), + code: lambda.Code.fromAsset(`lambda`), runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler' }, }); - ``` -## Initializer - -``` text -new DynamoDBStreamsToLambda(scope: Construct, id: string, props: DynamoDBStreamsToLambdaProps); +Python +``` python +from aws_solutions_constructs.aws_dynamodbstreams_lambda import DynamoDBStreamsToLambda +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +DynamoDBStreamsToLambda(self, 'test-dynamodbstreams-lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`DynamoDBStreamsToLambdaProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.dynamodbstreamslambda.*; + +new DynamoDBStreamsToLambda(this, "test-dynamodbstreams-lambda", + new DynamoDBStreamsToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisfirehose-s3/README.md index 8fe08c6fa..ad6e38788 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisfirehose-s3/README.md @@ -20,34 +20,58 @@ This AWS Solutions Construct implements an Amazon EventBridge Rule to send data to an Amazon Kinesis Data Firehose delivery stream connected to an Amazon S3 bucket. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition : +Typescript ``` javascript -import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { Stack, StackProps, Duration } from 'aws-cdk-lib'; import { EventbridgeToKinesisFirehoseToS3, EventbridgeToKinesisFirehoseToS3Props } from '@aws-solutions-constructs/aws-eventbridge-kinesisfirehose-s3'; +import * as events from 'aws-cdk-lib/aws-events'; const EventbridgeToKinesisFirehoseToS3Props: EventbridgeToKinesisFirehoseToS3Props = { eventRuleProps: { - schedule: events.Schedule.rate(cdk.Duration.minutes(5)) + schedule: events.Schedule.rate(Duration.minutes(5)) } }; new EventbridgeToKinesisFirehoseToS3(this, 'test-eventbridge-firehose-s3', EventbridgeToKinesisFirehoseToS3Props); - ``` -## Initializer - -``` text -new EventbridgeToKinesisFirehoseToS3(scope: Construct, id: string, props: EventbridgeToKinesisFirehoseToS3Props); +Python +``` python +from aws_solutions_constructs.aws_eventbridge_kinesis_firehose_s3 import EventbridgeToKinesisFirehoseToS3, EventbridgeToKinesisFirehoseToS3Props +from aws_cdk import ( + aws_events as events, + Duration, + Stack +) +from constructs import Construct + +EventbridgeToKinesisFirehoseToS3(self, 'test-eventbridge-firehose-s3', + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + Duration.minutes(5)) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventbridgeToKinesisFirehoseToS3Props`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.events.*; +import software.amazon.awsconstructs.services.eventbridgekinesisfirehoses3.*; + +new EventbridgeToKinesisFirehoseToS3(this, "test-eventbridge-firehose-s3", + new EventbridgeToKinesisFirehoseToS3Props.Builder() + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/README.md b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/README.md index 01510ac13..625b4e54e 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/README.md @@ -24,33 +24,57 @@ This AWS Solutions Construct implements an Amazon EventBridge rule to send data to an Amazon Kinesis Data Stream -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import * as cdk from '@aws-cdk/core'; -import {EventbridgeToKinesisStreams, EventbridgeToKinesisStreamsProps} from "@aws-solutions-constructs/aws-eventbridge-kinesisstreams"; - -const props: EventbridgeToKinesisStreamsProps = { - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)), - } +import { Construct } from 'constructs'; +import { Stack, StackProps, Duration } from 'aws-cdk-lib'; +import { EventbridgeToKinesisStreams, EventbridgeToKinesisStreamsProps } from "@aws-solutions-constructs/aws-eventbridge-kinesisstreams"; +import * as events from 'aws-cdk-lib/aws-events'; + +const constructProps: EventbridgeToKinesisStreamsProps = { + eventRuleProps: { + schedule: events.Schedule.rate(Duration.minutes(5)), + } }; -new EventbridgeToKinesisStreams(this, 'test-eventbridge-kinesis-streams', props); +new EventbridgeToKinesisStreams(this, 'test-eventbridge-kinesis-streams', constructProps); ``` -## Initializer - -``` text -new EventbridgeToKinesisStreams(scope: Construct, id: string, props: EventbridgeToKinesisStreamsProps); +Python +``` Python +from aws_solutions_constructs.aws_eventbridge_kinesis_streams import EventbridgeToKinesisStreams, EventbridgeToKinesisStreamsProps +from aws_cdk import ( + aws_events as events, + Duration, + Stack +) +from constructs import Construct + +EventbridgeToKinesisStreams(self, 'test-eventbridge-kinesis-streams', + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate(Duration.minutes(5)), + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventbridgeToKinesisStreamsProps`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.events.*; +import software.amazon.awsconstructs.services.eventbridgekinesisstreams.*; + +new EventbridgeToKinesisStreams(this, "test-eventbridge-kinesis-streams", + new EventbridgeToKinesisStreamsProps.Builder() + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-eventbridge-lambda/README.md index a642d2499..e30afa303 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-lambda/README.md @@ -20,36 +20,77 @@ This AWS Solutions Construct implements an AWS EventBridge rule and an AWS Lambda function. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { EventbridgeToLambdaProps, EventbridgeToLambda } from '@aws-solutions-constructs/aws-eventbridge-lambda'; - -const props: EventbridgeToLambdaProps = { - lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' - }, - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)) - } +import { Construct } from 'constructs'; +import { Stack, StackProps, Duration } from 'aws-cdk-lib'; +import { EventbridgeToLambdaProps, EventbridgeToLambda } from '@aws-solutions-constructs/aws-eventbridge-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as events from 'aws-cdk-lib/aws-events'; + +const constructProps: EventbridgeToLambdaProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, + eventRuleProps: { + schedule: events.Schedule.rate(Duration.minutes(5)) + } }; -new EventbridgeToLambda(this, 'test-eventbridge-lambda', props); +new EventbridgeToLambda(this, 'test-eventbridge-lambda', constructProps); ``` -## Initializer - -``` text -new EventbridgeToLambda(scope: Construct, id: string, props: EventbridgeToLambdaProps); +Python +``` python +from aws_solutions_constructs.aws_eventbridge_lambda import EventbridgeToLambda, EventbridgeToLambdaProps +from aws_cdk import ( + aws_lambda as _lambda, + aws_events as events, + Duration, + Stack +) +from constructs import Construct + +EventbridgeToLambda(self, 'test-eventbridge-lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + Duration.minutes(5)) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventbridgeToLambdaProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.events.*; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.eventbridgelambda.*; + +new EventbridgeToLambda(this, "test-eventbridge-lambda", + new EventbridgeToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-sns/README.md b/source/patterns/@aws-solutions-constructs/aws-eventbridge-sns/README.md index 4c7f72cb7..94fe8880c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-sns/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-sns/README.md @@ -20,44 +20,92 @@ This AWS Solutions Construct implements an AWS Events rule and an AWS SNS Topic. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import { Duration } from '@aws-cdk/core'; -import * as events from '@aws-cdk/aws-events'; -import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from 'constructs'; +import { Stack, StackProps, Duration } from 'aws-cdk-lib'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as iam from 'aws-cdk-lib/aws-iam'; import { EventbridgeToSnsProps, EventbridgeToSns } from "@aws-solutions-constructs/aws-eventbridge-sns"; -const props: EventbridgeToSnsProps = { - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)), - } +const constructProps: EventbridgeToSnsProps = { + eventRuleProps: { + schedule: events.Schedule.rate(Duration.minutes(5)) + } }; -const constructStack = new EventbridgeToSns(this, 'test-construct', props); +const constructStack = new EventbridgeToSns(this, 'test-construct', constructProps); // Grant yourself permissions to use the Customer Managed KMS Key const policyStatement = new iam.PolicyStatement({ - actions: ["kms:Encrypt", "kms:Decrypt"], - effect: iam.Effect.ALLOW, - principals: [ new iam.AccountRootPrincipal() ], - resources: [ "*" ] + actions: ["kms:Encrypt", "kms:Decrypt"], + effect: iam.Effect.ALLOW, + principals: [new iam.AccountRootPrincipal()], + resources: ["*"] }); constructStack.encryptionKey?.addToResourcePolicy(policyStatement); ``` -## Initializer - -``` text -new EventbridgeToSns(scope: Construct, id: string, props: EventbridgeToSnsProps); +Python +``` Python +from aws_solutions_constructs.aws_eventbridge_sns import EventbridgeToSns, EventbridgeToSnsProps +from aws_cdk import ( + aws_events as events, + aws_iam as iam, + Duration, + Stack +) +from constructs import Construct + +construct_stack = EventbridgeToSns(self, 'test-construct', + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + Duration.minutes(5)) + )) + +# Grant yourself permissions to use the Customer Managed KMS Key +policy_statement = iam.PolicyStatement( + actions=["kms:Encrypt", "kms:Decrypt"], + effect=iam.Effect.ALLOW, + principals=[iam.AccountRootPrincipal()], + resources=["*"] +) + +construct_stack.encryption_key.add_to_resource_policy(policy_statement) ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.events.*; +import software.amazon.awscdk.services.iam.*; +import software.amazon.awsconstructs.services.eventbridgesns.*; + +final EventbridgeToSns constructStack = new EventbridgeToSns(this, "test-construct", + new EventbridgeToSnsProps.Builder() + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventbridgeToSnsProps`](#pattern-construct-props) +// Grant yourself permissions to use the Customer Managed KMS Key +final PolicyStatement policyStatement = PolicyStatement.Builder.create() + .actions(List.of("kms:Encrypt", "kms:Decrypt")) + .effect(Effect.ALLOW) + .principals(List.of(new AccountRootPrincipal())) + .resources(List.of("*")) + .build(); + +constructStack.getEncryptionKey().addToResourcePolicy(policyStatement); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-eventbridge-sqs/README.md index 9d4fa70fb..3c6830f4c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-sqs/README.md @@ -20,44 +20,92 @@ This AWS Solutions Construct implements an Amazon EventBridge rule and an AWS SQS Queue. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import { Duration } from '@aws-cdk/core'; -import * as events from '@aws-cdk/aws-events'; -import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from 'constructs'; +import { Stack, StackProps, Duration } from 'aws-cdk-lib'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as iam from 'aws-cdk-lib/aws-iam'; import { EventbridgeToSqsProps, EventbridgeToSqs } from "@aws-solutions-constructs/aws-eventbridge-sqs"; -const props: EventbridgeToSqsProps = { +const constructProps: EventbridgeToSqsProps = { eventRuleProps: { schedule: events.Schedule.rate(Duration.minutes(5)) } }; -const constructStack = new EventbridgeToSqs(this, 'test-construct', props); +const constructStack = new EventbridgeToSqs(this, 'test-construct', constructProps); // Grant yourself permissions to use the Customer Managed KMS Key const policyStatement = new iam.PolicyStatement({ actions: ["kms:Encrypt", "kms:Decrypt"], effect: iam.Effect.ALLOW, - principals: [ new iam.AccountRootPrincipal() ], - resources: [ "*" ] + principals: [new iam.AccountRootPrincipal()], + resources: ["*"] }); constructStack.encryptionKey?.addToResourcePolicy(policyStatement); ``` -## Initializer - -``` text -new EventbridgeToSqs(scope: Construct, id: string, props: EventbridgeToSqsProps); +Python +``` Python +from aws_solutions_constructs.aws_eventbridge_sqs import EventbridgeToSqsProps, EventbridgeToSqs +from aws_cdk import ( + aws_events as events, + aws_iam as iam, + Duration, + Stack +) +from constructs import Construct + +construct_stack = EventbridgeToSqs(self, 'test-construct', + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + Duration.minutes(5)) + )) + +# Grant yourself permissions to use the Customer Managed KMS Key +policy_statement = iam.PolicyStatement( + actions=["kms:Encrypt", "kms:Decrypt"], + effect=iam.Effect.ALLOW, + principals=[iam.AccountRootPrincipal()], + resources=["*"] +) + +construct_stack.encryption_key.add_to_resource_policy(policy_statement) ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.events.*; +import software.amazon.awscdk.services.iam.*; +import software.amazon.awsconstructs.services.eventbridgesqs.*; + +final EventbridgeToSqs constructStack = new EventbridgeToSqs(this, "test-construct", + new EventbridgeToSqsProps.Builder() + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventbridgeToSqsProps`](#pattern-construct-props) +// Grant yourself permissions to use the Customer Managed KMS Key +final PolicyStatement policyStatement = PolicyStatement.Builder.create() + .actions(List.of("kms:Encrypt", "kms:Decrypt")) + .effect(Effect.ALLOW) + .principals(List.of(new AccountRootPrincipal())) + .resources(List.of("*")) + .build(); + +constructStack.getEncryptionKey().addToResourcePolicy(policyStatement); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/README.md b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/README.md index cac4b2368..70d177844 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/README.md @@ -20,36 +20,77 @@ This AWS Solutions Construct implements an AWS Events rule and an AWS Step Functions State Machine -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { EventbridgeToStepfunctions, EventbridgeToStepfunctionsProps } from '@aws-solutions-constructs/aws-eventbridge-stepfunctions'; +import { Construct } from 'constructs'; +import { Stack, StackProps, Duration } from 'aws-cdk-lib'; +import { EventbridgeToStepfunctions, EventbridgeToStepfunctionsProps } from '@aws-solutions-constructs/aws-eventbridge-stepfunctions'; +import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions'; +import * as events from 'aws-cdk-lib/aws-events'; const startState = new stepfunctions.Pass(this, 'StartState'); -const props: EventbridgeToStepfunctionsProps = { - stateMachineProps: { - definition: startState - }, - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)) - } +const constructProps: EventbridgeToStepfunctionsProps = { + stateMachineProps: { + definition: startState + }, + eventRuleProps: { + schedule: events.Schedule.rate(Duration.minutes(5)) + } }; -new EventbridgeToStepfunctions(stack, 'test-eventbridge-stepfunctions-stack', props); +new EventbridgeToStepfunctions(this, 'test-eventbridge-stepfunctions-stack', constructProps); ``` -## Initializer - -``` text -new EventbridgeToStepfunctions(scope: Construct, id: string, props: EventbridgeToStepfunctionsProps); +Python +``` python +from aws_solutions_constructs.aws_eventbridge_stepfunctions import EventbridgeToStepfunctions, EventbridgeToStepfunctionsProps +from aws_cdk import ( + aws_stepfunctions as stepfunctions, + aws_events as events, + Duration, + Stack +) +from constructs import Construct + +startState = stepfunctions.Pass(self, 'StartState') + +EventbridgeToStepfunctions(self, 'test-eventbridge-stepfunctions-stack', + state_machine_props=stepfunctions.StateMachineProps( + definition=startState + ), + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + Duration.minutes(5)) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventbridgeToStepfunctionsProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.events.*; +import software.amazon.awscdk.services.stepfunctions.*; +import software.amazon.awsconstructs.services.eventbridgestepfunctions.*; + +final Pass startState = new Pass(this, "StartState"); + +new EventbridgeToStepfunctions(this, + "test-eventbridge-stepfunctions-stack", + new EventbridgeToStepfunctionsProps.Builder() + .stateMachineProps(new StateMachineProps.Builder() + .definition(startState) + .build()) + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisfirehose-s3/README.md index 5c1c36d2b..ae83d3809 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisfirehose-s3/README.md @@ -22,33 +22,60 @@ This AWS Solutions Construct implements an Amazon CloudWatch Events rule to send data to an Amazon Kinesis Data Firehose delivery stream connected to an Amazon S3 bucket. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: -``` javascript +Typescript +``` typescript +// aws-events-rule-kinesisfirehose-s3 has been deprecated for CDK V2 in favor of aws-eventbridge-kinesisfirehose-s3. +// This sample uses the CDK V1 syntax import * as cdk from '@aws-cdk/core'; -import { EventsRuleToKinesisFirehoseToS3, EventsRuleToKinesisFirehoseToS3Props } from '@aws-solutions-constructs/aws-events-rule-kinesisfirehose-s3'; +import * as events from '@aws-cdk/aws-events'; +import { EventsRuleToKinesisFirehoseToS3Props, EventsRuleToKinesisFirehoseToS3 } from '@aws-solutions-constructs/aws-events-rule-kinesisfirehose-s3'; const eventsRuleToKinesisFirehoseToS3Props: EventsRuleToKinesisFirehoseToS3Props = { - eventRuleProps: { + eventRuleProps: { schedule: events.Schedule.rate(cdk.Duration.minutes(5)) - } + } }; new EventsRuleToKinesisFirehoseToS3(this, 'test-events-rule-firehose-s3', eventsRuleToKinesisFirehoseToS3Props); +``` +Python +``` python +# aws-events-rule-kinesisfirehose-s3 has been deprecated for CDK V2 in favor of aws-eventbridge-kinesisfirehose-s3. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_events_rule_kinesis_firehose_s3 import EventsRuleToKinesisFirehoseToS3, EventsRuleToKinesisFirehoseToS3Props +from aws_cdk import ( + aws_events as events, + core +) + +EventsRuleToKinesisFirehoseToS3(self, 'test_events_rule_firehose_s3', + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + core.Duration.minutes(5)) + )) ``` -## Initializer +Java +``` java +// aws-events-rule-kinesisfirehose-s3 has been deprecated for CDK V2 in favor of aws-eventbridge-kinesisfirehose-s3. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; +import software.amazon.awscdk.core.*; -``` text -new EventsRuleToKinesisFirehoseToS3(scope: Construct, id: string, props: EventsRuleToKinesisFirehoseToS3Props); -``` +import software.amazon.awscdk.services.events.*; +import software.amazon.awsconstructs.services.eventsrulekinesisfirehoses3.*; -_Parameters_ -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventsRuleToKinesisFirehoseToS3Props`](#pattern-construct-props) +new EventsRuleToKinesisFirehoseToS3(this, "test-events-rule-firehose-s3", + new EventsRuleToKinesisFirehoseToS3Props.Builder() + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/README.md b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/README.md index 3d771ed94..7f6309c74 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/README.md @@ -22,32 +22,59 @@ This AWS Solutions Construct implements an Amazon CloudWatch Events rule to send data to an Amazon Kinesis data stream. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +// aws-events-rule-kinesisstreams has been deprecated for CDK V2 in favor of aws-eventbridge-kinesisstreams. +// This sample uses the CDK V1 syntax import * as cdk from '@aws-cdk/core'; -import {EventsRuleToKinesisStreams, EventsRuleToKinesisStreamsProps} from "@aws-solutions-constructs/aws-events-rule-kinesisstreams"; +import * as events from '@aws-cdk/aws-events'; +import { EventsRuleToKinesisStreams, EventsRuleToKinesisStreamsProps } from "@aws-solutions-constructs/aws-events-rule-kinesisstreams"; -const props: EventsRuleToKinesisStreamsProps = { - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)), - } +const constructProps: EventsRuleToKinesisStreamsProps = { + eventRuleProps: { + schedule: events.Schedule.rate(cdk.Duration.minutes(5)), + } }; -new EventsRuleToKinesisStreams(this, 'test-events-rule-kinesis-streams', props); +new EventsRuleToKinesisStreams(this, 'test-events-rule-kinesis-streams', constructProps); ``` -## Initializer - -``` text -new EventsRuleToKinesisStreams(scope: Construct, id: string, props: EventsRuleToKinesisStreamsProps); +Python +``` python +# aws-events-rule-kinesisstreams has been deprecated for CDK V2 in favor of aws-eventbridge-kinesisstreams. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_events_rule_kinesis_streams import EventsRuleToKinesisStreams, EventsRuleToKinesisStreamsProps +from aws_cdk import ( + aws_events as events, + core +) + +EventsRuleToKinesisStreams(self, 'test_events_rule_kinesis_streams', + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + core.Duration.minutes(5)), + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventsRuleToKinesisStreamsProps`](#pattern-construct-props) +Java +``` java +// aws-events-rule-kinesisstreams has been deprecated for CDK V2 in favor of aws-eventbridge-kinesisstreams. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.events.*; +import software.amazon.awsconstructs.services.eventsrulekinesisstreams.*; + +new EventsRuleToKinesisStreams(this, "test-events-rule-kinesis-streams", + new EventsRuleToKinesisStreamsProps.Builder() + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/README.md index e63157f03..3c0087832 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/README.md @@ -22,36 +22,77 @@ This AWS Solutions Construct implements an AWS Events rule and an AWS Lambda function. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript +// aws-events-rule-lambda has been deprecated for CDK V2 in favor of aws-eventbridge-lambda. +// This sample uses the CDK V1 syntax +import * as cdk from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as events from '@aws-cdk/aws-events'; import { EventsRuleToLambdaProps, EventsRuleToLambda } from '@aws-solutions-constructs/aws-events-rule-lambda'; -const props: EventsRuleToLambdaProps = { - lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' - }, - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)) - } +const constructProps: EventsRuleToLambdaProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, + eventRuleProps: { + schedule: events.Schedule.rate(cdk.Duration.minutes(5)) + } }; -new EventsRuleToLambda(this, 'test-events-rule-lambda', props); +new EventsRuleToLambda(this, 'test-events-rule-lambda', constructProps); ``` -## Initializer - -``` text -new EventsRuleToLambda(scope: Construct, id: string, props: EventsRuleToLambdaProps); +Python +``` python +# aws-events-rule-lambda has been deprecated for CDK V2 in favor of aws-eventbridge-lambda. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_events_rule_lambda import EventsRuleToLambdaProps, EventsRuleToLambda +from aws_cdk import ( + aws_lambda as _lambda, + aws_events as events, + core +) + +EventsRuleToLambda(self, 'test_events_rule_lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate(core.Duration.minutes(5)) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventsRuleToLambdaProps`](#pattern-construct-props) +Java +``` java +// aws-events-rule-lambda has been deprecated for CDK V2 in favor of aws-eventbridge-lambda. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.events.*; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.eventsrulelambda.*; + +new EventsRuleToLambda(this, "test-events-rule-lambda", + new EventsRuleToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-sns/README.md b/source/patterns/@aws-solutions-constructs/aws-events-rule-sns/README.md index c45bf8914..81e0a0d32 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-sns/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-sns/README.md @@ -21,44 +21,93 @@ This AWS Solutions Construct implements an AWS Events rule and an AWS SNS Topic. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import { Duration } from '@aws-cdk/core'; +// aws-events-rule-sns has been deprecated for CDK V2 in favor of aws-eventbridge-sns. +// This sample uses the CDK V1 syntax +import * as cdk from '@aws-cdk/core'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { EventsRuleToSnsProps, EventsRuleToSns } from "@aws-solutions-constructs/aws-events-rule-sns"; -const props: EventsRuleToSnsProps = { - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)), - } +const constructProps: EventsRuleToSnsProps = { + eventRuleProps: { + schedule: events.Schedule.rate(cdk.Duration.minutes(5)), + } }; -const constructStack = new EventsRuleToSns(this, 'test-construct', props); +const constructStack = new EventsRuleToSns(this, 'test', constructProps); // Grant yourself permissions to use the Customer Managed KMS Key const policyStatement = new iam.PolicyStatement({ - actions: ["kms:Encrypt", "kms:Decrypt"], - effect: iam.Effect.ALLOW, - principals: [ new iam.AccountRootPrincipal() ], - resources: [ "*" ] + actions: ["kms:Encrypt", "kms:Decrypt"], + effect: iam.Effect.ALLOW, + principals: [new iam.AccountRootPrincipal()], + resources: ["*"] }); constructStack.encryptionKey?.addToResourcePolicy(policyStatement); ``` -## Initializer - -``` text -new EventsRuleToSns(scope: Construct, id: string, props: EventsRuleToSnsProps); +Python +``` python +# aws-events-rule-sns has been deprecated for CDK V2 in favor of aws-eventbridge-sns. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_events_rule_sns import EventsRuleToSns, EventsRuleToSnsProps +from aws_cdk import ( + aws_events as events, + aws_iam as iam, + core +) + +construct_stack = EventsRuleToSns(self, 'test', + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + core.Duration.minutes(5)) + )) + +# Grant yourself permissions to use the Customer Managed KMS Key +policy_statement = iam.PolicyStatement( + actions=["kms:Encrypt", "kms:Decrypt"], + effect=iam.Effect.ALLOW, + principals=[iam.AccountRootPrincipal()], + resources=["*"] +) + +construct_stack.encryption_key.add_to_resource_policy(policy_statement) ``` -_Parameters_ +Java +``` java +// aws-events-rule-sns has been deprecated for CDK V2 in favor of aws-eventbridge-sns. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.events.*; +import software.amazon.awscdk.services.iam.*; +import software.amazon.awsconstructs.services.eventsrulesns.*; + +final EventsRuleToSns constructStack = new EventsRuleToSns(this, "test", + new EventsRuleToSnsProps.Builder() + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventsRuleToSnsProps`](#pattern-construct-props) +// Grant yourself permissions to use the Customer Managed KMS Key +final PolicyStatement policyStatement = PolicyStatement.Builder.create() + .actions(List.of("kms:Encrypt", "kms:Decrypt")) + .effect(Effect.ALLOW) + .principals(List.of(new AccountRootPrincipal())) + .resources(List.of("*")) + .build(); + +constructStack.getEncryptionKey().addToResourcePolicy(policyStatement); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-events-rule-sqs/README.md index 1f14a5332..6ea791e66 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-sqs/README.md @@ -22,45 +22,93 @@ This AWS Solutions Construct implements an AWS Events rule and an AWS SQS Queue. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import { Duration } from '@aws-cdk/core'; +// aws-events-rule-sqs has been deprecated for CDK V2 in favor of aws-eventbridge-sqs. +// This sample uses the CDK V1 syntax +import * as cdk from '@aws-cdk/core'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { EventsRuleToSqsProps, EventsRuleToSqs } from "@aws-solutions-constructs/aws-events-rule-sqs"; -const props: EventsRuleToSqsProps = { - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)) - } +const constructProps: EventsRuleToSqsProps = { + eventRuleProps: { + schedule: events.Schedule.rate(cdk.Duration.minutes(5)) + } }; -const constructStack = new EventsRuleToSqs(this, 'test-construct', props); +const constructStack = new EventsRuleToSqs(this, 'test', constructProps); // Grant yourself permissions to use the Customer Managed KMS Key const policyStatement = new iam.PolicyStatement({ - actions: ["kms:Encrypt", "kms:Decrypt"], - effect: iam.Effect.ALLOW, - principals: [ new iam.AccountRootPrincipal() ], - resources: [ "*" ] + actions: ["kms:Encrypt", "kms:Decrypt"], + effect: iam.Effect.ALLOW, + principals: [new iam.AccountRootPrincipal()], + resources: ["*"] }); constructStack.encryptionKey?.addToResourcePolicy(policyStatement); ``` -## Initializer - -``` text -new EventsRuleToSqs(scope: Construct, id: string, props: EventsRuleToSqsProps); +Python +``` python +# aws-events-rule-sqs has been deprecated for CDK V2 in favor of aws-eventbridge-sqs. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_events_rule_sqs import EventsRuleToSqs +from aws_cdk import ( + aws_events as events, + aws_iam as iam, + core +) + +construct_stack = EventsRuleToSqs(self, 'test', + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + core.Duration.minutes(5)) + )) + +# Grant yourself permissions to use the Customer Managed KMS Key +policy_statement = iam.PolicyStatement( + actions=["kms:Encrypt", "kms:Decrypt"], + effect=iam.Effect.ALLOW, + principals=[iam.AccountRootPrincipal()], + resources=["*"] +) + +construct_stack.encryption_key.add_to_resource_policy(policy_statement) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventsRuleToSqsProps`](#pattern-construct-props) +Java +``` java +// aws-events-rule-sqs has been deprecated for CDK V2 in favor of aws-eventbridge-sqs. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.events.*; +import software.amazon.awscdk.services.iam.*; +import software.amazon.awsconstructs.services.eventsrulesqs.*; + +final EventsRuleToSqs constructStack = new EventsRuleToSqs(this, "test-construct", + new EventsRuleToSqsProps.Builder() + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +// Grant yourself permissions to use the Customer Managed KMS Key +final PolicyStatement policyStatement = PolicyStatement.Builder.create() + .actions(List.of("kms:Encrypt", "kms:Decrypt")) + .effect(Effect.ALLOW) + .principals(List.of(new AccountRootPrincipal())) + .resources(List.of("*")) + .build(); + +constructStack.getEncryptionKey().addToResourcePolicy(policyStatement); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/README.md b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/README.md index c4122fef9..9436187c9 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/README.md @@ -22,36 +22,79 @@ This AWS Solutions Construct implements an AWS Events rule and an AWS Step function. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { EventsRuleToStepFunction, EventsRuleToStepFunctionProps } from '@aws-solutions-constructs/aws-events-rule-step-function'; +// aws-events-rule-step-function has been deprecated for CDK V2 in favor of aws-eventbridge-stepfunctions. +// This sample uses the CDK V1 syntax +import * as cdk from '@aws-cdk/core'; +import * as events from '@aws-cdk/aws-events'; +import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; +import { EventsRuleToStepFunction, EventsRuleToStepFunctionProps } from '@aws-solutions-constructs/aws-events-rule-step-function'; const startState = new stepfunctions.Pass(this, 'StartState'); -const props: EventsRuleToStepFunctionProps = { - stateMachineProps: { - definition: startState - }, - eventRuleProps: { - schedule: events.Schedule.rate(Duration.minutes(5)) - } +const constructProps: EventsRuleToStepFunctionProps = { + stateMachineProps: { + definition: startState + }, + eventRuleProps: { + schedule: events.Schedule.rate(cdk.Duration.minutes(5)) + } }; -new EventsRuleToStepFunction(stack, 'test-events-rule-step-function-stack', props); +new EventsRuleToStepFunction(this, 'test-events-rules-step-function-stack', constructProps); ``` -## Initializer +Python +``` Python +# aws-events-rule-step-function has been deprecated for CDK V2 in favor of aws-eventbridge-stepfunctions. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_events_rule_step_function import EventsRuleToStepFunction, EventsRuleToStepFunctionProps +from aws_cdk import ( + aws_stepfunctions as stepfunctions, + aws_events as events, + core +) + +startState = stepfunctions.Pass(self, 'StartState') + +EventsRuleToStepFunction(self, 'test', + state_machine_props=stepfunctions.StateMachineProps( + definition=startState + ), + event_rule_props=events.RuleProps( + schedule=events.Schedule.rate( + core.Duration.minutes(5)) + )) -``` text -new EventsRuleToStepFunction(scope: Construct, id: string, props: EventsRuleToStepFunctionProps); ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`EventsRuleToStepFunctionProps`](#pattern-construct-props) +Java +``` java +// aws-events-rule-step-function has been deprecated for CDK V2 in favor of aws-eventbridge-stepfunctions. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.events.*; +import software.amazon.awscdk.services.stepfunctions.*; +import software.amazon.awsconstructs.services.eventsrulestepfunction.*; + +final Pass startState = new Pass(this, "StartState"); + +new EventsRuleToStepFunction(this, + "test-eventbridge-stepfunctions-stack", + new EventsRuleToStepFunctionProps.Builder() + .stateMachineProps(new StateMachineProps.Builder() + .definition(startState) + .build()) + .eventRuleProps(new RuleProps.Builder() + .schedule(Schedule.rate(Duration.minutes(5))) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/README.md index 158a01948..be6592805 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/README.md @@ -24,19 +24,48 @@ This AWS Solutions Construct implements an AWS Fargate service that can write/read to an Amazon S3 Bucket -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { FargateToS3, FargateToS3Props } from '@aws-solutions-constructs/aws-fargate-s3'; -const props: FargateToS3Props = { +const constructProps: FargateToS3Props = { publicApi: true, - ecrRepositoryArn: "arn of a repo in ECR in your account", -}); + ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", +}; -new FargateToS3(stack, 'test-construct', props); +new FargateToS3(this, 'test-construct', constructProps); ``` +Python +``` python +from aws_solutions_constructs.aws_fargate_s3 import FargateToS3, FargateToS3Props +from aws_cdk import ( + Stack +) +from constructs import Construct + +FargateToS3(self, 'test_construct', + public_api=True, + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.fargates3.*; + +new FargateToS3(this, "test_construct", new FargateToS3Props.Builder() + .publicApi(true) + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/README.md index a388aa729..cd0b685c1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/README.md @@ -24,37 +24,48 @@ This AWS Solutions Construct implements an AWS Fargate service that can write to an Amazon SNS topic -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript - import { FargateToSns, FargateToSnsProps } from '@aws-solutions-constructs/aws-fargate-sns'; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { FargateToSns, FargateToSnsProps } from '@aws-solutions-constructs/aws-fargate-sns'; - // Obtain a pre-existing certificate from your account - const certificate = acm.Certificate.fromCertificateArn( - scope, - 'existing-cert', - "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" - ); - - const props: FargateToSnsProps = { +const constructProps: FargateToSnsProps = { publicApi: true, - ecrRepositoryArn: "arn of a repo in ECR in your account", - }); + ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo" +}; - new FargateToSns(stack, 'test-construct', props); +new FargateToSns(this, 'test-construct', constructProps); ``` -## Initializer - -``` text -new FargateToSns(scope: Construct, id: string, props: FargateToSnsProps); +Python +``` python +from aws_solutions_constructs.aws_fargate_sns import FargateToSns, FargateToSnsProps +from aws_cdk import ( + Stack +) +from constructs import Construct + +FargateToSns(self, 'test_construct', + public_api=True, + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`FargateToSnsProps`](#pattern-construct-props) +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.fargatesns.*; + +new FargateToSns(this, "test_construct", new FargateToSnsProps.Builder() + .publicApi(true) + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/README.md index bac1870ac..e2e56737b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/README.md @@ -28,14 +28,43 @@ Here is a minimal deployable pattern definition: Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { FargateToSqs, FargateToSqsProps } from '@aws-solutions-constructs/aws-fargate-sqs'; -const props: FargateToSqsProps = { +const constructProps: FargateToSqsProps = { publicApi: true, - ecrRepositoryArn: "arn of a repo in ECR in your account", -}); + ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo" +}; -new FargateToSqs(stack, 'test-construct', props); +new FargateToSqs(this, 'test-construct', constructProps); +``` + +Python +``` python +from aws_solutions_constructs.aws_fargate_sqs import FargateToSqs, FargateToSqsProps +from aws_cdk import ( + Stack +) +from constructs import Construct + +FargateToSqs(self, 'test_construct', + public_api=True, + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.fargatesqs.*; + +new FargateToSqs(this, "test_construct", new FargateToSqsProps.Builder() + .publicApi(true) + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") + .build()); ``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md index 4f4c3187b..90726a44e 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/README.md @@ -24,12 +24,15 @@ This AWS Solutions Construct implements an AWS IoT MQTT topic rule to send data to an Amazon Kinesis Data Firehose delivery stream connected to an Amazon S3 bucket. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { IotToKinesisFirehoseToS3Props, IotToKinesisFirehoseToS3 } from '@aws-solutions-constructs/aws-iot-kinesisfirehose-s3'; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { IotToKinesisFirehoseToS3Props, IotToKinesisFirehoseToS3 } from '@aws-solutions-constructs/aws-iot-kinesisfirehose-s3'; -const props: IotToKinesisFirehoseToS3Props = { +const constructProps: IotToKinesisFirehoseToS3Props = { iotTopicRuleProps: { topicRulePayload: { ruleDisabled: false, @@ -40,21 +43,52 @@ const props: IotToKinesisFirehoseToS3Props = { } }; -new IotToKinesisFirehoseToS3(this, 'test-iot-firehose-s3', props); - +new IotToKinesisFirehoseToS3(this, 'test-iot-firehose-s3', constructProps); ``` -## Initializer +Python +```python +from aws_solutions_constructs.aws_iot_kinesisfirehose_s3 import IotToKinesisFirehoseToS3Props, IotToKinesisFirehoseToS3 +from aws_cdk import ( + aws_iot as iot, + Stack +) +from constructs import Construct + +IotToKinesisFirehoseToS3(self, 'test_iot_firehose_s3', + iot_topic_rule_props=iot.CfnTopicRuleProps( + topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( + rule_disabled=False, + description="Persistent storage of connected vehicle telematics data", + sql="SELECT * FROM 'connectedcar/telemetry/#'", + actions=[] + ) + )) -``` text -new IotToKinesisFirehoseToS3(scope: Construct, id: string, props: IotToKinesisFirehoseToS3Props); ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`IotToKinesisFirehoseToS3Props`](#pattern-construct-props) +Java +```java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.iot.*; +import software.amazon.awscdk.services.iot.CfnTopicRule.TopicRulePayloadProperty; +import software.amazon.awsconstructs.services.iotkinesisfirehoses3.*; + +new IotToKinesisFirehoseToS3(this, "test-iot-firehose-s3", new IotToKinesisFirehoseToS3Props.Builder() + .iotTopicRuleProps(new CfnTopicRuleProps.Builder() + .topicRulePayload(new TopicRulePayloadProperty.Builder() + .ruleDisabled(false) + .description("Persistent storage of connected vehicle telematics data") + .sql("SELECT * FROM 'connectedcar/telemetry/#'") + .actions(List.of()) + .build()) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/README.md index a9b19b582..d907934bd 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/README.md @@ -24,38 +24,70 @@ This AWS Solutions Construct implements an AWS IoT MQTT topic rule to send data to an Amazon Kinesis Data Stream. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -const { IotToKinesisStreamsProps, - IotToKinesisStreams } from '@aws-solutions-constructs/aws-iot-kinesisstreams'; - -const props: IotToKinesisStreamsProps = { - iotTopicRuleProps: { - topicRulePayload: { - ruleDisabled: false, - description: "Sends data to kinesis data stream", - sql: "SELECT * FROM 'solutions/construct'", - actions: [] - } +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { IotToKinesisStreamsProps, IotToKinesisStreams } from '@aws-solutions-constructs/aws-iot-kinesisstreams'; + +const constructProps: IotToKinesisStreamsProps = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Sends data to kinesis data stream", + sql: "SELECT * FROM 'solutions/construct'", + actions: [] } + } }; -new IotToKinesisStreams(this, 'test-iot-kinesisstream', props); - +new IotToKinesisStreams(this, 'test-iot-kinesisstreams', constructProps); ``` -## Initializer - -``` text -new IotToKinesisStreams(scope: Construct, id: string, props: IotToKinesisStreamsProps); +Python +``` python +from aws_solutions_constructs.aws_iot_kinesisstreams import IotToKinesisStreamsProps, IotToKinesisStreams +from aws_cdk import ( + aws_iot as iot, + Stack +) +from constructs import Construct + +IotToKinesisStreams(self, 'test-iot-kinesisstreams', + iot_topic_rule_props=iot.CfnTopicRuleProps( + topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( + rule_disabled=False, + description="Sends data to kinesis data stream", + sql="SELECT * FROM 'solutions/construct'", + actions=[] + ) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`IotToKinesisStreamsProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.iot.*; +import software.amazon.awscdk.services.iot.CfnTopicRule.TopicRulePayloadProperty; +import software.amazon.awsconstructs.services.iotkinesisstreams.*; + +new IotToKinesisStreams(this, "test-iot-kinesisstreams", new IotToKinesisStreamsProps.Builder() + .iotTopicRuleProps(new CfnTopicRuleProps.Builder() + .topicRulePayload(new TopicRulePayloadProperty.Builder() + .ruleDisabled(false) + .description("Sends data to kinesis data stream") + .sql("SELECT * FROM 'solutions/construct'") + .actions(List.of()) + .build()) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/README.md index 3b36e462b..e6312b6c7 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/README.md @@ -24,42 +24,89 @@ This AWS Solutions Construct implements an AWS IoT topic rule, an AWS Lambda function and Amazon DynamoDB table with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { IotToLambdaToDynamoDBProps, IotToLambdaToDynamoDB } from '@aws-solutions-constructs/aws-iot-lambda-dynamodb'; - -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: [] - } - } +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { IotToLambdaToDynamoDBProps, IotToLambdaToDynamoDB } from '@aws-solutions-constructs/aws-iot-lambda-dynamodb'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +const constructProps: IotToLambdaToDynamoDBProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`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: [] + } + } }; -new IotToLambdaToDynamoDB(this, 'test-iot-lambda-dynamodb-stack', props); - +new IotToLambdaToDynamoDB(this, 'test-iot-lambda-dynamodb-stack', constructProps); ``` -## Initializer - -``` text -new IotToLambdaToDynamoDB(scope: Construct, id: string, props: IotToLambdaToDynamoDBProps); +Python +``` python +from aws_solutions_constructs.aws_iot_lambda_dynamodb import IotToLambdaToDynamoDB +from aws_cdk import ( + aws_iot as iot, + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +IotToLambdaToDynamoDB(self, 'test-iot-lambda-dynamodb-stack', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + iot_topic_rule_props=iot.CfnTopicRuleProps( + topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( + rule_disabled=False, + description="Processing of DTC messages from the AWS Connected Vehicle Solution.", + sql="SELECT * FROM 'connectedcar/dtc/#'", + actions=[] + ) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`IotToLambdaToDynamoDBProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.iot.*; +import software.amazon.awscdk.services.iot.CfnTopicRule.TopicRulePayloadProperty; +import software.amazon.awsconstructs.services.iotlambdadynamodb.*; + +new IotToLambdaToDynamoDB(this, "test-iot-lambda-dynamodb-stack", new IotToLambdaToDynamoDBProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .iotTopicRuleProps(new CfnTopicRuleProps.Builder() + .topicRulePayload(new TopicRulePayloadProperty.Builder() + .ruleDisabled(false) + .description("Processing of DTC messages from the AWS Connected Vehicle Solution.") + .sql("SELECT * FROM 'connectedcar/dtc/#'") + .actions(List.of()) + .build()) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-lambda/README.md index efb20accf..60eb219c0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-lambda/README.md @@ -24,42 +24,89 @@ This AWS Solutions Construct implements an AWS IoT MQTT topic rule and an AWS Lambda function pattern. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { IotToLambdaProps, IotToLambda } from '@aws-solutions-constructs/aws-iot-lambda'; - -const props: IotToLambdaProps = { - 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: [] - } +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { IotToLambdaProps, IotToLambda } from '@aws-solutions-constructs/aws-iot-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +const constructProps: IotToLambdaProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`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: [] } + } }; -new IotToLambda(this, 'test-iot-lambda-integration', props); +new IotToLambda(this, 'test-iot-lambda-integration', constructProps); ``` -## Initializer - -``` text -new IotToLambda(scope: Construct, id: string, props: IotToLambdaProps); +Python +``` python +from aws_solutions_constructs.aws_iot_lambda import IotToLambdaProps, IotToLambda +from aws_cdk import ( + aws_iot as iot, + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +IotToLambda(self, 'test_iot_lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + iot_topic_rule_props=iot.CfnTopicRuleProps( + topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( + rule_disabled=False, + description="Sends data to kinesis data stream", + sql="SELECT * FROM 'solutions/construct'", + actions=[] + ) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`IotToLambdaProps`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.iot.*; +import software.amazon.awscdk.services.iot.CfnTopicRule.TopicRulePayloadProperty; +import software.amazon.awsconstructs.services.iotlambda.*; + +new IotToLambda(this, "test-iot-lambda-integration", new IotToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .iotTopicRuleProps(new CfnTopicRuleProps.Builder() + .topicRulePayload(new TopicRulePayloadProperty.Builder() + .ruleDisabled(false) + .description("Processing of DTC messages from the AWS Connected Vehicle Solution.") + .sql("SELECT * FROM 'connectedcar/dtc/#'") + .actions(List.of()) + .build()) + .build()) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-s3/README.md index 6db3afc11..db0733821 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-s3/README.md @@ -24,37 +24,71 @@ This AWS Solutions Construct implements an AWS IoT MQTT topic rule and an Amazon S3 Bucket pattern. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -const { IotToS3Props, IotToS3 } from '@aws-solutions-constructs/aws-iot-s3'; - -const props: IotToS3Props = { - iotTopicRuleProps: { - topicRulePayload: { - ruleDisabled: false, - description: "Testing the IotToS3 Pattern", - sql: "SELECT * FROM 'solutions/constructs'", - actions: [] - } +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { IotToS3Props, IotToS3 } from '@aws-solutions-constructs/aws-iot-s3'; + +const constructProps: IotToS3Props = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Testing the IotToS3 Pattern", + sql: "SELECT * FROM 'solutions/constructs'", + actions: [] } + } }; -new IotToS3(this, 'test-iot-s3-integration', props); +new IotToS3(this, 'test-iot-s3-integration', constructProps); ``` -## Initializer - -``` text -new IotToS3(scope: Construct, id: string, props: IotToS3Props); +Python +``` python +from aws_solutions_constructs.aws_iot_s3 import IotToS3Props, IotToS3 +from aws_cdk import ( + aws_iot as iot, + Stack +) +from constructs import Construct + + +IotToS3(self, 'test_iot_s3', + iot_topic_rule_props=iot.CfnTopicRuleProps( + topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( + rule_disabled=False, + description="Testing the IotToS3 Pattern", + sql="SELECT * FROM 'solutions/constructs'", + actions=[] + ) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`IotToS3Props`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.iot.*; +import software.amazon.awscdk.services.iot.CfnTopicRule.TopicRulePayloadProperty; +import software.amazon.awsconstructs.services.iots3.*; + +new IotToS3(this, "test_iot_s3", new IotToS3Props.Builder() + .iotTopicRuleProps(new CfnTopicRuleProps.Builder() + .topicRulePayload(new TopicRulePayloadProperty.Builder() + .ruleDisabled(false) + .description("Testing the IotToS3 Pattern") + .sql("SELECT * FROM 'solutions/constructs'") + .actions(List.of()) + .build()) + .build()) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-iot-sqs/README.md index 5ebccb509..78157133a 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-iot-sqs/README.md @@ -24,36 +24,70 @@ This AWS Solutions Construct implements an AWS IoT MQTT topic rule and an AWS SQS Queue pattern. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -const { IotToSqsProps, IotToSqs } from '@aws-solutions-constructs/aws-iot-sqs'; - -const props: IotToSqsProps = { - iotTopicRuleProps: { - topicRulePayload: { - ruleDisabled: false, - description: "Testing the IotToSqs Pattern", - sql: "SELECT * FROM 'iot/sqs/#'", - actions: [] - } +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { IotToSqsProps, IotToSqs } from '@aws-solutions-constructs/aws-iot-sqs'; + +const constructProps: IotToSqsProps = { + iotTopicRuleProps: { + topicRulePayload: { + ruleDisabled: false, + description: "Testing the IotToSqs Pattern", + sql: "SELECT * FROM 'iot/sqs/#'", + actions: [] } + } }; -new IotToSqs(this, 'test-iot-sqs-integration', props); +new IotToSqs(this, 'test-iot-sqs-integration', constructProps); ``` -## Initializer - -``` text -new IotToSqs(scope: Construct, id: string, props: IotToSqsProps); +Python +``` python +from aws_solutions_constructs.aws_iot_sqs import IotToSqs +from aws_cdk import ( + aws_iot as iot, + Stack +) +from constructs import Construct + +IotToSqs(self, 'test_iot_sqs', + iot_topic_rule_props=iot.CfnTopicRuleProps( + topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty( + rule_disabled=False, + description="Testing the IotToSqs Pattern", + sql="SELECT * FROM 'iot/sqs/#'", + actions=[] + ) + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`IotToSqsProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.iot.*; +import software.amazon.awscdk.services.iot.CfnTopicRule.TopicRulePayloadProperty; +import software.amazon.awsconstructs.services.iotsqs.*; + +new IotToSqs(this, "test_iot_sqs", new IotToSqsProps.Builder() + .iotTopicRuleProps(new CfnTopicRuleProps.Builder() + .topicRulePayload(new TopicRulePayloadProperty.Builder() + .ruleDisabled(false) + .description("Testing the IotToSqs Pattern") + .sql("SELECT * FROM 'iot/sqs/#'") + .actions(List.of()) + .build()) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md index 2b2d6140f..6008416ba 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/README.md @@ -24,55 +24,134 @@ This AWS Solutions Construct implements an Amazon Kinesis Firehose delivery stream connected to an Amazon S3 bucket, and an Amazon Kinesis Analytics application. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { KinesisFirehoseToAnalyticsAndS3 } from '@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics'; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { KinesisFirehoseToAnalyticsAndS3 } from '@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics'; new KinesisFirehoseToAnalyticsAndS3(this, 'FirehoseToS3AndAnalyticsPattern', { - kinesisAnalyticsProps: { - inputs: [{ - inputSchema: { - recordColumns: [{ - name: 'ticker_symbol', - sqlType: 'VARCHAR(4)', - mapping: '$.ticker_symbol' - }, { - name: 'sector', - sqlType: 'VARCHAR(16)', - mapping: '$.sector' - }, { - name: 'change', - sqlType: 'REAL', - mapping: '$.change' - }, { - name: 'price', - sqlType: 'REAL', - mapping: '$.price' - }], - recordFormat: { - recordFormatType: 'JSON' - }, - recordEncoding: 'UTF-8' - }, - namePrefix: 'SOURCE_SQL_STREAM' - }] - } + kinesisAnalyticsProps: { + inputs: [{ + inputSchema: { + recordColumns: [{ + name: 'ticker_symbol', + sqlType: 'VARCHAR(4)', + mapping: '$.ticker_symbol' + }, { + name: 'sector', + sqlType: 'VARCHAR(16)', + mapping: '$.sector' + }, { + name: 'change', + sqlType: 'REAL', + mapping: '$.change' + }, { + name: 'price', + sqlType: 'REAL', + mapping: '$.price' + }], + recordFormat: { + recordFormatType: 'JSON' + }, + recordEncoding: 'UTF-8' + }, + namePrefix: 'SOURCE_SQL_STREAM' + }] + } }); - ``` -## Initializer - -``` text -new KinesisFirehoseToAnalyticsAndS3(scope: Construct, id: string, props: KinesisFirehoseToAnalyticsAndS3Props); +Python +```python +from aws_solutions_constructs.aws_kinesis_firehose_s3_kinesis_analytics import KinesisFirehoseToAnalyticsAndS3 +from aws_cdk import ( + aws_kinesisanalytics as kinesisanalytics, + Stack +) +from constructs import Construct + +KinesisFirehoseToAnalyticsAndS3(self, 'FirehoseToS3AndAnalyticsPattern', + kinesis_analytics_props=kinesisanalytics.CfnApplicationProps( + inputs=[kinesisanalytics.CfnApplication.InputProperty( + input_schema=kinesisanalytics.CfnApplication.InputSchemaProperty( + record_columns=[kinesisanalytics.CfnApplication.RecordColumnProperty( + name='ticker_symbol', + sql_type='VARCHAR(4)', + mapping='$.ticker_symbol' + ), kinesisanalytics.CfnApplication.RecordColumnProperty( + name='sector', + sql_type='VARCHAR(16)', + mapping='$.sector' + ), kinesisanalytics.CfnApplication.RecordColumnProperty( + name='change', + sql_type='REAL', + mapping='$.change' + ), kinesisanalytics.CfnApplication.RecordColumnProperty( + name='price', + sql_type='REAL', + mapping='$.price' + )], + record_format=kinesisanalytics.CfnApplication.RecordFormatProperty( + record_format_type='JSON' + ), + record_encoding='UTF-8' + ), + name_prefix='SOURCE_SQL_STREAM' + )] + ) + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`KinesisFirehoseToAnalyticsAndS3Props`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; +import java.util.List; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.kinesisanalytics.*; +import software.amazon.awscdk.services.kinesisanalytics.CfnApplication.*; +import software.amazon.awsconstructs.services.kinesisfirehoses3kinesisanalytics.*; + +new KinesisFirehoseToAnalyticsAndS3(this, "FirehoseToS3AndAnalyticsPattern", + new KinesisFirehoseToAnalyticsAndS3Props.Builder() + .kinesisAnalyticsProps(new CfnApplicationProps.Builder() + .inputs(List.of(new InputProperty.Builder() + .inputSchema(new InputSchemaProperty.Builder() + .recordColumns(List.of( + new RecordColumnProperty.Builder() + .name("ticker_symbol") + .sqlType("VARCHAR(4)") + .mapping("$.ticker_symbol") + .build(), + new RecordColumnProperty.Builder() + .name("sector") + .sqlType("VARCHAR(16)") + .mapping("$.sector") + .build(), + new RecordColumnProperty.Builder() + .name("change") + .sqlType("REAL") + .mapping("$.change") + .build(), + new RecordColumnProperty.Builder() + .name("price") + .sqlType("REAL") + .mapping("$.price") + .build())) + .recordFormat(new RecordFormatProperty.Builder() + .recordFormatType("JSON") + .build()) + .recordEncoding("UTF-8") + .build()) + .namePrefix("SOURCE_SQL_STREAM") + .build())) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md index a823ee740..9916fc726 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/README.md @@ -20,26 +20,37 @@ This AWS Solutions Construct implements an Amazon Kinesis Data Firehose delivery stream connected to an Amazon S3 bucket. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { KinesisFirehoseToS3 } from '@aws-solutions-constructs/aws-kinesisfirehose-s3'; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { KinesisFirehoseToS3 } from '@aws-solutions-constructs/aws-kinesisfirehose-s3'; new KinesisFirehoseToS3(this, 'test-firehose-s3', {}); - ``` -## Initializer +Python +```python +from aws_solutions_constructs.aws_kinesis_firehose_s3 import KinesisFirehoseToS3 +from aws_cdk import Stack +from constructs import Construct -``` text -new KinesisFirehoseToS3(scope: Construct, id: string, props: KinesisFirehoseToS3Props); +KinesisFirehoseToS3(self, 'test_firehose_s3') ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.kinesisfirehoses3.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`KinesisFirehoseToS3Props`](#pattern-construct-props) +new KinesisFirehoseToS3(this, "test_firehose_s3", new KinesisFirehoseToS3Props.Builder() + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/README.md index cbff5c585..4459f0a84 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/README.md @@ -27,8 +27,9 @@ This AWS Solutions Construct deploys a Kinesis Stream and configures a AWS Glue Job to perform custom ETL transformation with the appropriate resources/properties for interaction and security. It also creates an S3 bucket where the python script for the AWS Glue Job can be uploaded. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ```javascript import * as glue from "@aws-cdk/aws-glue"; import * as s3assets from "@aws-cdk/aws-s3-assets"; @@ -71,18 +72,6 @@ const customEtlJob = new KinesisstreamsToGluejob(this, "CustomETL", { }); ``` -## Initializer - -```text -new KinesisstreamsToGluejob(scope: Construct, id: string, props: KinesisstreamsToGluejobProps); -``` - -_Parameters_ - -- scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -- id `string` -- props [`KinesisstreamsToGluejobProps`](#pattern-construct-props) - ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/README.md index 14ad702ae..d7ca78e01 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/README.md @@ -20,26 +20,37 @@ This AWS Solutions Construct implements an Amazon Kinesis Data Stream (KDS) connected to Amazon Kinesis Data Firehose (KDF) delivery stream connected to an Amazon S3 bucket. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { KinesisStreamsToKinesisFirehoseToS3 } from '@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3'; new KinesisStreamsToKinesisFirehoseToS3(this, 'test-stream-firehose-s3', {}); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_kinesis_streams_kinesis_firehose_s3 import KinesisStreamsToKinesisFirehoseToS3 +from aws_cdk import Stack +from constructs import Construct -``` text -new KinesisStreamsToKinesisFirehoseToS3(scope: Construct, id: string, props: KinesisStreamsToKinesisFirehoseToS3Props); +KinesisStreamsToKinesisFirehoseToS3(self, 'test_stream_firehose_s3') ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.kinesisstreamskinesisfirehoses3.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`KinesisStreamsToKinesisFirehoseToS3Props`](#pattern-construct-props) +new KinesisStreamsToKinesisFirehoseToS3(this, "test_stream_firehose_s3", new KinesisStreamsToKinesisFirehoseToS3Props.Builder() + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/README.md index ae6407a81..431477afa 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/README.md @@ -20,36 +20,77 @@ This AWS Solutions Construct deploys a Kinesis Stream and Lambda function with the appropriate resources/properties for interaction and security. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { KinesisStreamsToLambda } from '@aws-solutions-constructs/aws-kinesisstreams-lambda'; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { KinesisStreamsToLambda } from '@aws-solutions-constructs/aws-kinesisstreams-lambda'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new KinesisStreamsToLambda(this, 'KinesisToLambdaPattern', { - kinesisEventSourceProps: { - startingPosition: lambda.StartingPosition.TRIM_HORIZON, - batchSize: 1 - }, - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) - } + kinesisEventSourceProps: { + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + batchSize: 1 + }, + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) + } }); - ``` -## Initializer +Python +``` python +from aws_solutions_constructs.aws_kinesis_streams_lambda import KinesisStreamsToLambda +from aws_cdk import ( + aws_lambda as _lambda, + aws_lambda_event_sources as sources, + aws_kinesis as kinesis, + Stack +) +from constructs import Construct + +KinesisStreamsToLambda(self, 'KinesisToLambdaPattern', + kinesis_event_source_props=sources.KinesisEventSourceProps( + starting_position=_lambda.StartingPosition.TRIM_HORIZON, + batch_size=1 + ), + lambda_function_props=_lambda.FunctionProps( + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler', + code=_lambda.Code.from_asset( + 'lambda') + ) + ) -``` text -new KinesisStreamsToLambda(scope: Construct, id: string, props: KinesisStreamsToLambdaProps); ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`KinesisStreamsToLambdaProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.eventsources.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.kinesisstreamslambda.*; + +new KinesisStreamsToLambda(this, "KinesisToLambdaPattern", new KinesisStreamsToLambdaProps.Builder() + .kinesisEventSourceProps(new KinesisEventSourceProps.Builder() + .startingPosition(StartingPosition.TRIM_HORIZON) + .batchSize(1) + .build()) + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/README.md index 382141c42..64292598a 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/README.md @@ -20,34 +20,62 @@ This AWS Solutions Construct implements the AWS Lambda function and Amazon DynamoDB table with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { LambdaToDynamoDBProps, LambdaToDynamoDB } from '@aws-solutions-constructs/aws-lambda-dynamodb'; - -const props: LambdaToDynamoDBProps = { - lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' - }, +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { LambdaToDynamoDBProps, LambdaToDynamoDB } from '@aws-solutions-constructs/aws-lambda-dynamodb'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +const constructProps: LambdaToDynamoDBProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, }; -new LambdaToDynamoDB(this, 'test-lambda-dynamodb-stack', props); - +new LambdaToDynamoDB(this, 'test-lambda-dynamodb-stack', constructProps); ``` -## Initializer - -``` text -new LambdaToDynamoDB(scope: Construct, id: string, props: LambdaToDynamoDBProps); +Python +```python +from aws_solutions_constructs.aws_lambda_dynamodb import LambdaToDynamoDBProps, LambdaToDynamoDB +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +LambdaToDynamoDB(self, 'test_lambda_dynamodb_stack', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset( + 'lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + )) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToDynamoDBProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdadynamodb.*; + +new LambdaToDynamoDB(this, "test_lambda_dynamodb_stack", new LambdaToDynamoDBProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/README.md index 330af28c2..464891b08 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/README.md @@ -24,39 +24,76 @@ This AWS Solutions Construct implements the AWS Lambda function and Amazon Elasticsearch Service with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps, Aws } from 'aws-cdk-lib'; import { LambdaToElasticSearchAndKibana } from '@aws-solutions-constructs/aws-lambda-elasticsearch-kibana'; -import { Aws } from "@aws-cdk/core"; +import * as lambda from "aws-cdk-lib/aws-lambda"; const lambdaProps: lambda.FunctionProps = { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' }; -new LambdaToElasticSearchAndKibana(this, 'test-lambda-elasticsearch-kibana', { - lambdaFunctionProps: lambdaProps, - domainName: 'test-domain', - // TODO: Ensure the Cognito domain name is globally unique - cognitoDomainName: 'globallyuniquedomain' + Aws.ACCOUNT_ID; +new LambdaToElasticSearchAndKibana(this, 'sample', { + lambdaFunctionProps: lambdaProps, + domainName: 'testdomain', + // TODO: Ensure the Cognito domain name is globally unique + cognitoDomainName: 'globallyuniquedomain' + Aws.ACCOUNT_ID }); - ``` -## Initializer - -``` text -new LambdaToElasticSearchAndKibana(scope: Construct, id: string, props: LambdaToElasticSearchAndKibanaProps); +Python +```python +from aws_solutions_constructs.aws_lambda_elasticsearch_kibana import LambdaToElasticSearchAndKibana +from aws_cdk import ( + aws_lambda as _lambda, + Aws, + Stack +) +from constructs import Construct + +lambda_props = _lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' +) + +LambdaToElasticSearchAndKibana(self, 'sample', + lambda_function_props=lambda_props, + domain_name='testdomain', + # TODO: Ensure the Cognito domain name is globally unique + cognito_domain_name='globallyuniquedomain' + Aws.ACCOUNT_ID + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToElasticSearchAndKibanaProps`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Aws; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdaelasticsearchkibana.*; + +new LambdaToElasticSearchAndKibana(this, "sample", + new LambdaToElasticSearchAndKibanaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .domainName("testdomain") + // TODO: Ensure the Cognito domain name is globally unique + .cognitoDomainName("globallyuniquedomain" + Aws.ACCOUNT_ID) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-eventbridge/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-eventbridge/README.md index acb7d7433..2c4cf9200 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-eventbridge/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-eventbridge/README.md @@ -24,33 +24,60 @@ This AWS Solutions Construct implements an AWS Lambda function connected to an Amazon EventBridge. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps, Aws } from 'aws-cdk-lib'; import { LambdaToEventbridge, LambdaToEventbridgeProps } from "@aws-solutions-constructs/aws-lambda-eventbridge"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new LambdaToEventbridge(this, 'LambdaToEventbridgePattern', { - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) - } + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) + } }); - ``` -## Initializer - -``` text -new LambdaToEventbridge(scope: Construct, id: string, props: LambdaToEventbridgeProps); +Python +```python +from aws_solutions_constructs.aws_lambda_eventbridge import LambdaToEventbridge +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +LambdaToEventbridge(self, 'LambdaToEventbridgePattern', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToEventbridgeProps`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdaeventbridge.*; + +new LambdaToEventbridge(this, "LambdaToEventbridgePattern", new LambdaToEventbridgeProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | 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 d3a83bea7..aceff41ff 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-s3/README.md @@ -20,32 +20,60 @@ This AWS Solutions Construct implements an AWS Lambda function connected to an Amazon S3 bucket. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition : +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { LambdaToS3 } from '@aws-solutions-constructs/aws-lambda-s3'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new LambdaToS3(this, 'LambdaToS3Pattern', { lambdaFunctionProps: { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) + code: lambda.Code.fromAsset(`lambda`) } }); - ``` -## Initializer - -``` text -new LambdaToS3(scope: Construct, id: string, props: LambdaToS3Props); +Python +```python +from aws_solutions_constructs.aws_lambda_s3 import LambdaToS3 +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +LambdaToS3(self, 'LambdaToS3Pattern', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToS3Props`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdas3.*; + +new LambdaToS3(this, "LambdaToS3Pattern", new LambdaToS3Props.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sagemakerendpoint/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-sagemakerendpoint/README.md index 99e7b5311..14a65b251 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sagemakerendpoint/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sagemakerendpoint/README.md @@ -28,26 +28,25 @@ This AWS Solutions Construct implements an AWS Lambda function connected to an Amazon Sagemaker Endpoint. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ```typescript -import { Duration } from '@aws-cdk/core'; -import * as lambda from '@aws-cdk/aws-lambda'; -import { - LambdaToSagemakerEndpoint, - LambdaToSagemakerEndpointProps, -} from '@aws-solutions-constructs/aws-lambda-sagemakerendpoint'; +import { Construct } from 'constructs'; +import { Stack, StackProps, Duration } from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { LambdaToSagemakerEndpoint, LambdaToSagemakerEndpointProps } from '@aws-solutions-constructs/aws-lambda-sagemakerendpoint'; const constructProps: LambdaToSagemakerEndpointProps = { modelProps: { primaryContainer: { image: '.dkr.ecr..amazonaws.com/linear-learner:latest', - modelDataUrl: 's3:////model.tar.gz', + modelDataUrl: "s3:////model.tar.gz", }, }, lambdaFunctionProps: { runtime: lambda.Runtime.PYTHON_3_8, - code: lambda.Code.fromAsset(`${__dirname}/lambda`), + code: lambda.Code.fromAsset(`lambda`), handler: 'index.handler', timeout: Duration.minutes(5), memorySize: 128, @@ -57,17 +56,65 @@ const constructProps: LambdaToSagemakerEndpointProps = { new LambdaToSagemakerEndpoint(this, 'LambdaToSagemakerEndpointPattern', constructProps); ``` -## Initializer - -```text -new LambdaToSagemakerEndpoint(scope: Construct, id: string, props: LambdaToSagemakerEndpointProps); +Python +```python +from constructs import Construct +from aws_solutions_constructs.aws_lambda_sagemakerendpoint import LambdaToSagemakerEndpoint, LambdaToSagemakerEndpointProps +from aws_cdk import ( + aws_lambda as _lambda, + aws_sagemaker as sagemaker, + Duration, + Stack +) +from constructs import Construct + +LambdaToSagemakerEndpoint( + self, 'LambdaToSagemakerEndpointPattern', + model_props=sagemaker.CfnModelProps( + primary_container=sagemaker.CfnModel.ContainerDefinitionProperty( + image='.dkr.ecr..amazonaws.com/linear-learner:latest', + model_data_url='s3:////model.tar.gz', + ), + execution_role_arn="executionRoleArn" + ), + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler', + timeout=Duration.minutes(5), + memory_size=128 + )) ``` -_Parameters_ - -- scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -- id `string` -- props [`LambdaToSagemakerEndpointProps`](#pattern-construct-props) +Java +```java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.sagemaker.*; +import software.amazon.awsconstructs.services.lambdasagemakerendpoint.*; + +new LambdaToSagemakerEndpoint(this, "LambdaToSagemakerEndpointPattern", + new LambdaToSagemakerEndpointProps.Builder() + .modelProps(new CfnModelProps.Builder() + .primaryContainer(new CfnModel.ContainerDefinitionProperty.Builder() + .image(".dkr.ecr..amazonaws.com/linear_learner:latest") + .modelDataUrl("s3:////model.tar.gz") + .build()) + .executionRoleArn("executionRoleArn") + .build()) + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .timeout(Duration.minutes(5)) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md index fb50924f8..07244b563 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md @@ -24,36 +24,64 @@ This AWS Solutions Construct implements the AWS Lambda function and AWS Secrets Manager secret with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { LambdaToSecretsmanagerProps, LambdaToSecretsmanager } from '@aws-solutions-constructs/aws-lambda-secretsmanager'; - -const props: LambdaToSecretsmanagerProps = { - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - // This assumes a handler function in lib/lambda/index.js - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - handler: 'index.handler' - }, +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { LambdaToSecretsmanagerProps, LambdaToSecretsmanager } from '@aws-solutions-constructs/aws-lambda-secretsmanager'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +const constructProps: LambdaToSecretsmanagerProps = { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + code: lambda.Code.fromAsset(`lambda`), + handler: 'index.handler' + }, }; -new LambdaToSecretsmanager(this, 'test-lambda-secretsmanager-stack', props); - +new LambdaToSecretsmanager(this, 'test-lambda-secretsmanager-stack', constructProps); ``` -## Initializer - -``` text -new LambdaToSecretsmanager(scope: Construct, id: string, props: LambdaToSecretsmanagerProps); +Python +``` python +from aws_solutions_constructs.aws_lambda_secretsmanager import LambdaToSecretsmanagerProps, LambdaToSecretsmanager +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + + +LambdaToSecretsmanager( + self, 'test-lambda-secretsmanager-stack', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) +) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToSecretsmanagerProps`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdasecretsmanager.*; + +new LambdaToSecretsmanager(this, "test-lambda-secretsmanager-stack", new LambdaToSecretsmanagerProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | 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 f82fe30a9..a6d11356e 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sns/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sns/README.md @@ -20,32 +20,61 @@ This AWS Solutions Construct implements an AWS Lambda function connected to an Amazon SNS topic. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { LambdaToSns, LambdaToSnsProps } from "@aws-solutions-constructs/aws-lambda-sns"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new LambdaToSns(this, 'test-lambda-sns', { - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) - } + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) + } }); - ``` -## Initializer - -``` text -new LambdaToSns(scope: Construct, id: string, props: LambdaToSnsProps); +Python +``` python +from aws_solutions_constructs.aws_lambda_sns import LambdaToSns +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +LambdaToSns( + self, 'test-lambda-sns-stack', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) +) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToSnsProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdasns.*; + +new LambdaToSns(this, "test-lambda-sns-stack", new LambdaToSnsProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sqs-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-sqs-lambda/README.md index cf76b3389..ea019928d 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sqs-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sqs-lambda/README.md @@ -24,38 +24,76 @@ This AWS Solutions Construct implements (1) an AWS Lambda function that is configured to send messages to a queue; (2) an Amazon SQS queue; and (3) an AWS Lambda function configured to consume messages from the queue. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { LambdaToSqsToLambda, LambdaToSqsToLambdaProps } from "@aws-solutions-constructs/aws-lambda-sqs-lambda"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new LambdaToSqsToLambda(this, 'LambdaToSqsToLambdaPattern', { producerLambdaFunctionProps: { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda/producer-function`) + code: lambda.Code.fromAsset(`producer-lambda`) }, consumerLambdaFunctionProps: { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda/consumer-function`) + code: lambda.Code.fromAsset(`consumer-lambda`) } }); - ``` -## Initializer - -``` text -new LambdaToSqsToLambda(scope: Construct, id: string, props: LambdaToSqsToLambdaProps); +Python +```python +from aws_solutions_constructs.aws_lambda_sqs_lambda import LambdaToSqsToLambda +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +LambdaToSqsToLambda( + self, 'LambdaToSqsToLambdaPattern', + producer_lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('producer_lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + consumer_lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('consumer_lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) +) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToSqsToLambdaProps`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdasqslambda.*; + +new LambdaToSqsToLambda(this, "LambdaToSqsToLambdaPattern", new LambdaToSqsToLambdaProps.Builder() + .producerLambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("producer-lambda")) + .handler("index.handler") + .build()) + .consumerLambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("consumer-lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-sqs/README.md index 34ad9d80d..25836880a 100755 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-sqs/README.md @@ -20,32 +20,61 @@ This AWS Solutions Construct implements an AWS Lambda function connected to an Amazon SQS queue. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { LambdaToSqs, LambdaToSqsProps } from "@aws-solutions-constructs/aws-lambda-sqs"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new LambdaToSqs(this, 'LambdaToSqsPattern', { - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) - } + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) + } }); - ``` -## Initializer - -``` text -new LambdaToSqs(scope: Construct, id: string, props: LambdaToSqsProps); +Python +``` python +from aws_solutions_constructs.aws_lambda_sqs import LambdaToSqs +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +LambdaToSqs( + self, 'test-lambda-sqs-stack', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) +) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToSqsProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdasqs.*; + +new LambdaToSqs(this, "test-lambda-sqs-stack", new LambdaToSqsProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/README.md index 4cb84dcb6..5d97fa9b7 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/README.md @@ -24,36 +24,72 @@ This AWS Solutions Construct implements the AWS Lambda function and AWS Systems Manager Parameter Store String parameter with the least privileged permissions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` javascript -const { LambdaToSsmstringparameterProps, LambdaToSsmstringparameter } from '@aws-solutions-constructs/aws-lambda-ssmstringparameter'; - -const props: LambdaToSsmstringparameterProps = { - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - // This assumes a handler function in lib/lambda/index.js - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - handler: 'index.handler' - }, - stringParameterProps: { stringValue: "test-string-value" } +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { LambdaToSsmstringparameterProps, LambdaToSsmstringparameter } from '@aws-solutions-constructs/aws-lambda-ssmstringparameter'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +const constructProps: LambdaToSsmstringparameterProps = { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + code: lambda.Code.fromAsset(`lambda`), + handler: 'index.handler' + }, + stringParameterProps: { stringValue: "test-string-value" } }; -new LambdaToSsmstringparameter(this, 'test-lambda-ssmstringparameter-stack', props); - +new LambdaToSsmstringparameter(this, 'test-lambda-ssmstringparameter-stack', constructProps); ``` -## Initializer - -``` text -new LambdaToSsmstringparameter(scope: Construct, id: string, props: LambdaToSsmstringparameterProps); +Python +```python +from aws_solutions_constructs.aws_lambda_ssmstringparameter import LambdaToSsmstringparameter +from aws_cdk import ( + aws_lambda as _lambda, + aws_ssm as ssm, + Stack +) +from constructs import Construct + +LambdaToSsmstringparameter( + self, 'test-lambda-ssmstringparameter-stack', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + string_parameter_props=ssm.StringParameterProps( + string_value="test-string-value") +) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToSsmstringparameterProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.ssm.*; +import software.amazon.awsconstructs.services.lambdassmstringparameter.*; + +new LambdaToSsmstringparameter(this, "test-lambda-ssmstringparameter-stack", + new LambdaToSsmstringparameterProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .stringParameterProps(new StringParameterProps.Builder() + .stringValue("test-string-value") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-step-function/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-step-function/README.md index b200d9839..61c43c397 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-step-function/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-step-function/README.md @@ -22,38 +22,82 @@ This AWS Solutions Construct implements an AWS Lambda function connected to an AWS Step Function. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import { LambdaToStepFunction } from '@aws-solutions-constructs/aws-lambda-step-function'; +// aws-lambda-step-function has been deprecated for CDK V2 in favor of aws-lambda-stepfunctions. +// This sample uses the CDK V1 syntax +import * as cdk from '@aws-cdk/core'; import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { LambdaToStepFunction } from '@aws-solutions-constructs/aws-lambda-step-function'; -const startState = new stepfunctions.Pass(stack, 'StartState'); +const startState = new stepfunctions.Pass(this, 'StartState'); new LambdaToStepFunction(this, 'LambdaToStepFunctionPattern', { lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) }, stateMachineProps: { definition: startState } }); - ``` -## Initializer - -``` text -new LambdaToStepFunction(scope: Construct, id: string, props: LambdaToStepFunctionProps); +Python +``` python +# aws-lambda-step-function has been deprecated for CDK V2 in favor of aws-lambda-stepfunctions. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_lambda_step_function import LambdaToStepFunction +from aws_cdk import ( + aws_lambda as _lambda, + aws_stepfunctions as stepfunctions, + core +) + +start_state = stepfunctions.Pass(self, 'start_state') + +LambdaToStepFunction( + self, 'test-lambda-stepfunctions-stack', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + state_machine_props=stepfunctions.StateMachineProps( + definition=start_state) +) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToStepFunctionProps`](#pattern-construct-props) +Java +``` java +// aws-lambda-step-function has been deprecated for CDK V2 in favor of aws-lambda-stepfunctions. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.stepfunctions.*; +import software.amazon.awsconstructs.services.lambdastepfunction.*; + +final Pass startState = new Pass(this, "StartState"); + +new LambdaToStepFunction(this, "test-lambda-stepfunctions-stack", + new LambdaToStepFunctionProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .stateMachineProps(new StateMachineProps.Builder() + .definition(startState) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/README.md index cd3b88320..594c34904 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/README.md @@ -20,38 +20,79 @@ This AWS Solutions Construct implements an AWS Lambda function connected to an AWS Step Functions. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { LambdaToStepfunctions } from '@aws-solutions-constructs/aws-lambda-stepfunctions'; -import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; +import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; -const startState = new stepfunctions.Pass(stack, 'StartState'); +const startState = new stepfunctions.Pass(this, 'StartState'); new LambdaToStepfunctions(this, 'LambdaToStepfunctionsPattern', { lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) }, stateMachineProps: { definition: startState } }); - ``` -## Initializer - -``` text -new LambdaToStepfunctions(scope: Construct, id: string, props: LambdaToStepfunctionsProps); +Python +```python +from aws_solutions_constructs.aws_lambda_stepfunctions import LambdaToStepfunctions +from aws_cdk import ( + aws_lambda as _lambda, + aws_stepfunctions as stepfunctions, + Stack +) +from constructs import Construct + +start_state = stepfunctions.Pass(self, 'start_state') + +LambdaToStepfunctions( + self, 'test-lambda-stepfunctions-stack', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ), + state_machine_props=stepfunctions.StateMachineProps( + definition=start_state) +) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`LambdaToStepfunctionsProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.stepfunctions.*; +import software.amazon.awsconstructs.services.lambdastepfunctions.*; + +final Pass startState = new Pass(this, "StartState"); + +new LambdaToStepfunctions(this, "test-lambda-stepfunctions-stack", + new LambdaToStepfunctionsProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .stateMachineProps(new StateMachineProps.Builder() + .definition(startState) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-route53-alb/README.md b/source/patterns/@aws-solutions-constructs/aws-route53-alb/README.md index ecb9a4b7b..d2f3e1736 100644 --- a/source/patterns/@aws-solutions-constructs/aws-route53-alb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-route53-alb/README.md @@ -24,31 +24,72 @@ This AWS Solutions Construct implements an Amazon Route53 Hosted Zone routing to an Application Load Balancer -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { Route53ToAlb } from '@aws-solutions-constructs/aws-route53-alb'; +// Note - all alb constructs turn on ELB logging by default, so require that an environment including account +// and region be provided when creating the stack +// +// new MyStack(app, 'id', {env: {account: '123456789012', region: 'us-east-1' }}); new Route53ToAlb(this, 'Route53ToAlbPattern', { privateHostedZoneProps: { zoneName: 'www.example.com', - } + }, publicApi: false, }); - ``` -## Initializer - -``` text -new Route53ToAlb(scope: Construct, id: string, props: Route53ToAlbProps); +Python +```python +from aws_solutions_constructs.aws_route53_alb import Route53ToAlb +from aws_cdk import ( + aws_route53 as route53, + Stack +) +from constructs import Construct + +# Note - all alb constructs turn on ELB logging by default, so require that an environment including account +# and region be provided when creating the stack +# +# MyStack(app, 'id', env=cdk.Environment(account='679431688440', region='us-east-1')) +Route53ToAlb(self, 'Route53ToAlbPattern', + public_api=False, + private_hosted_zone_props=route53.HostedZoneProps( + zone_name='www.example.com', + ) + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`Route53ToAlbProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.route53.*; +import software.amazon.awsconstructs.services.route53alb.*; + +// Note - all alb constructs turn on ELB logging by default, so require that an environment including account +// and region be provided when creating the stack +// +// new MyStack(app, "id", StackProps.builder() +// .env(Environment.builder() +// .account("123456789012") +// .region("us-east-1") +// .build()); +new Route53ToAlb(this, "Route53ToAlbPattern", + new Route53ToAlbProps.Builder() + .privateHostedZoneProps(new HostedZoneProps.Builder() + .zoneName("www.example.com") + .build()) + .publicApi(false) + .build()); +``` ## Pattern Construct Props @@ -65,7 +106,7 @@ This construct can create Private Hosted Zones. If you want a Private Hosted Zon | vpcProps? | [ec2.VpcProps](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcProps.html) | Optional custom properties for a VPC the construct will create. This VPC will be used by the new ALB and any Private Hosted Zone the construct creates (that's why loadBalancerProps and privateHostedZoneProps can't include a VPC). Providing both this and existingVpc is an error. | | existingVpc? | [ec2.IVpc](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | An existing VPC in which to deploy the construct. Providing both this and vpcProps is an error. If the client provides an existing load balancer and/or existing Private Hosted Zone, those constructs must exist in this VPC. | | logAlbAccessLogs? | boolean| Whether to turn on Access Logs for the Application Load Balancer. Uses an S3 bucket with associated storage costs.Enabling Access Logging is a best practice. default - true | -| albLoggingBucketProps? | [s3.BucketProps](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html) | Optional properties to customize the bucket used to store the ALB Access Logs. Supplying this and setting logAccessLogs to false is an error. @default - none | +| albLoggingBucketProps? | [s3.BucketProps](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html) | Optional properties to customize the bucket used to store the ALB Access Logs. Supplying this and setting logAlbAccessLogs to false is an error. @default - none | | publicApi | boolean | Whether the construct is deploying a private or public API. This has implications for the Hosted Zone, VPC and ALB. | diff --git a/source/patterns/@aws-solutions-constructs/aws-route53-apigateway/README.md b/source/patterns/@aws-solutions-constructs/aws-route53-apigateway/README.md index 9e033fc07..19fa79403 100755 --- a/source/patterns/@aws-solutions-constructs/aws-route53-apigateway/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-route53-apigateway/README.md @@ -26,49 +26,113 @@ This AWS Solutions Construct implements an Amazon Route 53 connected to a configured Amazon API Gateway REST API. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import * as api from '@aws-cdk/aws-apigateway'; -import * as lambda from "@aws-cdk/aws-lambda"; -import * as route53 from "@aws-cdk/aws-route53"; -import { Route53ToApigateway } from '@aws-solutions-constructs/aws-route53-apigateway'; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import * as route53 from "aws-cdk-lib/aws-route53"; +import * as acm from "aws-cdk-lib/aws-certificatemanager"; +import { Route53ToApiGateway } from '@aws-solutions-constructs/aws-route53-apigateway'; // The construct requires an existing REST API, this can be created in raw CDK or extracted // from a previously instantiated construct that created an API Gateway REST API const existingRestApi = previouslyCreatedApigatewayToLambdaConstruct.apiGateway; +// domainName must match existing hosted zone in your account and the existing certificate const ourHostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { domainName: "example.com", - }); +}); const certificate = acm.Certificate.fromCertificateArn( - stack, + this, "fake-cert", "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" - ); +); // This construct can only be attached to a configured API Gateway. -new Route53ToApigateway(this, 'Route53ToApigatewayPattern', { - existingApiGatewayObj: existingRestApi, +new Route53ToApiGateway(this, 'Route53ToApiGatewayPattern', { + existingApiGatewayInterface: existingRestApi, existingHostedZoneInterface: ourHostedZone, publicApi: true, existingCertificateInterface: certificate }); - ``` -## Initializer +Python +```python +from aws_solutions_constructs.aws_route53_apigateway import Route53ToApiGateway +from aws_cdk import ( + aws_route53 as route53, + aws_certificatemanager as acm, + Stack +) +from constructs import Construct + +# The construct requires an existing REST API, this can be created in raw CDK or extracted +# from a previously instantiated construct that created an API Gateway REST API +existingRestApi = previouslyCreatedApigatewayToLambdaConstruct.apiGateway + +# domain_name must match existing hosted zone in your account and the existing certificate +ourHostedZone = route53.HostedZone.from_lookup(self, 'HostedZone', + domain_name="example.com", + ) + +# Obtain a pre-existing certificate from your account +certificate = acm.Certificate.from_certificate_arn( + self, + 'existing-cert', + "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" +) + +# This construct can only be attached to a configured API Gateway. +Route53ToApiGateway(self, 'Route53ToApigatewayPattern', + existing_api_gateway_interface=existingRestApi, + existing_hosted_zone_interface=ourHostedZone, + public_api=True, + existing_certificate_interface=certificate + ) -``` text -new Route53ToApigateway(scope: Construct, id: string, props: Route53ToApigatewayProps); ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.route53.*; +import software.amazon.awscdk.services.apigateway.*; +import software.amazon.awscdk.services.certificatemanager.*; +import software.amazon.awsconstructs.services.route53apigateway.*; + +// The construct requires an existing REST API, this can be created in raw CDK +// or extracted from a previously instantiated construct that created an API +// Gateway REST API +final IRestApi existingRestApi = previouslyCreatedApigatewayToLambdaConstruct.getApiGateway(); + +// domainName must match existing hosted zone in your account and the existing certificate +final IHostedZone ourHostedZone = HostedZone.fromLookup(this, "HostedZone", + new HostedZoneProviderProps.Builder() + .domainName("example.com") + .build()); + +// Obtain a pre-existing certificate from your account +final ICertificate certificate = Certificate.fromCertificateArn( + this, + "existing-cert", + "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012"); -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`Route53ToApigatewayProps`](#pattern-construct-props) +// This construct can only be attached to a configured API Gateway. +new Route53ToApiGateway(this, "Route53ToApiGatewayPattern", + new Route53ToApiGatewayProps.Builder() + .existingApiGatewayInterface(existingRestApi) + .existingHostedZoneInterface(ourHostedZone) + .publicApi(true) + .existingCertificateInterface(certificate) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-s3-lambda/README.md index ec7a2ff1f..9a7928c7f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-s3-lambda/README.md @@ -20,32 +20,61 @@ This AWS Solutions Construct implements an Amazon S3 bucket connected to an AWS Lambda function. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import * as lambda from "aws-cdk-lib/aws-lambda"; import { S3ToLambdaProps, S3ToLambda } from '@aws-solutions-constructs/aws-s3-lambda'; new S3ToLambda(this, 'test-s3-lambda', { - lambdaFunctionProps: { - code: lambda.Code.fromAsset(`${__dirname}/lambda`), - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler' - }, + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, }); +``` +Python +```python +from aws_solutions_constructs.aws_s3_lambda import S3ToLambda +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +S3ToLambda(self, 'test_s3_lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) ``` -## Initializer -``` text -new S3ToLambda(scope: Construct, id: string, props: S3ToLambdaProps); -``` +Java +``` java +import software.constructs.Construct; -_Parameters_ +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.s3lambda.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`S3ToLambdaProps`](#pattern-construct-props) +new S3ToLambda(this, "test-s3-lambda'", new S3ToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/README.md index 2788cc214..cccfd13f5 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/README.md @@ -25,25 +25,37 @@ This AWS Solutions Construct implements an Amazon S3 Bucket that is configured to send notifications to an Amazon SQS queue. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import {S3ToSqs} from "@aws-solutions-constructs/aws-s3-sqs"; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { S3ToSqs } from "@aws-solutions-constructs/aws-s3-sqs"; -new S3ToSqs(stack, 'S3ToSQSPattern', {}); +new S3ToSqs(this, 'S3ToSQSPattern', {}); ``` -## Initializer +Python +```python +from aws_solutions_constructs.aws_s3_sqs import S3ToSqs +from aws_cdk import Stack +from constructs import Construct -``` text -new S3ToSqs(scope: Construct, id: string, props: S3ToSqsProps); +S3ToSqs(self, 'S3ToSQSPattern') ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`S3ToSqsProps`](#pattern-construct-props) +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.s3sqs.*; + +new S3ToSqs(this, "S3ToSQSPattern", new S3ToSqsProps.Builder() + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/README.md b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/README.md index 76eb2fa4e..97f7416d3 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/README.md @@ -26,32 +26,63 @@ This AWS Solutions Construct implements an Amazon S3 bucket connected to an AWS *An alternative architecture can be built that triggers a Lambda function from S3 Event notifications using aws-s3-lambda and aws-lambda-stepfunctions. Channelling the S3 events through Lambda is less flexible than EventBridge, but is more cost effective and has lower latency.* -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +// aws-s3-step-function has been deprecated for CDK V2 in favor of aws-s3-stepfunctions. +// This sample uses the CDK V1 syntax +import * as cdk from '@aws-cdk/core'; import { S3ToStepFunction, S3ToStepFunctionProps } from '@aws-solutions-constructs/aws-s3-step-function'; import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; -const startState = new stepfunctions.Pass(stack, 'StartState'); +const startState = new stepfunctions.Pass(this, 'StartState'); new S3ToStepFunction(this, 'test-s3-step-function-stack', { - stateMachineProps: { - definition: startState - } + stateMachineProps: { + definition: startState + } }); ``` -## Initializer - -``` text -new S3ToStepFunction(scope: Construct, id: string, props: S3ToStepFunctionProps); +Python +``` python +# aws-s3-step-function has been deprecated for CDK V2 in favor of aws-s3-stepfunctions. +# This sample uses the CDK V1 syntax +from aws_solutions_constructs.aws_s3_step_function import S3ToStepFunction +from aws_cdk import ( + aws_stepfunctions as stepfunctions, + core +) + +start_state = stepfunctions.Pass(self, 'start_state') + +S3ToStepFunction( + self, 'test_s3_step_function_stack', + state_machine_props=stepfunctions.StateMachineProps( + definition=start_state) +) ``` -_Parameters_ +Java +``` java +// aws-s3-step-function has been deprecated for CDK V2 in favor of aws-s3-stepfunctions. +// This sample uses the CDK V1 syntax +import software.constructs.Construct; + +import software.amazon.awscdk.core.*; +import software.amazon.awscdk.services.stepfunctions.*; +import software.amazon.awsconstructs.services.s3stepfunction.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`S3ToStepFunctionProps`](#pattern-construct-props) +final Pass startState = new Pass(this, "StartState"); + +new S3ToStepFunction(this, "test_s3_stepfunctions_stack", + new S3ToStepFunctionProps.Builder() + .stateMachineProps(new StateMachineProps.Builder() + .definition(startState) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/README.md b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/README.md index 54fcbf4b4..4650475c9 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/README.md @@ -28,13 +28,16 @@ This AWS Solutions Construct implements an Amazon S3 bucket connected to an AWS *An alternative architecture can be built that triggers a Lambda function from S3 Event notifications using aws-s3-lambda and aws-lambda-stepfunctions. Channelling the S3 events through Lambda is less flexible than EventBridge, but is more cost effective and has lower latency.* -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { S3ToStepfunctions, S3ToStepfunctionsProps } from '@aws-solutions-constructs/aws-s3-stepfunctions'; -import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; +import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions'; -const startState = new stepfunctions.Pass(stack, 'StartState'); +const startState = new stepfunctions.Pass(this, 'StartState'); new S3ToStepfunctions(this, 'test-s3-stepfunctions-stack', { stateMachineProps: { @@ -43,17 +46,42 @@ new S3ToStepfunctions(this, 'test-s3-stepfunctions-stack', { }); ``` -## Initializer - -``` text -new S3ToStepfunctions(scope: Construct, id: string, props: S3ToStepfunctionsProps); +Python +```python +from aws_solutions_constructs.aws_s3_stepfunctions import S3ToStepfunctions +from aws_cdk import ( + aws_stepfunctions as stepfunctions, + Stack +) +from constructs import Construct + +start_state = stepfunctions.Pass(self, 'start_state') + +S3ToStepfunctions( + self, 'test_s3_stepfunctions_stack', + state_machine_props=stepfunctions.StateMachineProps( + definition=start_state) +) ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.stepfunctions.*; +import software.amazon.awsconstructs.services.s3stepfunctions.*; -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`S3ToStepfunctionsProps`](#pattern-construct-props) +final Pass startState = new Pass(this, "StartState"); + +new S3ToStepfunctions(this, "test_s3_stepfunctions_stack", + new S3ToStepfunctionsProps.Builder() + .stateMachineProps(new StateMachineProps.Builder() + .definition(startState) + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-sns-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-sns-lambda/README.md index af3df2c2a..014e6e901 100644 --- a/source/patterns/@aws-solutions-constructs/aws-sns-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-sns-lambda/README.md @@ -20,32 +20,59 @@ This AWS Solutions Construct implements an Amazon SNS connected to an AWS Lambda function. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { SnsToLambda, SnsToLambdaProps } from "@aws-solutions-constructs/aws-sns-lambda"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new SnsToLambda(this, 'test-sns-lambda', { - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) - } + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) + } }); - ``` - -## Initializer - -``` text -new SnsToLambda(scope: Construct, id: string, props: SnsToLambdaProps); +Python +```python +from aws_solutions_constructs.aws_sns_lambda import SnsToLambda +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +SnsToLambda(self, 'test_sns_lambda', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`SnsToLambdaProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.snslambda.*; + +new SnsToLambda(this, "test-lambda-sqs-stack", new SnsToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-sns-sqs/README.md b/source/patterns/@aws-solutions-constructs/aws-sns-sqs/README.md index 6a6666bd4..8b851ad28 100644 --- a/source/patterns/@aws-solutions-constructs/aws-sns-sqs/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-sns-sqs/README.md @@ -24,11 +24,14 @@ This AWS Solutions Construct implements an Amazon SNS topic connected to an Amazon SQS queue. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { SnsToSqs, SnsToSqsProps } from "@aws-solutions-constructs/aws-sns-sqs"; -import * as iam from '@aws-cdk/aws-iam'; +import * as iam from 'aws-cdk-lib/aws-iam'; const snsToSqsStack = new SnsToSqs(this, 'SnsToSqsPattern', {}); @@ -41,20 +44,53 @@ const policyStatement = new iam.PolicyStatement({ }); snsToSqsStack.encryptionKey?.addToResourcePolicy(policyStatement); +``` +Python +``` python +from aws_solutions_constructs.aws_sns_sqs import SnsToSqs +from aws_cdk import ( + aws_iam as iam, + Stack +) +from constructs import Construct + +construct_stack = SnsToSqs(self, 'SnsToSqsPattern') + +policy_statement = iam.PolicyStatement( + actions=["kms:Encrypt", "kms:Decrypt"], + effect=iam.Effect.ALLOW, + principals=[iam.AccountRootPrincipal()], + resources=["*"] +) + +construct_stack.encryption_key.add_to_resource_policy(policy_statement) ``` -## Initializer +Java +``` java +import software.constructs.Construct; +import java.util.List; -``` text -new SnsToSqs(scope: Construct, id: string, props: SnsToSqsProps); -``` +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.iam.*; +import software.amazon.awsconstructs.services.snssqs.*; -_Parameters_ +final SnsToSqs constructStack = new SnsToSqs(this, "SnsToSqsPattern", + new SnsToSqsProps.Builder() + .build()); -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`SnsToSqsProps`](#pattern-construct-props) +// Grant yourself permissions to use the Customer Managed KMS Key +final PolicyStatement policyStatement = PolicyStatement.Builder.create() + .actions(List.of("kms:Encrypt", "kms:Decrypt")) + .effect(Effect.ALLOW) + .principals(List.of(new AccountRootPrincipal())) + .resources(List.of("*")) + .build(); + +constructStack.getEncryptionKey().addToResourcePolicy(policyStatement); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-sqs-lambda/README.md b/source/patterns/@aws-solutions-constructs/aws-sqs-lambda/README.md index ea9d952ff..26b0c177f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-sqs-lambda/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-sqs-lambda/README.md @@ -20,32 +20,61 @@ This AWS Solutions Construct implements an Amazon SQS queue connected to an AWS Lambda function. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { SqsToLambda, SqsToLambdaProps } from "@aws-solutions-constructs/aws-sqs-lambda"; +import * as lambda from 'aws-cdk-lib/aws-lambda'; new SqsToLambda(this, 'SqsToLambdaPattern', { lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`${__dirname}/lambda`) + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) } }); - ``` -## Initializer - -``` text -new SqsToLambda(scope: Construct, id: string, props: SqsToLambdaProps); +Python +``` python +from aws_solutions_constructs.aws_sqs_lambda import SqsToLambda +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + + +SqsToLambda(self, 'SqsToLambdaPattern', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`SqsToLambdaProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.sqslambda.*; + +new SqsToLambda(this, "SnsToSqsPattern", new SqsToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-wafwebacl-alb/README.md b/source/patterns/@aws-solutions-constructs/aws-wafwebacl-alb/README.md index bbcd7e292..615d2924d 100644 --- a/source/patterns/@aws-solutions-constructs/aws-wafwebacl-alb/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-wafwebacl-alb/README.md @@ -26,40 +26,79 @@ ## Overview This AWS Solutions Construct implements an AWS WAF web ACL connected to an Application Load Balancer. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import { Route53ToAlb } from '@aws-solutions-constructs/aws-route53-alb'; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { WafwebaclToAlbProps, WafwebaclToAlb } from "@aws-solutions-constructs/aws-wafwebacl-alb"; -// A constructed ALB is required to be attached to the WAF Web ACL. -// In this case, we are using this construct to create one. -const r53ToAlb = new Route53ToAlb(this, 'Route53ToAlbPattern', { - privateHostedZoneProps: { - zoneName: 'www.example.com', - }, - publicApi: false, - logAccessLogs: false -}); +// Use an existing ALB, such as one created by Route53toAlb or AlbToLambda +const existingLoadBalancer = previouslyCreatedLoadBalancer + +// Note - all alb constructs turn on ELB logging by default, so require that an environment including account +// and region be provided when creating the stack +// +// new MyStack(app, 'id', {env: {account: '123456789012', region: 'us-east-1' }}); +// // This construct can only be attached to a configured Application Load Balancer. new WafwebaclToAlb(this, 'test-wafwebacl-alb', { - existingLoadBalancerObj: r53ToAlb.loadBalancer + existingLoadBalancerObj: existingLoadBalancer }); ``` -## Initializer - -``` text -new WafwebaclToAlb(scope: Construct, id: string, props: WafwebaclToAlbProps); +Python +```python +from aws_solutions_constructs.aws_route53_alb import Route53ToAlb +from aws_solutions_constructs.aws_wafwebacl_alb import WafwebaclToAlbProps, WafwebaclToAlb +from aws_cdk import ( + aws_route53 as route53, + Stack +) +from constructs import Construct + +# Use an existing ALB, such as one created by Route53toAlb or AlbToLambda +existingLoadBalancer = previouslyCreatedLoadBalancer + +# Note - all alb constructs turn on ELB logging by default, so require that an environment including account +# and region be provided when creating the stack +# +# MyStack(app, 'id', env=cdk.Environment(account='679431688440', region='us-east-1')) +# +# This construct can only be attached to a configured Application Load Balancer. +WafwebaclToAlb(self, 'test_wafwebacl_alb', + existing_load_balancer_obj=existingLoadBalancer + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`WafwebaclToAlbProps`](#pattern-construct-props) - +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.route53.*; +import software.amazon.awsconstructs.services.wafwebaclalb.*; + +// Use an existing ALB, such as one created by Route53toAlb or AlbToLambda +final existingLoadBalancer = previouslyCreatedLoadBalancer + +// Note - all alb constructs turn on ELB logging by default, so require that an environment including account +// and region be provided when creating the stack +// +// new MyStack(app, "id", StackProps.builder() +// .env(Environment.builder() +// .account("123456789012") +// .region("us-east-1") +// .build()); +// +// This construct can only be attached to a configured Application Load Balancer. +new WafwebaclToAlb(this, "test-wafwebacl-alb", new WafwebaclToAlbProps.Builder() + .existingLoadBalancerObj(existingLoadBalancer) + .build()); +``` ## Pattern Construct Props | **Name** | **Type** | **Description** | diff --git a/source/patterns/@aws-solutions-constructs/aws-wafwebacl-apigateway/README.md b/source/patterns/@aws-solutions-constructs/aws-wafwebacl-apigateway/README.md index 4dda43370..023b166a1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-wafwebacl-apigateway/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-wafwebacl-apigateway/README.md @@ -26,39 +26,83 @@ ## Overview This AWS Solutions Construct implements an AWS WAF web ACL connected to Amazon API Gateway REST API. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript -import * as api from '@aws-cdk/aws-apigateway'; -import * as lambda from "@aws-cdk/aws-lambda"; +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import * as lambda from "aws-cdk-lib/aws-lambda"; import { ApiGatewayToLambda } from '@aws-solutions-constructs/aws-apigateway-lambda'; import { WafwebaclToApiGatewayProps, WafwebaclToApiGateway } from "@aws-solutions-constructs/aws-wafwebacl-apigateway"; const apiGatewayToLambda = new ApiGatewayToLambda(this, 'ApiGatewayToLambdaPattern', { - lambdaFunctionProps: { - runtime: lambda.Runtime.NODEJS_14_X, - handler: 'index.handler', - code: lambda.Code.fromAsset(`lambda`) - } + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) + } }); // This construct can only be attached to a configured API Gateway. new WafwebaclToApiGateway(this, 'test-wafwebacl-apigateway', { - existingApiGatewayInterface: apiGatewayToLambda.apiGateway + existingApiGatewayInterface: apiGatewayToLambda.apiGateway }); ``` -## Initializer - -``` text -new WafwebaclToApiGateway(scope: Construct, id: string, props: WafwebaclToApiGatewayProps); +Python +``` python +from aws_solutions_constructs.aws_apigateway_lambda import ApiGatewayToLambda +from aws_solutions_constructs.aws_wafwebacl_apigateway import WafwebaclToApiGatewayProps, WafwebaclToApiGateway +from aws_cdk import ( + aws_apigateway as api, + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +api_gateway_to_lambda = ApiGatewayToLambda(self, 'ApiGatewayToLambdaPattern', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset( + 'lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) + +# This construct can only be attached to a configured API Gateway. +WafwebaclToApiGateway(self, 'test_wafwebacl_apigateway', + existing_api_gateway_interface=api_gateway_to_lambda.api_gateway + ) ``` -_Parameters_ - -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`WafwebaclToApiGatewayProps`](#pattern-construct-props) +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.apigateway.*; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.apigatewaylambda.*; +import software.amazon.awsconstructs.services.wafwebaclapigateway.*; + +final ApiGatewayToLambda apiGatewayToLambda = new ApiGatewayToLambda(this, "ApiGatewayToLambdaPattern", + new ApiGatewayToLambdaProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); + +// This construct can only be attached to a configured Application Load +// Balancer. +new WafwebaclToApiGateway(this, "test-wafwebacl-apigateway", new WafwebaclToApiGatewayProps.Builder() + .existingApiGatewayInterface(apiGatewayToLambda.getApiGateway()) + .build()); +``` ## Pattern Construct Props diff --git a/source/patterns/@aws-solutions-constructs/aws-wafwebacl-cloudfront/README.md b/source/patterns/@aws-solutions-constructs/aws-wafwebacl-cloudfront/README.md index 4afc97f93..2da02e312 100644 --- a/source/patterns/@aws-solutions-constructs/aws-wafwebacl-cloudfront/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-wafwebacl-cloudfront/README.md @@ -26,9 +26,12 @@ ## Overview This AWS Solutions Construct implements an AWS WAF web ACL connected to Amazon CloudFront. -Here is a minimal deployable pattern definition in Typescript: +Here is a minimal deployable pattern definition: +Typescript ``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3'; import { WafwebaclToCloudFront } from "@aws-solutions-constructs/aws-wafwebacl-cloudfront"; @@ -40,17 +43,39 @@ new WafwebaclToCloudFront(this, 'test-wafwebacl-cloudfront', { }); ``` -## Initializer +Python +```python +from aws_solutions_constructs.aws_cloudfront_s3 import CloudFrontToS3 +from aws_solutions_constructs.aws_wafwebacl_cloudfront import WafwebaclToCloudFront +from aws_cdk import Stack +from constructs import Construct -``` text -new WafwebaclToCloudFront(scope: Construct, id: string, props: WafwebaclToCloudFrontProps); +cloudfront_to_s3 = CloudFrontToS3(self, 'test_cloudfront_s3') + +# This construct can only be attached to a configured CloudFront. +WafwebaclToCloudFront(self, 'test_wafwebacl_cloudfront', + existing_cloud_front_web_distribution=cloudfront_to_s3.cloud_front_web_distribution + ) ``` -_Parameters_ +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.cloudfronts3.*; +import software.amazon.awsconstructs.services.wafwebaclcloudfront.*; + +final CloudFrontToS3 cloudfrontToS3 = new CloudFrontToS3(this, "test-cloudfront-s3", + new CloudFrontToS3Props.Builder() + .build()); -* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) -* id `string` -* props [`WafwebaclToCloudFrontProps`](#pattern-construct-props) +// This construct can only be attached to a configured CloudFront. +new WafwebaclToCloudFront(this, "test-wafwebacl-cloudfront", new WafwebaclToCloudFrontProps.Builder() + .existingCloudFrontWebDistribution(cloudfrontToS3.getCloudFrontWebDistribution()) + .build()); +``` ## Pattern Construct Props @@ -81,7 +106,7 @@ Out of the box implementation of the Construct without any override will set the * AWSManagedRulesAdminProtectionRuleSet * AWSManagedRulesSQLiRuleSet - *Note that the default rules can be replaced by specifying the rules property of CfnWebACLProps* + *Note that the default rules can be replaced by specifying the rules property of CfnWebACLProps* * Send metrics to Amazon CloudWatch ### Amazon CloudFront From cf167bdef7af393c7ffcf5d51810fd3425e74de1 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Mon, 4 Apr 2022 15:24:58 -0400 Subject: [PATCH 02/34] Issue 644 - check permission inputs (#645) Micky has been added as an approved reviewer, perhaps this PR was created before that change. --- .../aws-fargate-s3/README.md | 2 +- .../aws-fargate-s3/lib/index.ts | 8 +++-- .../aws-fargate-s3/test/fargate-s3.test.ts | 30 +++++++++++++++++++ .../aws-fargate-sqs/lib/index.ts | 4 +++ .../aws-fargate-sqs/test/fargate-sqs.test.ts | 21 +++++++++++++ .../aws-iot-lambda-dynamodb/lib/index.ts | 6 ++++ .../test/iot-lambda-dynamodb.test.ts | 28 +++++++++++++++++ .../aws-lambda-dynamodb/lib/index.ts | 6 ++++ .../test/lambda-dynamodb.test.ts | 20 +++++++++++++ .../aws-lambda-s3/lib/index.ts | 4 +++ .../aws-lambda-s3/test/lambda-s3.test.ts | 25 +++++++++++++++- .../lib/index.ts | 5 ++++ .../test/lambda-ssmstringparameter.test.ts | 21 +++++++++++++ .../core/lib/input-validation.ts | 10 ++++++- .../core/test/input-validation.test.ts | 20 ++++++++----- 15 files changed, 197 insertions(+), 13 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/README.md index be6592805..3d94bc761 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/README.md @@ -85,7 +85,7 @@ new FargateToS3(this, "test_construct", new FargateToS3Props.Builder() |bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for the S3 Bucket.| |loggingBucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for the S3 Logging Bucket.| |logS3AccessLogs?| boolean|Whether to turn on Access Logging for the S3 bucket. Creates an S3 bucket with associated storage costs for the logs. Enabling Access Logging is a best practice. default - true| -|bucketPermissions?|`string[]`|Optional bucket permissions to grant to the Fargate service. One or more of the following may be specified: `Delete`, `Read`, and `Write`. Default is `ReadWrite` which includes `[s3:GetObject*, s3:GetBucket*, s3:List*, s3:DeleteObject*, s3:PutObject*, s3:Abort*]`.| +|bucketPermissions?|`string[]`|Optional bucket permissions to grant to the Fargate service. One or more of the following may be specified: `Delete`, `Read`, and `Write`. Default is ["Read", "Write"] which includes `[s3:GetObject*, s3:GetBucket*, s3:List*, s3:DeleteObject*, s3:PutObject*, s3:Abort*]`.| |bucketArnEnvironmentVariableName?|string|Optional Name for the S3 bucket arn environment variable set for the container.| |bucketEnvironmentVariableName?|string|Optional Name for the S3 bucket name environment variable set for the container.| diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/lib/index.ts index 3c11cc6ec..e27d11b6c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/lib/index.ts @@ -122,9 +122,9 @@ export interface FargateToS3Props { readonly logS3AccessLogs?: boolean; /** * Optional bucket permissions to grant to the Fargate service. - * One or more of the following may be specified: "Delete", "Put", "Read", "ReadWrite", "Write". + * One or more of the following may be specified: "Delete", "Read", "Write". * - * @default - Read/write access is given to the Fargate service if no value is specified. + * @default - [ "Read", "Write" ] */ readonly bucketPermissions?: string[]; /** @@ -161,6 +161,10 @@ export class FargateToS3 extends Construct { defaults.CheckProps(props); defaults.CheckFargateProps(props); + if (props.bucketPermissions) { + defaults.CheckListValues(['Delete', 'Read', 'Write'], props.bucketPermissions, 'bucket permission'); + } + this.vpc = defaults.buildVpc(scope, { existingVpc: props.existingVpc, defaultVpcProps: props.publicApi ? defaults.DefaultPublicPrivateVpcProps() : defaults.DefaultIsolatedVpcProps(), diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/test/fargate-s3.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/test/fargate-s3.test.ts index 446954e39..9767f8b9c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/test/fargate-s3.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/test/fargate-s3.test.ts @@ -308,6 +308,36 @@ test('New service/new bucket, private API, new VPC', () => { expect(stack).toCountResources('AWS::ECS::Service', 1); }); +test('Specify bad bucket permission', () => { + + // An environment with region is required to enable logging on an ALB + const stack = new cdk.Stack(); + const publicApi = false; + const bucketName = 'bucket-name'; + const loggingBucketName = 'logging-bucket-name'; + + const props = { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + bucketProps: { + bucketName + }, + bucketPermissions: ['Write', 'Delete', 'Reed'], + loggingBucketProps: { + bucketName: loggingBucketName + } + }; + + const app = () => { + new FargateToS3(stack, 'test-one', props); + }; + // Assertion + expect(app).toThrowError( + /Invalid bucket permission submitted - Reed/); + +}); + test('New service/existing bucket, private API, existing VPC', () => { // An environment with region is required to enable logging on an ALB const stack = new cdk.Stack(); diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/lib/index.ts index 260724579..e68739238 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/lib/index.ts @@ -163,6 +163,10 @@ export class FargateToSqs extends Construct { defaults.CheckProps(props); defaults.CheckFargateProps(props); + if (props.queuePermissions) { + defaults.CheckListValues(['Read', 'Write'], props.queuePermissions, 'queue permission'); + } + this.vpc = defaults.buildVpc(scope, { existingVpc: props.existingVpc, defaultVpcProps: props.publicApi ? defaults.DefaultPublicPrivateVpcProps() : defaults.DefaultIsolatedVpcProps(), diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/test/fargate-sqs.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/test/fargate-sqs.test.ts index bc49bad04..727bdf00c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/test/fargate-sqs.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sqs/test/fargate-sqs.test.ts @@ -483,3 +483,24 @@ test('Existing service/existing queue, private API, existing VPC', () => { expect(stack).toCountResources('AWS::SQS::Queue', 1); expect(stack).toCountResources('AWS::ECS::Service', 1); }); + +test('Test bad queuePermissions', () => { + + // An environment with region is required to enable logging on an ALB + const stack = new cdk.Stack(); + const publicApi = false; + + const props = { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + deployDeadLetterQueue: false, + queuePermissions: ['Reed'], + }; + + const app = () => { + new FargateToSqs(stack, 'test-construct', props); + }; + + expect(app).toThrowError('Invalid queue permission submitted - Reed'); +}); 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 545ec0192..62debba1c 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 @@ -74,6 +74,12 @@ export class IotToLambdaToDynamoDB extends Construct { super(scope, id); defaults.CheckProps(props); + // Other permissions for constructs are accepted as arrays, turning tablePermissions into + // an array to use the same validation function. + if (props.tablePermissions) { + defaults.CheckListValues(['All', 'Read', 'ReadWrite', 'Write'], [props.tablePermissions], 'table permission'); + } + // Setup the IotToLambda const iotToLambda = new IotToLambda(this, 'IotToLambda', props); this.iotTopicRule = iotToLambda.iotTopicRule; 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 f7010f24a..56b12c9ae 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 @@ -275,4 +275,32 @@ test('check exception for Missing existingObj from props for deploy = false', () } catch (e) { expect(e).toBeInstanceOf(Error); } +}); + +test('Check incorrect table permission', () => { + const stack = new cdk.Stack(); + + 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: [] + } + }, + tablePermissions: 'Reed' + }; + + const app = () => { + new IotToLambdaToDynamoDB(stack, 'test-iot-lambda-dynamodb-stack', props); + }; + + // Assertion + expect(app).toThrowError(/Invalid table permission submitted - Reed/); }); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts index 20836caaf..d65071f9c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts @@ -92,6 +92,12 @@ export class LambdaToDynamoDB extends Construct { super(scope, id); defaults.CheckProps(props); + // Other permissions for constructs are accepted as arrays, turning tablePermissions into + // an array to use the same validation function. + if (props.tablePermissions) { + defaults.CheckListValues(['All', 'Read', 'ReadWrite', 'Write'], [props.tablePermissions], 'table permission'); + } + if (props.deployVpc || props.existingVpc) { if (props.deployVpc && props.existingVpc) { throw new Error("More than 1 VPC specified in the properties"); 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 2dead9419..3dd5a1e83 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 @@ -769,3 +769,23 @@ test("Test bad call with existingVpc and deployVpc", () => { // Assertion expect(app).toThrowError(); }); + +test('Test bad table permission', () => { + const stack = new cdk.Stack(); + + const props: LambdaToDynamoDBProps = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, + tablePermissions: 'Reed', + }; + + const app = () => { + new LambdaToDynamoDB(stack, 'test-lambda-dynamodb-stack', props); + }; + + // Assertion + expect(app).toThrowError(/Invalid table permission submitted - Reed/); +}); \ No newline at end of file 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 aef31c561..6fc7a36a7 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 @@ -111,6 +111,10 @@ export class LambdaToS3 extends Construct { super(scope, id); defaults.CheckProps(props); + if (props.bucketPermissions) { + defaults.CheckListValues(['Delete', 'Put', 'Read', 'ReadWrite', 'Write'], props.bucketPermissions, 'bucket permission'); + } + let bucket: s3.IBucket; if (props.deployVpc || props.existingVpc) { 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 de004476c..3e48e9c38 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 @@ -423,4 +423,27 @@ test('s3 bucket with one content bucket and no logging bucket', () => { }); expect(stack).toCountResources("AWS::S3::Bucket", 1); -}); \ No newline at end of file +}); + +test('Test bad bucket permission', () => { + const stack = new Stack(); + + const props = { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler' + }, + bucketProps: { + removalPolicy: RemovalPolicy.DESTROY, + }, + logS3AccessLogs: false, + bucketPermissions: ['Reed'] + }; + + const alb = () => { + new LambdaToS3(stack, 'lambda-s3', props); + }; + + expect(alb).toThrowError('Invalid bucket permission submitted - Reed'); +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/lib/index.ts index aa3f5b5ab..20d849282 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/lib/index.ts @@ -96,6 +96,11 @@ export class LambdaToSsmstringparameter extends Construct { super(scope, id); defaults.CheckProps(props); + // This should have been an array, we will make it an array for validation + if (props.stringParameterPermissions) { + defaults.CheckListValues(['Read', 'ReadWrite'], [props.stringParameterPermissions], 'String Parameter permission'); + } + if (props.deployVpc || props.existingVpc) { this.vpc = defaults.buildVpc(scope, { defaultVpcProps: defaults.DefaultIsolatedVpcProps(), diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/test/lambda-ssmstringparameter.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/test/lambda-ssmstringparameter.test.ts index e94ea2440..67b288d66 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/test/lambda-ssmstringparameter.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/test/lambda-ssmstringparameter.test.ts @@ -368,3 +368,24 @@ test("Test bad call with existingVpc and deployVpc", () => { // Assertion expect(app).toThrowError(); }); + +test("Test bad call with invalid string parameter permission", () => { + // Stack + const stack = new Stack(); + + const app = () => { + // Helper declaration + new LambdaToSsmstringparameter(stack, "lambda-to-ssm-stack", { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: "index.handler", + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + }, + stringParameterProps: { stringValue: "test-string-value" }, + deployVpc: true, + stringParameterPermissions: 'Reed', + }); + }; + // Assertion + expect(app).toThrowError('Invalid String Parameter permission submitted - Reed'); +}); diff --git a/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts b/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts index 3fbac5344..8d130f9ca 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts @@ -205,4 +205,12 @@ export function CheckProps(propsObject: VerifiedProps | any) { if (errorFound) { throw new Error(errorMessages); } -} \ No newline at end of file +} + +export function CheckListValues(allowedPermissions: string[], submittedValues: string[], valueType: string) { + submittedValues.forEach((submittedValue) => { + if (!allowedPermissions.includes(submittedValue)) { + throw Error(`Invalid ${valueType} submitted - ${submittedValue}` ); + } + }); +} diff --git a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts index 6d20bb07c..15d08d163 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts @@ -480,18 +480,22 @@ test('Test fail existingBucketObj and loggingBucketProps check', () => { expect(app).toThrowError('Error - If existingBucketObj is provided, supplying loggingBucketProps or logS3AccessLogs is an error.\n'); }); -test('Test fail false logAlbAccessLogs and albLoggingBucketProps check', () => { - const props: defaults.VerifiedProps = { - logAlbAccessLogs: false, - albLoggingBucketProps: { - autoDeleteObjects: true - } +test('Test successful CheckListValues', () => { + + const app = () => { + defaults.CheckListValues(['one', 'two', 'four'], ['four', 'one'], 'test value'); }; + // Assertion + expect(app).not.toThrowError(); +}); + +test('Test unsuccessful CheckListValues', () => { + const app = () => { - defaults.CheckProps(props); + defaults.CheckListValues(['one', 'two', 'four'], ['four', 'three'], 'test value'); }; // Assertion - expect(app).toThrowError('Error - If logAlbAccessLogs is false, supplying albLoggingBucketProps is invalid.\n'); + expect(app).toThrowError('Invalid test value submitted - three'); }); From 0f0779b9dc427757f32229a04ab640fa44a7ab87 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:22:25 -0400 Subject: [PATCH 03/34] Update CODEOWNERS Remove Hitendra, add new tools team members --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 66113cb5f..97821cc51 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,4 +3,4 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, # @hnishar will be requested for review when someone opens a pull request. -* @hnishar @biffgaut @hayesry +* @biffgaut @hayesry @mobri2a @emcfins @mickychetta From 79e1b09544c2d029fb73a2b500dde5e35edbf63a Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Wed, 6 Apr 2022 13:37:57 -0400 Subject: [PATCH 04/34] fix(Sonarqube configuration): Replace comma between constructs (#646) * Replace comma between constructs * Add missing constructs --- sonar-project.properties | 126 +++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 59 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index e825ec85b..2d7843c80 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -22,65 +22,73 @@ sonar.exclusions=\ # source/test/coverage-reports/jest/*/lcov.info # so we have to provide an explicit list of reportPaths sonar.javascript.lcov.reportPaths= \ - source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-apigateway-iot/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-apigateway-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-cloudfront-mediastore/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-cloudfront-s3/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-cognito-apigateway-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda-elasticsearch-kibana/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda-elasticsearch-kibana/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisfirehose-s3/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-eventbridge-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-eventbridge-sns/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-eventbridge-sqs/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisfirehose-s3/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-events-rule-sns/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-events-rule-sqs/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-iot-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-iot-sqs/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-eventbridge/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-s3/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-sagemakerendpoint/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-sns/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-sqs/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-sqs-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-step-function/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-route53-alb/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-s3-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-s3-sqs/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-s3-step-function/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-sns-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-sns-sqs/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-sqs-lambda/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-wafwebacl-apigateway/coverage/lcov.info \ - source/patterns/@aws-solutions-constructs/aws-wafwebacl-cloudfront/coverage/lcov.info + source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-apigateway-iot/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-apigateway-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-apigateway-sagemakerendpoint/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-apigateway-sqs/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-cloudfront-mediastore/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-cloudfront-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-cognito-apigateway-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-dynamodb-stream-lambda-elasticsearch-kibana/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-dynamodbstreams-lambda-elasticsearch-kibana/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisfirehose-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-eventbridge-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-eventbridge-sns/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-eventbridge-sqs/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisfirehose-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-events-rule-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-events-rule-sns/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-events-rule-sqs/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-iot-kinesisfirehose-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-iot-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-iot-lambda-dynamodb/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-iot-sqs/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3-and-kinesisanalytics/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-elasticsearch-kibana/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-eventbridge/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-sagemakerendpoint/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-sns/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-sqs/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-sqs-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-ssmstringparameter/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-step-function/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-lambda-stepfunctions/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-route53-alb/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-s3-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-s3-sqs/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-s3-step-function/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-sns-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-sns-sqs/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-sqs-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-wafwebacl-apigateway/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-wafwebacl-cloudfront/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-alb-fargate, \ + source/patterns/@aws-solutions-constructs/aws-iot-s3, \ + source/patterns/@aws-solutions-constructs/aws-route53-apigateway, \ + source/patterns/@aws-solutions-constructs/aws-fargate-sns, \ + source/patterns/@aws-solutions-constructs/aws-fargate-s3, \ + source/patterns/@aws-solutions-constructs/aws-fargate-sqs, \ + source/patterns/@aws-solutions-constructs/aws-alb-lambda, \ + source/patterns/@aws-solutions-constructs/core # Encoding of the source files sonar.sourceEncoding=UTF-8 From b620ce6929194f08d5c2789fbf92fc463b718ddb Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Wed, 6 Apr 2022 14:48:32 -0400 Subject: [PATCH 05/34] Update sonar-project.properties (#647) --- sonar-project.properties | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 2d7843c80..7467978c4 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -81,14 +81,14 @@ sonar.javascript.lcov.reportPaths= \ source/patterns/@aws-solutions-constructs/aws-sqs-lambda/coverage/lcov.info, \ source/patterns/@aws-solutions-constructs/aws-wafwebacl-apigateway/coverage/lcov.info, \ source/patterns/@aws-solutions-constructs/aws-wafwebacl-cloudfront/coverage/lcov.info, \ - source/patterns/@aws-solutions-constructs/aws-alb-fargate, \ - source/patterns/@aws-solutions-constructs/aws-iot-s3, \ - source/patterns/@aws-solutions-constructs/aws-route53-apigateway, \ - source/patterns/@aws-solutions-constructs/aws-fargate-sns, \ - source/patterns/@aws-solutions-constructs/aws-fargate-s3, \ - source/patterns/@aws-solutions-constructs/aws-fargate-sqs, \ - source/patterns/@aws-solutions-constructs/aws-alb-lambda, \ - source/patterns/@aws-solutions-constructs/core + source/patterns/@aws-solutions-constructs/aws-alb-fargate/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-iot-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-route53-apigateway/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-fargate-sns/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-fargate-s3/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-fargate-sqs/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/aws-alb-lambda/coverage/lcov.info, \ + source/patterns/@aws-solutions-constructs/core/coverage/lcov.info # Encoding of the source files sonar.sourceEncoding=UTF-8 From 26e9ec08257a90034b76a91ea4a3d703d13eb0a2 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Thu, 7 Apr 2022 11:59:38 -0400 Subject: [PATCH 06/34] fix(Remove debug statement): Remove extra debug statement in kinesisfirehose-s3 (#649) * Remove debug statement The JSON.stringify() call in this statement caused problems * Update index.ts --- .../aws-kinesisfirehose-s3/lib/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/lib/index.ts index 7eedf2d6e..d519df75c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisfirehose-s3/lib/index.ts @@ -16,7 +16,7 @@ import { Construct } from "@aws-cdk/core"; import * as s3 from "@aws-cdk/aws-s3"; import * as defaults from "@aws-solutions-constructs/core"; import * as iam from "@aws-cdk/aws-iam"; -import { overrideProps, printWarning, consolidateProps } from "@aws-solutions-constructs/core"; +import { overrideProps, consolidateProps } from "@aws-solutions-constructs/core"; import * as logs from "@aws-cdk/aws-logs"; import * as cdk from "@aws-cdk/core"; import * as kms from "@aws-cdk/aws-kms"; @@ -170,7 +170,6 @@ export class KinesisFirehoseToS3 extends Construct { awsManagedKey ); - printWarning(`kinesisFirehoseProps: ${JSON.stringify(props.kinesisFirehoseProps, null, 2)}`); // if the client didn't explicity say it was a Kinesis client, then turn on encryption if (!props.kinesisFirehoseProps || !props.kinesisFirehoseProps.deliveryStreamType || @@ -194,4 +193,4 @@ export class KinesisFirehoseToS3 extends Construct { kinesisFirehoseProps ); } -} \ No newline at end of file +} From a5e616a2337a686701f07af7a9814d221cde7331 Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Automation Date: Thu, 7 Apr 2022 17:09:25 +0000 Subject: [PATCH 07/34] chore(release): 1.149.0 --- CHANGELOG.md | 13 +++++++++++++ source/lerna.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ac1c94d..422291cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ 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.149.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.5.0...v1.149.0) (2022-04-07) + + +### Features + +* **README.md:** add python and java minimal deployment ([#582](https://github.com/awslabs/aws-solutions-constructs/issues/582)) ([2ecd9dd](https://github.com/awslabs/aws-solutions-constructs/commit/2ecd9dd935b731d2e4705ed9c146efcad0961fd8)) + + +### Bug Fixes + +* **Remove debug statement:** Remove extra debug statement in kinesisfirehose-s3 ([#649](https://github.com/awslabs/aws-solutions-constructs/issues/649)) ([26e9ec0](https://github.com/awslabs/aws-solutions-constructs/commit/26e9ec08257a90034b76a91ea4a3d703d13eb0a2)) +* **Sonarqube configuration:** Replace comma between constructs ([#646](https://github.com/awslabs/aws-solutions-constructs/issues/646)) ([79e1b09](https://github.com/awslabs/aws-solutions-constructs/commit/79e1b09544c2d029fb73a2b500dde5e35edbf63a)) + ## [1.148.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.147.0...v1.148.0) (2022-03-30) * Upgraded all patterns to CDK v1.148.0 diff --git a/source/lerna.json b/source/lerna.json index a40f9724a..e62d7fb7b 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "1.148.0" + "version": "1.149.0" } From 765e810e23bb67d86f4d055dcdf9361c2b79b633 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Thu, 7 Apr 2022 13:12:42 -0400 Subject: [PATCH 08/34] chore(changelog): Updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 422291cab..aa77dfd81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. See [standa ## [1.149.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.5.0...v1.149.0) (2022-04-07) +* Upgraded all patterns to CDK v1.149.0 ### Features From 0b35418b41e24b32b6064a649d77a70f1c6d7bd8 Mon Sep 17 00:00:00 2001 From: mickychetta <45010053+mickychetta@users.noreply.github.com> Date: Thu, 7 Apr 2022 13:14:49 -0700 Subject: [PATCH 09/34] feat(aws-fargate-dynamodb): create new construct (#633) * created README for aws-fargate-dynamodb * created aws-fargate-dynamodb construct * revised buildDynamoDBTable helper function to support old/new constructs * updated prop variable name * added error and endpoint checking tests * checked for table permissions * checked props in input-validation * moved table interface and object check to buildDynamoDBTable function * created prop check function in dynamo helper file --- .../aws-apigateway-dynamodb/lib/index.ts | 13 +- .../aws-fargate-dynamodb/.eslintignore | 4 + .../aws-fargate-dynamodb/.gitignore | 15 + .../aws-fargate-dynamodb/.npmignore | 21 + .../aws-fargate-dynamodb/README.md | 122 ++ .../aws-fargate-dynamodb/architecture.png | Bin 0 -> 129003 bytes .../aws-fargate-dynamodb/lib/index.ts | 210 +++ .../aws-fargate-dynamodb/package.json | 104 ++ .../test/fargate-dynamodb.test.ts | 672 ++++++++++ .../integ.existing-resources.expected.json | 1166 +++++++++++++++++ .../test/integ.existing-resources.ts | 57 + .../test/integ.new-resources.expected.json | 1166 +++++++++++++++++ .../test/integ.new-resources.ts | 40 + .../aws-lambda-dynamodb/lib/index.ts | 5 +- .../core/lib/dynamodb-table-helper.ts | 47 +- .../core/test/dynamo-table.test.ts | 105 ++ .../core/test/input-validation.test.ts | 2 +- 17 files changed, 3736 insertions(+), 13 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/fargate-dynamodb.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts index 482ce8453..4d157410b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts @@ -131,10 +131,13 @@ export class ApiGatewayToDynamoDB extends Construct { partitionKeyName = getPartitionKeyNameFromTable(props.existingTableObj); } + // Since we are only invoking this function with an existing Table or tableProps, + // (not a table interface), we know that the implementation will always return + // a Table object and we can safely cast away the optional aspect of the type. this.dynamoTable = defaults.buildDynamoDBTable(this, { existingTableObj: props.existingTableObj, - dynamoTableProps, - }); + dynamoTableProps: props.dynamoTableProps + })[1] as dynamodb.Table; // Setup the API Gateway [this.apiGateway, this.apiGatewayCloudWatchRole, this.apiGatewayLogGroup] = defaults.GlobalRestApi(this, @@ -170,7 +173,7 @@ export class ApiGatewayToDynamoDB extends Construct { readRequestTemplate = props.readRequestTemplate; } else { readRequestTemplate = - `{ \ + `{ \ "TableName": "${this.dynamoTable.tableName}", \ "KeyConditionExpression": "${partitionKeyName} = :v1", \ "ExpressionAttributeValues": { \ @@ -212,7 +215,7 @@ export class ApiGatewayToDynamoDB extends Construct { deleteRequestTemplate = props.deleteRequestTemplate; } else { deleteRequestTemplate = - `{ \ + `{ \ "TableName": "${this.dynamoTable.tableName}", \ "Key": { \ "${partitionKeyName}": { \ @@ -240,7 +243,7 @@ export class ApiGatewayToDynamoDB extends Construct { resources: [ this.dynamoTable.tableArn ], - actions: [ `${action}` ] + actions: [`${action}`] })); } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.eslintignore new file mode 100644 index 000000000..e6f7801ea --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.eslintignore @@ -0,0 +1,4 @@ +lib/*.js +test/*.js +*.d.ts +coverage diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.gitignore b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.npmignore b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/README.md new file mode 100644 index 000000000..6744b8c7b --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/README.md @@ -0,0 +1,122 @@ +# aws-fargate-dynamodb module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **Reference Documentation**:| https://docs.aws.amazon.com/solutions/latest/constructs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png) Python|`aws_solutions_constructs.aws_fargate_dynamodb`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-fargate-dynamodb`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.fargatedynamodb`| + +This AWS Solutions Construct implements an AWS Fargate service that can write/read to an Amazon DynamoDB table + +Here is a minimal deployable pattern definition: + +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { FargateToDynamoDB, FargateToDynamoDBProps } from '@aws-solutions-constructs/aws-fargate-dynamodb'; + +const constructProps: FargateToDynamoDBProps = { + publicApi: true, + ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", +}; + +new FargateToDynamoDB(stack, 'test-construct', constructProps); +``` + +Python +``` python +from aws_solutions_constructs.aws_fargate_dynamodb import FargateToDynamoDB, FargateToDynamoDBProps +from aws_cdk import ( + Stack +) +from constructs import Construct + +FargateToDynamoDB(self, 'test_construct', + public_api=True, + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.fargatedynamodb.*; + +new FargateToDynamoDB(this, "test-construct", new FargateToDynamoDBProps.Builder() + .publicApi(true) + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") + .build()); +``` + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| publicApi | `boolean` | Whether the construct is deploying a private or public API. This has implications for the VPC. | +| vpcProps? | [`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcProps.html) | Optional custom properties for a VPC the construct will create. This VPC will be used by any Private Hosted Zone the construct creates (that's why loadBalancerProps and privateHostedZoneProps can't include a VPC). Providing both this and existingVpc is an error. | +| existingVpc? | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | An existing VPC in which to deploy the construct. Providing both this and vpcProps is an error. If the client provides an existing load balancer and/or existing Private Hosted Zone, those constructs must exist in this VPC. | +| clusterProps? | [`ecs.ClusterProps`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ClusterProps.html) | Optional properties to create a new ECS cluster. To provide an existing cluster, use the cluster attribute of fargateServiceProps. | +| ecrRepositoryArn? | `string` | The arn of an ECR Repository containing the image to use to generate the containers. Either this or the image property of containerDefinitionProps must be provided. format: arn:aws:ecr:*region*:*account number*:repository/*Repository Name* | +| ecrImageVersion? | `string` | The version of the image to use from the repository. Defaults to 'Latest' | +| containerDefinitionProps? | [`ecs.ContainerDefinitionProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinitionProps.html) | Optional props to define the container created for the Fargate Service (defaults found in fargate-defaults.ts) | +| fargateTaskDefinitionProps? | [`ecs.FargateTaskDefinitionProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateTaskDefinitionProps.html) | Optional props to define the Fargate Task Definition for this construct (defaults found in fargate-defaults.ts) | +| fargateServiceProps? | [`ecs.FargateServiceProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateServiceProps.html) | Optional values to override default Fargate Task definition properties (fargate-defaults.ts). The construct will default to launching the service is the most isolated subnets available (precedence: Isolated, Private and Public). Override those and other defaults here. | +| existingFargateServiceObject? | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html) | A Fargate Service already instantiated (probably by another Solutions Construct). If this is specified, then no props defining a new service can be provided, including: ecrImageVersion, containerDefinitionProps, fargateTaskDefinitionProps, ecrRepositoryArn, fargateServiceProps, clusterProps | +| existingContainerDefinitionObject? | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinition.html) | A container definition already instantiated as part of a Fargate service. This must be the container in the existingFargateServiceObject | +| dynamoTableProps? | [`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table.| +|existingTableInterface?|[`dynamodb.ITable`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.ITable.html)|Existing instance of DynamoDB table object or interface, providing both this and `dynamoTableProps` will cause an error.| +| tablePermissions? |`string`|Optional table permissions to grant to the Fargate service. One of the following may be specified: `All`, `Read`, `ReadWrite`, `Write`.| +|tableArnEnvironmentVariableName?|`string`|Optional Name for the DynamoDB table arn environment variable set for the container.| +|tableEnvironmentVariableName?|`string`|Optional Name for the DynamoDB table name environment variable set for the container.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| vpc | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | The VPC used by the construct (whether created by the construct or provided by the client) | +| service | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html) | The AWS Fargate service used by this construct (whether created by this construct or passed to this construct at initialization) | +| container | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinition.html) | The container associated with the AWS Fargate service in the service property. | +|dynamoTableInterface|[`dynamodb.ITable`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.ITable.html)|Returns an instance of `dynamodb.ITable` created by the construct or the interface provided in existingTableInterface.| +|dynamoTable?|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.Table.html)|Returns an instance of `dynamodb.Table` created by the construct. IMPORTANT: If existingTableInterface was provided in Pattern Construct Props, this property will be `undefined`.| + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### AWS Fargate Service +* Sets up an AWS Fargate service + * Uses the existing service if provided + * Creates a new service if none provided. + * Service will run in isolated subnets if available, then private subnets if available and finally public subnets + * Adds environment variables to the container with the ARN and Name of the DynamoDB table + * Add permissions to the container IAM role allowing it to publish to the DynamoDB table + +### Amazon DynamoDB Table +* Sets up an Amazon DynamoDB table + * Uses an existing table if one is provided, otherwise creates a new one +* Adds an Interface Endpoint to the VPC for DynamoDB (the service by default runs in Isolated or Private subnets) + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/architecture.png b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e32ad60487fbb50e6826fed8e613f4c20b0fce GIT binary patch literal 129003 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 z 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 Z3mnLLpP{~3`v>?>SKHuv;nn+3{ulbq0{Q>| literal 0 HcmV?d00001 diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/lib/index.ts new file mode 100644 index 000000000..016db217f --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/lib/index.ts @@ -0,0 +1,210 @@ +/** + * Copyright 2022 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. + */ + +import * as ec2 from "@aws-cdk/aws-ec2"; +import * as dynamodb from "@aws-cdk/aws-dynamodb"; +// Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate +import { Construct } from "@aws-cdk/core"; +import * as defaults from "@aws-solutions-constructs/core"; +import * as ecs from "@aws-cdk/aws-ecs"; + +export interface FargateToDynamoDBProps { + /** + * Whether the construct is deploying a private or public API. This has implications for the VPC deployed + * by this construct. + * + * @default - none + */ + readonly publicApi: boolean; + /** + * Optional custom properties for a VPC the construct will create. This VPC will + * be used by the new Fargate service the construct creates (that's + * why targetGroupProps can't include a VPC). Providing + * both this and existingVpc is an error. An DynamoDB Interface + * endpoint will be included in this VPC. + * + * @default - none + */ + readonly vpcProps?: ec2.VpcProps; + /** + * An existing VPC in which to deploy the construct. Providing both this and + * vpcProps is an error. If the client provides an existing Fargate service, + * this value must be the VPC where the service is running. An DynamoDB Interface + * endpoint will be added to this VPC. + * + * @default - none + */ + readonly existingVpc?: ec2.IVpc; + /** + * Optional properties to create a new ECS cluster + */ + readonly clusterProps?: ecs.ClusterProps; + /** + * The arn of an ECR Repository containing the image to use + * to generate the containers + * + * format: + * arn:aws:ecr:[region]:[account number]:repository/[Repository Name] + */ + readonly ecrRepositoryArn?: string; + /** + * The version of the image to use from the repository + * + * @default - 'latest' + */ + readonly ecrImageVersion?: string; + /* + * Optional props to define the container created for the Fargate Service + * + * defaults - fargate-defaults.ts + */ + readonly containerDefinitionProps?: ecs.ContainerDefinitionProps | any; + /* + * Optional props to define the Fargate Task Definition for this construct + * + * defaults - fargate-defaults.ts + */ + readonly fargateTaskDefinitionProps?: ecs.FargateTaskDefinitionProps | any; + /** + * Optional values to override default Fargate Task definition properties + * (fargate-defaults.ts). The construct will default to launching the service + * is the most isolated subnets available (precedence: Isolated, Private and + * Public). Override those and other defaults here. + * + * defaults - fargate-defaults.ts + */ + readonly fargateServiceProps?: ecs.FargateServiceProps | any; + /** + * A Fargate Service already instantiated (probably by another Solutions Construct). If + * this is specified, then no props defining a new service can be provided, including: + * existingImageObject, ecrImageVersion, containerDefintionProps, fargateTaskDefinitionProps, + * ecrRepositoryArn, fargateServiceProps, clusterProps, existingClusterInterface. If this value + * is provided, then existingContainerDefinitionObject must be provided as well. + * + * @default - none + */ + readonly existingFargateServiceObject?: ecs.FargateService; + /* + * A container definition already instantiated as part of a Fargate service. This must + * be the container in the existingFargateServiceObject. + * + * @default - None + */ + readonly existingContainerDefinitionObject?: ecs.ContainerDefinition; + /** + * Optional user provided props to override the default props for DynamoDB Table. + * + * @default - Default props are used + */ + readonly dynamoTableProps?: dynamodb.TableProps; + /** + * Optional user provided props to override the default props for DynamoDB Table. + * + * @default - None + */ + readonly existingTableInterface?: dynamodb.ITable; + /** + * Optional table permissions to grant to the Fargate service. One of the following may be specified: `All`, `Read`, `ReadWrite`, `Write`. + * + * @default - 'ReadWrite' + */ + readonly tablePermissions?: string + /** + * Optional Name for the DynamoDB table arn environment variable set for the container. + * + * @default - None + */ + readonly tableArnEnvironmentVariableName?: string; + /** + * Optional Name for the DynamoDB table name environment variable set for the container. + * + * @default - None + */ + readonly tableEnvironmentVariableName?: string; +} + +export class FargateToDynamoDB extends Construct { + public readonly vpc: ec2.IVpc; + public readonly service: ecs.FargateService; + public readonly container: ecs.ContainerDefinition; + public readonly dynamoTableInterface: dynamodb.ITable; + public readonly dynamoTable?: dynamodb.Table; + + constructor(scope: Construct, id: string, props: FargateToDynamoDBProps) { + super(scope, id); + defaults.CheckProps(props); + defaults.CheckFargateProps(props); + + // Other permissions for constructs are accepted as arrays, turning tablePermissions into + // an array to use the same validation function. + if (props.tablePermissions) { + const allowedPermissions = ['ALL', 'READ', 'READWRITE', 'WRITE']; + defaults.CheckListValues(allowedPermissions, [props.tablePermissions.toUpperCase()], 'tablePermission'); + } + + this.vpc = defaults.buildVpc(scope, { + existingVpc: props.existingVpc, + defaultVpcProps: props.publicApi ? defaults.DefaultPublicPrivateVpcProps() : defaults.DefaultIsolatedVpcProps(), + userVpcProps: props.vpcProps, + constructVpcProps: { enableDnsHostnames: true, enableDnsSupport: true } + }); + + defaults.AddAwsServiceEndpoint(scope, this.vpc, defaults.ServiceEndpointTypes.DYNAMODB); + + if (props.existingFargateServiceObject) { + this.service = props.existingFargateServiceObject; + // CheckFargateProps confirms that the container is provided + this.container = props.existingContainerDefinitionObject!; + } else { + [this.service, this.container] = defaults.CreateFargateService( + scope, + id, + this.vpc, + props.clusterProps, + props.ecrRepositoryArn, + props.ecrImageVersion, + props.fargateTaskDefinitionProps, + props.containerDefinitionProps, + props.fargateServiceProps + ); + } + + [this.dynamoTableInterface, this.dynamoTable] = defaults.buildDynamoDBTable(this, { + dynamoTableProps: props.dynamoTableProps, + existingTableInterface: props.existingTableInterface + }); + + // Add the requested or default table permissions + if (props.tablePermissions) { + const permission = props.tablePermissions.toUpperCase(); + + if (permission === 'ALL') { + this.dynamoTableInterface.grantFullAccess(this.service.taskDefinition.taskRole); + } else if (permission === 'READ') { + this.dynamoTableInterface.grantReadData(this.service.taskDefinition.taskRole); + } else if (permission === 'READWRITE') { + this.dynamoTableInterface.grantReadWriteData(this.service.taskDefinition.taskRole); + } else if (permission === 'WRITE') { + this.dynamoTableInterface.grantWriteData(this.service.taskDefinition.taskRole); + } + } else { + this.dynamoTableInterface.grantReadWriteData(this.service.taskDefinition.taskRole); + } + + // Add environment variables + const tableArnEnvironmentVariableName = props.tableArnEnvironmentVariableName || 'DYNAMODB_TABLE_ARN'; + this.container.addEnvironment(tableArnEnvironmentVariableName, this.dynamoTableInterface.tableArn); + const tableEnvironmentVariableName = props.tableEnvironmentVariableName || 'DYNAMODB_TABLE_NAME'; + this.container.addEnvironment(tableEnvironmentVariableName, this.dynamoTableInterface.tableName); + } +} diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/package.json b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/package.json new file mode 100644 index 000000000..60df16306 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/package.json @@ -0,0 +1,104 @@ +{ + "name": "@aws-solutions-constructs/aws-fargate-dynamodb", + "version": "0.0.0", + "description": "CDK Constructs for AWS Fargate to Amazon DynamoDB integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-constructs.git", + "directory": "source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awsconstructs.services.fargatedynamodb", + "maven": { + "groupId": "software.amazon.awsconstructs", + "artifactId": "fargatedynamodb" + } + }, + "dotnet": { + "namespace": "Amazon.SolutionsConstructs.AWS.FargateDynamoDB", + "packageId": "Amazon.SolutionsConstructs.AWS.FargateDynamoDB", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-constructs.aws-fargate-dynamodb", + "module": "aws_solutions_constructs.aws_fargate_dynamodb" + } + } + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@types/jest": "^26.0.22", + "@aws-solutions-constructs/core": "0.0.0", + "@types/node": "^10.3.0", + "constructs": "3.2.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ], + "coverageReporters": [ + "text", + [ + "lcov", + { + "projectRoot": "../../../../" + } + ] + ] + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "keywords": [ + "aws", + "cdk", + "awscdk", + "AWS Solutions Constructs", + "Amazon DynamoDB", + "AWS Fargate" + ] +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/fargate-dynamodb.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/fargate-dynamodb.test.ts new file mode 100644 index 000000000..be4d5347e --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/fargate-dynamodb.test.ts @@ -0,0 +1,672 @@ +/** + * Copyright 2022 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. + */ + +import '@aws-cdk/assert/jest'; +import * as defaults from '@aws-solutions-constructs/core'; +import * as cdk from "@aws-cdk/core"; +import { FargateToDynamoDB } from "../lib"; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as ecs from '@aws-cdk/aws-ecs'; + +test('New service/new table, public API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + const clusterName = "custom-cluster-name"; + const containerName = "custom-container-name"; + const serviceName = "custom-service-name"; + const tableName = "custom-table-name"; + const familyName = "family-name"; + + const construct = new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: familyName }, + fargateServiceProps: { serviceName }, + dynamoTableProps: { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + }, + tablePermissions: 'ReadWrite' + }); + + expect(construct.vpc !== null); + expect(construct.service !== null); + expect(construct.container !== null); + expect(construct.dynamoTable !== null); + expect(construct.dynamoTableInterface !== null); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Cluster", { + ClusterName: clusterName + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + 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": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + Family: familyName, + ContainerDefinitions: [ + { + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: containerName, + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.0.0.0/16' + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/new table, private API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const tableName = 'table-name'; + + new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + dynamoTableProps: { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + }, + tablePermissions: 'Read', + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName, + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.0.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/existing table, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const tableName = 'custom-table-name'; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const existingTable = new dynamodb.Table(stack, 'MyTable', { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + }); + + const construct = new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + existingVpc, + existingTableInterface: existingTable, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + tablePermissions: 'ALL' + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: "dynamodb:*", + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "MyTable794EDED1", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + expect(construct.dynamoTable == null); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); +}); + +test('Existing service/new table, public API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + const serviceName = 'custom-name'; + const customName = 'CUSTOM_NAME'; + const customArn = 'CUSTOM_ARN'; + const tableName = 'table-name'; + + const existingVpc = defaults.getTestVpc(stack); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const construct = new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + tableArnEnvironmentVariableName: customArn, + tableEnvironmentVariableName: customName, + dynamoTableProps: { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + } + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: customArn, + Value: { + "Fn::GetAtt": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + } + }, + { + Name: customName, + Value: { + Ref: "testconstructDynamoTable67BDAFC5" + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + 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": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + expect(construct.dynamoTable == null); + + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); +}); + +test('Existing service/existing table, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const serviceName = 'custom-name'; + const tableName = 'custom-table-name'; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingTable = new dynamodb.Table(stack, 'MyTablet', { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + }); + + const construct = new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + tablePermissions: 'Write', + existingTableInterface: existingTable + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "DYNAMODB_TABLE_ARN", + Value: { + "Fn::GetAtt": [ + "MyTabletD7ADAF4F", + "Arn" + ] + } + }, + { + Name: "DYNAMODB_TABLE_NAME", + Value: { + Ref: "MyTabletD7ADAF4F" + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "MyTabletD7ADAF4F", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + expect(construct.dynamoTable == null); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); +}); + +test('test error invalid table permission', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const serviceName = 'custom-name'; + const tableName = 'custom-table-name'; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingTable = new dynamodb.Table(stack, 'MyTablet', { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + }); + + const app = () => { + new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + tablePermissions: 'reed', + existingTableInterface: existingTable + }); + }; + + expect(app).toThrowError('Invalid tablePermission submitted - REED'); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.expected.json new file mode 100644 index 000000000..a146762a0 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.expected.json @@ -0,0 +1,1166 @@ +{ + "Description": "Integration Test with new VPC, Service and Table", + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "172.168.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "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": "existing-resources/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, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/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)" + } + ] + } + } + }, + "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": "existing-resources/Vpc" + } + ] + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRAPIsecuritygroup78294485", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRDKRsecuritygroup598BA37E", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "VpcDDB49FBEC5F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "DynamoTableB2B22E15": { + "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" + }, + "existingresourcesECRAPIsecuritygroup78294485": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-ECR_API-security-group", + "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" + } + ] + } + } + }, + "existingresourcesECRDKRsecuritygroup598BA37E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-ECR_DKR-security-group", + "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" + } + ] + } + } + }, + "testclusterDF8B0D19": { + "Type": "AWS::ECS::Cluster" + }, + "testtaskdefTaskRoleB2DEF113": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testtaskdefTaskRoleDefaultPolicy5D591D1C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "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": [ + "DynamoTableB2B22E15", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testtaskdefTaskRoleDefaultPolicy5D591D1C", + "Roles": [ + { + "Ref": "testtaskdefTaskRoleB2DEF113" + } + ] + } + }, + "testtaskdefF924AD58": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "CUSTOM_ARN", + "Value": { + "Fn::GetAtt": [ + "DynamoTableB2B22E15", + "Arn" + ] + } + }, + { + "Name": "CUSTOM_NAME", + "Value": { + "Ref": "DynamoTableB2B22E15" + } + } + ], + "Essential": true, + "Image": "nginx", + "MemoryReservation": 512, + "Name": "test-container", + "PortMappings": [ + { + "ContainerPort": 8080, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "existingresourcestesttaskdef88B214A2", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "testtaskdefTaskRoleB2DEF113", + "Arn" + ] + } + } + }, + "testsg872EB48A": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Construct created security group", + "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" + } + ] + } + } + }, + "testserviceService2730C249": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "testclusterDF8B0D19" + }, + "DeploymentConfiguration": { + "MaximumPercent": 150, + "MinimumHealthyPercent": 75 + }, + "DesiredCount": 2, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "testsg872EB48A", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "PlatformVersion": "LATEST", + "TaskDefinition": { + "Ref": "testtaskdefF924AD58" + } + } + } + }, + "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-fargate-dynamodb/test/integ.existing-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.ts new file mode 100644 index 000000000..673ea291d --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2022 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 { Aws, App, Stack } from "@aws-cdk/core"; +import { FargateToDynamoDB, FargateToDynamoDBProps } from "../lib"; +import { generateIntegStackName, getTestVpc, CreateFargateService } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as defaults from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and Table'; + +const existingVpc = getTestVpc(stack); +const [ existingTable ] = defaults.buildDynamoDBTable(stack, {}); + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const [testService, testContainer] = CreateFargateService(stack, + 'test', + existingVpc, + undefined, + undefined, + undefined, + undefined, + { image }, +); + +const constructProps: FargateToDynamoDBProps = { + publicApi: true, + existingVpc, + existingTableInterface: existingTable, + existingContainerDefinitionObject: testContainer, + existingFargateServiceObject: testService, + tableArnEnvironmentVariableName: 'CUSTOM_ARN', + tableEnvironmentVariableName: 'CUSTOM_NAME' +}; + +new FargateToDynamoDB(stack, 'test-construct', constructProps); + +defaults.suppressAutoDeleteHandlerWarnings(stack); +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.expected.json new file mode 100644 index 000000000..fe14f4736 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.expected.json @@ -0,0 +1,1166 @@ +{ + "Description": "Integration Test with new VPC, Service and Table", + "Resources": { + "testconstructDynamoTable67BDAFC5": { + "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" + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "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": "new-resources/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, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/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)" + } + ] + } + } + }, + "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": "new-resources/Vpc" + } + ] + } + }, + "VpcDDB49FBEC5F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRAPIsecuritygroupE52BAE3F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRDKRsecuritygroupBA34F94F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "newresourcesECRAPIsecuritygroupE52BAE3F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_API-security-group", + "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" + } + ] + } + } + }, + "newresourcesECRDKRsecuritygroupBA34F94F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_DKR-security-group", + "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" + } + ] + } + } + }, + "testconstructcluster7B6231C5": { + "Type": "AWS::ECS::Cluster" + }, + "testconstructtaskdefTaskRoleC60414C4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testconstructtaskdefTaskRoleDefaultPolicyF34A1535": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "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": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testconstructtaskdefTaskRoleDefaultPolicyF34A1535", + "Roles": [ + { + "Ref": "testconstructtaskdefTaskRoleC60414C4" + } + ] + } + }, + "testconstructtaskdef8BD1F9E4": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "DYNAMODB_TABLE_ARN", + "Value": { + "Fn::GetAtt": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + } + }, + { + "Name": "DYNAMODB_TABLE_NAME", + "Value": { + "Ref": "testconstructDynamoTable67BDAFC5" + } + } + ], + "Essential": true, + "Image": "nginx", + "MemoryReservation": 512, + "Name": "test-construct-container", + "PortMappings": [ + { + "ContainerPort": 8080, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "newresourcestestconstructtaskdefE4616A0D", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "testconstructtaskdefTaskRoleC60414C4", + "Arn" + ] + } + } + }, + "testconstructsgA602AA29": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Construct created security group", + "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" + } + ] + } + } + }, + "testconstructserviceService13074A8F": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "testconstructcluster7B6231C5" + }, + "DeploymentConfiguration": { + "MaximumPercent": 150, + "MinimumHealthyPercent": 75 + }, + "DesiredCount": 2, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "testconstructsgA602AA29", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "PlatformVersion": "LATEST", + "TaskDefinition": { + "Ref": "testconstructtaskdef8BD1F9E4" + } + } + } + }, + "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-fargate-dynamodb/test/integ.new-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.ts new file mode 100644 index 000000000..242691ffa --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2022 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 { Aws, App, Stack } from "@aws-cdk/core"; +import { FargateToDynamoDB, FargateToDynamoDBProps } from "../lib"; +import { generateIntegStackName, suppressAutoDeleteHandlerWarnings } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and Table'; + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const testProps: FargateToDynamoDBProps = { + publicApi: true, + containerDefinitionProps: { + image + } +}; + +new FargateToDynamoDB(stack, 'test-construct', testProps); + +suppressAutoDeleteHandlerWarnings(stack); +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts index d65071f9c..f18e349d1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts @@ -122,10 +122,13 @@ export class LambdaToDynamoDB extends Construct { vpc: this.vpc }); + // Since we are only invoking this function with an existing Table or tableProps, + // (not a table interface), we know that the implementation will always return + // a Table object and we can safely cast away the optional aspect of the type. this.dynamoTable = defaults.buildDynamoDBTable(this, { dynamoTableProps: props.dynamoTableProps, existingTableObj: props.existingTableObj - }); + })[1] as dynamodb.Table; // Configure environment variables const tableEnvironmentVariableName = props.tableEnvironmentVariableName || 'DDB_TABLE_NAME'; diff --git a/source/patterns/@aws-solutions-constructs/core/lib/dynamodb-table-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/dynamodb-table-helper.ts index 492eae612..ef6dace07 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/dynamodb-table-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/dynamodb-table-helper.ts @@ -31,6 +31,13 @@ export interface BuildDynamoDBTableProps { * @default - None */ readonly existingTableObj?: dynamodb.Table + /** + * Existing instance of dynamodb interface. + * Providing both this and `dynamoTableProps` will cause an error. + * + * @default - None + */ + readonly existingTableInterface?: dynamodb.ITable } export interface BuildDynamoDBTableWithStreamProps { @@ -49,14 +56,42 @@ export interface BuildDynamoDBTableWithStreamProps { readonly existingTableInterface?: dynamodb.ITable } -export function buildDynamoDBTable(scope: Construct, props: BuildDynamoDBTableProps): dynamodb.Table { +export function buildDynamoDBTable(scope: Construct, props: BuildDynamoDBTableProps): [dynamodb.ITable, dynamodb.Table?] { + checkTableProps(props); + // Conditional DynamoDB Table creation - if (!props.existingTableObj) { - // Set the default props for DynamoDB table - const dynamoTableProps = consolidateProps(DefaultTableProps, props.dynamoTableProps); - return new dynamodb.Table(scope, 'DynamoTable', dynamoTableProps); + if (props.existingTableObj) { + return [props.existingTableObj, props.existingTableObj]; + } else if (props.existingTableInterface) { + return [props.existingTableInterface, undefined]; } else { - return props.existingTableObj; + const consolidatedTableProps = consolidateProps(DefaultTableProps, props.dynamoTableProps); + const newTable = new dynamodb.Table(scope, 'DynamoTable', consolidatedTableProps); + return [newTable, newTable]; + } +} + +export function checkTableProps(props: BuildDynamoDBTableProps) { + let errorMessages = ''; + let errorFound = false; + + if (props.dynamoTableProps && props.existingTableObj) { + errorMessages += 'Error - Either provide existingTableObj or dynamoTableProps, but not both.\n'; + errorFound = true; + } + + if (props.dynamoTableProps && props.existingTableInterface) { + errorMessages += 'Error - Either provide existingTableInterface or dynamoTableProps, but not both.\n'; + errorFound = true; + } + + if (props.existingTableObj && props.existingTableInterface) { + errorMessages += 'Error - Either provide existingTableInterface or existingTableObj, but not both.\n'; + errorFound = true; + } + + if (errorFound) { + throw new Error(errorMessages); } } diff --git a/source/patterns/@aws-solutions-constructs/core/test/dynamo-table.test.ts b/source/patterns/@aws-solutions-constructs/core/test/dynamo-table.test.ts index 2f90157cf..204af56fa 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/dynamo-table.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/dynamo-table.test.ts @@ -332,6 +332,39 @@ test('test buildDynamoDBTableWithStream with existingTableObj', () => { })); }); +test('test buildDynamoDBTable with existingTableInterface', () => { + const stack = new Stack(); + + const tableProps: dynamodb.TableProps = { + partitionKey: { + name: 'table_id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const existingTableInterface = new dynamodb.Table(stack, 'DynamoTable', tableProps); + + defaults.buildDynamoDBTable(stack, { + existingTableInterface + }); + + expectCDK(stack).to(haveResource('AWS::DynamoDB::Table', { + KeySchema: [ + { + AttributeName: "table_id", + KeyType: "HASH" + } + ] + })); + + expectCDK(stack).to(haveResource('AWS::DynamoDB::Table', { + StreamSpecification: { + StreamViewType: "NEW_IMAGE" + } + })); +}); + test('test getPartitionKeyNameFromTable()', () => { const partitionKeyName = 'testPartitionKey'; @@ -357,3 +390,75 @@ test('test getPartitionKeyNameFromTable()', () => { expect(testKeyName).toEqual(partitionKeyName); }); + +test('Test providing both existingTableInterface and existingTableObj', () => { + const stack = new Stack(); + + const tableProps: dynamodb.TableProps = { + partitionKey: { + name: 'table_id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const existingTableInterface = new dynamodb.Table(stack, 'DynamoTable', tableProps) + ; + const newProps = { + existingTableInterface, + existingTableObj: existingTableInterface + }; + const app = () => { + defaults.buildDynamoDBTable(stack, newProps); + }; + + expect(app).toThrowError('Error - Either provide existingTableInterface or existingTableObj, but not both.\n'); +}); + +test('Test providing both existingTableInterface and dynamoTableProps', () => { + const stack = new Stack(); + + const dynamoTableProps: dynamodb.TableProps = { + partitionKey: { + name: 'table_id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const existingTableInterface = new dynamodb.Table(stack, 'DynamoTable', dynamoTableProps) + ; + const newProps = { + existingTableInterface, + dynamoTableProps + }; + const app = () => { + defaults.buildDynamoDBTable(stack, newProps); + }; + + expect(app).toThrowError('Error - Either provide existingTableInterface or dynamoTableProps, but not both.\n'); +}); + +test('Test providing both existingTableObj and dynamoTableProps', () => { + const stack = new Stack(); + + const dynamoTableProps: dynamodb.TableProps = { + partitionKey: { + name: 'table_id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const existingTableObj = new dynamodb.Table(stack, 'DynamoTable', dynamoTableProps) + ; + const newProps = { + existingTableObj, + dynamoTableProps + }; + const app = () => { + defaults.buildDynamoDBTable(stack, newProps); + }; + + expect(app).toThrowError('Error - Either provide existingTableObj or dynamoTableProps, but not both.\n'); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts index 15d08d163..ea3759672 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts @@ -498,4 +498,4 @@ test('Test unsuccessful CheckListValues', () => { // Assertion expect(app).toThrowError('Invalid test value submitted - three'); -}); +}); \ No newline at end of file From a22694b89f69638582dba055841d5492b2f5cceb Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Team <67720492+aws-solutions-constructs-team@users.noreply.github.com> Date: Fri, 8 Apr 2022 21:13:17 -0400 Subject: [PATCH 10/34] chore(release): 1.150.0 (#654) * feat(aws-fargate-dynamodb): create new construct (#633) * created README for aws-fargate-dynamodb * created aws-fargate-dynamodb construct * revised buildDynamoDBTable helper function to support old/new constructs * updated prop variable name * added error and endpoint checking tests * checked for table permissions * checked props in input-validation * moved table interface and object check to buildDynamoDBTable function * created prop check function in dynamo helper file * chore(release): 1.150.0 * chore(changelog): Updated CHANGELOG.md * CDK version driven changes * Align CDK version * Sync CDk Versions * Sync CDK versions Co-authored-by: biffgaut <78155736+biffgaut@users.noreply.github.com> Co-authored-by: mickychetta <45010053+mickychetta@users.noreply.github.com> --- CHANGELOG.md | 8 + deployment/v2/align-version.js | 2 +- source/lerna.json | 2 +- .../aws-apigateway-dynamodb/lib/index.ts | 13 +- ...pfunctions-existing-eventbus.expected.json | 28 +- ...e-stepfunctions-new-eventbus.expected.json | 28 +- ...ge-stepfunctions-with-lambda.expected.json | 28 +- ...le-step-function-with-lambda.expected.json | 28 +- ...pfunctions-existing-eventbus.expected.json | 28 +- ...e-stepfunctions-new-eventbus.expected.json | 28 +- .../aws-fargate-dynamodb/.eslintignore | 4 + .../aws-fargate-dynamodb/.gitignore | 15 + .../aws-fargate-dynamodb/.npmignore | 21 + .../aws-fargate-dynamodb/README.md | 122 ++ .../aws-fargate-dynamodb/architecture.png | Bin 0 -> 129003 bytes .../aws-fargate-dynamodb/lib/index.ts | 210 +++ .../aws-fargate-dynamodb/package.json | 104 ++ .../test/fargate-dynamodb.test.ts | 672 ++++++++++ .../integ.existing-resources.expected.json | 1166 +++++++++++++++++ .../test/integ.existing-resources.ts | 57 + .../test/integ.new-resources.expected.json | 1166 +++++++++++++++++ .../test/integ.new-resources.ts | 40 + .../aws-lambda-dynamodb/lib/index.ts | 5 +- .../core/lib/dynamodb-table-helper.ts | 47 +- .../core/test/dynamo-table.test.ts | 105 ++ .../core/test/input-validation.test.ts | 2 +- 26 files changed, 3878 insertions(+), 51 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/fargate-dynamodb.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index aa77dfd81..1b3b0a671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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.150.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.149.0...v1.150.0) (2022-04-08) + +* Upgraded all patterns to CDK v1.150.0 + +### Features + +* **aws-fargate-dynamodb:** create new construct ([#633](https://github.com/awslabs/aws-solutions-constructs/issues/633)) ([0b35418](https://github.com/awslabs/aws-solutions-constructs/commit/0b35418b41e24b32b6064a649d77a70f1c6d7bd8)) + ## [1.149.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.5.0...v1.149.0) (2022-04-07) * Upgraded all patterns to CDK v1.149.0 diff --git a/deployment/v2/align-version.js b/deployment/v2/align-version.js index 8ea7db94f..2e2460be3 100755 --- a/deployment/v2/align-version.js +++ b/deployment/v2/align-version.js @@ -10,7 +10,7 @@ const findVersion = process.argv[2]; const replaceVersion = process.argv[3]; // these versions need to be sourced from a config file -const awsCdkLibVersion = '2.15.0'; +const awsCdkLibVersion = '2.18.0'; const constructsVersion = '10.0.0'; const MODULE_EXEMPTIONS = new Set([ '@aws-cdk/cloudformation-diff', diff --git a/source/lerna.json b/source/lerna.json index e62d7fb7b..24cc03ee9 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "1.149.0" + "version": "1.150.0" } diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts index 482ce8453..4d157410b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-dynamodb/lib/index.ts @@ -131,10 +131,13 @@ export class ApiGatewayToDynamoDB extends Construct { partitionKeyName = getPartitionKeyNameFromTable(props.existingTableObj); } + // Since we are only invoking this function with an existing Table or tableProps, + // (not a table interface), we know that the implementation will always return + // a Table object and we can safely cast away the optional aspect of the type. this.dynamoTable = defaults.buildDynamoDBTable(this, { existingTableObj: props.existingTableObj, - dynamoTableProps, - }); + dynamoTableProps: props.dynamoTableProps + })[1] as dynamodb.Table; // Setup the API Gateway [this.apiGateway, this.apiGatewayCloudWatchRole, this.apiGatewayLogGroup] = defaults.GlobalRestApi(this, @@ -170,7 +173,7 @@ export class ApiGatewayToDynamoDB extends Construct { readRequestTemplate = props.readRequestTemplate; } else { readRequestTemplate = - `{ \ + `{ \ "TableName": "${this.dynamoTable.tableName}", \ "KeyConditionExpression": "${partitionKeyName} = :v1", \ "ExpressionAttributeValues": { \ @@ -212,7 +215,7 @@ export class ApiGatewayToDynamoDB extends Construct { deleteRequestTemplate = props.deleteRequestTemplate; } else { deleteRequestTemplate = - `{ \ + `{ \ "TableName": "${this.dynamoTable.tableName}", \ "Key": { \ "${partitionKeyName}": { \ @@ -240,7 +243,7 @@ export class ApiGatewayToDynamoDB extends Construct { resources: [ this.dynamoTable.tableArn ], - actions: [ `${action}` ] + actions: [`${action}`] })); } } \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-existing-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-existing-eventbus.expected.json index 1e42dc9bc..e32840aaf 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-existing-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-existing-eventbus.expected.json @@ -210,12 +210,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-new-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-new-eventbus.expected.json index a5859fc61..7a01a4b7b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-new-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-new-eventbus.expected.json @@ -204,12 +204,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-with-lambda.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-with-lambda.expected.json index 9f8b60298..8502e7869 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-with-lambda.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-with-lambda.expected.json @@ -204,12 +204,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-step-function-with-lambda.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-step-function-with-lambda.expected.json index cf61862bb..b65d8e2c6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-step-function-with-lambda.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-step-function-with-lambda.expected.json @@ -204,12 +204,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-existing-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-existing-eventbus.expected.json index f64f4c73f..490c8ce5c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-existing-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-existing-eventbus.expected.json @@ -210,12 +210,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-new-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-new-eventbus.expected.json index f964d3fbe..6d4b63b22 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-new-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-new-eventbus.expected.json @@ -204,12 +204,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.eslintignore new file mode 100644 index 000000000..e6f7801ea --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.eslintignore @@ -0,0 +1,4 @@ +lib/*.js +test/*.js +*.d.ts +coverage diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.gitignore b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.npmignore b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/README.md new file mode 100644 index 000000000..6744b8c7b --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/README.md @@ -0,0 +1,122 @@ +# aws-fargate-dynamodb module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **Reference Documentation**:| https://docs.aws.amazon.com/solutions/latest/constructs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png) Python|`aws_solutions_constructs.aws_fargate_dynamodb`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-fargate-dynamodb`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.fargatedynamodb`| + +This AWS Solutions Construct implements an AWS Fargate service that can write/read to an Amazon DynamoDB table + +Here is a minimal deployable pattern definition: + +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { FargateToDynamoDB, FargateToDynamoDBProps } from '@aws-solutions-constructs/aws-fargate-dynamodb'; + +const constructProps: FargateToDynamoDBProps = { + publicApi: true, + ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", +}; + +new FargateToDynamoDB(stack, 'test-construct', constructProps); +``` + +Python +``` python +from aws_solutions_constructs.aws_fargate_dynamodb import FargateToDynamoDB, FargateToDynamoDBProps +from aws_cdk import ( + Stack +) +from constructs import Construct + +FargateToDynamoDB(self, 'test_construct', + public_api=True, + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.fargatedynamodb.*; + +new FargateToDynamoDB(this, "test-construct", new FargateToDynamoDBProps.Builder() + .publicApi(true) + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") + .build()); +``` + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| publicApi | `boolean` | Whether the construct is deploying a private or public API. This has implications for the VPC. | +| vpcProps? | [`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcProps.html) | Optional custom properties for a VPC the construct will create. This VPC will be used by any Private Hosted Zone the construct creates (that's why loadBalancerProps and privateHostedZoneProps can't include a VPC). Providing both this and existingVpc is an error. | +| existingVpc? | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | An existing VPC in which to deploy the construct. Providing both this and vpcProps is an error. If the client provides an existing load balancer and/or existing Private Hosted Zone, those constructs must exist in this VPC. | +| clusterProps? | [`ecs.ClusterProps`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ClusterProps.html) | Optional properties to create a new ECS cluster. To provide an existing cluster, use the cluster attribute of fargateServiceProps. | +| ecrRepositoryArn? | `string` | The arn of an ECR Repository containing the image to use to generate the containers. Either this or the image property of containerDefinitionProps must be provided. format: arn:aws:ecr:*region*:*account number*:repository/*Repository Name* | +| ecrImageVersion? | `string` | The version of the image to use from the repository. Defaults to 'Latest' | +| containerDefinitionProps? | [`ecs.ContainerDefinitionProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinitionProps.html) | Optional props to define the container created for the Fargate Service (defaults found in fargate-defaults.ts) | +| fargateTaskDefinitionProps? | [`ecs.FargateTaskDefinitionProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateTaskDefinitionProps.html) | Optional props to define the Fargate Task Definition for this construct (defaults found in fargate-defaults.ts) | +| fargateServiceProps? | [`ecs.FargateServiceProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateServiceProps.html) | Optional values to override default Fargate Task definition properties (fargate-defaults.ts). The construct will default to launching the service is the most isolated subnets available (precedence: Isolated, Private and Public). Override those and other defaults here. | +| existingFargateServiceObject? | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html) | A Fargate Service already instantiated (probably by another Solutions Construct). If this is specified, then no props defining a new service can be provided, including: ecrImageVersion, containerDefinitionProps, fargateTaskDefinitionProps, ecrRepositoryArn, fargateServiceProps, clusterProps | +| existingContainerDefinitionObject? | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinition.html) | A container definition already instantiated as part of a Fargate service. This must be the container in the existingFargateServiceObject | +| dynamoTableProps? | [`dynamodb.TableProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.TableProps.html)|Optional user provided props to override the default props for DynamoDB Table.| +|existingTableInterface?|[`dynamodb.ITable`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.ITable.html)|Existing instance of DynamoDB table object or interface, providing both this and `dynamoTableProps` will cause an error.| +| tablePermissions? |`string`|Optional table permissions to grant to the Fargate service. One of the following may be specified: `All`, `Read`, `ReadWrite`, `Write`.| +|tableArnEnvironmentVariableName?|`string`|Optional Name for the DynamoDB table arn environment variable set for the container.| +|tableEnvironmentVariableName?|`string`|Optional Name for the DynamoDB table name environment variable set for the container.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| vpc | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | The VPC used by the construct (whether created by the construct or provided by the client) | +| service | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html) | The AWS Fargate service used by this construct (whether created by this construct or passed to this construct at initialization) | +| container | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinition.html) | The container associated with the AWS Fargate service in the service property. | +|dynamoTableInterface|[`dynamodb.ITable`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.ITable.html)|Returns an instance of `dynamodb.ITable` created by the construct or the interface provided in existingTableInterface.| +|dynamoTable?|[`dynamodb.Table`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.Table.html)|Returns an instance of `dynamodb.Table` created by the construct. IMPORTANT: If existingTableInterface was provided in Pattern Construct Props, this property will be `undefined`.| + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### AWS Fargate Service +* Sets up an AWS Fargate service + * Uses the existing service if provided + * Creates a new service if none provided. + * Service will run in isolated subnets if available, then private subnets if available and finally public subnets + * Adds environment variables to the container with the ARN and Name of the DynamoDB table + * Add permissions to the container IAM role allowing it to publish to the DynamoDB table + +### Amazon DynamoDB Table +* Sets up an Amazon DynamoDB table + * Uses an existing table if one is provided, otherwise creates a new one +* Adds an Interface Endpoint to the VPC for DynamoDB (the service by default runs in Isolated or Private subnets) + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/architecture.png b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e32ad60487fbb50e6826fed8e613f4c20b0fce GIT binary patch literal 129003 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 z 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 Z3mnLLpP{~3`v>?>SKHuv;nn+3{ulbq0{Q>| literal 0 HcmV?d00001 diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/lib/index.ts new file mode 100644 index 000000000..016db217f --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/lib/index.ts @@ -0,0 +1,210 @@ +/** + * Copyright 2022 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. + */ + +import * as ec2 from "@aws-cdk/aws-ec2"; +import * as dynamodb from "@aws-cdk/aws-dynamodb"; +// Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate +import { Construct } from "@aws-cdk/core"; +import * as defaults from "@aws-solutions-constructs/core"; +import * as ecs from "@aws-cdk/aws-ecs"; + +export interface FargateToDynamoDBProps { + /** + * Whether the construct is deploying a private or public API. This has implications for the VPC deployed + * by this construct. + * + * @default - none + */ + readonly publicApi: boolean; + /** + * Optional custom properties for a VPC the construct will create. This VPC will + * be used by the new Fargate service the construct creates (that's + * why targetGroupProps can't include a VPC). Providing + * both this and existingVpc is an error. An DynamoDB Interface + * endpoint will be included in this VPC. + * + * @default - none + */ + readonly vpcProps?: ec2.VpcProps; + /** + * An existing VPC in which to deploy the construct. Providing both this and + * vpcProps is an error. If the client provides an existing Fargate service, + * this value must be the VPC where the service is running. An DynamoDB Interface + * endpoint will be added to this VPC. + * + * @default - none + */ + readonly existingVpc?: ec2.IVpc; + /** + * Optional properties to create a new ECS cluster + */ + readonly clusterProps?: ecs.ClusterProps; + /** + * The arn of an ECR Repository containing the image to use + * to generate the containers + * + * format: + * arn:aws:ecr:[region]:[account number]:repository/[Repository Name] + */ + readonly ecrRepositoryArn?: string; + /** + * The version of the image to use from the repository + * + * @default - 'latest' + */ + readonly ecrImageVersion?: string; + /* + * Optional props to define the container created for the Fargate Service + * + * defaults - fargate-defaults.ts + */ + readonly containerDefinitionProps?: ecs.ContainerDefinitionProps | any; + /* + * Optional props to define the Fargate Task Definition for this construct + * + * defaults - fargate-defaults.ts + */ + readonly fargateTaskDefinitionProps?: ecs.FargateTaskDefinitionProps | any; + /** + * Optional values to override default Fargate Task definition properties + * (fargate-defaults.ts). The construct will default to launching the service + * is the most isolated subnets available (precedence: Isolated, Private and + * Public). Override those and other defaults here. + * + * defaults - fargate-defaults.ts + */ + readonly fargateServiceProps?: ecs.FargateServiceProps | any; + /** + * A Fargate Service already instantiated (probably by another Solutions Construct). If + * this is specified, then no props defining a new service can be provided, including: + * existingImageObject, ecrImageVersion, containerDefintionProps, fargateTaskDefinitionProps, + * ecrRepositoryArn, fargateServiceProps, clusterProps, existingClusterInterface. If this value + * is provided, then existingContainerDefinitionObject must be provided as well. + * + * @default - none + */ + readonly existingFargateServiceObject?: ecs.FargateService; + /* + * A container definition already instantiated as part of a Fargate service. This must + * be the container in the existingFargateServiceObject. + * + * @default - None + */ + readonly existingContainerDefinitionObject?: ecs.ContainerDefinition; + /** + * Optional user provided props to override the default props for DynamoDB Table. + * + * @default - Default props are used + */ + readonly dynamoTableProps?: dynamodb.TableProps; + /** + * Optional user provided props to override the default props for DynamoDB Table. + * + * @default - None + */ + readonly existingTableInterface?: dynamodb.ITable; + /** + * Optional table permissions to grant to the Fargate service. One of the following may be specified: `All`, `Read`, `ReadWrite`, `Write`. + * + * @default - 'ReadWrite' + */ + readonly tablePermissions?: string + /** + * Optional Name for the DynamoDB table arn environment variable set for the container. + * + * @default - None + */ + readonly tableArnEnvironmentVariableName?: string; + /** + * Optional Name for the DynamoDB table name environment variable set for the container. + * + * @default - None + */ + readonly tableEnvironmentVariableName?: string; +} + +export class FargateToDynamoDB extends Construct { + public readonly vpc: ec2.IVpc; + public readonly service: ecs.FargateService; + public readonly container: ecs.ContainerDefinition; + public readonly dynamoTableInterface: dynamodb.ITable; + public readonly dynamoTable?: dynamodb.Table; + + constructor(scope: Construct, id: string, props: FargateToDynamoDBProps) { + super(scope, id); + defaults.CheckProps(props); + defaults.CheckFargateProps(props); + + // Other permissions for constructs are accepted as arrays, turning tablePermissions into + // an array to use the same validation function. + if (props.tablePermissions) { + const allowedPermissions = ['ALL', 'READ', 'READWRITE', 'WRITE']; + defaults.CheckListValues(allowedPermissions, [props.tablePermissions.toUpperCase()], 'tablePermission'); + } + + this.vpc = defaults.buildVpc(scope, { + existingVpc: props.existingVpc, + defaultVpcProps: props.publicApi ? defaults.DefaultPublicPrivateVpcProps() : defaults.DefaultIsolatedVpcProps(), + userVpcProps: props.vpcProps, + constructVpcProps: { enableDnsHostnames: true, enableDnsSupport: true } + }); + + defaults.AddAwsServiceEndpoint(scope, this.vpc, defaults.ServiceEndpointTypes.DYNAMODB); + + if (props.existingFargateServiceObject) { + this.service = props.existingFargateServiceObject; + // CheckFargateProps confirms that the container is provided + this.container = props.existingContainerDefinitionObject!; + } else { + [this.service, this.container] = defaults.CreateFargateService( + scope, + id, + this.vpc, + props.clusterProps, + props.ecrRepositoryArn, + props.ecrImageVersion, + props.fargateTaskDefinitionProps, + props.containerDefinitionProps, + props.fargateServiceProps + ); + } + + [this.dynamoTableInterface, this.dynamoTable] = defaults.buildDynamoDBTable(this, { + dynamoTableProps: props.dynamoTableProps, + existingTableInterface: props.existingTableInterface + }); + + // Add the requested or default table permissions + if (props.tablePermissions) { + const permission = props.tablePermissions.toUpperCase(); + + if (permission === 'ALL') { + this.dynamoTableInterface.grantFullAccess(this.service.taskDefinition.taskRole); + } else if (permission === 'READ') { + this.dynamoTableInterface.grantReadData(this.service.taskDefinition.taskRole); + } else if (permission === 'READWRITE') { + this.dynamoTableInterface.grantReadWriteData(this.service.taskDefinition.taskRole); + } else if (permission === 'WRITE') { + this.dynamoTableInterface.grantWriteData(this.service.taskDefinition.taskRole); + } + } else { + this.dynamoTableInterface.grantReadWriteData(this.service.taskDefinition.taskRole); + } + + // Add environment variables + const tableArnEnvironmentVariableName = props.tableArnEnvironmentVariableName || 'DYNAMODB_TABLE_ARN'; + this.container.addEnvironment(tableArnEnvironmentVariableName, this.dynamoTableInterface.tableArn); + const tableEnvironmentVariableName = props.tableEnvironmentVariableName || 'DYNAMODB_TABLE_NAME'; + this.container.addEnvironment(tableEnvironmentVariableName, this.dynamoTableInterface.tableName); + } +} diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/package.json b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/package.json new file mode 100644 index 000000000..60df16306 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/package.json @@ -0,0 +1,104 @@ +{ + "name": "@aws-solutions-constructs/aws-fargate-dynamodb", + "version": "0.0.0", + "description": "CDK Constructs for AWS Fargate to Amazon DynamoDB integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-constructs.git", + "directory": "source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awsconstructs.services.fargatedynamodb", + "maven": { + "groupId": "software.amazon.awsconstructs", + "artifactId": "fargatedynamodb" + } + }, + "dotnet": { + "namespace": "Amazon.SolutionsConstructs.AWS.FargateDynamoDB", + "packageId": "Amazon.SolutionsConstructs.AWS.FargateDynamoDB", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-constructs.aws-fargate-dynamodb", + "module": "aws_solutions_constructs.aws_fargate_dynamodb" + } + } + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@types/jest": "^26.0.22", + "@aws-solutions-constructs/core": "0.0.0", + "@types/node": "^10.3.0", + "constructs": "3.2.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ], + "coverageReporters": [ + "text", + [ + "lcov", + { + "projectRoot": "../../../../" + } + ] + ] + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "keywords": [ + "aws", + "cdk", + "awscdk", + "AWS Solutions Constructs", + "Amazon DynamoDB", + "AWS Fargate" + ] +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/fargate-dynamodb.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/fargate-dynamodb.test.ts new file mode 100644 index 000000000..be4d5347e --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/fargate-dynamodb.test.ts @@ -0,0 +1,672 @@ +/** + * Copyright 2022 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. + */ + +import '@aws-cdk/assert/jest'; +import * as defaults from '@aws-solutions-constructs/core'; +import * as cdk from "@aws-cdk/core"; +import { FargateToDynamoDB } from "../lib"; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as ecs from '@aws-cdk/aws-ecs'; + +test('New service/new table, public API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + const clusterName = "custom-cluster-name"; + const containerName = "custom-container-name"; + const serviceName = "custom-service-name"; + const tableName = "custom-table-name"; + const familyName = "family-name"; + + const construct = new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: familyName }, + fargateServiceProps: { serviceName }, + dynamoTableProps: { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + }, + tablePermissions: 'ReadWrite' + }); + + expect(construct.vpc !== null); + expect(construct.service !== null); + expect(construct.container !== null); + expect(construct.dynamoTable !== null); + expect(construct.dynamoTableInterface !== null); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Cluster", { + ClusterName: clusterName + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + 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": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + Family: familyName, + ContainerDefinitions: [ + { + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: containerName, + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.0.0.0/16' + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/new table, private API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const tableName = 'table-name'; + + new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + dynamoTableProps: { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + }, + tablePermissions: 'Read', + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName, + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.0.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan", + "dynamodb:ConditionCheckItem", + "dynamodb:DescribeTable" + ], + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/existing table, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const tableName = 'custom-table-name'; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const existingTable = new dynamodb.Table(stack, 'MyTable', { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + }, + }); + + const construct = new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + existingVpc, + existingTableInterface: existingTable, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + tablePermissions: 'ALL' + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: "dynamodb:*", + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "MyTable794EDED1", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + expect(construct.dynamoTable == null); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); +}); + +test('Existing service/new table, public API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + const serviceName = 'custom-name'; + const customName = 'CUSTOM_NAME'; + const customArn = 'CUSTOM_ARN'; + const tableName = 'table-name'; + + const existingVpc = defaults.getTestVpc(stack); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const construct = new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + tableArnEnvironmentVariableName: customArn, + tableEnvironmentVariableName: customName, + dynamoTableProps: { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + } + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: customArn, + Value: { + "Fn::GetAtt": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + } + }, + { + Name: customName, + Value: { + Ref: "testconstructDynamoTable67BDAFC5" + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + 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": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + expect(construct.dynamoTable == null); + + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); +}); + +test('Existing service/existing table, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const serviceName = 'custom-name'; + const tableName = 'custom-table-name'; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingTable = new dynamodb.Table(stack, 'MyTablet', { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + }); + + const construct = new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + tablePermissions: 'Write', + existingTableInterface: existingTable + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "DYNAMODB_TABLE_ARN", + Value: { + "Fn::GetAtt": [ + "MyTabletD7ADAF4F", + "Arn" + ] + } + }, + { + Name: "DYNAMODB_TABLE_NAME", + Value: { + Ref: "MyTabletD7ADAF4F" + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::DynamoDB::Table", { + TableName: tableName + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "dynamodb:BatchWriteItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" + ], + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "MyTabletD7ADAF4F", + "Arn" + ] + }, + { + Ref: "AWS::NoValue" + } + ] + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + }); + + expect(construct.dynamoTable == null); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::DynamoDB::Table', 1); +}); + +test('test error invalid table permission', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const serviceName = 'custom-name'; + const tableName = 'custom-table-name'; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingTable = new dynamodb.Table(stack, 'MyTablet', { + tableName, + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING + } + }); + + const app = () => { + new FargateToDynamoDB(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + tablePermissions: 'reed', + existingTableInterface: existingTable + }); + }; + + expect(app).toThrowError('Invalid tablePermission submitted - REED'); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.expected.json new file mode 100644 index 000000000..a146762a0 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.expected.json @@ -0,0 +1,1166 @@ +{ + "Description": "Integration Test with new VPC, Service and Table", + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "172.168.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "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": "existing-resources/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, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/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)" + } + ] + } + } + }, + "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": "existing-resources/Vpc" + } + ] + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRAPIsecuritygroup78294485", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRDKRsecuritygroup598BA37E", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "VpcDDB49FBEC5F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "DynamoTableB2B22E15": { + "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" + }, + "existingresourcesECRAPIsecuritygroup78294485": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-ECR_API-security-group", + "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" + } + ] + } + } + }, + "existingresourcesECRDKRsecuritygroup598BA37E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-ECR_DKR-security-group", + "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" + } + ] + } + } + }, + "testclusterDF8B0D19": { + "Type": "AWS::ECS::Cluster" + }, + "testtaskdefTaskRoleB2DEF113": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testtaskdefTaskRoleDefaultPolicy5D591D1C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "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": [ + "DynamoTableB2B22E15", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testtaskdefTaskRoleDefaultPolicy5D591D1C", + "Roles": [ + { + "Ref": "testtaskdefTaskRoleB2DEF113" + } + ] + } + }, + "testtaskdefF924AD58": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "CUSTOM_ARN", + "Value": { + "Fn::GetAtt": [ + "DynamoTableB2B22E15", + "Arn" + ] + } + }, + { + "Name": "CUSTOM_NAME", + "Value": { + "Ref": "DynamoTableB2B22E15" + } + } + ], + "Essential": true, + "Image": "nginx", + "MemoryReservation": 512, + "Name": "test-container", + "PortMappings": [ + { + "ContainerPort": 8080, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "existingresourcestesttaskdef88B214A2", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "testtaskdefTaskRoleB2DEF113", + "Arn" + ] + } + } + }, + "testsg872EB48A": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Construct created security group", + "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" + } + ] + } + } + }, + "testserviceService2730C249": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "testclusterDF8B0D19" + }, + "DeploymentConfiguration": { + "MaximumPercent": 150, + "MinimumHealthyPercent": 75 + }, + "DesiredCount": 2, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "testsg872EB48A", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "PlatformVersion": "LATEST", + "TaskDefinition": { + "Ref": "testtaskdefF924AD58" + } + } + } + }, + "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-fargate-dynamodb/test/integ.existing-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.ts new file mode 100644 index 000000000..673ea291d --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.existing-resources.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2022 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 { Aws, App, Stack } from "@aws-cdk/core"; +import { FargateToDynamoDB, FargateToDynamoDBProps } from "../lib"; +import { generateIntegStackName, getTestVpc, CreateFargateService } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as defaults from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and Table'; + +const existingVpc = getTestVpc(stack); +const [ existingTable ] = defaults.buildDynamoDBTable(stack, {}); + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const [testService, testContainer] = CreateFargateService(stack, + 'test', + existingVpc, + undefined, + undefined, + undefined, + undefined, + { image }, +); + +const constructProps: FargateToDynamoDBProps = { + publicApi: true, + existingVpc, + existingTableInterface: existingTable, + existingContainerDefinitionObject: testContainer, + existingFargateServiceObject: testService, + tableArnEnvironmentVariableName: 'CUSTOM_ARN', + tableEnvironmentVariableName: 'CUSTOM_NAME' +}; + +new FargateToDynamoDB(stack, 'test-construct', constructProps); + +defaults.suppressAutoDeleteHandlerWarnings(stack); +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.expected.json new file mode 100644 index 000000000..fe14f4736 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.expected.json @@ -0,0 +1,1166 @@ +{ + "Description": "Integration Test with new VPC, Service and Table", + "Resources": { + "testconstructDynamoTable67BDAFC5": { + "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" + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "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": "new-resources/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, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/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)" + } + ] + } + } + }, + "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": "new-resources/Vpc" + } + ] + } + }, + "VpcDDB49FBEC5F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".dynamodb" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRAPIsecuritygroupE52BAE3F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRDKRsecuritygroupBA34F94F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "newresourcesECRAPIsecuritygroupE52BAE3F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_API-security-group", + "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" + } + ] + } + } + }, + "newresourcesECRDKRsecuritygroupBA34F94F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_DKR-security-group", + "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" + } + ] + } + } + }, + "testconstructcluster7B6231C5": { + "Type": "AWS::ECS::Cluster" + }, + "testconstructtaskdefTaskRoleC60414C4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testconstructtaskdefTaskRoleDefaultPolicyF34A1535": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "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": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testconstructtaskdefTaskRoleDefaultPolicyF34A1535", + "Roles": [ + { + "Ref": "testconstructtaskdefTaskRoleC60414C4" + } + ] + } + }, + "testconstructtaskdef8BD1F9E4": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "DYNAMODB_TABLE_ARN", + "Value": { + "Fn::GetAtt": [ + "testconstructDynamoTable67BDAFC5", + "Arn" + ] + } + }, + { + "Name": "DYNAMODB_TABLE_NAME", + "Value": { + "Ref": "testconstructDynamoTable67BDAFC5" + } + } + ], + "Essential": true, + "Image": "nginx", + "MemoryReservation": 512, + "Name": "test-construct-container", + "PortMappings": [ + { + "ContainerPort": 8080, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "newresourcestestconstructtaskdefE4616A0D", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "testconstructtaskdefTaskRoleC60414C4", + "Arn" + ] + } + } + }, + "testconstructsgA602AA29": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Construct created security group", + "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" + } + ] + } + } + }, + "testconstructserviceService13074A8F": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "testconstructcluster7B6231C5" + }, + "DeploymentConfiguration": { + "MaximumPercent": 150, + "MinimumHealthyPercent": 75 + }, + "DesiredCount": 2, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "testconstructsgA602AA29", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "PlatformVersion": "LATEST", + "TaskDefinition": { + "Ref": "testconstructtaskdef8BD1F9E4" + } + } + } + }, + "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-fargate-dynamodb/test/integ.new-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.ts new file mode 100644 index 000000000..242691ffa --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-dynamodb/test/integ.new-resources.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2022 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 { Aws, App, Stack } from "@aws-cdk/core"; +import { FargateToDynamoDB, FargateToDynamoDBProps } from "../lib"; +import { generateIntegStackName, suppressAutoDeleteHandlerWarnings } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and Table'; + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const testProps: FargateToDynamoDBProps = { + publicApi: true, + containerDefinitionProps: { + image + } +}; + +new FargateToDynamoDB(stack, 'test-construct', testProps); + +suppressAutoDeleteHandlerWarnings(stack); +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts index d65071f9c..f18e349d1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-dynamodb/lib/index.ts @@ -122,10 +122,13 @@ export class LambdaToDynamoDB extends Construct { vpc: this.vpc }); + // Since we are only invoking this function with an existing Table or tableProps, + // (not a table interface), we know that the implementation will always return + // a Table object and we can safely cast away the optional aspect of the type. this.dynamoTable = defaults.buildDynamoDBTable(this, { dynamoTableProps: props.dynamoTableProps, existingTableObj: props.existingTableObj - }); + })[1] as dynamodb.Table; // Configure environment variables const tableEnvironmentVariableName = props.tableEnvironmentVariableName || 'DDB_TABLE_NAME'; diff --git a/source/patterns/@aws-solutions-constructs/core/lib/dynamodb-table-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/dynamodb-table-helper.ts index 492eae612..ef6dace07 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/dynamodb-table-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/dynamodb-table-helper.ts @@ -31,6 +31,13 @@ export interface BuildDynamoDBTableProps { * @default - None */ readonly existingTableObj?: dynamodb.Table + /** + * Existing instance of dynamodb interface. + * Providing both this and `dynamoTableProps` will cause an error. + * + * @default - None + */ + readonly existingTableInterface?: dynamodb.ITable } export interface BuildDynamoDBTableWithStreamProps { @@ -49,14 +56,42 @@ export interface BuildDynamoDBTableWithStreamProps { readonly existingTableInterface?: dynamodb.ITable } -export function buildDynamoDBTable(scope: Construct, props: BuildDynamoDBTableProps): dynamodb.Table { +export function buildDynamoDBTable(scope: Construct, props: BuildDynamoDBTableProps): [dynamodb.ITable, dynamodb.Table?] { + checkTableProps(props); + // Conditional DynamoDB Table creation - if (!props.existingTableObj) { - // Set the default props for DynamoDB table - const dynamoTableProps = consolidateProps(DefaultTableProps, props.dynamoTableProps); - return new dynamodb.Table(scope, 'DynamoTable', dynamoTableProps); + if (props.existingTableObj) { + return [props.existingTableObj, props.existingTableObj]; + } else if (props.existingTableInterface) { + return [props.existingTableInterface, undefined]; } else { - return props.existingTableObj; + const consolidatedTableProps = consolidateProps(DefaultTableProps, props.dynamoTableProps); + const newTable = new dynamodb.Table(scope, 'DynamoTable', consolidatedTableProps); + return [newTable, newTable]; + } +} + +export function checkTableProps(props: BuildDynamoDBTableProps) { + let errorMessages = ''; + let errorFound = false; + + if (props.dynamoTableProps && props.existingTableObj) { + errorMessages += 'Error - Either provide existingTableObj or dynamoTableProps, but not both.\n'; + errorFound = true; + } + + if (props.dynamoTableProps && props.existingTableInterface) { + errorMessages += 'Error - Either provide existingTableInterface or dynamoTableProps, but not both.\n'; + errorFound = true; + } + + if (props.existingTableObj && props.existingTableInterface) { + errorMessages += 'Error - Either provide existingTableInterface or existingTableObj, but not both.\n'; + errorFound = true; + } + + if (errorFound) { + throw new Error(errorMessages); } } diff --git a/source/patterns/@aws-solutions-constructs/core/test/dynamo-table.test.ts b/source/patterns/@aws-solutions-constructs/core/test/dynamo-table.test.ts index 2f90157cf..204af56fa 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/dynamo-table.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/dynamo-table.test.ts @@ -332,6 +332,39 @@ test('test buildDynamoDBTableWithStream with existingTableObj', () => { })); }); +test('test buildDynamoDBTable with existingTableInterface', () => { + const stack = new Stack(); + + const tableProps: dynamodb.TableProps = { + partitionKey: { + name: 'table_id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const existingTableInterface = new dynamodb.Table(stack, 'DynamoTable', tableProps); + + defaults.buildDynamoDBTable(stack, { + existingTableInterface + }); + + expectCDK(stack).to(haveResource('AWS::DynamoDB::Table', { + KeySchema: [ + { + AttributeName: "table_id", + KeyType: "HASH" + } + ] + })); + + expectCDK(stack).to(haveResource('AWS::DynamoDB::Table', { + StreamSpecification: { + StreamViewType: "NEW_IMAGE" + } + })); +}); + test('test getPartitionKeyNameFromTable()', () => { const partitionKeyName = 'testPartitionKey'; @@ -357,3 +390,75 @@ test('test getPartitionKeyNameFromTable()', () => { expect(testKeyName).toEqual(partitionKeyName); }); + +test('Test providing both existingTableInterface and existingTableObj', () => { + const stack = new Stack(); + + const tableProps: dynamodb.TableProps = { + partitionKey: { + name: 'table_id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const existingTableInterface = new dynamodb.Table(stack, 'DynamoTable', tableProps) + ; + const newProps = { + existingTableInterface, + existingTableObj: existingTableInterface + }; + const app = () => { + defaults.buildDynamoDBTable(stack, newProps); + }; + + expect(app).toThrowError('Error - Either provide existingTableInterface or existingTableObj, but not both.\n'); +}); + +test('Test providing both existingTableInterface and dynamoTableProps', () => { + const stack = new Stack(); + + const dynamoTableProps: dynamodb.TableProps = { + partitionKey: { + name: 'table_id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const existingTableInterface = new dynamodb.Table(stack, 'DynamoTable', dynamoTableProps) + ; + const newProps = { + existingTableInterface, + dynamoTableProps + }; + const app = () => { + defaults.buildDynamoDBTable(stack, newProps); + }; + + expect(app).toThrowError('Error - Either provide existingTableInterface or dynamoTableProps, but not both.\n'); +}); + +test('Test providing both existingTableObj and dynamoTableProps', () => { + const stack = new Stack(); + + const dynamoTableProps: dynamodb.TableProps = { + partitionKey: { + name: 'table_id', + type: dynamodb.AttributeType.STRING + }, + stream: dynamodb.StreamViewType.NEW_IMAGE + }; + + const existingTableObj = new dynamodb.Table(stack, 'DynamoTable', dynamoTableProps) + ; + const newProps = { + existingTableObj, + dynamoTableProps + }; + const app = () => { + defaults.buildDynamoDBTable(stack, newProps); + }; + + expect(app).toThrowError('Error - Either provide existingTableObj or dynamoTableProps, but not both.\n'); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts index 15d08d163..ea3759672 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/input-validation.test.ts @@ -498,4 +498,4 @@ test('Test unsuccessful CheckListValues', () => { // Assertion expect(app).toThrowError('Invalid test value submitted - three'); -}); +}); \ No newline at end of file From 5cd5e60c9fb1c6fd65a082a9548021f067cec397 Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Team <67720492+aws-solutions-constructs-team@users.noreply.github.com> Date: Fri, 8 Apr 2022 21:54:44 -0400 Subject: [PATCH 11/34] chore(release): 1.150.0 (#654) (#655) * feat(aws-fargate-dynamodb): create new construct (#633) * created README for aws-fargate-dynamodb * created aws-fargate-dynamodb construct * revised buildDynamoDBTable helper function to support old/new constructs * updated prop variable name * added error and endpoint checking tests * checked for table permissions * checked props in input-validation * moved table interface and object check to buildDynamoDBTable function * created prop check function in dynamo helper file * chore(release): 1.150.0 * chore(changelog): Updated CHANGELOG.md * CDK version driven changes * Align CDK version * Sync CDk Versions * Sync CDK versions Co-authored-by: biffgaut <78155736+biffgaut@users.noreply.github.com> Co-authored-by: mickychetta <45010053+mickychetta@users.noreply.github.com> Co-authored-by: biffgaut <78155736+biffgaut@users.noreply.github.com> Co-authored-by: mickychetta <45010053+mickychetta@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++ deployment/v2/align-version.js | 2 +- source/lerna.json | 2 +- ...pfunctions-existing-eventbus.expected.json | 28 +++++++++++++++---- ...e-stepfunctions-new-eventbus.expected.json | 28 +++++++++++++++---- ...ge-stepfunctions-with-lambda.expected.json | 28 +++++++++++++++---- ...le-step-function-with-lambda.expected.json | 28 +++++++++++++++---- ...pfunctions-existing-eventbus.expected.json | 28 +++++++++++++++---- ...e-stepfunctions-new-eventbus.expected.json | 28 +++++++++++++++---- 9 files changed, 142 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa77dfd81..1b3b0a671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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.150.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.149.0...v1.150.0) (2022-04-08) + +* Upgraded all patterns to CDK v1.150.0 + +### Features + +* **aws-fargate-dynamodb:** create new construct ([#633](https://github.com/awslabs/aws-solutions-constructs/issues/633)) ([0b35418](https://github.com/awslabs/aws-solutions-constructs/commit/0b35418b41e24b32b6064a649d77a70f1c6d7bd8)) + ## [1.149.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.5.0...v1.149.0) (2022-04-07) * Upgraded all patterns to CDK v1.149.0 diff --git a/deployment/v2/align-version.js b/deployment/v2/align-version.js index 8ea7db94f..2e2460be3 100755 --- a/deployment/v2/align-version.js +++ b/deployment/v2/align-version.js @@ -10,7 +10,7 @@ const findVersion = process.argv[2]; const replaceVersion = process.argv[3]; // these versions need to be sourced from a config file -const awsCdkLibVersion = '2.15.0'; +const awsCdkLibVersion = '2.18.0'; const constructsVersion = '10.0.0'; const MODULE_EXEMPTIONS = new Set([ '@aws-cdk/cloudformation-diff', diff --git a/source/lerna.json b/source/lerna.json index e62d7fb7b..24cc03ee9 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "1.149.0" + "version": "1.150.0" } diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-existing-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-existing-eventbus.expected.json index 1e42dc9bc..e32840aaf 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-existing-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-existing-eventbus.expected.json @@ -210,12 +210,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-new-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-new-eventbus.expected.json index a5859fc61..7a01a4b7b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-new-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-new-eventbus.expected.json @@ -204,12 +204,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-with-lambda.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-with-lambda.expected.json index 9f8b60298..8502e7869 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-with-lambda.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-stepfunctions/test/integ.eventbridge-stepfunctions-with-lambda.expected.json @@ -204,12 +204,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-step-function-with-lambda.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-step-function-with-lambda.expected.json index cf61862bb..b65d8e2c6 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-step-function-with-lambda.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-step-function-with-lambda.expected.json @@ -204,12 +204,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-existing-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-existing-eventbus.expected.json index f64f4c73f..490c8ce5c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-existing-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-existing-eventbus.expected.json @@ -210,12 +210,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-new-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-new-eventbus.expected.json index f964d3fbe..6d4b63b22 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-new-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-step-function/test/integ.events-rule-stepfunctions-new-eventbus.expected.json @@ -204,12 +204,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "LambdaFunctionBF21E41F", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaFunctionBF21E41F", + "Arn" + ] + }, + ":*" + ] + ] + } + ] }, { "Action": [ From 12151b18cd7b57c2271fc788e867edae90fce126 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 9 Apr 2022 10:50:03 -0400 Subject: [PATCH 12/34] chore(release): 1.150.0 (#654) (#658) * feat(aws-fargate-dynamodb): create new construct (#633) * created README for aws-fargate-dynamodb * created aws-fargate-dynamodb construct * revised buildDynamoDBTable helper function to support old/new constructs * updated prop variable name * added error and endpoint checking tests * checked for table permissions * checked props in input-validation * moved table interface and object check to buildDynamoDBTable function * created prop check function in dynamo helper file * chore(release): 1.150.0 * chore(changelog): Updated CHANGELOG.md * CDK version driven changes * Align CDK version * Sync CDk Versions * Sync CDK versions Co-authored-by: biffgaut <78155736+biffgaut@users.noreply.github.com> Co-authored-by: mickychetta <45010053+mickychetta@users.noreply.github.com> Co-authored-by: AWS Solutions Constructs Team <67720492+aws-solutions-constructs-team@users.noreply.github.com> Co-authored-by: mickychetta <45010053+mickychetta@users.noreply.github.com> From 7a1451f7fa3550baef49e3200bf466baabb36968 Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Automation Date: Sat, 9 Apr 2022 14:53:06 +0000 Subject: [PATCH 13/34] chore(release): 1.151.0 --- CHANGELOG.md | 7 +++++++ source/lerna.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3b0a671..8f92099c4 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.151.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.149.0...v1.151.0) (2022-04-09) + + +### Features + +* **aws-fargate-dynamodb:** create new construct ([#633](https://github.com/awslabs/aws-solutions-constructs/issues/633)) ([0b35418](https://github.com/awslabs/aws-solutions-constructs/commit/0b35418b41e24b32b6064a649d77a70f1c6d7bd8)) + ## [1.150.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.149.0...v1.150.0) (2022-04-08) * Upgraded all patterns to CDK v1.150.0 diff --git a/source/lerna.json b/source/lerna.json index 24cc03ee9..120fbc9da 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "1.150.0" + "version": "1.151.0" } From 15aaad634b0ad34febff540ebbe672231421c16d Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 9 Apr 2022 10:54:48 -0400 Subject: [PATCH 14/34] chore(changelog): Updated CHANGELOG.md --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f92099c4..ed9e855c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,7 @@ All notable changes to this project will be documented in this file. See [standa ## [1.151.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.149.0...v1.151.0) (2022-04-09) - -### Features - -* **aws-fargate-dynamodb:** create new construct ([#633](https://github.com/awslabs/aws-solutions-constructs/issues/633)) ([0b35418](https://github.com/awslabs/aws-solutions-constructs/commit/0b35418b41e24b32b6064a649d77a70f1c6d7bd8)) +* Upgraded all patterns to CDK v1.151.0 ## [1.150.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.149.0...v1.150.0) (2022-04-08) From 02c4e274ab933e8a423d47f161cfa7f448f54039 Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Automation Date: Sun, 10 Apr 2022 00:30:24 +0000 Subject: [PATCH 15/34] chore(release): 1.152.0 --- CHANGELOG.md | 2 ++ source/lerna.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed9e855c4..29d0c634b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ 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.152.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.151.0...v1.152.0) (2022-04-10) + ## [1.151.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.149.0...v1.151.0) (2022-04-09) * Upgraded all patterns to CDK v1.151.0 diff --git a/source/lerna.json b/source/lerna.json index 120fbc9da..d2d706160 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "1.151.0" + "version": "1.152.0" } From 5654067954c742fe7fe07f63d4ef6dfb66a7e188 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 9 Apr 2022 20:32:09 -0400 Subject: [PATCH 16/34] chore(changelog): Updated CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d0c634b..2de95a4e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. See [standa ## [1.152.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.151.0...v1.152.0) (2022-04-10) +* Upgraded all patterns to CDK v1.152.0 + ## [1.151.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.149.0...v1.151.0) (2022-04-09) * Upgraded all patterns to CDK v1.151.0 From bd4bcfc3bf27f9f6aa250e0b240b8d8db2562d36 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 9 Apr 2022 20:50:05 -0400 Subject: [PATCH 17/34] Sync with CDK changes --- .../aws-s3-lambda/test/integ.existing-s3-bucket.expected.json | 2 +- .../aws-s3-lambda/test/integ.no-arguments.expected.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-lambda/test/integ.existing-s3-bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-lambda/test/integ.existing-s3-bucket.expected.json index 0ee54450b..882fbd773 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-lambda/test/integ.existing-s3-bucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-lambda/test/integ.existing-s3-bucket.expected.json @@ -297,7 +297,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n\n # find external notifications\n external_notifications = find_external_notifications(bucket, stack_id)\n\n # if delete, that's all we need\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n # otherwise, merge external with incoming config and augment with id\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n return notifications\n\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n # if the notification was created by us, we know what id to expect\n # so we can filter by it.\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n return external_notifications\n\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" }, "Handler": "index.handler", "Role": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-lambda/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-lambda/test/integ.no-arguments.expected.json index 3e9927b1f..9da52e52a 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-lambda/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-lambda/test/integ.no-arguments.expected.json @@ -355,7 +355,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n\n # find external notifications\n external_notifications = find_external_notifications(bucket, stack_id)\n\n # if delete, that's all we need\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n # otherwise, merge external with incoming config and augment with id\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n return notifications\n\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n # if the notification was created by us, we know what id to expect\n # so we can filter by it.\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n return external_notifications\n\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" }, "Handler": "index.handler", "Role": { From ae14a261026ec920d44334f7dd555ee1b37a0b7b Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 9 Apr 2022 21:31:37 -0400 Subject: [PATCH 18/34] Sync CDK Changes --- .../aws-s3-sqs/test/integ.creatingNewQueue.expected.json | 2 +- .../aws-s3-sqs/test/integ.customLoggingBucket.expected.json | 2 +- .../aws-s3-sqs/test/integ.existingQueue.expected.json | 2 +- .../aws-s3-sqs/test/integ.existingS3Bucket.expected.json | 2 +- .../aws-s3-sqs/test/integ.noArguments.expected.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.creatingNewQueue.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.creatingNewQueue.expected.json index 10ea7c2bb..321370708 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.creatingNewQueue.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.creatingNewQueue.expected.json @@ -518,7 +518,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n\n # find external notifications\n external_notifications = find_external_notifications(bucket, stack_id)\n\n # if delete, that's all we need\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n # otherwise, merge external with incoming config and augment with id\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n return notifications\n\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n # if the notification was created by us, we know what id to expect\n # so we can filter by it.\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n return external_notifications\n\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" }, "Handler": "index.handler", "Role": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.customLoggingBucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.customLoggingBucket.expected.json index 6ed14cce3..4776606f0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.customLoggingBucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.customLoggingBucket.expected.json @@ -544,7 +544,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n\n # find external notifications\n external_notifications = find_external_notifications(bucket, stack_id)\n\n # if delete, that's all we need\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n # otherwise, merge external with incoming config and augment with id\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n return notifications\n\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n # if the notification was created by us, we know what id to expect\n # so we can filter by it.\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n return external_notifications\n\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" }, "Handler": "index.handler", "Role": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.existingQueue.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.existingQueue.expected.json index ac93bc340..874dc5b09 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.existingQueue.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.existingQueue.expected.json @@ -377,7 +377,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n\n # find external notifications\n external_notifications = find_external_notifications(bucket, stack_id)\n\n # if delete, that's all we need\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n # otherwise, merge external with incoming config and augment with id\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n return notifications\n\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n # if the notification was created by us, we know what id to expect\n # so we can filter by it.\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n return external_notifications\n\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" }, "Handler": "index.handler", "Role": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.existingS3Bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.existingS3Bucket.expected.json index 090879240..8e4a38eaf 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.existingS3Bucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.existingS3Bucket.expected.json @@ -543,7 +543,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n\n # find external notifications\n external_notifications = find_external_notifications(bucket, stack_id)\n\n # if delete, that's all we need\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n # otherwise, merge external with incoming config and augment with id\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n return notifications\n\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n # if the notification was created by us, we know what id to expect\n # so we can filter by it.\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n return external_notifications\n\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" }, "Handler": "index.handler", "Role": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.noArguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.noArguments.expected.json index 35e4918ff..9eaaa45e5 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.noArguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-sqs/test/integ.noArguments.expected.json @@ -543,7 +543,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n\n # find external notifications\n external_notifications = find_external_notifications(bucket, stack_id)\n\n # if delete, that's all we need\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n # otherwise, merge external with incoming config and augment with id\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n return notifications\n\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n # if the notification was created by us, we know what id to expect\n # so we can filter by it.\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n return external_notifications\n\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" }, "Handler": "index.handler", "Role": { From 20391ddb8fe05e4a2201d784e3bd8b0ab67f3b14 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 9 Apr 2022 21:43:14 -0400 Subject: [PATCH 19/34] sync CDK versions --- deployment/v2/align-version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/v2/align-version.js b/deployment/v2/align-version.js index 2e2460be3..e9fd11738 100755 --- a/deployment/v2/align-version.js +++ b/deployment/v2/align-version.js @@ -10,7 +10,7 @@ const findVersion = process.argv[2]; const replaceVersion = process.argv[3]; // these versions need to be sourced from a config file -const awsCdkLibVersion = '2.18.0'; +const awsCdkLibVersion = '2.19.0'; const constructsVersion = '10.0.0'; const MODULE_EXEMPTIONS = new Set([ '@aws-cdk/cloudformation-diff', From ddfa6fc4136432178fc911b45bccbed618c849f0 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 9 Apr 2022 21:54:17 -0400 Subject: [PATCH 20/34] sync CDK versions --- deployment/v2/align-version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/v2/align-version.js b/deployment/v2/align-version.js index e9fd11738..76f72e1ab 100755 --- a/deployment/v2/align-version.js +++ b/deployment/v2/align-version.js @@ -10,7 +10,7 @@ const findVersion = process.argv[2]; const replaceVersion = process.argv[3]; // these versions need to be sourced from a config file -const awsCdkLibVersion = '2.19.0'; +const awsCdkLibVersion = '2.20.0'; const constructsVersion = '10.0.0'; const MODULE_EXEMPTIONS = new Set([ '@aws-cdk/cloudformation-diff', From bb217597fcd3b16a8a33354503708e2100284fdb Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Tue, 12 Apr 2022 16:53:51 -0400 Subject: [PATCH 21/34] Made code more readable (#664) --- .../aws-route53-alb/lib/index.ts | 21 ++-- .../core/lib/vpc-helper.ts | 118 ++++++++++-------- 2 files changed, 78 insertions(+), 61 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-route53-alb/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-route53-alb/lib/index.ts index 5f17d54c0..9e50008cb 100644 --- a/source/patterns/@aws-solutions-constructs/aws-route53-alb/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-route53-alb/lib/index.ts @@ -121,18 +121,15 @@ export class Route53ToAlb extends Construct { throw new Error('An existing Private Hosted Zone already exists in a VPC, so that VPC must be passed to the construct in props.existingVpc'); } - if (props.existingVpc) { - this.vpc = props.existingVpc; - } else { - this.vpc = defaults.buildVpc(scope, { - defaultVpcProps: props.publicApi ? - defaults.DefaultPublicPrivateVpcProps() : - // If this is an internal app, we're going to turn on DNS - // by default to allow gateway and interface service endpoints - defaults.overrideProps(defaults.DefaultIsolatedVpcProps(), { enableDnsHostnames: true, enableDnsSupport: true, }), - userVpcProps: props.vpcProps, - }); - } + this.vpc = defaults.buildVpc(scope, { + existingVpc: props.existingVpc, + defaultVpcProps: props.publicApi ? + defaults.DefaultPublicPrivateVpcProps() : + // If this is an internal app, we're going to turn on DNS + // by default to allow gateway and interface service endpoints + defaults.overrideProps(defaults.DefaultIsolatedVpcProps(), { enableDnsHostnames: true, enableDnsSupport: true, }), + userVpcProps: props.vpcProps, + }); if (props.existingHostedZoneInterface) { this.hostedZone = props.existingHostedZoneInterface; diff --git a/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts index 170186d64..f8809948a 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/vpc-helper.ts @@ -51,25 +51,8 @@ export function buildVpc(scope: Construct, props: BuildVpcProps): ec2.IVpc { // Add VPC FlowLogs with the default setting of trafficType:ALL and destination: CloudWatch Logs const flowLog: ec2.FlowLog = vpc.addFlowLog("FlowLog"); - // Add Cfn Nag suppression for PUBLIC subnets to suppress WARN W33: EC2 Subnet should not have MapPublicIpOnLaunch set to true - vpc.publicSubnets.forEach((subnet) => { - const cfnSubnet = subnet.node.defaultChild as ec2.CfnSubnet; - addCfnSuppressRules(cfnSubnet, [ - { - id: 'W33', - reason: 'Allow Public Subnets to have MapPublicIpOnLaunch set to true' - } - ]); - }); - - // Add Cfn Nag suppression for CloudWatchLogs LogGroups data is encrypted - const cfnLogGroup: CfnLogGroup = flowLog.logGroup?.node.defaultChild as CfnLogGroup; - addCfnSuppressRules(cfnLogGroup, [ - { - id: 'W84', - reason: 'By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)' - } - ]); + SuppressMapPublicIpWarnings(vpc); + SuppressEncryptedLogWarnings(flowLog); return vpc; } @@ -163,39 +146,76 @@ export function AddAwsServiceEndpoint( vpc: ec2.IVpc, interfaceTag: ServiceEndpointTypes ) { - if (!vpc.node.children.some((child) => child.node.id === interfaceTag)) { - const service = endpointSettings.find( - (endpoint) => endpoint.endpointName === interfaceTag - ); + if (CheckIfEndpointAlreadyExists(vpc, interfaceTag)) { + return; + } - if (!service) { - throw new Error("Unsupported Service sent to AddServiceEndpoint"); - } + const service = endpointSettings.find( + (endpoint) => endpoint.endpointName === interfaceTag + ); - if (service.endpointType === EndpointTypes.GATEWAY) { - vpc.addGatewayEndpoint(interfaceTag, { - service: service.endpointGatewayService as ec2.GatewayVpcEndpointAwsService, - }); - } - if (service.endpointType === EndpointTypes.INTERFACE) { - - const endpointDefaultSecurityGroup = buildSecurityGroup( - scope, - `${scope.node.id}-${service.endpointName}`, - { - vpc, - allowAllOutbound: true, - }, - [{ peer: ec2.Peer.ipv4(vpc.vpcCidrBlock), connection: ec2.Port.tcp(443) }], - [] - ); - - vpc.addInterfaceEndpoint(interfaceTag, { - service: service.endpointInterfaceService as ec2.InterfaceVpcEndpointAwsService, - securityGroups: [endpointDefaultSecurityGroup], - }); - } + if (!service) { + throw new Error("Unsupported Service sent to AddServiceEndpoint"); + } + + if (service.endpointType === EndpointTypes.GATEWAY) { + AddGatewayEndpoint(vpc, service, interfaceTag); + } + if (service.endpointType === EndpointTypes.INTERFACE) { + AddInterfaceEndpoint(scope, vpc, service, interfaceTag); } return; } + +function CheckIfEndpointAlreadyExists(vpc: ec2.IVpc, interfaceTag: ServiceEndpointTypes): boolean { + return vpc.node.children.some((child) => child.node.id === interfaceTag); +} + +function SuppressMapPublicIpWarnings(vpc: ec2.Vpc) { + // Add Cfn Nag suppression for PUBLIC subnets to suppress WARN W33: EC2 Subnet should not have MapPublicIpOnLaunch set to true + vpc.publicSubnets.forEach((subnet) => { + const cfnSubnet = subnet.node.defaultChild as ec2.CfnSubnet; + addCfnSuppressRules(cfnSubnet, [ + { + id: 'W33', + reason: 'Allow Public Subnets to have MapPublicIpOnLaunch set to true' + } + ]); + }); +} + +function SuppressEncryptedLogWarnings(flowLog: ec2.FlowLog) { + // Add Cfn Nag suppression for CloudWatchLogs LogGroups data is encrypted + const cfnLogGroup: CfnLogGroup = flowLog.logGroup?.node.defaultChild as CfnLogGroup; + addCfnSuppressRules(cfnLogGroup, [ + { + id: 'W84', + reason: 'By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)' + } + ]); +} + +function AddInterfaceEndpoint(scope: Construct, vpc: ec2.IVpc, service: EndpointDefinition, interfaceTag: ServiceEndpointTypes) { + const endpointDefaultSecurityGroup = buildSecurityGroup( + scope, + `${scope.node.id}-${service.endpointName}`, + { + vpc, + allowAllOutbound: true, + }, + [{ peer: ec2.Peer.ipv4(vpc.vpcCidrBlock), connection: ec2.Port.tcp(443) }], + [] + ); + + vpc.addInterfaceEndpoint(interfaceTag, { + service: service.endpointInterfaceService as ec2.InterfaceVpcEndpointAwsService, + securityGroups: [endpointDefaultSecurityGroup], + }); +} + +function AddGatewayEndpoint(vpc: ec2.IVpc, service: EndpointDefinition, interfaceTag: ServiceEndpointTypes) { + vpc.addGatewayEndpoint(interfaceTag, { + service: service.endpointGatewayService as ec2.GatewayVpcEndpointAwsService, + }); +} \ No newline at end of file From bc2f733879a5363407729e1f236302c9361ff652 Mon Sep 17 00:00:00 2001 From: mickychetta <45010053+mickychetta@users.noreply.github.com> Date: Fri, 22 Apr 2022 11:36:01 -0700 Subject: [PATCH 22/34] feat(aws-s3-stepfunctions): Changed escape hatch to eventBridgeEnabled prop (#666) * changed escape hatch to eventBridgeEnabled prop * suppres cfn nag notification handler warnings * suppress notification handler cfn nag warnings * suppress notification handler cfn nag warnings * added comments of required eventBridgeEnabled property --- .../integ.customLoggingBucket.expected.json | 125 +++++++++++++++++- .../integ.pre-existing-bucket.expected.json | 125 +++++++++++++++++- .../test/integ.pre-existing-bucket.ts | 12 +- ...s3-step-function-no-argument.expected.json | 125 +++++++++++++++++- .../aws-s3-stepfunctions/README.md | 2 +- .../aws-s3-stepfunctions/lib/index.ts | 10 +- .../integ.customLoggingBucket.expected.json | 125 +++++++++++++++++- .../test/integ.customLoggingBucket.ts | 1 + .../integ.pre-existing-bucket.expected.json | 125 +++++++++++++++++- .../test/integ.pre-existing-bucket.ts | 12 +- ...s3-stepfunctions-no-argument.expected.json | 125 +++++++++++++++++- .../test/s3-stepfunctions.test.ts | 5 +- 12 files changed, 741 insertions(+), 51 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.customLoggingBucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.customLoggingBucket.expected.json index a9bc42591..d862af4be 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.customLoggingBucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.customLoggingBucket.expected.json @@ -114,11 +114,6 @@ "Ref": "tests3stepfunctiontests3stepfunctionWS3LoggingBucketB716417C" } }, - "NotificationConfiguration": { - "EventBridgeConfiguration": { - "EventBridgeEnabled": true - } - }, "PublicAccessBlockConfiguration": { "BlockPublicAcls": true, "BlockPublicPolicy": true, @@ -179,6 +174,24 @@ } } }, + "tests3stepfunctiontests3stepfunctionWS3BucketNotificationsC4380C1D": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "tests3stepfunctiontests3stepfunctionWS3Bucket9BE64924" + }, + "NotificationConfiguration": { + "EventBridgeConfiguration": {} + }, + "Managed": true + } + }, "tests3stepfunctiontests3stepfunctionWtests3stepfunctionWeventrulestepfunctionconstructStateMachineLogGroupE83EECDD": { "Type": "AWS::Logs::LogGroup", "Properties": { @@ -461,6 +474,108 @@ "Statistic": "Maximum", "Threshold": 1 } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time" + } + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "python3.7", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ], + "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" + } + ] + } + } } }, "Mappings": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.expected.json index 268deaf53..2dd9fee6b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.expected.json @@ -12,11 +12,6 @@ } ] }, - "NotificationConfiguration": { - "EventBridgeConfiguration": { - "EventBridgeEnabled": true - } - }, "VersioningConfiguration": { "Status": "Enabled" } @@ -42,6 +37,126 @@ } } }, + "existingScriptLocationNotificationsC550EA17": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "existingScriptLocation845F3C51" + }, + "NotificationConfiguration": { + "EventBridgeConfiguration": {} + }, + "Managed": true + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time" + } + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "python3.7", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ], + "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" + } + ] + } + } + }, "tests3stepfunctionpreexistingbucketconstructtests3stepfunctionpreexistingbucketconstructWtests3stepfunctionpreexistingbucketconstructWeventrulestepfunctionconstructStateMachineLogGroupF215071C": { "Type": "AWS::Logs::LogGroup", "Properties": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.ts b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.ts index 029b41fbb..85e5ca118 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.ts @@ -15,16 +15,14 @@ import { App, Stack, RemovalPolicy } from "@aws-cdk/core"; import { S3ToStepFunction, S3ToStepFunctionProps } from "../lib"; import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; -import * as s3 from '@aws-cdk/aws-s3'; -import { CreateScrapBucket } from '@aws-solutions-constructs/core'; -import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import { CreateScrapBucket, generateIntegStackName, addCfnNagS3BucketNotificationRulesToSuppress } from '@aws-solutions-constructs/core'; const app = new App(); const stack = new Stack(app, generateIntegStackName(__filename)); -const existingBucket = CreateScrapBucket(stack, {}); -const cfnBucket = existingBucket.node.defaultChild as s3.CfnBucket; -cfnBucket.addPropertyOverride('NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled', true); +const existingBucket = CreateScrapBucket(stack, { + eventBridgeEnabled: true +}); const startState = new stepfunctions.Pass(stack, 'StartState'); @@ -40,4 +38,6 @@ const props: S3ToStepFunctionProps = { new S3ToStepFunction(stack, 'test-s3-step-function-pre-existing-bucket-construct', props); +addCfnNagS3BucketNotificationRulesToSuppress(stack, 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834'); + app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.s3-step-function-no-argument.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.s3-step-function-no-argument.expected.json index c059c1fc0..6f0ff8635 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.s3-step-function-no-argument.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.s3-step-function-no-argument.expected.json @@ -25,11 +25,6 @@ } ] }, - "NotificationConfiguration": { - "EventBridgeConfiguration": { - "EventBridgeEnabled": true - } - }, "PublicAccessBlockConfiguration": { "BlockPublicAcls": true, "BlockPublicPolicy": true, @@ -100,6 +95,24 @@ } } }, + "tests3stepfunctionconstructtests3stepfunctionconstructWS3BucketNotificationsB8877F95": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "tests3stepfunctionconstructtests3stepfunctionconstructWS3Bucket474FE3A1" + }, + "NotificationConfiguration": { + "EventBridgeConfiguration": {} + }, + "Managed": true + } + }, "tests3stepfunctionconstructtests3stepfunctionconstructWtests3stepfunctionconstructWeventrulestepfunctionconstructStateMachineLogGroup9A7BB9BF": { "Type": "AWS::Logs::LogGroup", "Properties": { @@ -382,6 +395,108 @@ "Statistic": "Maximum", "Threshold": 1 } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time" + } + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "python3.7", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ], + "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" + } + ] + } + } } }, "Mappings": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/README.md b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/README.md index 4650475c9..c03af2bb7 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/README.md @@ -87,7 +87,7 @@ new S3ToStepfunctions(this, "test_s3_stepfunctions_stack", | **Name** | **Type** | **Description** | |:-------------|:----------------|-----------------| -|existingBucketObj?|[`s3.IBucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.IBucket.html)|Existing instance of S3 Bucket object. The existing bucket must have [EventBridge enabled](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-event-notifications-eventbridge.html) for this to work. If this is provided, then also providing bucketProps is an error.| +|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 provided, then also providing bucketProps is an error. **The existing bucket must have [EventBridge enabled](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-event-notifications-eventbridge.html) for this to work.**| |bucketProps?|[`s3.BucketProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html)|Optional user provided props to override the default props for the S3 Bucket.| |stateMachineProps|[`sfn.StateMachineProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-stepfunctions.StateMachineProps.html)|User provided props to override the default props for sfn.StateMachine.| |eventRuleProps?|[`events.RuleProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-events.RuleProps.html)|Optional user provided eventRuleProps to override the defaults.| diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts index 7d1979826..a8c96ede0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/lib/index.ts @@ -16,7 +16,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as defaults from '@aws-solutions-constructs/core'; import { EventbridgeToStepfunctions } from '@aws-solutions-constructs/aws-eventbridge-stepfunctions'; // Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate -import { Construct } from '@aws-cdk/core'; +import { Construct, Stack } from '@aws-cdk/core'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as events from '@aws-cdk/aws-events'; import * as logs from '@aws-cdk/aws-logs'; @@ -27,6 +27,7 @@ import * as logs from '@aws-cdk/aws-logs'; export interface S3ToStepfunctionsProps { /** * Existing instance of S3 Bucket object, providing both this and `bucketProps` will cause an error. + * The Amazon EventBridge property must be enabled in the existing bucket for the construct to work. * * @default - None */ @@ -110,15 +111,14 @@ export class S3ToStepfunctions extends Construct { if (!props.existingBucketObj) { [this.s3Bucket, this.s3LoggingBucket] = defaults.buildS3Bucket(this, { - bucketProps: props.bucketProps, + bucketProps: defaults.consolidateProps({}, props.bucketProps, { eventBridgeEnabled: true }), loggingBucketProps: props.loggingBucketProps, logS3AccessLogs: props.logS3AccessLogs }); bucket = this.s3Bucket; - const cfnBucket = bucket.node.defaultChild as s3.CfnBucket; - cfnBucket.addPropertyOverride('NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled', true); - + // Suppress cfn-nag rules that generate warns for S3 bucket notification CDK resources + defaults.addCfnNagS3BucketNotificationRulesToSuppress(Stack.of(this), 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834'); } else { bucket = props.existingBucketObj; } diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.customLoggingBucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.customLoggingBucket.expected.json index 368ff55af..22f3d5e98 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.customLoggingBucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.customLoggingBucket.expected.json @@ -114,11 +114,6 @@ "Ref": "tests3stepfunctionsS3LoggingBucketF7586A92" } }, - "NotificationConfiguration": { - "EventBridgeConfiguration": { - "EventBridgeEnabled": true - } - }, "PublicAccessBlockConfiguration": { "BlockPublicAcls": true, "BlockPublicPolicy": true, @@ -179,6 +174,24 @@ } } }, + "tests3stepfunctionsS3BucketNotificationsDBB3D484": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "tests3stepfunctionsS3Bucket2B08AD28" + }, + "NotificationConfiguration": { + "EventBridgeConfiguration": {} + }, + "Managed": true + } + }, "tests3stepfunctionstests3stepfunctionseventrulestepfunctionconstructStateMachineLogGroupB4555776": { "Type": "AWS::Logs::LogGroup", "Properties": { @@ -461,6 +474,108 @@ "Statistic": "Maximum", "Threshold": 1 } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time" + } + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "python3.7", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ], + "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" + } + ] + } + } } }, "Mappings": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.customLoggingBucket.ts b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.customLoggingBucket.ts index 590df605c..676ad9abd 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.customLoggingBucket.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.customLoggingBucket.ts @@ -42,4 +42,5 @@ new S3ToStepfunctions(stack, 'test-s3-stepfunctions', { removalPolicy: RemovalPolicy.DESTROY } }); + app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.pre-existing-bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.pre-existing-bucket.expected.json index d653adb4a..2fc40ce12 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.pre-existing-bucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.pre-existing-bucket.expected.json @@ -12,11 +12,6 @@ } ] }, - "NotificationConfiguration": { - "EventBridgeConfiguration": { - "EventBridgeEnabled": true - } - }, "VersioningConfiguration": { "Status": "Enabled" } @@ -42,6 +37,126 @@ } } }, + "existingScriptLocationNotificationsC550EA17": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "existingScriptLocation845F3C51" + }, + "NotificationConfiguration": { + "EventBridgeConfiguration": {} + }, + "Managed": true + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time" + } + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "python3.7", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ], + "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" + } + ] + } + } + }, "tests3stepfunctionspreexistingbucketconstructtests3stepfunctionspreexistingbucketconstructeventrulestepfunctionconstructStateMachineLogGroup9D5E3E4D": { "Type": "AWS::Logs::LogGroup", "Properties": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.pre-existing-bucket.ts b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.pre-existing-bucket.ts index 41a3a9fe6..4d51217d5 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.pre-existing-bucket.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.pre-existing-bucket.ts @@ -15,16 +15,14 @@ import { App, Stack, RemovalPolicy } from "@aws-cdk/core"; import { S3ToStepfunctions, S3ToStepfunctionsProps } from "../lib"; import * as stepfunctions from '@aws-cdk/aws-stepfunctions'; -import * as s3 from '@aws-cdk/aws-s3'; -import { CreateScrapBucket } from '@aws-solutions-constructs/core'; -import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import { CreateScrapBucket, generateIntegStackName, addCfnNagS3BucketNotificationRulesToSuppress } from '@aws-solutions-constructs/core'; const app = new App(); const stack = new Stack(app, generateIntegStackName(__filename)); -const existingBucket = CreateScrapBucket(stack, {}); -const cfnBucket = existingBucket.node.defaultChild as s3.CfnBucket; -cfnBucket.addPropertyOverride('NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled', true); +const existingBucket = CreateScrapBucket(stack, { + eventBridgeEnabled: true +}); const startState = new stepfunctions.Pass(stack, 'StartState'); @@ -41,4 +39,6 @@ const props: S3ToStepfunctionsProps = { new S3ToStepfunctions(stack, 'test-s3-stepfunctions-pre-existing-bucket-construct', props); +addCfnNagS3BucketNotificationRulesToSuppress(stack, 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834'); + app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.s3-stepfunctions-no-argument.expected.json b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.s3-stepfunctions-no-argument.expected.json index 7655d7c8b..9d79f9610 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.s3-stepfunctions-no-argument.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/integ.s3-stepfunctions-no-argument.expected.json @@ -25,11 +25,6 @@ } ] }, - "NotificationConfiguration": { - "EventBridgeConfiguration": { - "EventBridgeEnabled": true - } - }, "PublicAccessBlockConfiguration": { "BlockPublicAcls": true, "BlockPublicPolicy": true, @@ -100,6 +95,24 @@ } } }, + "tests3stepfunctionsconstructS3BucketNotificationsD6B8ACDA": { + "Type": "Custom::S3BucketNotifications", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691", + "Arn" + ] + }, + "BucketName": { + "Ref": "tests3stepfunctionsconstructS3Bucket78CA0724" + }, + "NotificationConfiguration": { + "EventBridgeConfiguration": {} + }, + "Managed": true + } + }, "tests3stepfunctionsconstructtests3stepfunctionsconstructeventrulestepfunctionconstructStateMachineLogGroupE86C2CF5": { "Type": "AWS::Logs::LogGroup", "Properties": { @@ -382,6 +395,108 @@ "Statistic": "Maximum", "Threshold": 1 } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutBucketNotification", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "Roles": [ + { + "Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W12", + "reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time" + } + ] + } + } + }, + "BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", + "Code": { + "ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC", + "Arn" + ] + }, + "Runtime": "python3.7", + "Timeout": 300 + }, + "DependsOn": [ + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36", + "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC" + ], + "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" + } + ] + } + } } }, "Mappings": { diff --git a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts index 5ee9516a5..02acae597 100644 --- a/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-s3-stepfunctions/test/s3-stepfunctions.test.ts @@ -152,9 +152,7 @@ test('s3 bucket with bucket, loggingBucket, and auto delete objects', () => { new S3ToStepfunctions(stack, 'test-s3-stepfunctions', testProps); - expect(stack).toHaveResource("AWS::S3::Bucket", { - AccessControl: "LogDeliveryWrite" - }); + expect(stack).toHaveResource("Custom::S3BucketNotifications", {}); expect(stack).toHaveResource("Custom::S3AutoDeleteObjects", { ServiceToken: { @@ -186,5 +184,6 @@ test('s3 bucket with no logging bucket', () => { logS3AccessLogs: false }); + expect(stack).toHaveResource("Custom::S3BucketNotifications", {}); expect(construct.s3LoggingBucket).toEqual(undefined); }); \ No newline at end of file From 5c1391ecefaaef1c3380ca6f367815b52932552f Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Fri, 22 Apr 2022 15:45:07 -0400 Subject: [PATCH 23/34] Change kinesis storage threshold (#667) --- .../test/integ.apigateway-kinesis-overwrite.expected.json | 2 +- .../test/integ.no-arguments.expected.json | 2 +- ...g.eventbridge-kinesisstreams-existing-eventbus.expected.json | 2 +- .../integ.eventbridge-kinesisstreams-existing.expected.json | 2 +- .../integ.eventbridge-kinesisstreams-new-eventbus.expected.json | 2 +- .../integ.eventbridge-kinesisstreams-no-arguments.expected.json | 2 +- ...g.events-rule-kinesisstreams-existing-eventbus.expected.json | 2 +- .../integ.events-rule-kinesisstreams-existing.expected.json | 2 +- .../integ.events-rule-kinesisstreams-new-eventbus.expected.json | 2 +- .../integ.events-rule-kinesisstreams-no-arguments.expected.json | 2 +- .../test/integ.existing-kinesisstream.expected.json | 2 +- .../test/integ.new-kinesisstream.expected.json | 2 +- .../test/integ.no-arguments.expected.json | 2 +- .../test/integ.code-asset-job.expected.json | 2 +- .../test/integ.existing-job.expected.json | 2 +- .../test/integ.no-arguments.expected.json | 2 +- .../test/integ.customLoggingBucket.expected.json | 2 +- .../test/integ.existing-bucket.expected.json | 2 +- .../test/integ.existing-logging-bucket.expected.json | 2 +- .../test/integ.existingStreamObj.expected.json | 2 +- .../test/integ.no-arguments.expected.json | 2 +- .../aws-kinesisstreams-lambda/test/integ.existing.expected.json | 2 +- .../test/integ.no-arguments.expected.json | 2 +- .../core/lib/kinesis-streams-helper.ts | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.apigateway-kinesis-overwrite.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.apigateway-kinesis-overwrite.expected.json index 2b7a3505f..f57775306 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.apigateway-kinesis-overwrite.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.apigateway-kinesis-overwrite.expected.json @@ -488,7 +488,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testapigatewaykinesisoverwriteKinesisStreamReadProvisionedThroughputExceededAlarm5C0040FB": { diff --git a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.no-arguments.expected.json index f218789a8..e8e5edabc 100644 --- a/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-apigateway-kinesisstreams/test/integ.no-arguments.expected.json @@ -524,7 +524,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testapigatewaykinesisdefaultKinesisStreamReadProvisionedThroughputExceededAlarmE7251F6A": { diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-existing-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-existing-eventbus.expected.json index 23b892d29..0234510be 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-existing-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-existing-eventbus.expected.json @@ -110,7 +110,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testeventbridgekinesisstreamexistingKinesisStreamReadProvisionedThroughputExceededAlarm078FEB59": { diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-existing.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-existing.expected.json index 675841421..73e3488d4 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-existing.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-existing.expected.json @@ -97,7 +97,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testeventbridgekinesisstreamexistingKinesisStreamReadProvisionedThroughputExceededAlarm078FEB59": { diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-new-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-new-eventbus.expected.json index 4afb048fa..8ceb32343 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-new-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-new-eventbus.expected.json @@ -110,7 +110,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testeventbridgekinesisstreamKinesisStreamReadProvisionedThroughputExceededAlarm600B4281": { diff --git a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-no-arguments.expected.json index e9378b595..6f9748c42 100644 --- a/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-eventbridge-kinesisstreams/test/integ.eventbridge-kinesisstreams-no-arguments.expected.json @@ -97,7 +97,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testeventbridgekinesisstreamKinesisStreamReadProvisionedThroughputExceededAlarm600B4281": { diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-existing-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-existing-eventbus.expected.json index 390864a83..22f05af5c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-existing-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-existing-eventbus.expected.json @@ -110,7 +110,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testeventsrulekinesisstreamexistingtesteventsrulekinesisstreamexistingWKinesisStreamReadProvisionedThroughputExceededAlarmC7B9BAE4": { diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-existing.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-existing.expected.json index 2c17aa915..e731cada8 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-existing.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-existing.expected.json @@ -97,7 +97,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testeventsrulekinesisstreamexistingtesteventsrulekinesisstreamexistingWKinesisStreamReadProvisionedThroughputExceededAlarmB84DCE25": { diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-new-eventbus.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-new-eventbus.expected.json index 124fd4b2b..1a6855568 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-new-eventbus.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-new-eventbus.expected.json @@ -110,7 +110,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testeventsrulekinesisstreamtesteventsrulekinesisstreamWKinesisStreamReadProvisionedThroughputExceededAlarm5F32E34D": { diff --git a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-no-arguments.expected.json index 7d85f4da5..59bf03fa1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-events-rule-kinesisstreams/test/integ.events-rule-kinesisstreams-no-arguments.expected.json @@ -97,7 +97,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testeventsrulekinesisstreamtesteventsrulekinesisstreamWKinesisStreamReadProvisionedThroughputExceededAlarmE9C8ACA5": { diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.existing-kinesisstream.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.existing-kinesisstream.expected.json index 3039b9b90..4fd3b8f4c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.existing-kinesisstream.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.existing-kinesisstream.expected.json @@ -93,7 +93,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testiotkinesisstreamsKinesisStreamReadProvisionedThroughputExceededAlarm45349C37": { diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.new-kinesisstream.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.new-kinesisstream.expected.json index 8e9df20d7..e7e2f5c7b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.new-kinesisstream.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.new-kinesisstream.expected.json @@ -93,7 +93,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testiotkinesisstreamsKinesisStreamReadProvisionedThroughputExceededAlarm45349C37": { diff --git a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.no-arguments.expected.json index b7fb87a4c..683747c3b 100644 --- a/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-iot-kinesisstreams/test/integ.no-arguments.expected.json @@ -93,7 +93,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testiotkinesisstreamsKinesisStreamReadProvisionedThroughputExceededAlarm45349C37": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.code-asset-job.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.code-asset-job.expected.json index 20eb71893..2ede6c7af 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.code-asset-job.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.code-asset-job.expected.json @@ -434,7 +434,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testkinesisstreamslambdaKinesisStreamReadProvisionedThroughputExceededAlarm5ABF4346": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.existing-job.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.existing-job.expected.json index 3f84bf1bc..60135909f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.existing-job.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.existing-job.expected.json @@ -105,7 +105,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testkinesisstreamslambdaKinesisStreamReadProvisionedThroughputExceededAlarm5ABF4346": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.no-arguments.expected.json index 7bf9babe7..e821d5246 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-gluejob/test/integ.no-arguments.expected.json @@ -425,7 +425,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testkinesisstreamslambdaKinesisStreamReadProvisionedThroughputExceededAlarm5ABF4346": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.customLoggingBucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.customLoggingBucket.expected.json index 762795064..78e4bce9f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.customLoggingBucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.customLoggingBucket.expected.json @@ -396,7 +396,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testkinesisfirehoses3KinesisStreamReadProvisionedThroughputExceededAlarm4A9C6943": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existing-bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existing-bucket.expected.json index 764ccefbd..801baca85 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existing-bucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existing-bucket.expected.json @@ -280,7 +280,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testexistingbucketfirehoses3stackKinesisStreamReadProvisionedThroughputExceededAlarmEC9D97D6": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existing-logging-bucket.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existing-logging-bucket.expected.json index 626e433fa..95806f4e8 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existing-logging-bucket.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existing-logging-bucket.expected.json @@ -351,7 +351,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testexistingloggingbucketstreamsfirehoses3stackKinesisStreamReadProvisionedThroughputExceededAlarmCF515815": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existingStreamObj.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existingStreamObj.expected.json index ba8b80380..03c685f08 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existingStreamObj.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.existingStreamObj.expected.json @@ -313,7 +313,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testkinesislambdaKinesisStreamReadProvisionedThroughputExceededAlarmAE98686F": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.no-arguments.expected.json index f09724bd2..1c32f3975 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-kinesisfirehose-s3/test/integ.no-arguments.expected.json @@ -396,7 +396,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "teststreamfirehoses3KinesisStreamReadProvisionedThroughputExceededAlarm7C631AC0": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/test/integ.existing.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/test/integ.existing.expected.json index eb4f3870e..8fe8db2ed 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/test/integ.existing.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/test/integ.existing.expected.json @@ -283,7 +283,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testkslambdaKinesisStreamReadProvisionedThroughputExceededAlarmFAEF1236": { diff --git a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/test/integ.no-arguments.expected.json b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/test/integ.no-arguments.expected.json index db07655e2..f1fe3c8e9 100644 --- a/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/test/integ.no-arguments.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-kinesisstreams-lambda/test/integ.no-arguments.expected.json @@ -313,7 +313,7 @@ "Namespace": "AWS/Kinesis", "Period": 300, "Statistic": "Maximum", - "Threshold": 2592000 + "Threshold": 43200 } }, "testkinesisstreamslambdaKinesisStreamReadProvisionedThroughputExceededAlarm5ABF4346": { diff --git a/source/patterns/@aws-solutions-constructs/core/lib/kinesis-streams-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/kinesis-streams-helper.ts index 3d96b585a..d360a433c 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/kinesis-streams-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/kinesis-streams-helper.ts @@ -63,7 +63,7 @@ export function buildKinesisStreamCWAlarms(scope: Construct): cloudwatch.Alarm[] statistic: 'Maximum', period: cdk.Duration.minutes(5), }), - threshold: 2592000, // 12 Hours (50% of 24 hours - default record retention period) + threshold: 43200, // 12 Hours (50% of 24 hours - default record retention period) evaluationPeriods: 1, comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, alarmDescription: 'Consumer Record Processing Falling Behind, there is risk for data loss due to record expiration.' From 824bb19a55530183da579d93f0b7f7625e1b1976 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Thu, 28 Apr 2022 08:14:44 -0400 Subject: [PATCH 24/34] Bring use cases up to date (#671) --- .../test/integ.gluejob.expected.json | 269 +- .../.eslintignore | 3 +- .../bin/restaurant-management-system-demo.ts | 1 + .../package.json | 3 +- .../existingResourcesStack.test.js.snap | 13 + .../kitchenStaffStack.test.js.snap | 1028 ++++++ .../__snapshots__/managerStack.test.js.snap | 3201 +++++++++++++++++ .../serviceStaffStack.test.js.snap | 1028 ++++++ .../__snapshots__/sharedStack.test.js.snap | 171 + .../s3-static-site-stack.test.js.snap | 334 +- .../test/integ.basic-deployment.expected.json | 496 ++- .../test/s3-static-site-stack.test.ts | 121 +- .../aws-serverless-image-handler/lib/index.ts | 21 +- ...test.serverless-image-handler.test.js.snap | 398 +- .../test/integ.basic-deployment.expected.json | 500 ++- .../test.serverless-image-handler.test.ts | 2 +- .../tsconfig.json | 10 +- .../s3-static-site-stack.test.js.snap | 325 +- .../serverless-backend-stack.test.js.snap | 214 +- ...s3-static-website-deployment.expected.json | 491 ++- ...integ.002-backend-deployment.expected.json | 384 +- .../test/s3-static-site-stack.test.ts | 248 +- source/use_cases/license-header.js | 2 +- 23 files changed, 7469 insertions(+), 1794 deletions(-) create mode 100644 source/use_cases/aws-restaurant-management-demo/test/__snapshots__/existingResourcesStack.test.js.snap create mode 100644 source/use_cases/aws-restaurant-management-demo/test/__snapshots__/kitchenStaffStack.test.js.snap create mode 100644 source/use_cases/aws-restaurant-management-demo/test/__snapshots__/managerStack.test.js.snap create mode 100644 source/use_cases/aws-restaurant-management-demo/test/__snapshots__/serviceStaffStack.test.js.snap create mode 100644 source/use_cases/aws-restaurant-management-demo/test/__snapshots__/sharedStack.test.js.snap diff --git a/source/use_cases/aws-custom-glue-etl/test/integ.gluejob.expected.json b/source/use_cases/aws-custom-glue-etl/test/integ.gluejob.expected.json index 45de5e173..684fdc1ef 100644 --- a/source/use_cases/aws-custom-glue-etl/test/integ.gluejob.expected.json +++ b/source/use_cases/aws-custom-glue-etl/test/integ.gluejob.expected.json @@ -1,28 +1,17 @@ { "Description": "Integration Test for sample application that uses aws-kineisstream-glue-job construct", - "Parameters": { - "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3Bucket96760533": { - "Type": "String", - "Description": "S3 bucket for asset \"d4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4\"" - }, - "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3VersionKey856519B9": { - "Type": "String", - "Description": "S3 key for asset version \"d4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4\"" - }, - "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4ArtifactHashC026BAAB": { - "Type": "String", - "Description": "Artifact hash for asset \"d4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4\"" - } - }, "Resources": { "CustomETLKinesisStreamB4F1869F": { "Type": "AWS::Kinesis::Stream", "Properties": { - "ShardCount": 1, "RetentionPeriodHours": 24, + "ShardCount": 1, "StreamEncryption": { "EncryptionType": "KMS", "KeyId": "alias/aws/kinesis" + }, + "StreamModeDetails": { + "StreamMode": "PROVISIONED" } } }, @@ -135,7 +124,11 @@ "s3:GetBucket*", "s3:List*", "s3:DeleteObject*", - "s3:PutObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -180,34 +173,7 @@ }, ":s3:::", { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3Bucket96760533" - }, - "/", - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3VersionKey856519B9" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3VersionKey856519B9" - } - ] - } - ] + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" } ] ] @@ -222,34 +188,7 @@ }, ":s3:::", { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3Bucket96760533" - }, - "/", - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3VersionKey856519B9" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3VersionKey856519B9" - } - ] - } - ] + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, "/*" ] @@ -286,6 +225,9 @@ "BlockPublicPolicy": true, "IgnorePublicAcls": true, "RestrictPublicBuckets": true + }, + "VersioningConfiguration": { + "Status": "Enabled" } }, "UpdateReplacePolicy": "Retain", @@ -310,7 +252,7 @@ "PolicyDocument": { "Statement": [ { - "Action": "*", + "Action": "s3:*", "Condition": { "Bool": { "aws:SecureTransport": "false" @@ -320,21 +262,28 @@ "Principal": { "AWS": "*" }, - "Resource": { - "Fn::Join": [ - "", - [ - { - "Fn::GetAtt": [ - "CustomETLS3LoggingBucketBBDD45CB", - "Arn" - ] - }, - "/*" + "Resource": [ + { + "Fn::GetAtt": [ + "CustomETLS3LoggingBucketBBDD45CB", + "Arn" ] - ] - }, - "Sid": "HttpsOnly" + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CustomETLS3LoggingBucketBBDD45CB", + "Arn" + ] + }, + "/*" + ] + ] + } + ] } ], "Version": "2012-10-17" @@ -393,7 +342,7 @@ "PolicyDocument": { "Statement": [ { - "Action": "*", + "Action": "s3:*", "Condition": { "Bool": { "aws:SecureTransport": "false" @@ -403,21 +352,28 @@ "Principal": { "AWS": "*" }, - "Resource": { - "Fn::Join": [ - "", - [ - { - "Fn::GetAtt": [ - "CustomETLS3Bucket3EE58725", - "Arn" - ] - }, - "/*" + "Resource": [ + { + "Fn::GetAtt": [ + "CustomETLS3Bucket3EE58725", + "Arn" ] - ] - }, - "Sid": "HttpsOnly" + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CustomETLS3Bucket3EE58725", + "Arn" + ] + }, + "/*" + ] + ] + } + ] } ], "Version": "2012-10-17" @@ -431,42 +387,7 @@ "Name": "gluestreaming", "PythonVersion": "3", "ScriptLocation": { - "Fn::Join": [ - "", - [ - "s3://", - { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3Bucket96760533" - }, - "/", - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3VersionKey856519B9" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParametersd4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4S3VersionKey856519B9" - } - ] - } - ] - } - ] - ] + "Fn::Sub": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/d4dd6643ee852c9578ae2d36cf2dc8a48630fd2219a15e74502c7ea75bff70c4.py" } }, "Role": { @@ -488,7 +409,7 @@ "Fn::Join": [ "", [ - "s3://", + "s3a://", { "Ref": "CustomETLS3Bucket3EE58725" }, @@ -498,7 +419,35 @@ } }, "GlueVersion": "2.0", - "SecurityConfiguration": "ETLJobSecurityConfig" + "NumberOfWorkers": 2, + "SecurityConfiguration": "ETLJobSecurityConfig", + "WorkerType": "G.1X" + } + }, + "CustomETLKinesisStreamGetRecordsIteratorAgeAlarmCD7F4E34": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Consumer Record Processing Falling Behind, there is risk for data loss due to record expiration.", + "MetricName": "GetRecords.IteratorAgeMilliseconds", + "Namespace": "AWS/Kinesis", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 43200 + } + }, + "CustomETLKinesisStreamReadProvisionedThroughputExceededAlarm6AB32664": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Consumer Application is Reading at a Slower Rate Than Expected.", + "MetricName": "ReadProvisionedThroughputExceeded", + "Namespace": "AWS/Kinesis", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 } }, "GlueDatabase": { @@ -523,7 +472,7 @@ }, "TableInput": { "Parameters": { - "classication": "json" + "classification": "json" }, "StorageDescriptor": { "Columns": [ @@ -599,7 +548,7 @@ } } }, - "GlueJobPolicyAEA4B94E": { + "CustomETLGlueJobPolicy0AE5B5CB": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -745,7 +694,7 @@ ], "Version": "2012-10-17" }, - "PolicyName": "GlueJobPolicyAEA4B94E", + "PolicyName": "CustomETLGlueJobPolicy0AE5B5CB", "Roles": [ { "Ref": "CustomETLJobRole53A1671F" @@ -783,5 +732,39 @@ ] } } + }, + "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/use_cases/aws-restaurant-management-demo/.eslintignore b/source/use_cases/aws-restaurant-management-demo/.eslintignore index 8773c034a..56378a21b 100644 --- a/source/use_cases/aws-restaurant-management-demo/.eslintignore +++ b/source/use_cases/aws-restaurant-management-demo/.eslintignore @@ -3,4 +3,5 @@ test/*.js bin/*.js *.d.ts coverage -test/lambda/index.js \ No newline at end of file +test/lambda/index.js +*/lambda/**/*.js diff --git a/source/use_cases/aws-restaurant-management-demo/bin/restaurant-management-system-demo.ts b/source/use_cases/aws-restaurant-management-demo/bin/restaurant-management-system-demo.ts index 911bc80ff..8b0719fa2 100644 --- a/source/use_cases/aws-restaurant-management-demo/bin/restaurant-management-system-demo.ts +++ b/source/use_cases/aws-restaurant-management-demo/bin/restaurant-management-system-demo.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node + /** * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * diff --git a/source/use_cases/aws-restaurant-management-demo/package.json b/source/use_cases/aws-restaurant-management-demo/package.json index e491915aa..005d56c74 100644 --- a/source/use_cases/aws-restaurant-management-demo/package.json +++ b/source/use_cases/aws-restaurant-management-demo/package.json @@ -44,6 +44,7 @@ "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", "@aws-cdk/aws-redshift": "0.0.0", @@ -55,7 +56,7 @@ "@aws-solutions-constructs/aws-lambda-dynamodb": "0.0.0", "@aws-solutions-constructs/aws-lambda-s3": "0.0.0", "@aws-solutions-constructs/aws-lambda-sns": "0.0.0", - "@aws-solutions-constructs/aws-lambda-step-function": "0.0.0", + "@aws-solutions-constructs/aws-lambda-stepfunctions": "0.0.0", "source-map-support": "^0.5.16", "typescript": "^4.2.4" } diff --git a/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/existingResourcesStack.test.js.snap b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/existingResourcesStack.test.js.snap new file mode 100644 index 000000000..045ea91a1 --- /dev/null +++ b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/existingResourcesStack.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ExistingResourcesStack 1`] = ` +Object { + "Resources": Object { + "existingorderarchivebucket95AB7C99": Object { + "DeletionPolicy": "Retain", + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, +} +`; diff --git a/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/kitchenStaffStack.test.js.snap b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/kitchenStaffStack.test.js.snap new file mode 100644 index 000000000..b7fbe604c --- /dev/null +++ b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/kitchenStaffStack.test.js.snap @@ -0,0 +1,1028 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test-kitchen-staff-stack 1`] = ` +Object { + "Outputs": Object { + "kitchenstaffapiLambdaRestApiEndpoint79E1D2E0": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "kitchenstaffapiLambdaRestApiDeploymentStageprodB9D4F4B3", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08aArtifactHashF06EE331": Object { + "Description": "Artifact hash for asset \\"70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08a\\"", + "Type": "String", + }, + "AssetParameters70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08aS3Bucket757BF55F": Object { + "Description": "S3 bucket for asset \\"70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08a\\"", + "Type": "String", + }, + "AssetParameters70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08aS3VersionKey35483A87": Object { + "Description": "S3 key for asset version \\"70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08a\\"", + "Type": "String", + }, + "AssetParameterse2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6ArtifactHash19177DB6": Object { + "Description": "Artifact hash for asset \\"e2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6\\"", + "Type": "String", + }, + "AssetParameterse2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6S3Bucket387AC028": Object { + "Description": "S3 bucket for asset \\"e2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6\\"", + "Type": "String", + }, + "AssetParameterse2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6S3VersionKeyAE03533F": Object { + "Description": "S3 key for asset version \\"e2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6\\"", + "Type": "String", + }, + }, + "Resources": Object { + "completeorderLambdaFunction1B59DF2A": Object { + "DependsOn": Array [ + "completeorderLambdaFunctionServiceRoleDefaultPolicyB81B4D1C", + "completeorderLambdaFunctionServiceRoleFDF8F85D", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08aS3Bucket757BF55F", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08aS3VersionKey35483A87", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters70bd4fd4308c840ccf8c83659645c3bdca4d50fd6103f8c37f9a1eac8781c08aS3VersionKey35483A87", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "completeorderLambdaFunctionServiceRoleFDF8F85D", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "completeorderLambdaFunctionServiceRoleDefaultPolicyB81B4D1C": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "completeorderLambdaFunctionServiceRoleDefaultPolicyB81B4D1C", + "Roles": Array [ + Object { + "Ref": "completeorderLambdaFunctionServiceRoleFDF8F85D", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "completeorderLambdaFunctionServiceRoleFDF8F85D": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "getopenordersLambdaFunctionCAD5B34A": Object { + "DependsOn": Array [ + "getopenordersLambdaFunctionServiceRoleDefaultPolicy2BBF8B21", + "getopenordersLambdaFunctionServiceRoleFA20DF64", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameterse2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6S3Bucket387AC028", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameterse2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6S3VersionKeyAE03533F", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameterse2a5b561be3a843c8fd1113f472938bb46cece6597ae4222db21547cb0bf48d6S3VersionKeyAE03533F", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "getopenordersLambdaFunctionServiceRoleFA20DF64", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "getopenordersLambdaFunctionServiceRoleDefaultPolicy2BBF8B21": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "getopenordersLambdaFunctionServiceRoleDefaultPolicy2BBF8B21", + "Roles": Array [ + Object { + "Ref": "getopenordersLambdaFunctionServiceRoleFA20DF64", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "getopenordersLambdaFunctionServiceRoleFA20DF64": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "kitchenstaffapiApiAccessLogGroup30DB1160": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely", + }, + Object { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)", + }, + ], + }, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "kitchenstaffapiCognitoAuthorizerA7F33D82": Object { + "Properties": Object { + "IdentitySource": "method.request.header.Authorization", + "Name": "authorizer", + "ProviderARNs": Array [ + Object { + "Fn::GetAtt": Array [ + "kitchenstaffapiCognitoUserPool21035423", + "Arn", + ], + }, + ], + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + "Type": "COGNITO_USER_POOLS", + }, + "Type": "AWS::ApiGateway::Authorizer", + }, + "kitchenstaffapiCognitoUserPool21035423": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AccountRecoverySetting": Object { + "RecoveryMechanisms": Array [ + Object { + "Name": "verified_phone_number", + "Priority": 1, + }, + Object { + "Name": "verified_email", + "Priority": 2, + }, + ], + }, + "AdminCreateUserConfig": Object { + "AllowAdminCreateUserOnly": true, + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + "VerificationMessageTemplate": Object { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}", + }, + }, + "Type": "AWS::Cognito::UserPool", + "UpdateReplacePolicy": "Retain", + }, + "kitchenstaffapiCognitoUserPoolClient064BFF69": Object { + "Properties": Object { + "AllowedOAuthFlows": Array [ + "implicit", + "code", + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": Array [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin", + ], + "CallbackURLs": Array [ + "https://example.com", + ], + "SupportedIdentityProviders": Array [ + "COGNITO", + ], + "UserPoolId": Object { + "Ref": "kitchenstaffapiCognitoUserPool21035423", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "kitchenstaffapiLambdaRestApiAccount3F0B8F7C": Object { + "DependsOn": Array [ + "kitchenstaffapiLambdaRestApiF3F7D4FC", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "kitchenstaffapiLambdaRestApiCloudWatchRole0A152CE6", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "kitchenstaffapiLambdaRestApiCloudWatchRole0A152CE6": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "kitchenstaffapiLambdaRestApiDeployment832EAD6B983d9ce6284ee8e6f3c6844bc18ebcc3": Object { + "DependsOn": Array [ + "kitchenstaffapiLambdaRestApicompleteorderproxyANYAA8E4C52", + "kitchenstaffapiLambdaRestApicompleteorderproxyF9BF19EB", + "kitchenstaffapiLambdaRestApicompleteorder8E72FD84", + "kitchenstaffapiLambdaRestApigetopenordersproxyANY7A3BF144", + "kitchenstaffapiLambdaRestApigetopenordersproxyFF3E7DFD", + "kitchenstaffapiLambdaRestApigetopenorders25EC2468", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "kitchenstaffapiLambdaRestApiDeploymentStageprodB9D4F4B3": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "kitchenstaffapiApiAccessLogGroup30DB1160", + "Arn", + ], + }, + "Format": "{\\"requestId\\":\\"$context.requestId\\",\\"ip\\":\\"$context.identity.sourceIp\\",\\"user\\":\\"$context.identity.user\\",\\"caller\\":\\"$context.identity.caller\\",\\"requestTime\\":\\"$context.requestTime\\",\\"httpMethod\\":\\"$context.httpMethod\\",\\"resourcePath\\":\\"$context.resourcePath\\",\\"status\\":\\"$context.status\\",\\"protocol\\":\\"$context.protocol\\",\\"responseLength\\":\\"$context.responseLength\\"}", + }, + "DeploymentId": Object { + "Ref": "kitchenstaffapiLambdaRestApiDeployment832EAD6B983d9ce6284ee8e6f3c6844bc18ebcc3", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + "StageName": "prod", + "TracingEnabled": true, + }, + "Type": "AWS::ApiGateway::Stage", + }, + "kitchenstaffapiLambdaRestApiF3F7D4FC": Object { + "Properties": Object { + "Description": "Demo: Kitchen staff API", + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "LambdaRestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "kitchenstaffapiLambdaRestApiUsagePlan1249F8F9": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + "Stage": Object { + "Ref": "kitchenstaffapiLambdaRestApiDeploymentStageprodB9D4F4B3", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "kitchenstaffapiLambdaRestApicompleteorder8E72FD84": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "kitchenstaffapiLambdaRestApiF3F7D4FC", + "RootResourceId", + ], + }, + "PathPart": "complete-order", + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "kitchenstaffapiLambdaRestApicompleteorderproxyANYAA8E4C52": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "kitchenstaffapiCognitoAuthorizerA7F33D82", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "completeorderLambdaFunction1B59DF2A", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "kitchenstaffapiLambdaRestApicompleteorderproxyF9BF19EB", + }, + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "kitchenstaffapiLambdaRestApicompleteorderproxyANYApiPermissionKitchenStaffStackkitchenstaffapiLambdaRestApi652BB0C4ANYcompleteorderproxyD7DDAD90": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "completeorderLambdaFunction1B59DF2A", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + "/", + Object { + "Ref": "kitchenstaffapiLambdaRestApiDeploymentStageprodB9D4F4B3", + }, + "/*/complete-order/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "kitchenstaffapiLambdaRestApicompleteorderproxyANYApiPermissionTestKitchenStaffStackkitchenstaffapiLambdaRestApi652BB0C4ANYcompleteorderproxy3F69EF93": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "completeorderLambdaFunction1B59DF2A", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + "/test-invoke-stage/*/complete-order/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "kitchenstaffapiLambdaRestApicompleteorderproxyF9BF19EB": Object { + "Properties": Object { + "ParentId": Object { + "Ref": "kitchenstaffapiLambdaRestApicompleteorder8E72FD84", + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "kitchenstaffapiLambdaRestApigetopenorders25EC2468": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "kitchenstaffapiLambdaRestApiF3F7D4FC", + "RootResourceId", + ], + }, + "PathPart": "get-open-orders", + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "kitchenstaffapiLambdaRestApigetopenordersproxyANY7A3BF144": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "kitchenstaffapiCognitoAuthorizerA7F33D82", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "getopenordersLambdaFunctionCAD5B34A", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "kitchenstaffapiLambdaRestApigetopenordersproxyFF3E7DFD", + }, + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "kitchenstaffapiLambdaRestApigetopenordersproxyANYApiPermissionKitchenStaffStackkitchenstaffapiLambdaRestApi652BB0C4ANYgetopenordersproxy897FB53F": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "getopenordersLambdaFunctionCAD5B34A", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + "/", + Object { + "Ref": "kitchenstaffapiLambdaRestApiDeploymentStageprodB9D4F4B3", + }, + "/*/get-open-orders/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "kitchenstaffapiLambdaRestApigetopenordersproxyANYApiPermissionTestKitchenStaffStackkitchenstaffapiLambdaRestApi652BB0C4ANYgetopenordersproxy966B02A6": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "getopenordersLambdaFunctionCAD5B34A", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + "/test-invoke-stage/*/get-open-orders/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "kitchenstaffapiLambdaRestApigetopenordersproxyFF3E7DFD": Object { + "Properties": Object { + "ParentId": Object { + "Ref": "kitchenstaffapiLambdaRestApigetopenorders25EC2468", + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "kitchenstaffapiLambdaRestApiF3F7D4FC", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + }, +} +`; diff --git a/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/managerStack.test.js.snap b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/managerStack.test.js.snap new file mode 100644 index 000000000..e9196af84 --- /dev/null +++ b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/managerStack.test.js.snap @@ -0,0 +1,3201 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test-manager-stack 1`] = ` +Object { + "Mappings": Object { + "ServiceprincipalMap": Object { + "af-south-1": Object { + "states": "states.af-south-1.amazonaws.com", + }, + "ap-east-1": Object { + "states": "states.ap-east-1.amazonaws.com", + }, + "ap-northeast-1": Object { + "states": "states.ap-northeast-1.amazonaws.com", + }, + "ap-northeast-2": Object { + "states": "states.ap-northeast-2.amazonaws.com", + }, + "ap-northeast-3": Object { + "states": "states.ap-northeast-3.amazonaws.com", + }, + "ap-south-1": Object { + "states": "states.ap-south-1.amazonaws.com", + }, + "ap-southeast-1": Object { + "states": "states.ap-southeast-1.amazonaws.com", + }, + "ap-southeast-2": Object { + "states": "states.ap-southeast-2.amazonaws.com", + }, + "ap-southeast-3": Object { + "states": "states.ap-southeast-3.amazonaws.com", + }, + "ca-central-1": Object { + "states": "states.ca-central-1.amazonaws.com", + }, + "cn-north-1": Object { + "states": "states.cn-north-1.amazonaws.com", + }, + "cn-northwest-1": Object { + "states": "states.cn-northwest-1.amazonaws.com", + }, + "eu-central-1": Object { + "states": "states.eu-central-1.amazonaws.com", + }, + "eu-north-1": Object { + "states": "states.eu-north-1.amazonaws.com", + }, + "eu-south-1": Object { + "states": "states.eu-south-1.amazonaws.com", + }, + "eu-south-2": Object { + "states": "states.eu-south-2.amazonaws.com", + }, + "eu-west-1": Object { + "states": "states.eu-west-1.amazonaws.com", + }, + "eu-west-2": Object { + "states": "states.eu-west-2.amazonaws.com", + }, + "eu-west-3": Object { + "states": "states.eu-west-3.amazonaws.com", + }, + "me-south-1": Object { + "states": "states.me-south-1.amazonaws.com", + }, + "sa-east-1": Object { + "states": "states.sa-east-1.amazonaws.com", + }, + "us-east-1": Object { + "states": "states.us-east-1.amazonaws.com", + }, + "us-east-2": Object { + "states": "states.us-east-2.amazonaws.com", + }, + "us-gov-east-1": Object { + "states": "states.us-gov-east-1.amazonaws.com", + }, + "us-gov-west-1": Object { + "states": "states.us-gov-west-1.amazonaws.com", + }, + "us-iso-east-1": Object { + "states": "states.amazonaws.com", + }, + "us-iso-west-1": Object { + "states": "states.amazonaws.com", + }, + "us-isob-east-1": Object { + "states": "states.amazonaws.com", + }, + "us-west-1": Object { + "states": "states.us-west-1.amazonaws.com", + }, + "us-west-2": Object { + "states": "states.us-west-2.amazonaws.com", + }, + }, + }, + "Outputs": Object { + "managerapiLambdaRestApiEndpointD1A41897": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "managerapiLambdaRestApiDeploymentStageprod59C4A8D4", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13ArtifactHashD2A2C2EB": Object { + "Description": "Artifact hash for asset \\"3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13\\"", + "Type": "String", + }, + "AssetParameters3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13S3Bucket6D0E979D": Object { + "Description": "S3 bucket for asset \\"3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13\\"", + "Type": "String", + }, + "AssetParameters3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13S3VersionKeyF08F3926": Object { + "Description": "S3 key for asset version \\"3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13\\"", + "Type": "String", + }, + "AssetParameters417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729aArtifactHash06F9F843": Object { + "Description": "Artifact hash for asset \\"417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729a\\"", + "Type": "String", + }, + "AssetParameters417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729aS3BucketEFF1BA01": Object { + "Description": "S3 bucket for asset \\"417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729a\\"", + "Type": "String", + }, + "AssetParameters417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729aS3VersionKey9BBF88A7": Object { + "Description": "S3 key for asset version \\"417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729a\\"", + "Type": "String", + }, + "AssetParameters4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194ArtifactHash049C6120": Object { + "Description": "Artifact hash for asset \\"4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194\\"", + "Type": "String", + }, + "AssetParameters4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194S3Bucket6E9AF9DD": Object { + "Description": "S3 bucket for asset \\"4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194\\"", + "Type": "String", + }, + "AssetParameters4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194S3VersionKey00C1823E": Object { + "Description": "S3 key for asset version \\"4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194\\"", + "Type": "String", + }, + "AssetParameters83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0ArtifactHashF35F1FB9": Object { + "Description": "Artifact hash for asset \\"83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0\\"", + "Type": "String", + }, + "AssetParameters83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0S3Bucket0FECA056": Object { + "Description": "S3 bucket for asset \\"83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0\\"", + "Type": "String", + }, + "AssetParameters83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0S3VersionKeyA5DB7D1B": Object { + "Description": "S3 key for asset version \\"83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0\\"", + "Type": "String", + }, + "AssetParameters99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2ArtifactHashF4AAF0F9": Object { + "Description": "Artifact hash for asset \\"99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2\\"", + "Type": "String", + }, + "AssetParameters99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2S3Bucket75F8025A": Object { + "Description": "S3 bucket for asset \\"99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2\\"", + "Type": "String", + }, + "AssetParameters99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2S3VersionKey0BAC175C": Object { + "Description": "S3 key for asset version \\"99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2\\"", + "Type": "String", + }, + "AssetParametersbee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393ArtifactHashEA0663AC": Object { + "Description": "Artifact hash for asset \\"bee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393\\"", + "Type": "String", + }, + "AssetParametersbee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393S3Bucket514B0A32": Object { + "Description": "S3 bucket for asset \\"bee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393\\"", + "Type": "String", + }, + "AssetParametersbee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393S3VersionKey0D96EFA5": Object { + "Description": "S3 key for asset version \\"bee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393\\"", + "Type": "String", + }, + "AssetParameterse247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967baArtifactHashE5FF10CC": Object { + "Description": "Artifact hash for asset \\"e247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967ba\\"", + "Type": "String", + }, + "AssetParameterse247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967baS3Bucket0B99817B": Object { + "Description": "S3 bucket for asset \\"e247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967ba\\"", + "Type": "String", + }, + "AssetParameterse247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967baS3VersionKey6F469E09": Object { + "Description": "S3 key for asset version \\"e247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967ba\\"", + "Type": "String", + }, + }, + "Resources": Object { + "archiveordersLambdaFunction66659E04": Object { + "DependsOn": Array [ + "archiveordersLambdaFunctionServiceRoleDefaultPolicyDB67FB52", + "archiveordersLambdaFunctionServiceRoleE513ACF7", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2S3Bucket75F8025A", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2S3VersionKey0BAC175C", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters99da7fc0497f7d8697128700d73d1010c153a6af5446b3f86452c62b633be3e2S3VersionKey0BAC175C", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + "S3_BUCKET_NAME": Object { + "Fn::ImportValue": "ExistingResourcesStack:ExportsOutputRefexistingorderarchivebucket95AB7C994BD2255B", + }, + }, + }, + "Handler": "index.handler", + "Layers": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefshareddbfunctionslayerA9CFEC5A89EBE406", + }, + ], + "Role": Object { + "Fn::GetAtt": Array [ + "archiveordersLambdaFunctionServiceRoleE513ACF7", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "archiveordersLambdaFunctionServiceRoleDefaultPolicyDB67FB52": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::ImportValue": "ExistingResourcesStack:ExportsOutputFnGetAttexistingorderarchivebucket95AB7C99Arn7A4BF717", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "ExistingResourcesStack:ExportsOutputFnGetAttexistingorderarchivebucket95AB7C99Arn7A4BF717", + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "archiveordersLambdaFunctionServiceRoleDefaultPolicyDB67FB52", + "Roles": Array [ + Object { + "Ref": "archiveordersLambdaFunctionServiceRoleE513ACF7", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "archiveordersLambdaFunctionServiceRoleE513ACF7": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "calculatetipsLambdaFunction9F2FE033": Object { + "DependsOn": Array [ + "calculatetipsLambdaFunctionServiceRoleDefaultPolicyED27C928", + "calculatetipsLambdaFunctionServiceRoleDE97A87F", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729aS3BucketEFF1BA01", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729aS3VersionKey9BBF88A7", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters417a2e0a2352189e861a2cadf25a3b8a7ccbc22a6d8e3d4466f611d46fdd729aS3VersionKey9BBF88A7", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + "SNS_TOPIC_ARN": Object { + "Ref": "calculatetipstopicSnsTopicD7FACC9C", + }, + "SNS_TOPIC_NAME": Object { + "Fn::GetAtt": Array [ + "calculatetipstopicSnsTopicD7FACC9C", + "TopicName", + ], + }, + }, + }, + "Handler": "index.handler", + "Layers": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefshareddbfunctionslayerA9CFEC5A89EBE406", + }, + ], + "Role": Object { + "Fn::GetAtt": Array [ + "calculatetipsLambdaFunctionServiceRoleDE97A87F", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "calculatetipsLambdaFunctionServiceRoleDE97A87F": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "calculatetipsLambdaFunctionServiceRoleDefaultPolicyED27C928": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + Object { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": Object { + "Ref": "calculatetipstopicSnsTopicD7FACC9C", + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "calculatetipsLambdaFunctionServiceRoleDefaultPolicyED27C928", + "Roles": Array [ + Object { + "Ref": "calculatetipsLambdaFunctionServiceRoleDE97A87F", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "calculatetipstopicSnsTopicD7FACC9C": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":kms:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":alias/aws/sns", + ], + ], + }, + }, + "Type": "AWS::SNS::Topic", + }, + "calculatetipstopicSnsTopicPolicy529CD9C3": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "SNS:Publish", + "SNS:RemovePermission", + "SNS:SetTopicAttributes", + "SNS:DeleteTopic", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:Receive", + "SNS:AddPermission", + "SNS:Subscribe", + ], + "Condition": Object { + "StringEquals": Object { + "AWS:SourceOwner": Object { + "Ref": "AWS::AccountId", + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": Object { + "Ref": "calculatetipstopicSnsTopicD7FACC9C", + }, + "Sid": "TopicOwnerOnlyAccess", + }, + Object { + "Action": Array [ + "SNS:Publish", + "SNS:RemovePermission", + "SNS:SetTopicAttributes", + "SNS:DeleteTopic", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:Receive", + "SNS:AddPermission", + "SNS:Subscribe", + ], + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Object { + "Ref": "calculatetipstopicSnsTopicD7FACC9C", + }, + "Sid": "HttpsOnly", + }, + ], + "Version": "2012-10-17", + }, + "Topics": Array [ + Object { + "Ref": "calculatetipstopicSnsTopicD7FACC9C", + }, + ], + }, + "Type": "AWS::SNS::TopicPolicy", + }, + "checklateordersLambdaFunctionAwsEventsLambdaInvokePermission124C4D337": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "checklateordersLambdaFunctionB46EDA7D", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": Object { + "Fn::GetAtt": Array [ + "checklateordersschedulerchecklateordersschedulerWEventsRule15B31DCC", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "checklateordersLambdaFunctionB46EDA7D": Object { + "DependsOn": Array [ + "checklateordersLambdaFunctionServiceRoleDefaultPolicy4DABEECE", + "checklateordersLambdaFunctionServiceRoleA8F250A0", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameterse247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967baS3Bucket0B99817B", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameterse247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967baS3VersionKey6F469E09", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameterse247a6949734ab1a72f6679bdeb6de1bd7e6c8714508986e7cfe184ad2b967baS3VersionKey6F469E09", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + "LATE_ORDER_THRESHOLD": "30", + "SNS_TOPIC_ARN": Object { + "Ref": "checklateordersnotifierSnsTopic2055CAD8", + }, + "SNS_TOPIC_NAME": Object { + "Fn::GetAtt": Array [ + "checklateordersnotifierSnsTopic2055CAD8", + "TopicName", + ], + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "checklateordersLambdaFunctionServiceRoleA8F250A0", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "checklateordersLambdaFunctionServiceRoleA8F250A0": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "checklateordersLambdaFunctionServiceRoleDefaultPolicy4DABEECE": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + Object { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": Object { + "Ref": "checklateordersnotifierSnsTopic2055CAD8", + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "checklateordersLambdaFunctionServiceRoleDefaultPolicy4DABEECE", + "Roles": Array [ + Object { + "Ref": "checklateordersLambdaFunctionServiceRoleA8F250A0", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "checklateordersnotifierSnsTopic2055CAD8": Object { + "Properties": Object { + "KmsMasterKeyId": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":kms:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":alias/aws/sns", + ], + ], + }, + }, + "Type": "AWS::SNS::Topic", + }, + "checklateordersnotifierSnsTopicPolicyCF0A65E0": Object { + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "SNS:Publish", + "SNS:RemovePermission", + "SNS:SetTopicAttributes", + "SNS:DeleteTopic", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:Receive", + "SNS:AddPermission", + "SNS:Subscribe", + ], + "Condition": Object { + "StringEquals": Object { + "AWS:SourceOwner": Object { + "Ref": "AWS::AccountId", + }, + }, + }, + "Effect": "Allow", + "Principal": Object { + "AWS": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::", + Object { + "Ref": "AWS::AccountId", + }, + ":root", + ], + ], + }, + }, + "Resource": Object { + "Ref": "checklateordersnotifierSnsTopic2055CAD8", + }, + "Sid": "TopicOwnerOnlyAccess", + }, + Object { + "Action": Array [ + "SNS:Publish", + "SNS:RemovePermission", + "SNS:SetTopicAttributes", + "SNS:DeleteTopic", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:Receive", + "SNS:AddPermission", + "SNS:Subscribe", + ], + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Object { + "Ref": "checklateordersnotifierSnsTopic2055CAD8", + }, + "Sid": "HttpsOnly", + }, + ], + "Version": "2012-10-17", + }, + "Topics": Array [ + Object { + "Ref": "checklateordersnotifierSnsTopic2055CAD8", + }, + ], + }, + "Type": "AWS::SNS::TopicPolicy", + }, + "checklateordersschedulerchecklateordersschedulerWEventsRule15B31DCC": Object { + "Properties": Object { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": Array [ + Object { + "Arn": Object { + "Fn::GetAtt": Array [ + "checklateordersLambdaFunctionB46EDA7D", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "closeoutserviceExecutionAbortedAlarm861B143E": Object { + "Properties": Object { + "AlarmDescription": "Alarm for the number of executions that aborted exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": Array [ + Object { + "Name": "StateMachineArn", + "Value": Object { + "Ref": "closeoutserviceStateMachineC9DD68A8", + }, + }, + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionsAborted", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "closeoutserviceExecutionFailedAlarm7495D8BE": Object { + "Properties": Object { + "AlarmDescription": "Alarm for the number of executions that failed exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": Array [ + Object { + "Name": "StateMachineArn", + "Value": Object { + "Ref": "closeoutserviceStateMachineC9DD68A8", + }, + }, + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionsFailed", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "closeoutserviceExecutionThrottledAlarmC5A6E612": Object { + "Properties": Object { + "AlarmDescription": "Alarm for the number of executions that throttled exceeded the threshold of 1. ", + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "Dimensions": Array [ + Object { + "Name": "StateMachineArn", + "Value": Object { + "Ref": "closeoutserviceStateMachineC9DD68A8", + }, + }, + ], + "EvaluationPeriods": 1, + "MetricName": "ExecutionThrottled", + "Namespace": "AWS/States", + "Period": 300, + "Statistic": "Sum", + "Threshold": 1, + }, + "Type": "AWS::CloudWatch::Alarm", + }, + "closeoutserviceLambdaFunction684B26D3": Object { + "DependsOn": Array [ + "closeoutserviceLambdaFunctionServiceRoleDefaultPolicy95A79CC3", + "closeoutserviceLambdaFunctionServiceRoleB49E9696", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParametersbee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393S3Bucket514B0A32", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersbee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393S3VersionKey0D96EFA5", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersbee7a88e44057a981dee36ffd34a5dfeebd59c93fc6b10b697156d547aca9393S3VersionKey0D96EFA5", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "STATE_MACHINE_ARN": Object { + "Ref": "closeoutserviceStateMachineC9DD68A8", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "closeoutserviceLambdaFunctionServiceRoleB49E9696", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "closeoutserviceLambdaFunctionServiceRoleB49E9696": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "closeoutserviceLambdaFunctionServiceRoleDefaultPolicy95A79CC3": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": Object { + "Ref": "closeoutserviceStateMachineC9DD68A8", + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "closeoutserviceLambdaFunctionServiceRoleDefaultPolicy95A79CC3", + "Roles": Array [ + Object { + "Ref": "closeoutserviceLambdaFunctionServiceRoleB49E9696", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "closeoutserviceStateMachineC9DD68A8": Object { + "DependsOn": Array [ + "closeoutserviceStateMachineRoleDefaultPolicy5F05BEB8", + "closeoutserviceStateMachineRoleCA7F9A09", + ], + "Properties": Object { + "DefinitionString": Object { + "Fn::Join": Array [ + "", + Array [ + "{\\"StartAt\\":\\"create-reports-task\\",\\"States\\":{\\"create-reports-task\\":{\\"Next\\":\\"calculate-tips-task\\",\\"Type\\":\\"Task\\",\\"Resource\\":\\"", + Object { + "Fn::GetAtt": Array [ + "createreportLambdaFunctionB37EBE52", + "Arn", + ], + }, + "\\"},\\"calculate-tips-task\\":{\\"Next\\":\\"archive-orders-task\\",\\"Type\\":\\"Task\\",\\"Resource\\":\\"", + Object { + "Fn::GetAtt": Array [ + "calculatetipsLambdaFunction9F2FE033", + "Arn", + ], + }, + "\\"},\\"archive-orders-task\\":{\\"End\\":true,\\"Type\\":\\"Task\\",\\"Resource\\":\\"", + Object { + "Fn::GetAtt": Array [ + "archiveordersLambdaFunction66659E04", + "Arn", + ], + }, + "\\"}}}", + ], + ], + }, + "LoggingConfiguration": Object { + "Destinations": Array [ + Object { + "CloudWatchLogsLogGroup": Object { + "LogGroupArn": Object { + "Fn::GetAtt": Array [ + "closeoutserviceStateMachineLogGroup56CA2C8F", + "Arn", + ], + }, + }, + }, + ], + "Level": "ERROR", + }, + "RoleArn": Object { + "Fn::GetAtt": Array [ + "closeoutserviceStateMachineRoleCA7F9A09", + "Arn", + ], + }, + }, + "Type": "AWS::StepFunctions::StateMachine", + }, + "closeoutserviceStateMachineLogGroup56CA2C8F": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely", + }, + Object { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)", + }, + ], + }, + }, + "Properties": Object { + "LogGroupName": "/aws/vendedlogs/states/managerstackcloseoutservicestatemachinelog5da3d5b2d585", + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "closeoutserviceStateMachineRoleCA7F9A09": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": Object { + "Fn::FindInMap": Array [ + "ServiceprincipalMap", + Object { + "Ref": "AWS::Region", + }, + "states", + ], + }, + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::Role", + }, + "closeoutserviceStateMachineRoleDefaultPolicy5F05BEB8": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "The 'LogDelivery' actions do not support resource-level authorizations", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogDelivery", + "logs:GetLogDelivery", + "logs:UpdateLogDelivery", + "logs:DeleteLogDelivery", + "logs:ListLogDeliveries", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "createreportLambdaFunctionB37EBE52", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "createreportLambdaFunctionB37EBE52", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + Object { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "calculatetipsLambdaFunction9F2FE033", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "calculatetipsLambdaFunction9F2FE033", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + Object { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "archiveordersLambdaFunction66659E04", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "archiveordersLambdaFunction66659E04", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "logs:PutResourcePolicy", + "logs:DescribeResourcePolicies", + "logs:DescribeLogGroups", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "closeoutserviceStateMachineRoleDefaultPolicy5F05BEB8", + "Roles": Array [ + Object { + "Ref": "closeoutserviceStateMachineRoleCA7F9A09", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "createreportLambdaFunctionB37EBE52": Object { + "DependsOn": Array [ + "createreportLambdaFunctionServiceRoleDefaultPolicy5A317B6C", + "createreportLambdaFunctionServiceRole33C78256", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194S3Bucket6E9AF9DD", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194S3VersionKey00C1823E", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters4f63f3314eba5199f6a3a67bcc61daa0b5818e6d1fd850453035e9897c9ea194S3VersionKey00C1823E", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + "S3_BUCKET_NAME": Object { + "Ref": "reportsbucketS3BucketCC334898", + }, + }, + }, + "Handler": "index.handler", + "Layers": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefshareddbfunctionslayerA9CFEC5A89EBE406", + }, + ], + "Role": Object { + "Fn::GetAtt": Array [ + "createreportLambdaFunctionServiceRole33C78256", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "createreportLambdaFunctionServiceRole33C78256": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "createreportLambdaFunctionServiceRoleDefaultPolicy5A317B6C": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "reportsbucketS3BucketCC334898", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "reportsbucketS3BucketCC334898", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "createreportLambdaFunctionServiceRoleDefaultPolicy5A317B6C", + "Roles": Array [ + Object { + "Ref": "createreportLambdaFunctionServiceRole33C78256", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "getallordersLambdaFunctionFEF65B05": Object { + "DependsOn": Array [ + "getallordersLambdaFunctionServiceRoleDefaultPolicy705A0D9A", + "getallordersLambdaFunctionServiceRole8AF6EF71", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13S3Bucket6D0E979D", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13S3VersionKeyF08F3926", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters3a83da329865c0ce17a9871113042571872da858fb0911bab4526333986b2e13S3VersionKeyF08F3926", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + }, + }, + "Handler": "index.handler", + "Layers": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefshareddbfunctionslayerA9CFEC5A89EBE406", + }, + ], + "Role": Object { + "Fn::GetAtt": Array [ + "getallordersLambdaFunctionServiceRole8AF6EF71", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "getallordersLambdaFunctionServiceRole8AF6EF71": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "getallordersLambdaFunctionServiceRoleDefaultPolicy705A0D9A": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "getallordersLambdaFunctionServiceRoleDefaultPolicy705A0D9A", + "Roles": Array [ + Object { + "Ref": "getallordersLambdaFunctionServiceRole8AF6EF71", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "getreportLambdaFunction2A26EACE": Object { + "DependsOn": Array [ + "getreportLambdaFunctionServiceRoleDefaultPolicy0A75937B", + "getreportLambdaFunctionServiceRole39064368", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0S3Bucket0FECA056", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0S3VersionKeyA5DB7D1B", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters83ada50af307c970f7adc20c9db53c485f8a494eb3f1f5c79964c30a6fc739c0S3VersionKeyA5DB7D1B", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "S3_BUCKET_NAME": Object { + "Ref": "reportsbucketS3BucketCC334898", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "getreportLambdaFunctionServiceRole39064368", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "getreportLambdaFunctionServiceRole39064368": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "getreportLambdaFunctionServiceRoleDefaultPolicy0A75937B": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "reportsbucketS3BucketCC334898", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "reportsbucketS3BucketCC334898", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "getreportLambdaFunctionServiceRoleDefaultPolicy0A75937B", + "Roles": Array [ + Object { + "Ref": "getreportLambdaFunctionServiceRole39064368", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "managerapiApiAccessLogGroup35F9537B": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely", + }, + Object { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)", + }, + ], + }, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "managerapiCognitoAuthorizer5745AF8D": Object { + "Properties": Object { + "IdentitySource": "method.request.header.Authorization", + "Name": "authorizer", + "ProviderARNs": Array [ + Object { + "Fn::GetAtt": Array [ + "managerapiCognitoUserPool03946196", + "Arn", + ], + }, + ], + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "Type": "COGNITO_USER_POOLS", + }, + "Type": "AWS::ApiGateway::Authorizer", + }, + "managerapiCognitoUserPool03946196": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AccountRecoverySetting": Object { + "RecoveryMechanisms": Array [ + Object { + "Name": "verified_phone_number", + "Priority": 1, + }, + Object { + "Name": "verified_email", + "Priority": 2, + }, + ], + }, + "AdminCreateUserConfig": Object { + "AllowAdminCreateUserOnly": true, + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + "VerificationMessageTemplate": Object { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}", + }, + }, + "Type": "AWS::Cognito::UserPool", + "UpdateReplacePolicy": "Retain", + }, + "managerapiCognitoUserPoolClientB3BC933D": Object { + "Properties": Object { + "AllowedOAuthFlows": Array [ + "implicit", + "code", + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": Array [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin", + ], + "CallbackURLs": Array [ + "https://example.com", + ], + "SupportedIdentityProviders": Array [ + "COGNITO", + ], + "UserPoolId": Object { + "Ref": "managerapiCognitoUserPool03946196", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "managerapiLambdaRestApi62BEEAC7": Object { + "Properties": Object { + "Description": "Demo: Manager API", + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "LambdaRestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "managerapiLambdaRestApiAccount350A78F5": Object { + "DependsOn": Array [ + "managerapiLambdaRestApi62BEEAC7", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "managerapiLambdaRestApiCloudWatchRole43F48235", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "managerapiLambdaRestApiCloudWatchRole43F48235": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "managerapiLambdaRestApiDeployment719D102Abf8fc62f0a59aea37747b68fec60f4a4": Object { + "DependsOn": Array [ + "managerapiLambdaRestApicloseoutserviceproxyANY182BEB46", + "managerapiLambdaRestApicloseoutserviceproxyECAF0FA1", + "managerapiLambdaRestApicloseoutservice2D3987E6", + "managerapiLambdaRestApigetallordersproxyANYEAB740ED", + "managerapiLambdaRestApigetallordersproxyEE336136", + "managerapiLambdaRestApigetallorders29C83FB0", + "managerapiLambdaRestApigetreportproxyANY93C7142D", + "managerapiLambdaRestApigetreportproxy2FB66C91", + "managerapiLambdaRestApigetreportDB310438", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "managerapiLambdaRestApiDeploymentStageprod59C4A8D4": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "managerapiApiAccessLogGroup35F9537B", + "Arn", + ], + }, + "Format": "{\\"requestId\\":\\"$context.requestId\\",\\"ip\\":\\"$context.identity.sourceIp\\",\\"user\\":\\"$context.identity.user\\",\\"caller\\":\\"$context.identity.caller\\",\\"requestTime\\":\\"$context.requestTime\\",\\"httpMethod\\":\\"$context.httpMethod\\",\\"resourcePath\\":\\"$context.resourcePath\\",\\"status\\":\\"$context.status\\",\\"protocol\\":\\"$context.protocol\\",\\"responseLength\\":\\"$context.responseLength\\"}", + }, + "DeploymentId": Object { + "Ref": "managerapiLambdaRestApiDeployment719D102Abf8fc62f0a59aea37747b68fec60f4a4", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "StageName": "prod", + "TracingEnabled": true, + }, + "Type": "AWS::ApiGateway::Stage", + }, + "managerapiLambdaRestApiUsagePlanF10CE619": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "Stage": Object { + "Ref": "managerapiLambdaRestApiDeploymentStageprod59C4A8D4", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "managerapiLambdaRestApicloseoutservice2D3987E6": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "managerapiLambdaRestApi62BEEAC7", + "RootResourceId", + ], + }, + "PathPart": "close-out-service", + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "managerapiLambdaRestApicloseoutserviceproxyANY182BEB46": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "managerapiCognitoAuthorizer5745AF8D", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "closeoutserviceLambdaFunction684B26D3", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "managerapiLambdaRestApicloseoutserviceproxyECAF0FA1", + }, + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "managerapiLambdaRestApicloseoutserviceproxyANYApiPermissionManagerStackmanagerapiLambdaRestApi386CA826ANYcloseoutserviceproxy132F7E1D": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "closeoutserviceLambdaFunction684B26D3", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "/", + Object { + "Ref": "managerapiLambdaRestApiDeploymentStageprod59C4A8D4", + }, + "/*/close-out-service/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "managerapiLambdaRestApicloseoutserviceproxyANYApiPermissionTestManagerStackmanagerapiLambdaRestApi386CA826ANYcloseoutserviceproxy8A25C882": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "closeoutserviceLambdaFunction684B26D3", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "/test-invoke-stage/*/close-out-service/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "managerapiLambdaRestApicloseoutserviceproxyECAF0FA1": Object { + "Properties": Object { + "ParentId": Object { + "Ref": "managerapiLambdaRestApicloseoutservice2D3987E6", + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "managerapiLambdaRestApigetallorders29C83FB0": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "managerapiLambdaRestApi62BEEAC7", + "RootResourceId", + ], + }, + "PathPart": "get-all-orders", + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "managerapiLambdaRestApigetallordersproxyANYApiPermissionManagerStackmanagerapiLambdaRestApi386CA826ANYgetallordersproxy57374F7E": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "getallordersLambdaFunctionFEF65B05", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "/", + Object { + "Ref": "managerapiLambdaRestApiDeploymentStageprod59C4A8D4", + }, + "/*/get-all-orders/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "managerapiLambdaRestApigetallordersproxyANYApiPermissionTestManagerStackmanagerapiLambdaRestApi386CA826ANYgetallordersproxy45104276": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "getallordersLambdaFunctionFEF65B05", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "/test-invoke-stage/*/get-all-orders/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "managerapiLambdaRestApigetallordersproxyANYEAB740ED": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "managerapiCognitoAuthorizer5745AF8D", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "getallordersLambdaFunctionFEF65B05", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "managerapiLambdaRestApigetallordersproxyEE336136", + }, + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "managerapiLambdaRestApigetallordersproxyEE336136": Object { + "Properties": Object { + "ParentId": Object { + "Ref": "managerapiLambdaRestApigetallorders29C83FB0", + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "managerapiLambdaRestApigetreportDB310438": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "managerapiLambdaRestApi62BEEAC7", + "RootResourceId", + ], + }, + "PathPart": "get-report", + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "managerapiLambdaRestApigetreportproxy2FB66C91": Object { + "Properties": Object { + "ParentId": Object { + "Ref": "managerapiLambdaRestApigetreportDB310438", + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "managerapiLambdaRestApigetreportproxyANY93C7142D": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "managerapiCognitoAuthorizer5745AF8D", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "getreportLambdaFunction2A26EACE", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "managerapiLambdaRestApigetreportproxy2FB66C91", + }, + "RestApiId": Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "managerapiLambdaRestApigetreportproxyANYApiPermissionManagerStackmanagerapiLambdaRestApi386CA826ANYgetreportproxy80B2B901": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "getreportLambdaFunction2A26EACE", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "/", + Object { + "Ref": "managerapiLambdaRestApiDeploymentStageprod59C4A8D4", + }, + "/*/get-report/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "managerapiLambdaRestApigetreportproxyANYApiPermissionTestManagerStackmanagerapiLambdaRestApi386CA826ANYgetreportproxy7868B7DA": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "getreportLambdaFunction2A26EACE", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "managerapiLambdaRestApi62BEEAC7", + }, + "/test-invoke-stage/*/get-report/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "reportsbucketS3BucketCC334898": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "GLACIER", + "TransitionInDays": 90, + }, + ], + "Status": "Enabled", + }, + ], + }, + "LoggingConfiguration": Object { + "DestinationBucketName": Object { + "Ref": "reportsbucketS3LoggingBucket088F7E0E", + }, + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "reportsbucketS3BucketPolicyCF9C1527": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "reportsbucketS3BucketCC334898", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "reportsbucketS3BucketCC334898", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "reportsbucketS3BucketCC334898", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "reportsbucketS3LoggingBucket088F7E0E": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W35", + "reason": "This S3 bucket is used as the access logging bucket for another bucket", + }, + ], + }, + }, + "Properties": Object { + "AccessControl": "LogDeliveryWrite", + "BucketEncryption": Object { + "ServerSideEncryptionConfiguration": Array [ + Object { + "ServerSideEncryptionByDefault": Object { + "SSEAlgorithm": "AES256", + }, + }, + ], + }, + "PublicAccessBlockConfiguration": Object { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + "VersioningConfiguration": Object { + "Status": "Enabled", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "reportsbucketS3LoggingBucketPolicy82A7D267": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "reportsbucketS3LoggingBucket088F7E0E", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "reportsbucketS3LoggingBucket088F7E0E", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "reportsbucketS3LoggingBucket088F7E0E", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + }, +} +`; diff --git a/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/serviceStaffStack.test.js.snap b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/serviceStaffStack.test.js.snap new file mode 100644 index 000000000..b47c344a9 --- /dev/null +++ b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/serviceStaffStack.test.js.snap @@ -0,0 +1,1028 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test-service-staff-stack 1`] = ` +Object { + "Outputs": Object { + "servicestaffapiLambdaRestApiEndpointF8896A96": Object { + "Value": Object { + "Fn::Join": Array [ + "", + Array [ + "https://", + Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + ".execute-api.", + Object { + "Ref": "AWS::Region", + }, + ".", + Object { + "Ref": "AWS::URLSuffix", + }, + "/", + Object { + "Ref": "servicestaffapiLambdaRestApiDeploymentStageprod2DCA6394", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": Object { + "AssetParameters8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8ArtifactHash025BE40D": Object { + "Description": "Artifact hash for asset \\"8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8\\"", + "Type": "String", + }, + "AssetParameters8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8S3BucketC4EDB090": Object { + "Description": "S3 bucket for asset \\"8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8\\"", + "Type": "String", + }, + "AssetParameters8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8S3VersionKey5D4A7E1B": Object { + "Description": "S3 key for asset version \\"8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8\\"", + "Type": "String", + }, + "AssetParametersf340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080ArtifactHash28E831DC": Object { + "Description": "Artifact hash for asset \\"f340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080\\"", + "Type": "String", + }, + "AssetParametersf340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080S3Bucket40BD61E4": Object { + "Description": "S3 bucket for asset \\"f340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080\\"", + "Type": "String", + }, + "AssetParametersf340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080S3VersionKey661F3160": Object { + "Description": "S3 key for asset version \\"f340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080\\"", + "Type": "String", + }, + }, + "Resources": Object { + "createorderLambdaFunction24B8D5B1": Object { + "DependsOn": Array [ + "createorderLambdaFunctionServiceRoleDefaultPolicyA4FD773D", + "createorderLambdaFunctionServiceRole407C1F58", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParameters8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8S3BucketC4EDB090", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8S3VersionKey5D4A7E1B", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters8a8cc7536577c2b18b202c3f2d96d2e6ef38d6cd26b3cbcc4547ba11759adff8S3VersionKey5D4A7E1B", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "createorderLambdaFunctionServiceRole407C1F58", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "createorderLambdaFunctionServiceRole407C1F58": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "createorderLambdaFunctionServiceRoleDefaultPolicyA4FD773D": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "createorderLambdaFunctionServiceRoleDefaultPolicyA4FD773D", + "Roles": Array [ + Object { + "Ref": "createorderLambdaFunctionServiceRole407C1F58", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "processpaymentLambdaFunction2BB176F5": Object { + "DependsOn": Array [ + "processpaymentLambdaFunctionServiceRoleDefaultPolicy3675D629", + "processpaymentLambdaFunctionServiceRole04F88DC8", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "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.", + }, + Object { + "id": "W89", + "reason": "This is not a rule for the general case, just for specific use cases/industries", + }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, + ], + }, + }, + "Properties": Object { + "Code": Object { + "S3Bucket": Object { + "Ref": "AssetParametersf340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080S3Bucket40BD61E4", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersf340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080S3VersionKey661F3160", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParametersf340dbb5bff487818e0bfbfcd1dcdb62fb8068b86b63fd5e15c68b4d9686a080S3VersionKey661F3160", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Environment": Object { + "Variables": Object { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "DDB_TABLE_NAME": Object { + "Fn::ImportValue": "SharedStack:ExportsOutputRefordertable80C5609084F626AD", + }, + }, + }, + "Handler": "index.handler", + "Role": Object { + "Fn::GetAtt": Array [ + "processpaymentLambdaFunctionServiceRole04F88DC8", + "Arn", + ], + }, + "Runtime": "nodejs14.x", + "Timeout": 15, + "TracingConfig": Object { + "Mode": "Active", + }, + }, + "Type": "AWS::Lambda::Function", + }, + "processpaymentLambdaFunctionServiceRole04F88DC8": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaFunctionServiceRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "processpaymentLambdaFunctionServiceRoleDefaultPolicy3675D629": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, + "Properties": Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, + Object { + "Action": Array [ + "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": Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::ImportValue": "SharedStack:ExportsOutputFnGetAttordertable80C56090ArnFF5A50B5", + }, + "/index/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "processpaymentLambdaFunctionServiceRoleDefaultPolicy3675D629", + "Roles": Array [ + Object { + "Ref": "processpaymentLambdaFunctionServiceRole04F88DC8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "servicestaffapiApiAccessLogGroupF2EF9693": Object { + "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely", + }, + Object { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)", + }, + ], + }, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "servicestaffapiCognitoAuthorizerAD8B40CD": Object { + "Properties": Object { + "IdentitySource": "method.request.header.Authorization", + "Name": "authorizer", + "ProviderARNs": Array [ + Object { + "Fn::GetAtt": Array [ + "servicestaffapiCognitoUserPool3DD3B0E0", + "Arn", + ], + }, + ], + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + "Type": "COGNITO_USER_POOLS", + }, + "Type": "AWS::ApiGateway::Authorizer", + }, + "servicestaffapiCognitoUserPool3DD3B0E0": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "AccountRecoverySetting": Object { + "RecoveryMechanisms": Array [ + Object { + "Name": "verified_phone_number", + "Priority": 1, + }, + Object { + "Name": "verified_email", + "Priority": 2, + }, + ], + }, + "AdminCreateUserConfig": Object { + "AllowAdminCreateUserOnly": true, + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": Object { + "AdvancedSecurityMode": "ENFORCED", + }, + "VerificationMessageTemplate": Object { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}", + }, + }, + "Type": "AWS::Cognito::UserPool", + "UpdateReplacePolicy": "Retain", + }, + "servicestaffapiCognitoUserPoolClientB6DED78F": Object { + "Properties": Object { + "AllowedOAuthFlows": Array [ + "implicit", + "code", + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": Array [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin", + ], + "CallbackURLs": Array [ + "https://example.com", + ], + "SupportedIdentityProviders": Array [ + "COGNITO", + ], + "UserPoolId": Object { + "Ref": "servicestaffapiCognitoUserPool3DD3B0E0", + }, + }, + "Type": "AWS::Cognito::UserPoolClient", + }, + "servicestaffapiLambdaRestApi81C353A0": Object { + "Properties": Object { + "Description": "Demo: Service staff API", + "EndpointConfiguration": Object { + "Types": Array [ + "EDGE", + ], + }, + "Name": "LambdaRestApi", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "servicestaffapiLambdaRestApiAccount03DEB06F": Object { + "DependsOn": Array [ + "servicestaffapiLambdaRestApi81C353A0", + ], + "Properties": Object { + "CloudWatchRoleArn": Object { + "Fn::GetAtt": Array [ + "servicestaffapiLambdaRestApiCloudWatchRoleCE0F0BA2", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + }, + "servicestaffapiLambdaRestApiCloudWatchRoleCE0F0BA2": Object { + "Properties": Object { + "AssumeRolePolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": Object { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": Array [ + Object { + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": Array [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + "logs:GetLogEvents", + "logs:FilterLogEvents", + ], + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "LambdaRestApiCloudWatchRolePolicy", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "servicestaffapiLambdaRestApiDeployment344E0BF0f02c858dd7350b71af50ff6d8f77f6c2": Object { + "DependsOn": Array [ + "servicestaffapiLambdaRestApicreateorderproxyANY3B15F9EA", + "servicestaffapiLambdaRestApicreateorderproxy0E60FED5", + "servicestaffapiLambdaRestApicreateorder06D2A86B", + "servicestaffapiLambdaRestApiprocesspaymentproxyANYE502AA7B", + "servicestaffapiLambdaRestApiprocesspaymentproxy8CA5C2E5", + "servicestaffapiLambdaRestApiprocesspayment6A5A2B17", + ], + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W45", + "reason": "ApiGateway has AccessLogging enabled in AWS::ApiGateway::Stage resource, but cfn_nag checkes for it in AWS::ApiGateway::Deployment resource", + }, + ], + }, + }, + "Properties": Object { + "Description": "Automatically created by the RestApi construct", + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "servicestaffapiLambdaRestApiDeploymentStageprod2DCA6394": Object { + "Properties": Object { + "AccessLogSetting": Object { + "DestinationArn": Object { + "Fn::GetAtt": Array [ + "servicestaffapiApiAccessLogGroupF2EF9693", + "Arn", + ], + }, + "Format": "{\\"requestId\\":\\"$context.requestId\\",\\"ip\\":\\"$context.identity.sourceIp\\",\\"user\\":\\"$context.identity.user\\",\\"caller\\":\\"$context.identity.caller\\",\\"requestTime\\":\\"$context.requestTime\\",\\"httpMethod\\":\\"$context.httpMethod\\",\\"resourcePath\\":\\"$context.resourcePath\\",\\"status\\":\\"$context.status\\",\\"protocol\\":\\"$context.protocol\\",\\"responseLength\\":\\"$context.responseLength\\"}", + }, + "DeploymentId": Object { + "Ref": "servicestaffapiLambdaRestApiDeployment344E0BF0f02c858dd7350b71af50ff6d8f77f6c2", + }, + "MethodSettings": Array [ + Object { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + "StageName": "prod", + "TracingEnabled": true, + }, + "Type": "AWS::ApiGateway::Stage", + }, + "servicestaffapiLambdaRestApiUsagePlanCCBD3C60": Object { + "Properties": Object { + "ApiStages": Array [ + Object { + "ApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + "Stage": Object { + "Ref": "servicestaffapiLambdaRestApiDeploymentStageprod2DCA6394", + }, + "Throttle": Object {}, + }, + ], + }, + "Type": "AWS::ApiGateway::UsagePlan", + }, + "servicestaffapiLambdaRestApicreateorder06D2A86B": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "servicestaffapiLambdaRestApi81C353A0", + "RootResourceId", + ], + }, + "PathPart": "create-order", + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "servicestaffapiLambdaRestApicreateorderproxy0E60FED5": Object { + "Properties": Object { + "ParentId": Object { + "Ref": "servicestaffapiLambdaRestApicreateorder06D2A86B", + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "servicestaffapiLambdaRestApicreateorderproxyANY3B15F9EA": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "servicestaffapiCognitoAuthorizerAD8B40CD", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "createorderLambdaFunction24B8D5B1", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "servicestaffapiLambdaRestApicreateorderproxy0E60FED5", + }, + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + "servicestaffapiLambdaRestApicreateorderproxyANYApiPermissionServiceStaffStackservicestaffapiLambdaRestApiA743D41DANYcreateorderproxyB767FAB2": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "createorderLambdaFunction24B8D5B1", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + "/", + Object { + "Ref": "servicestaffapiLambdaRestApiDeploymentStageprod2DCA6394", + }, + "/*/create-order/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "servicestaffapiLambdaRestApicreateorderproxyANYApiPermissionTestServiceStaffStackservicestaffapiLambdaRestApiA743D41DANYcreateorderproxy5FF339BE": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "createorderLambdaFunction24B8D5B1", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + "/test-invoke-stage/*/create-order/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "servicestaffapiLambdaRestApiprocesspayment6A5A2B17": Object { + "Properties": Object { + "ParentId": Object { + "Fn::GetAtt": Array [ + "servicestaffapiLambdaRestApi81C353A0", + "RootResourceId", + ], + }, + "PathPart": "process-payment", + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "servicestaffapiLambdaRestApiprocesspaymentproxy8CA5C2E5": Object { + "Properties": Object { + "ParentId": Object { + "Ref": "servicestaffapiLambdaRestApiprocesspayment6A5A2B17", + }, + "PathPart": "{proxy+}", + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + }, + "Type": "AWS::ApiGateway::Resource", + }, + "servicestaffapiLambdaRestApiprocesspaymentproxyANYApiPermissionServiceStaffStackservicestaffapiLambdaRestApiA743D41DANYprocesspaymentproxyD72A3BE0": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "processpaymentLambdaFunction2BB176F5", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + "/", + Object { + "Ref": "servicestaffapiLambdaRestApiDeploymentStageprod2DCA6394", + }, + "/*/process-payment/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "servicestaffapiLambdaRestApiprocesspaymentproxyANYApiPermissionTestServiceStaffStackservicestaffapiLambdaRestApiA743D41DANYprocesspaymentproxyFC2482DA": Object { + "Properties": Object { + "Action": "lambda:InvokeFunction", + "FunctionName": Object { + "Fn::GetAtt": Array [ + "processpaymentLambdaFunction2BB176F5", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":execute-api:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":", + Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + "/test-invoke-stage/*/process-payment/*", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "servicestaffapiLambdaRestApiprocesspaymentproxyANYE502AA7B": Object { + "Properties": Object { + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": Object { + "Ref": "servicestaffapiCognitoAuthorizerAD8B40CD", + }, + "HttpMethod": "ANY", + "Integration": Object { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":apigateway:", + Object { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + Object { + "Fn::GetAtt": Array [ + "processpaymentLambdaFunction2BB176F5", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + "ResourceId": Object { + "Ref": "servicestaffapiLambdaRestApiprocesspaymentproxy8CA5C2E5", + }, + "RestApiId": Object { + "Ref": "servicestaffapiLambdaRestApi81C353A0", + }, + }, + "Type": "AWS::ApiGateway::Method", + }, + }, +} +`; diff --git a/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/sharedStack.test.js.snap b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/sharedStack.test.js.snap new file mode 100644 index 000000000..6b8288345 --- /dev/null +++ b/source/use_cases/aws-restaurant-management-demo/test/__snapshots__/sharedStack.test.js.snap @@ -0,0 +1,171 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test-shared-stack 1`] = ` +Object { + "Parameters": Object { + "AssetParameters2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9ArtifactHash1A6D75D8": Object { + "Description": "Artifact hash for asset \\"2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9\\"", + "Type": "String", + }, + "AssetParameters2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9S3Bucket7C16E5DF": Object { + "Description": "S3 bucket for asset \\"2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9\\"", + "Type": "String", + }, + "AssetParameters2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9S3VersionKeyEA7A0324": Object { + "Description": "S3 key for asset version \\"2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9\\"", + "Type": "String", + }, + }, + "Resources": Object { + "ordertable80C56090": Object { + "DeletionPolicy": "Delete", + "Properties": Object { + "AttributeDefinitions": Array [ + Object { + "AttributeName": "id", + "AttributeType": "S", + }, + Object { + "AttributeName": "gsi1pk", + "AttributeType": "S", + }, + Object { + "AttributeName": "gsi1sk", + "AttributeType": "S", + }, + ], + "GlobalSecondaryIndexes": Array [ + Object { + "IndexName": "gsi1pk-gsi1sk-index", + "KeySchema": Array [ + Object { + "AttributeName": "gsi1pk", + "KeyType": "HASH", + }, + Object { + "AttributeName": "gsi1sk", + "KeyType": "RANGE", + }, + ], + "Projection": Object { + "ProjectionType": "ALL", + }, + "ProvisionedThroughput": Object { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, + }, + ], + "KeySchema": Array [ + Object { + "AttributeName": "id", + "KeyType": "HASH", + }, + ], + "ProvisionedThroughput": Object { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5, + }, + }, + "Type": "AWS::DynamoDB::Table", + "UpdateReplacePolicy": "Delete", + }, + "ordertableReadScalingTarget145F706F": Object { + "Properties": Object { + "MaxCapacity": 50, + "MinCapacity": 1, + "ResourceId": Object { + "Fn::Join": Array [ + "", + Array [ + "table/", + Object { + "Ref": "ordertable80C56090", + }, + ], + ], + }, + "RoleARN": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::ACCOUNT_NUMBER_HERE:role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_DynamoDBTable", + ], + ], + }, + "ScalableDimension": "dynamodb:table:ReadCapacityUnits", + "ServiceNamespace": "dynamodb", + }, + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + }, + "ordertableReadScalingTargetTracking6E5FE6D6": Object { + "Properties": Object { + "PolicyName": "SharedStackordertableReadScalingTargetTracking5EEE9186", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": Object { + "Ref": "ordertableReadScalingTarget145F706F", + }, + "TargetTrackingScalingPolicyConfiguration": Object { + "PredefinedMetricSpecification": Object { + "PredefinedMetricType": "DynamoDBReadCapacityUtilization", + }, + "TargetValue": 50, + }, + }, + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + }, + "shareddbfunctionslayerA9CFEC5A": Object { + "Properties": Object { + "CompatibleRuntimes": Array [ + "nodejs14.x", + ], + "Content": Object { + "S3Bucket": Object { + "Ref": "AssetParameters2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9S3Bucket7C16E5DF", + }, + "S3Key": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::Select": Array [ + 0, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9S3VersionKeyEA7A0324", + }, + ], + }, + ], + }, + Object { + "Fn::Select": Array [ + 1, + Object { + "Fn::Split": Array [ + "||", + Object { + "Ref": "AssetParameters2cde1830f2ad369a1330b9dd603fe0b939ec6d81fbe00f66ffb45945750d9da9S3VersionKeyEA7A0324", + }, + ], + }, + ], + }, + ], + ], + }, + }, + "Description": "Layer for common database access functions", + "LicenseInfo": "Apache-2.0", + }, + "Type": "AWS::Lambda::LayerVersion", + }, + }, +} +`; diff --git a/source/use_cases/aws-s3-static-website/test/__snapshots__/s3-static-site-stack.test.js.snap b/source/use_cases/aws-s3-static-website/test/__snapshots__/s3-static-site-stack.test.js.snap index f175f7992..e452686f5 100644 --- a/source/use_cases/aws-s3-static-website/test/__snapshots__/s3-static-site-stack.test.js.snap +++ b/source/use_cases/aws-s3-static-website/test/__snapshots__/s3-static-site-stack.test.js.snap @@ -11,8 +11,8 @@ Object { "https://", Object { "Fn::GetAtt": Array [ - "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E", - "RegionalDomainName", + "CloudFrontToS3CloudFrontDistribution241D9866", + "DomainName", ], }, ], @@ -33,21 +33,21 @@ Object { "Description": "S3 key for asset version \\"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061ArtifactHash40CCFA64": Object { - "Description": "Artifact hash for asset \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916ArtifactHashB448546A": Object { + "Description": "Artifact hash for asset \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1": Object { - "Description": "S3 bucket for asset \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3BucketC05003CF": Object { + "Description": "S3 bucket for asset \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC": Object { - "Description": "S3 key for asset version \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD": Object { + "Description": "S3 key for asset version \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, }, "Resources": Object { - "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E": Object { + "CloudFrontToS3CloudFrontDistribution241D9866": Object { "Metadata": Object { "cfn_nag": Object { "rules_to_suppress": Array [ @@ -61,30 +61,20 @@ Object { "Properties": Object { "DistributionConfig": Object { "DefaultCacheBehavior": Object { - "AllowedMethods": Array [ - "GET", - "HEAD", - ], - "CachedMethods": Array [ - "GET", - "HEAD", - ], + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "Compress": true, - "ForwardedValues": Object { - "Cookies": Object { - "Forward": "none", - }, - "QueryString": false, - }, - "LambdaFunctionAssociations": Array [ + "FunctionAssociations": Array [ Object { - "EventType": "origin-response", - "LambdaFunctionARN": Object { - "Ref": "CloudFrontToS3SetHttpSecurityHeadersVersion699208AE", + "EventType": "viewer-response", + "FunctionARN": Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3SetHttpSecurityHeaders9E6088E2", + "FunctionARN", + ], }, }, ], - "TargetOriginId": "origin1", + "TargetOriginId": "S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507", "ViewerProtocolPolicy": "redirect-to-https", }, "DefaultRootObject": "index.html", @@ -98,7 +88,6 @@ Object { "RegionalDomainName", ], }, - "IncludeCookies": false, }, "Origins": Array [ Object { @@ -108,7 +97,7 @@ Object { "RegionalDomainName", ], }, - "Id": "origin1", + "Id": "S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507", "S3OriginConfig": Object { "OriginAccessIdentity": Object { "Fn::Join": Array [ @@ -116,7 +105,7 @@ Object { Array [ "origin-access-identity/cloudfront/", Object { - "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "Ref": "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F", }, ], ], @@ -124,18 +113,14 @@ Object { }, }, ], - "PriceClass": "PriceClass_100", - "ViewerCertificate": Object { - "CloudFrontDefaultCertificate": true, - }, }, }, "Type": "AWS::CloudFront::Distribution", }, - "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91": Object { + "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F": Object { "Properties": Object { "CloudFrontOriginAccessIdentityConfig": Object { - "Comment": "Access S3 bucket content only through CloudFront", + "Comment": "Identity for S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507", }, }, "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", @@ -149,10 +134,6 @@ Object { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", }, - Object { - "id": "W51", - "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", - }, ], }, }, @@ -180,6 +161,53 @@ Object { "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, + "CloudFrontToS3CloudfrontLoggingBucketPolicy416B82D9": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, "CloudFrontToS3S3Bucket9CE6AB04": Object { "DeletionPolicy": "Retain", "Properties": Object { @@ -192,6 +220,19 @@ Object { }, ], }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "GLACIER", + "TransitionInDays": 90, + }, + ], + "Status": "Enabled", + }, + ], + }, "LoggingConfiguration": Object { "DestinationBucketName": Object { "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2", @@ -228,29 +269,16 @@ Object { "PolicyDocument": Object { "Statement": Array [ Object { - "Action": Array [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*", - ], - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::cloudfront:user/CloudFront Origin Access Identity ", - Object { - "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", - }, - ], - ], + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", }, }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, "Resource": Array [ Object { "Fn::GetAtt": Array [ @@ -280,7 +308,7 @@ Object { "Principal": Object { "CanonicalUser": Object { "Fn::GetAtt": Array [ - "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F", "S3CanonicalUserId", ], }, @@ -315,10 +343,6 @@ Object { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for another bucket", }, - Object { - "id": "W51", - "reason": "This S3 bucket Bucket does not need a bucket policy", - }, ], }, }, @@ -346,104 +370,64 @@ Object { "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, - "CloudFrontToS3SetHttpSecurityHeaders9E6088E2": Object { - "DependsOn": Array [ - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10", - ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "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.", - }, - Object { - "id": "W89", - "reason": "This is not a rule for the general case, just for specific use cases/industries", - }, - ], - }, - }, + "CloudFrontToS3S3LoggingBucketPolicy360F3875": Object { "Properties": Object { - "Code": Object { - "ZipFile": "exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; headers['x-xss-protection'] = [ { key: 'X-XSS-Protection', value: '1; mode=block' } ]; headers['x-frame-options'] = [ { key: 'X-Frame-Options', value: 'DENY' } ]; headers['x-content-type-options'] = [ { key: 'X-Content-Type-Options', value: 'nosniff' } ]; headers['strict-transport-security'] = [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload' } ]; headers['referrer-policy'] = [ { key: 'Referrer-Policy', value: 'same-origin' } ]; headers['content-security-policy'] = [ { key: 'Content-Security-Policy', value: \\"default-src 'none'; base-uri 'self'; img-src 'self'; script-src 'self'; style-src 'self' https:; object-src 'none'; frame-ancestors 'none'; font-src 'self' https:; form-action 'self'; manifest-src 'self'; connect-src 'self'\\" } ]; callback(null, response); };", - }, - "Handler": "index.handler", - "Role": Object { - "Fn::GetAtt": Array [ - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10", - "Arn", - ], + "Bucket": Object { + "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2", }, - "Runtime": "nodejs14.x", - }, - "Type": "AWS::Lambda::Function", - }, - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { + "PolicyDocument": Object { "Statement": Array [ Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "Service": "lambda.amazonaws.com", + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, }, - }, - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", + "Effect": "Deny", "Principal": Object { - "Service": "edgelambda.amazonaws.com", + "AWS": "*", }, - }, - ], - "Version": "2012-10-17", - }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ + "Resource": Array [ Object { - "Action": Array [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", + "Fn::GetAtt": Array [ + "CloudFrontToS3S3LoggingBucketEF5CD8B2", + "Arn", ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:aws:logs:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":log-group:/aws/lambda/*", - ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3LoggingBucketEF5CD8B2", + "Arn", + ], + }, + "/*", ], - }, + ], }, ], - "Version": "2012-10-17", }, - "PolicyName": "LambdaFunctionServiceRolePolicy", - }, - ], + ], + "Version": "2012-10-17", + }, }, - "Type": "AWS::IAM::Role", + "Type": "AWS::S3::BucketPolicy", }, - "CloudFrontToS3SetHttpSecurityHeadersVersion699208AE": Object { + "CloudFrontToS3SetHttpSecurityHeaders9E6088E2": Object { "Properties": Object { - "FunctionName": Object { - "Ref": "CloudFrontToS3SetHttpSecurityHeaders9E6088E2", + "AutoPublish": true, + "FunctionCode": "function handler(event) { var response = event.response; var headers = response.headers; headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; headers['content-security-policy'] = { value: \\"default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'\\"}; headers['x-content-type-options'] = { value: 'nosniff'}; headers['x-frame-options'] = {value: 'DENY'}; headers['x-xss-protection'] = {value: '1; mode=block'}; return response; }", + "FunctionConfig": Object { + "Comment": "SetHttpSecurityHeadersc876981af0ddffc7454da8dcf76a116580471cc611", + "Runtime": "cloudfront-js-1.0", }, + "Name": "SetHttpSecurityHeadersc876981af0ddffc7454da8dcf76a116580471cc611", }, - "Type": "AWS::Lambda::Version", + "Type": "AWS::CloudFront::Function", }, "CustomResource": Object { "DeletionPolicy": "Delete", @@ -471,7 +455,7 @@ Object { "Properties": Object { "Code": Object { "S3Bucket": Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3BucketC05003CF", }, "S3Key": Object { "Fn::Join": Array [ @@ -484,7 +468,7 @@ Object { "Fn::Split": Array [ "||", Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD", }, ], }, @@ -497,7 +481,7 @@ Object { "Fn::Split": Array [ "||", Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD", }, ], }, @@ -507,6 +491,7 @@ Object { ], }, }, + "Description": "AWS CDK resource provider framework - onEvent (S3StaticWebsiteStack/CustomResourceProvider)", "Environment": Object { "Variables": Object { "USER_ON_EVENT_FUNCTION_ARN": Object { @@ -524,7 +509,7 @@ Object { "Arn", ], }, - "Runtime": "nodejs14.x", + "Runtime": "nodejs12.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", @@ -567,12 +552,28 @@ Object { Object { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": Object { - "Fn::GetAtt": Array [ - "copyObjHandlerDA1C4669", - "Arn", - ], - }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "copyObjHandlerDA1C4669", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "copyObjHandlerDA1C4669", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], }, ], "Version": "2012-10-17", @@ -684,7 +685,18 @@ Object { ], "Effect": "Allow", "Resource": Array [ - "arn:aws:s3:::wildrydes-us-east-1", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::wildrydes-us-east-1", + ], + ], + }, "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*", ], }, @@ -705,7 +717,11 @@ Object { "Fn::Join": Array [ "", Array [ - "arn:aws:s3:::", + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::", Object { "Ref": "CloudFrontToS3S3Bucket9CE6AB04", }, diff --git a/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.expected.json b/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.expected.json index af4358c12..50924503d 100644 --- a/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.expected.json +++ b/source/use_cases/aws-s3-static-website/test/integ.basic-deployment.expected.json @@ -31,15 +31,58 @@ { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for another bucket" - }, - { - "id": "W51", - "reason": "This S3 bucket Bucket does not need a bucket policy" } ] } } }, + "CloudFrontToS3S3LoggingBucketPolicy360F3875": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3LoggingBucketEF5CD8B2", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3LoggingBucketEF5CD8B2", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "CloudFrontToS3S3Bucket9CE6AB04": { "Type": "AWS::S3::Bucket", "Properties": { @@ -52,6 +95,19 @@ } ] }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionTransitions": [ + { + "StorageClass": "GLACIER", + "TransitionInDays": 90 + } + ], + "Status": "Enabled" + } + ] + }, "LoggingConfiguration": { "DestinationBucketName": { "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2" @@ -79,29 +135,16 @@ "PolicyDocument": { "Statement": [ { - "Action": [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*" - ], - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::cloudfront:user/CloudFront Origin Access Identity ", - { - "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91" - } - ] - ] + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" } }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, "Resource": [ { "Fn::GetAtt": [ @@ -131,7 +174,7 @@ "Principal": { "CanonicalUser": { "Fn::GetAtt": [ - "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F", "S3CanonicalUserId" ] } @@ -166,110 +209,15 @@ } } }, - "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91": { - "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", - "Properties": { - "CloudFrontOriginAccessIdentityConfig": { - "Comment": "Access S3 bucket content only through CloudFront" - } - } - }, - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } - }, - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "edgelambda.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:/aws/lambda/*" - ] - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "LambdaFunctionServiceRolePolicy" - } - ] - } - }, "CloudFrontToS3SetHttpSecurityHeaders9E6088E2": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "ZipFile": "exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; headers['x-xss-protection'] = [ { key: 'X-XSS-Protection', value: '1; mode=block' } ]; headers['x-frame-options'] = [ { key: 'X-Frame-Options', value: 'DENY' } ]; headers['x-content-type-options'] = [ { key: 'X-Content-Type-Options', value: 'nosniff' } ]; headers['strict-transport-security'] = [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload' } ]; headers['referrer-policy'] = [ { key: 'Referrer-Policy', value: 'same-origin' } ]; headers['content-security-policy'] = [ { key: 'Content-Security-Policy', value: \"default-src 'none'; base-uri 'self'; img-src 'self'; script-src 'self'; style-src 'self' https:; object-src 'none'; frame-ancestors 'none'; font-src 'self' https:; form-action 'self'; manifest-src 'self'; connect-src 'self'\" } ]; callback(null, response); };" - }, - "Handler": "index.handler", - "Role": { - "Fn::GetAtt": [ - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10", - "Arn" - ] - }, - "Runtime": "nodejs14.x" - }, - "DependsOn": [ - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10" - ], - "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" - } - ] - } - } - }, - "CloudFrontToS3SetHttpSecurityHeadersVersion699208AE": { - "Type": "AWS::Lambda::Version", + "Type": "AWS::CloudFront::Function", "Properties": { - "FunctionName": { - "Ref": "CloudFrontToS3SetHttpSecurityHeaders9E6088E2" + "Name": "SetHttpSecurityHeadersc89bf7cb13bc7f7194252ea024cb8eeb4e31b4fd0e", + "AutoPublish": true, + "FunctionCode": "function handler(event) { var response = event.response; var headers = response.headers; headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; headers['content-security-policy'] = { value: \"default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'\"}; headers['x-content-type-options'] = { value: 'nosniff'}; headers['x-frame-options'] = {value: 'DENY'}; headers['x-xss-protection'] = {value: '1; mode=block'}; return response; }", + "FunctionConfig": { + "Comment": "SetHttpSecurityHeadersc89bf7cb13bc7f7194252ea024cb8eeb4e31b4fd0e", + "Runtime": "cloudfront-js-1.0" } } }, @@ -304,44 +252,85 @@ { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" - }, - { - "id": "W51", - "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" } ] } } }, - "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E": { + "CloudFrontToS3CloudfrontLoggingBucketPolicy416B82D9": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CloudFrontToS3CloudfrontLoggingBucket8350BE9B" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Identity for StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1EFE54F66" + } + } + }, + "CloudFrontToS3CloudFrontDistribution241D9866": { "Type": "AWS::CloudFront::Distribution", "Properties": { "DistributionConfig": { "DefaultCacheBehavior": { - "AllowedMethods": [ - "GET", - "HEAD" - ], - "CachedMethods": [ - "GET", - "HEAD" - ], + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "Compress": true, - "ForwardedValues": { - "Cookies": { - "Forward": "none" - }, - "QueryString": false - }, - "LambdaFunctionAssociations": [ + "FunctionAssociations": [ { - "EventType": "origin-response", - "LambdaFunctionARN": { - "Ref": "CloudFrontToS3SetHttpSecurityHeadersVersion699208AE" + "EventType": "viewer-response", + "FunctionARN": { + "Fn::GetAtt": [ + "CloudFrontToS3SetHttpSecurityHeaders9E6088E2", + "FunctionARN" + ] } } ], - "TargetOriginId": "origin1", + "TargetOriginId": "StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1EFE54F66", "ViewerProtocolPolicy": "redirect-to-https" }, "DefaultRootObject": "index.html", @@ -354,8 +343,7 @@ "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", "RegionalDomainName" ] - }, - "IncludeCookies": false + } }, "Origins": [ { @@ -365,7 +353,7 @@ "RegionalDomainName" ] }, - "Id": "origin1", + "Id": "StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1EFE54F66", "S3OriginConfig": { "OriginAccessIdentity": { "Fn::Join": [ @@ -373,18 +361,14 @@ [ "origin-access-identity/cloudfront/", { - "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91" + "Ref": "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F" } ] ] } } } - ], - "PriceClass": "PriceClass_100", - "ViewerCertificate": { - "CloudFrontDefaultCertificate": true - } + ] } }, "Metadata": { @@ -441,7 +425,18 @@ ], "Effect": "Allow", "Resource": [ - "arn:aws:s3:::wildrydes-us-east-1", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::wildrydes-us-east-1" + ] + ] + }, "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" ] }, @@ -462,7 +457,11 @@ "Fn::Join": [ "", [ - "arn:aws:s3:::", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", { "Ref": "CloudFrontToS3S3Bucket9CE6AB04" } @@ -499,49 +498,17 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3BucketC3836BD9" + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9" - } - ] - } - ] - } - ] - ] - } + "S3Key": "2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb.zip" }, - "Handler": "copy_s3_objects.on_event", "Role": { "Fn::GetAtt": [ "copyObjHandlerServiceRoleA0ECE649", "Arn" ] }, + "Handler": "copy_s3_objects.on_event", "Runtime": "python3.8", "Timeout": 300 }, @@ -589,12 +556,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "copyObjHandlerDA1C4669", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "copyObjHandlerDA1C4669", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "copyObjHandlerDA1C4669", + "Arn" + ] + }, + ":*" + ] + ] + } + ] } ], "Version": "2012-10-17" @@ -612,50 +595,17 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1" + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC" - } - ] - } - ] - } - ] - ] - } + "S3Key": "543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916.zip" }, - "Handler": "framework.onEvent", "Role": { "Fn::GetAtt": [ "CustomResourceProviderframeworkonEventServiceRole7EBC5835", "Arn" ] }, - "Runtime": "nodejs14.x", + "Description": "AWS CDK resource provider framework - onEvent (StaticWebsiteStack/CustomResourceProvider)", "Environment": { "Variables": { "USER_ON_EVENT_FUNCTION_ARN": { @@ -666,6 +616,8 @@ } } }, + "Handler": "framework.onEvent", + "Runtime": "nodejs12.x", "Timeout": 900 }, "DependsOn": [ @@ -692,32 +644,6 @@ "DeletionPolicy": "Delete" } }, - "Parameters": { - "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3BucketC3836BD9": { - "Type": "String", - "Description": "S3 bucket for asset \"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\"" - }, - "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debS3VersionKeyEDA18BF9": { - "Type": "String", - "Description": "S3 key for asset version \"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\"" - }, - "AssetParameters2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503debArtifactHashA2BE913F": { - "Type": "String", - "Description": "Artifact hash for asset \"2a96a41ef8e6db639865e8dc7826848e60ba75ebdb553c6c9bf2f961ef503deb\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1": { - "Type": "String", - "Description": "S3 bucket for asset \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC": { - "Type": "String", - "Description": "S3 key for asset version \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061ArtifactHash40CCFA64": { - "Type": "String", - "Description": "Artifact hash for asset \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" - } - }, "Outputs": { "websiteURL": { "Value": { @@ -727,13 +653,47 @@ "https://", { "Fn::GetAtt": [ - "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E", - "RegionalDomainName" + "CloudFrontToS3CloudFrontDistribution241D9866", + "DomainName" ] } ] ] } } + }, + "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/use_cases/aws-s3-static-website/test/s3-static-site-stack.test.ts b/source/use_cases/aws-s3-static-website/test/s3-static-site-stack.test.ts index e46ce063c..cfd5d269d 100644 --- a/source/use_cases/aws-s3-static-website/test/s3-static-site-stack.test.ts +++ b/source/use_cases/aws-s3-static-website/test/s3-static-site-stack.test.ts @@ -54,60 +54,74 @@ test('check s3 bucket public access setting', () => { test('check CR lambda function permissions', () => { const app = new cdk.App(); const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); - expect(stack).toHaveResource("AWS::IAM::Policy", { - PolicyDocument: { - Statement: [ - { - Action: [ - "s3:GetObject", - "s3:ListBucket" - ], - Effect: "Allow", - Resource: [ - "arn:aws:s3:::wildrydes-us-east-1", - "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" - ] - }, - { - Action: [ - "s3:ListBucket", - "s3:GetObject", - "s3:PutObject", - "s3:PutObjectAcl", - "s3:PutObjectVersionAcl", - "s3:DeleteObject", - "s3:DeleteObjectVersion", - "s3:CopyObject" - ], - Effect: "Allow", - Resource: [ - { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - Ref: "CloudFrontToS3S3Bucket9CE6AB04" - } + expect(stack).toHaveResourceLike("AWS::IAM::Policy",{ + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::wildrydes-us-east-1" + ] ] - ] - }, - { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - Ref: "CloudFrontToS3S3Bucket9CE6AB04" - }, - "/*" + }, + "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" + ] + }, + { + "Action": [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + } + ] ] - ] - } - ] - } - ], - Version: "2012-10-17" + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + }, + "/*" + ] + ] + } + ] + } +] }, PolicyName: "copyObjHandlerServiceRoleDefaultPolicyFCA51C18", Roles: [ @@ -116,4 +130,5 @@ test('check CR lambda function permissions', () => { } ] }); -}); \ No newline at end of file +}); + diff --git a/source/use_cases/aws-serverless-image-handler/lib/index.ts b/source/use_cases/aws-serverless-image-handler/lib/index.ts index bae358422..550d2be51 100644 --- a/source/use_cases/aws-serverless-image-handler/lib/index.ts +++ b/source/use_cases/aws-serverless-image-handler/lib/index.ts @@ -74,7 +74,7 @@ export interface ServerlessImageHandlerCustomProps { * * @default - false. */ - readonly cloudFrontDistributionProps?: cloudfront.DistributionProps | any, + readonly cloudFrontDistributionProps?: cloudFront.DistributionProps | any, /** * Optional user provided props to override the default props for the API Gateway REST API. * @@ -195,18 +195,18 @@ export class ServerlessImageHandler extends Construct { // Add the SOURCE_BUCKETS environment variable to the Lambda function const bucketsArr = (props.sourceBuckets !== "") ? props.sourceBuckets.split(',') : []; - bucketsArr.push(this.lambdaS3.s3Bucket.bucketName); + bucketsArr.push(this.safeGetBucketProperty().bucketName); const bucketsStr = bucketsArr.toString().replace(/\s+/g, ''); this.cloudFrontApiGatewayLambda.lambdaFunction.addEnvironment("SOURCE_BUCKETS", bucketsStr); } /** * @summary Returns an instance of cloudFront.CloudFrontWebDistribution created by the construct. - * @returns { cloudFront.CloudFrontWebDistribution } Instance of CloudFrontWebDistribution created by the construct. + * @returns { cloudFront.Distribution } Instance of CloudFrontWebDistribution created by the construct. * @since 0.8.0 * @access public */ - public cloudFrontDistribution(): cloudFront.CloudFrontWebDistribution { + public cloudFrontDistribution(): cloudFront.Distribution { return this.cloudFrontApiGatewayLambda.cloudFrontWebDistribution; } @@ -237,6 +237,17 @@ export class ServerlessImageHandler extends Construct { * @access public */ public s3Bucket(): s3.Bucket { - return this.lambdaS3.s3Bucket; + return this.safeGetBucketProperty(); + } + + private safeGetBucketProperty(): s3.Bucket { + // When LambdaToS3 was altered to accept IBucket, the + // s3Bucket property became optional. This app always + // has LambdaToS3 create a new bucket, so if the S3Bucket property + // is undefined, then an invalid situation has arisen. + if (!this.lambdaS3.s3Bucket) { + throw Error('s3Bucket is not set - this should never occur'); + } + return this.lambdaS3.s3Bucket; } } \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/test/__snapshots__/test.serverless-image-handler.test.js.snap b/source/use_cases/aws-serverless-image-handler/test/__snapshots__/test.serverless-image-handler.test.js.snap index cd74d7d04..c47014469 100644 --- a/source/use_cases/aws-serverless-image-handler/test/__snapshots__/test.serverless-image-handler.test.js.snap +++ b/source/use_cases/aws-serverless-image-handler/test/__snapshots__/test.serverless-image-handler.test.js.snap @@ -31,26 +31,40 @@ Object { }, }, "Parameters": Object { - "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96ArtifactHash7AE27721": Object { - "Description": "Artifact hash for asset \\"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\\"", + "AssetParameterscd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1ArtifactHash1C81AD0D": Object { + "Description": "Artifact hash for asset \\"cd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1\\"", "Type": "String", }, - "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3Bucket65CDB50E": Object { - "Description": "S3 bucket for asset \\"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\\"", + "AssetParameterscd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1S3Bucket55C0D3E0": Object { + "Description": "S3 bucket for asset \\"cd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1\\"", "Type": "String", }, - "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1": Object { - "Description": "S3 key for asset version \\"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\\"", + "AssetParameterscd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1S3VersionKeyE00C37AD": Object { + "Description": "S3 key for asset version \\"cd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1\\"", "Type": "String", }, }, "Resources": Object { "testserverlessimagehandlerCloudFrontApiGatewayLambdaApiAccessLogGroup75A8AB40": Object { "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely", + }, + Object { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)", + }, + ], + }, + }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Retain", }, - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistributionCFDistribution5DCC756A": Object { + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistribution73757325": Object { "Metadata": Object { "cfn_nag": Object { "rules_to_suppress": Array [ @@ -64,33 +78,22 @@ Object { "Properties": Object { "DistributionConfig": Object { "DefaultCacheBehavior": Object { - "AllowedMethods": Array [ - "GET", - "HEAD", - ], - "CachedMethods": Array [ - "GET", - "HEAD", - ], + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "Compress": true, - "ForwardedValues": Object { - "Cookies": Object { - "Forward": "none", - }, - "QueryString": false, - }, - "LambdaFunctionAssociations": Array [ + "FunctionAssociations": Array [ Object { - "EventType": "origin-response", - "LambdaFunctionARN": Object { - "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersVersion5920FF7F", + "EventType": "viewer-response", + "FunctionARN": Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersC49E7B59", + "FunctionARN", + ], }, }, ], - "TargetOriginId": "origin1", + "TargetOriginId": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistributionOrigin161738C6A", "ViewerProtocolPolicy": "redirect-to-https", }, - "DefaultRootObject": "index.html", "Enabled": true, "HttpVersion": "http2", "IPV6Enabled": true, @@ -101,16 +104,11 @@ Object { "RegionalDomainName", ], }, - "IncludeCookies": false, }, "Origins": Array [ Object { "CustomOriginConfig": Object { - "HTTPPort": 80, - "HTTPSPort": 443, - "OriginKeepaliveTimeout": 5, "OriginProtocolPolicy": "https-only", - "OriginReadTimeout": 30, "OriginSSLProtocols": Array [ "TLSv1.2", ], @@ -159,13 +157,20 @@ Object { }, ], }, - "Id": "origin1", + "Id": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistributionOrigin161738C6A", + "OriginPath": Object { + "Fn::Join": Array [ + "", + Array [ + "/", + Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiDeploymentStageprodBD57762D", + }, + ], + ], + }, }, ], - "PriceClass": "PriceClass_100", - "ViewerCertificate": Object { - "CloudFrontDefaultCertificate": true, - }, }, }, "Type": "AWS::CloudFront::Distribution", @@ -179,10 +184,6 @@ Object { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", }, - Object { - "id": "W51", - "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", - }, ], }, }, @@ -210,104 +211,64 @@ Object { "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersC49E7B59": Object { - "DependsOn": Array [ - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersServiceRole6A96B325", - ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "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.", - }, - Object { - "id": "W89", - "reason": "This is not a rule for the general case, just for specific use cases/industries", - }, - ], - }, - }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucketPolicy966FBA77": Object { "Properties": Object { - "Code": Object { - "ZipFile": "exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; headers['x-xss-protection'] = [ { key: 'X-XSS-Protection', value: '1; mode=block' } ]; headers['x-frame-options'] = [ { key: 'X-Frame-Options', value: 'DENY' } ]; headers['x-content-type-options'] = [ { key: 'X-Content-Type-Options', value: 'nosniff' } ]; headers['strict-transport-security'] = [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload' } ]; headers['referrer-policy'] = [ { key: 'Referrer-Policy', value: 'same-origin' } ]; headers['content-security-policy'] = [ { key: 'Content-Security-Policy', value: \\"default-src 'none'; base-uri 'self'; img-src 'self'; script-src 'self'; style-src 'self' https:; object-src 'none'; frame-ancestors 'none'; font-src 'self' https:; form-action 'self'; manifest-src 'self'; connect-src 'self'\\" } ]; callback(null, response); };", - }, - "Handler": "index.handler", - "Role": Object { - "Fn::GetAtt": Array [ - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersServiceRole6A96B325", - "Arn", - ], + "Bucket": Object { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378", }, - "Runtime": "nodejs14.x", - }, - "Type": "AWS::Lambda::Function", - }, - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersServiceRole6A96B325": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { + "PolicyDocument": Object { "Statement": Array [ Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "Service": "lambda.amazonaws.com", + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, }, - }, - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", + "Effect": "Deny", "Principal": Object { - "Service": "edgelambda.amazonaws.com", + "AWS": "*", }, - }, - ], - "Version": "2012-10-17", - }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ + "Resource": Array [ Object { - "Action": Array [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", + "Fn::GetAtt": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378", + "Arn", ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:aws:logs:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":log-group:/aws/lambda/*", - ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378", + "Arn", + ], + }, + "/*", ], - }, + ], }, ], - "Version": "2012-10-17", }, - "PolicyName": "LambdaFunctionServiceRolePolicy", - }, - ], + ], + "Version": "2012-10-17", + }, }, - "Type": "AWS::IAM::Role", + "Type": "AWS::S3::BucketPolicy", }, - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersVersion5920FF7F": Object { + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersC49E7B59": Object { "Properties": Object { - "FunctionName": Object { - "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersC49E7B59", + "AutoPublish": true, + "FunctionCode": "function handler(event) { var response = event.response; var headers = response.headers; headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; headers['content-security-policy'] = { value: \\"default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'\\"}; headers['x-content-type-options'] = { value: 'nosniff'}; headers['x-frame-options'] = {value: 'DENY'}; headers['x-xss-protection'] = {value: '1; mode=block'}; return response; }", + "FunctionConfig": Object { + "Comment": "SetHttpSecurityHeadersc892743f1cbff1695f678386f5b532b064c85bbbbd", + "Runtime": "cloudfront-js-1.0", }, + "Name": "SetHttpSecurityHeadersc892743f1cbff1695f678386f5b532b064c85bbbbd", }, - "Type": "AWS::Lambda::Version", + "Type": "AWS::CloudFront::Function", }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaFunctionF12AF5FC": Object { "DependsOn": Array [ @@ -325,13 +286,17 @@ Object { "id": "W89", "reason": "This is not a rule for the general case, just for specific use cases/industries", }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, ], }, }, "Properties": Object { "Code": Object { "S3Bucket": Object { - "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3Bucket65CDB50E", + "Ref": "AssetParameterscd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1S3Bucket55C0D3E0", }, "S3Key": Object { "Fn::Join": Array [ @@ -344,7 +309,7 @@ Object { "Fn::Split": Array [ "||", Object { - "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1", + "Ref": "AssetParameterscd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1S3VersionKeyE00C37AD", }, ], }, @@ -357,7 +322,7 @@ Object { "Fn::Split": Array [ "||", Object { - "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1", + "Ref": "AssetParameterscd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1S3VersionKeyE00C37AD", }, ], }, @@ -397,6 +362,9 @@ Object { ], }, "Runtime": "nodejs14.x", + "TracingConfig": Object { + "Mode": "Active", + }, }, "Type": "AWS::Lambda::Function", }, @@ -429,7 +397,11 @@ Object { "Fn::Join": Array [ "", Array [ - "arn:aws:logs:", + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", Object { "Ref": "AWS::Region", }, @@ -452,9 +424,27 @@ Object { "Type": "AWS::IAM::Role", }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaFunctionServiceRoleDefaultPolicy9E8AAE29": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, "Properties": Object { "PolicyDocument": Object { "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, Object { "Action": Array [ "s3:GetObject*", @@ -593,8 +583,18 @@ Object { "Type": "AWS::Lambda::Permission", }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiANYBB77827B": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W59", + "reason": "AWS::ApiGateway::Method AuthorizationType is set to 'NONE' because API Gateway behind CloudFront does not support AWS_IAM authentication", + }, + ], + }, + }, "Properties": Object { - "AuthorizationType": "AWS_IAM", + "AuthorizationType": "NONE", "HttpMethod": "ANY", "Integration": Object { "IntegrationHttpMethod": "POST", @@ -682,7 +682,11 @@ Object { "Fn::Join": Array [ "", Array [ - "arn:aws:logs:", + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", Object { "Ref": "AWS::Region", }, @@ -744,7 +748,7 @@ Object { }, "MethodSettings": Array [ Object { - "DataTraceEnabled": true, + "DataTraceEnabled": false, "HttpMethod": "*", "LoggingLevel": "INFO", "ResourcePath": "/*", @@ -754,6 +758,7 @@ Object { "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApi7B1E91FA", }, "StageName": "prod", + "TracingEnabled": true, }, "Type": "AWS::ApiGateway::Stage", }, @@ -818,7 +823,7 @@ Object { Object { "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApi7B1E91FA", }, - "/test-invoke-stage/*/{proxy+}", + "/test-invoke-stage/*/*", ], ], }, @@ -859,7 +864,7 @@ Object { Object { "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiDeploymentStageprodBD57762D", }, - "/*/{proxy+}", + "/*/*", ], ], }, @@ -867,8 +872,18 @@ Object { "Type": "AWS::Lambda::Permission", }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiproxyANYF4D41A65": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W59", + "reason": "AWS::ApiGateway::Method AuthorizationType is set to 'NONE' because API Gateway behind CloudFront does not support AWS_IAM authentication", + }, + ], + }, + }, "Properties": Object { - "AuthorizationType": "AWS_IAM", + "AuthorizationType": "NONE", "HttpMethod": "ANY", "Integration": Object { "IntegrationHttpMethod": "POST", @@ -908,16 +923,6 @@ Object { }, "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662": Object { "DeletionPolicy": "Retain", - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W51", - "reason": "This S3 bucket Bucket does not need a bucket policy", - }, - ], - }, - }, "Properties": Object { "BucketEncryption": Object { "ServerSideEncryptionConfiguration": Array [ @@ -928,6 +933,19 @@ Object { }, ], }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "GLACIER", + "TransitionInDays": 90, + }, + ], + "Status": "Enabled", + }, + ], + }, "LoggingConfiguration": Object { "DestinationBucketName": Object { "Ref": "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181", @@ -946,6 +964,53 @@ Object { "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, + "testserverlessimagehandlerExistingLambdaS3S3BucketPolicy4313A384": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181": Object { "DeletionPolicy": "Retain", "Metadata": Object { @@ -955,10 +1020,6 @@ Object { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for another bucket", }, - Object { - "id": "W51", - "reason": "This S3 bucket Bucket does not need a bucket policy", - }, ], }, }, @@ -986,6 +1047,53 @@ Object { "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, + "testserverlessimagehandlerExistingLambdaS3S3LoggingBucketPolicy1D599FE4": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, "testserverlessimagehandlerLambdaS3AccessPolicyD6DC56B2": Object { "Metadata": Object { "cfn_nag": Object { diff --git a/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.expected.json b/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.expected.json index ee3b6ef51..e210f84f4 100644 --- a/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.expected.json +++ b/source/use_cases/aws-serverless-image-handler/test/integ.basic-deployment.expected.json @@ -31,7 +31,11 @@ "Fn::Join": [ "", [ - "arn:aws:logs:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", { "Ref": "AWS::Region" }, @@ -57,13 +61,25 @@ "Properties": { "PolicyDocument": { "Statement": [ + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + }, { "Action": [ "s3:GetObject*", "s3:GetBucket*", "s3:List*", "s3:DeleteObject*", - "s3:PutObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging", "s3:Abort*" ], "Effect": "Allow", @@ -99,6 +115,16 @@ "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaFunctionServiceRole110B3FC6" } ] + }, + "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." + } + ] + } } }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaFunctionF12AF5FC": { @@ -106,56 +132,22 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3Bucket65CDB50E" + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1" - } - ] - } - ] - } - ] - ] - } + "S3Key": "cd62adae72264cea63bda88de1b14cf6c4cec29557ae44e526714470d2e2f1f1.zip" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaFunctionServiceRole110B3FC6", "Arn" ] }, - "Runtime": "nodejs14.x", "Environment": { "Variables": { - "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", "AUTO_WEBP": "No", "CORS_ENABLED": "Yes", "CORS_ORIGIN": "*", + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", "S3_BUCKET_NAME": { "Ref": "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662" }, @@ -171,6 +163,11 @@ ] } } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" } }, "DependsOn": [ @@ -187,6 +184,10 @@ { "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" } ] } @@ -195,7 +196,21 @@ "testserverlessimagehandlerCloudFrontApiGatewayLambdaApiAccessLogGroup75A8AB40": { "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApi7B1E91FA": { "Type": "AWS::ApiGateway::RestApi", @@ -255,13 +270,14 @@ }, "MethodSettings": [ { - "DataTraceEnabled": true, + "DataTraceEnabled": false, "HttpMethod": "*", "LoggingLevel": "INFO", "ResourcePath": "/*" } ], - "StageName": "prod" + "StageName": "prod", + "TracingEnabled": true } }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiproxy4612A938": { @@ -314,7 +330,7 @@ { "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiDeploymentStageprodBD57762D" }, - "/*/{proxy+}" + "/*/*" ] ] } @@ -351,7 +367,7 @@ { "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApi7B1E91FA" }, - "/test-invoke-stage/*/{proxy+}" + "/test-invoke-stage/*/*" ] ] } @@ -367,7 +383,7 @@ "RestApiId": { "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApi7B1E91FA" }, - "AuthorizationType": "AWS_IAM", + "AuthorizationType": "NONE", "Integration": { "IntegrationHttpMethod": "POST", "Type": "AWS_PROXY", @@ -395,6 +411,16 @@ ] } } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W59", + "reason": "AWS::ApiGateway::Method AuthorizationType is set to 'NONE' because API Gateway behind CloudFront does not support AWS_IAM authentication" + } + ] + } } }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiANYApiPermissiontestserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApi39D5B0E8ANYE42619BB": { @@ -488,7 +514,7 @@ "RestApiId": { "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApi7B1E91FA" }, - "AuthorizationType": "AWS_IAM", + "AuthorizationType": "NONE", "Integration": { "IntegrationHttpMethod": "POST", "Type": "AWS_PROXY", @@ -516,6 +542,16 @@ ] } } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W59", + "reason": "AWS::ApiGateway::Method AuthorizationType is set to 'NONE' because API Gateway behind CloudFront does not support AWS_IAM authentication" + } + ] + } } }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiUsagePlan24615316": { @@ -568,7 +604,11 @@ "Fn::Join": [ "", [ - "arn:aws:logs:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", { "Ref": "AWS::Region" }, @@ -603,102 +643,15 @@ "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApi7B1E91FA" ] }, - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersServiceRole6A96B325": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } - }, - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "edgelambda.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:/aws/lambda/*" - ] - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "LambdaFunctionServiceRolePolicy" - } - ] - } - }, "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersC49E7B59": { - "Type": "AWS::Lambda::Function", + "Type": "AWS::CloudFront::Function", "Properties": { - "Code": { - "ZipFile": "exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; headers['x-xss-protection'] = [ { key: 'X-XSS-Protection', value: '1; mode=block' } ]; headers['x-frame-options'] = [ { key: 'X-Frame-Options', value: 'DENY' } ]; headers['x-content-type-options'] = [ { key: 'X-Content-Type-Options', value: 'nosniff' } ]; headers['strict-transport-security'] = [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload' } ]; headers['referrer-policy'] = [ { key: 'Referrer-Policy', value: 'same-origin' } ]; headers['content-security-policy'] = [ { key: 'Content-Security-Policy', value: \"default-src 'none'; base-uri 'self'; img-src 'self'; script-src 'self'; style-src 'self' https:; object-src 'none'; frame-ancestors 'none'; font-src 'self' https:; form-action 'self'; manifest-src 'self'; connect-src 'self'\" } ]; callback(null, response); };" - }, - "Handler": "index.handler", - "Role": { - "Fn::GetAtt": [ - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersServiceRole6A96B325", - "Arn" - ] - }, - "Runtime": "nodejs14.x" - }, - "DependsOn": [ - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersServiceRole6A96B325" - ], - "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" - } - ] - } - } - }, - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersVersion5920FF7F": { - "Type": "AWS::Lambda::Version", - "Properties": { - "FunctionName": { - "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersC49E7B59" + "Name": "SetHttpSecurityHeadersc837763a6a3a9a67e25b11f161390c447daaa927a0", + "AutoPublish": true, + "FunctionCode": "function handler(event) { var response = event.response; var headers = response.headers; headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; headers['content-security-policy'] = { value: \"default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'\"}; headers['x-content-type-options'] = { value: 'nosniff'}; headers['x-frame-options'] = {value: 'DENY'}; headers['x-xss-protection'] = {value: '1; mode=block'}; return response; }", + "FunctionConfig": { + "Comment": "SetHttpSecurityHeadersc837763a6a3a9a67e25b11f161390c447daaa927a0", + "Runtime": "cloudfront-js-1.0" } } }, @@ -733,47 +686,79 @@ { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" - }, - { - "id": "W51", - "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" } ] } } }, - "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistributionCFDistribution5DCC756A": { + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucketPolicy966FBA77": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistribution73757325": { "Type": "AWS::CloudFront::Distribution", "Properties": { "DistributionConfig": { "DefaultCacheBehavior": { - "AllowedMethods": [ - "GET", - "HEAD" - ], - "CachedMethods": [ - "GET", - "HEAD" - ], + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "Compress": true, - "ForwardedValues": { - "Cookies": { - "Forward": "none" - }, - "QueryString": false - }, - "LambdaFunctionAssociations": [ + "FunctionAssociations": [ { - "EventType": "origin-response", - "LambdaFunctionARN": { - "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersVersion5920FF7F" + "EventType": "viewer-response", + "FunctionARN": { + "Fn::GetAtt": [ + "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewaySetHttpSecurityHeadersC49E7B59", + "FunctionARN" + ] } } ], - "TargetOriginId": "origin1", + "TargetOriginId": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistributionOrigin1C13C667A", "ViewerProtocolPolicy": "redirect-to-https" }, - "DefaultRootObject": "index.html", "Enabled": true, "HttpVersion": "http2", "IPV6Enabled": true, @@ -783,17 +768,12 @@ "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudfrontLoggingBucket58AA7378", "RegionalDomainName" ] - }, - "IncludeCookies": false + } }, "Origins": [ { "CustomOriginConfig": { - "HTTPPort": 80, - "HTTPSPort": 443, - "OriginKeepaliveTimeout": 5, "OriginProtocolPolicy": "https-only", - "OriginReadTimeout": 30, "OriginSSLProtocols": [ "TLSv1.2" ] @@ -842,13 +822,20 @@ } ] }, - "Id": "origin1" + "Id": "testserverlessimagehandlerCloudFrontApiGatewayLambdaCloudFrontToApiGatewayCloudFrontDistributionOrigin1C13C667A", + "OriginPath": { + "Fn::Join": [ + "", + [ + "/", + { + "Ref": "testserverlessimagehandlerCloudFrontApiGatewayLambdaLambdaRestApiDeploymentStageprodBD57762D" + } + ] + ] + } } - ], - "PriceClass": "PriceClass_100", - "ViewerCertificate": { - "CloudFrontDefaultCertificate": true - } + ] } }, "Metadata": { @@ -893,15 +880,58 @@ { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for another bucket" - }, - { - "id": "W51", - "reason": "This S3 bucket Bucket does not need a bucket policy" } ] } } }, + "testserverlessimagehandlerExistingLambdaS3S3LoggingBucketPolicy1D599FE4": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662": { "Type": "AWS::S3::Bucket", "Properties": { @@ -914,6 +944,19 @@ } ] }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionTransitions": [ + { + "StorageClass": "GLACIER", + "TransitionInDays": 90 + } + ], + "Status": "Enabled" + } + ] + }, "LoggingConfiguration": { "DestinationBucketName": { "Ref": "testserverlessimagehandlerExistingLambdaS3S3LoggingBucket406E2181" @@ -930,15 +973,52 @@ } }, "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain", - "Metadata": { - "cfn_nag": { - "rules_to_suppress": [ + "DeletionPolicy": "Retain" + }, + "testserverlessimagehandlerExistingLambdaS3S3BucketPolicy4313A384": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662" + }, + "PolicyDocument": { + "Statement": [ { - "id": "W51", - "reason": "This S3 bucket Bucket does not need a bucket policy" + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testserverlessimagehandlerExistingLambdaS3S3Bucket9203E662", + "Arn" + ] + }, + "/*" + ] + ] + } + ] } - ] + ], + "Version": "2012-10-17" } } }, @@ -1012,17 +1092,37 @@ } }, "Parameters": { - "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3Bucket65CDB50E": { - "Type": "String", - "Description": "S3 bucket for asset \"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\"" - }, - "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96S3VersionKeyCF89D2F1": { - "Type": "String", - "Description": "S3 key for asset version \"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\"" - }, - "AssetParameters5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96ArtifactHash7AE27721": { - "Type": "String", - "Description": "Artifact hash for asset \"5f752add658f79e0005d882c0bc5a08dc38a14dc55135d974ea1d2226cb28b96\"" + "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/use_cases/aws-serverless-image-handler/test/test.serverless-image-handler.test.ts b/source/use_cases/aws-serverless-image-handler/test/test.serverless-image-handler.test.ts index 63840d2c3..41cffc19a 100644 --- a/source/use_cases/aws-serverless-image-handler/test/test.serverless-image-handler.test.ts +++ b/source/use_cases/aws-serverless-image-handler/test/test.serverless-image-handler.test.ts @@ -70,5 +70,5 @@ test('Custom deployment unit testing', () => { // Assertion 4 expect(sih.cloudFrontDistribution()).toBeDefined(); // Assertion 5 - expect(sih.lambdaFunction()).toHaveProperty('environment.TEST_KEY', 'TEST_VALUE'); + expect(sih.lambdaFunction()).toHaveProperty('environment.TEST_KEY', {"value": "TEST_VALUE"}); }); \ No newline at end of file diff --git a/source/use_cases/aws-serverless-image-handler/tsconfig.json b/source/use_cases/aws-serverless-image-handler/tsconfig.json index 07afb1f64..7ca946aa3 100644 --- a/source/use_cases/aws-serverless-image-handler/tsconfig.json +++ b/source/use_cases/aws-serverless-image-handler/tsconfig.json @@ -4,12 +4,14 @@ "charset": "utf8", "declaration": true, "experimentalDecorators": true, + "incremental": true, "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2018" + "es2019" ], "module": "CommonJS", + "newLine": "lf", "noEmitOnError": true, "noFallthroughCasesInSwitch": true, "noImplicitAny": true, @@ -21,8 +23,10 @@ "strict": true, "strictNullChecks": true, "strictPropertyInitialization": true, - "stripInternal": true, - "target": "ES2018" + "stripInternal": false, + "target": "ES2019", + "composite": false, + "tsBuildInfoFile": "tsconfig.tsbuildinfo" }, "include": [ "**/*.ts" diff --git a/source/use_cases/aws-serverless-web-app/test/__snapshots__/s3-static-site-stack.test.js.snap b/source/use_cases/aws-serverless-web-app/test/__snapshots__/s3-static-site-stack.test.js.snap index 87c900291..c4ba423ce 100644 --- a/source/use_cases/aws-serverless-web-app/test/__snapshots__/s3-static-site-stack.test.js.snap +++ b/source/use_cases/aws-serverless-web-app/test/__snapshots__/s3-static-site-stack.test.js.snap @@ -19,8 +19,8 @@ Object { "https://", Object { "Fn::GetAtt": Array [ - "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E", - "RegionalDomainName", + "CloudFrontToS3CloudFrontDistribution241D9866", + "DomainName", ], }, ], @@ -41,21 +41,21 @@ Object { "Description": "S3 key for asset version \\"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061ArtifactHash40CCFA64": Object { - "Description": "Artifact hash for asset \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916ArtifactHashB448546A": Object { + "Description": "Artifact hash for asset \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1": Object { - "Description": "S3 bucket for asset \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3BucketC05003CF": Object { + "Description": "S3 bucket for asset \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC": Object { - "Description": "S3 key for asset version \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD": Object { + "Description": "S3 key for asset version \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, }, "Resources": Object { - "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E": Object { + "CloudFrontToS3CloudFrontDistribution241D9866": Object { "Metadata": Object { "cfn_nag": Object { "rules_to_suppress": Array [ @@ -69,30 +69,9 @@ Object { "Properties": Object { "DistributionConfig": Object { "DefaultCacheBehavior": Object { - "AllowedMethods": Array [ - "GET", - "HEAD", - ], - "CachedMethods": Array [ - "GET", - "HEAD", - ], + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "Compress": true, - "ForwardedValues": Object { - "Cookies": Object { - "Forward": "none", - }, - "QueryString": false, - }, - "LambdaFunctionAssociations": Array [ - Object { - "EventType": "origin-response", - "LambdaFunctionARN": Object { - "Ref": "CloudFrontToS3SetHttpSecurityHeadersVersion699208AE", - }, - }, - ], - "TargetOriginId": "origin1", + "TargetOriginId": "S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507", "ViewerProtocolPolicy": "redirect-to-https", }, "DefaultRootObject": "index.html", @@ -106,7 +85,6 @@ Object { "RegionalDomainName", ], }, - "IncludeCookies": false, }, "Origins": Array [ Object { @@ -116,7 +94,7 @@ Object { "RegionalDomainName", ], }, - "Id": "origin1", + "Id": "S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507", "S3OriginConfig": Object { "OriginAccessIdentity": Object { "Fn::Join": Array [ @@ -124,7 +102,7 @@ Object { Array [ "origin-access-identity/cloudfront/", Object { - "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "Ref": "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F", }, ], ], @@ -132,18 +110,14 @@ Object { }, }, ], - "PriceClass": "PriceClass_100", - "ViewerCertificate": Object { - "CloudFrontDefaultCertificate": true, - }, }, }, "Type": "AWS::CloudFront::Distribution", }, - "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91": Object { + "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F": Object { "Properties": Object { "CloudFrontOriginAccessIdentityConfig": Object { - "Comment": "Access S3 bucket content only through CloudFront", + "Comment": "Identity for S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507", }, }, "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", @@ -157,10 +131,6 @@ Object { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", }, - Object { - "id": "W51", - "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution", - }, ], }, }, @@ -188,6 +158,53 @@ Object { "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, + "CloudFrontToS3CloudfrontLoggingBucketPolicy416B82D9": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, + }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, "CloudFrontToS3S3Bucket9CE6AB04": Object { "DeletionPolicy": "Retain", "Properties": Object { @@ -200,6 +217,19 @@ Object { }, ], }, + "LifecycleConfiguration": Object { + "Rules": Array [ + Object { + "NoncurrentVersionTransitions": Array [ + Object { + "StorageClass": "GLACIER", + "TransitionInDays": 90, + }, + ], + "Status": "Enabled", + }, + ], + }, "LoggingConfiguration": Object { "DestinationBucketName": Object { "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2", @@ -236,29 +266,16 @@ Object { "PolicyDocument": Object { "Statement": Array [ Object { - "Action": Array [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*", - ], - "Effect": "Allow", - "Principal": Object { - "AWS": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::cloudfront:user/CloudFront Origin Access Identity ", - Object { - "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", - }, - ], - ], + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", }, }, + "Effect": "Deny", + "Principal": Object { + "AWS": "*", + }, "Resource": Array [ Object { "Fn::GetAtt": Array [ @@ -288,7 +305,7 @@ Object { "Principal": Object { "CanonicalUser": Object { "Fn::GetAtt": Array [ - "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F", "S3CanonicalUserId", ], }, @@ -323,10 +340,6 @@ Object { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for another bucket", }, - Object { - "id": "W51", - "reason": "This S3 bucket Bucket does not need a bucket policy", - }, ], }, }, @@ -354,104 +367,52 @@ Object { "Type": "AWS::S3::Bucket", "UpdateReplacePolicy": "Retain", }, - "CloudFrontToS3SetHttpSecurityHeaders9E6088E2": Object { - "DependsOn": Array [ - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10", - ], - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "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.", - }, - Object { - "id": "W89", - "reason": "This is not a rule for the general case, just for specific use cases/industries", - }, - ], - }, - }, + "CloudFrontToS3S3LoggingBucketPolicy360F3875": Object { "Properties": Object { - "Code": Object { - "ZipFile": "exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; headers['x-xss-protection'] = [ { key: 'X-XSS-Protection', value: '1; mode=block' } ]; headers['x-frame-options'] = [ { key: 'X-Frame-Options', value: 'DENY' } ]; headers['x-content-type-options'] = [ { key: 'X-Content-Type-Options', value: 'nosniff' } ]; headers['strict-transport-security'] = [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload' } ]; headers['referrer-policy'] = [ { key: 'Referrer-Policy', value: 'same-origin' } ]; headers['content-security-policy'] = [ { key: 'Content-Security-Policy', value: \\"default-src 'none'; base-uri 'self'; img-src 'self'; script-src 'self'; style-src 'self' https:; object-src 'none'; frame-ancestors 'none'; font-src 'self' https:; form-action 'self'; manifest-src 'self'; connect-src 'self'\\" } ]; callback(null, response); };", - }, - "Handler": "index.handler", - "Role": Object { - "Fn::GetAtt": Array [ - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10", - "Arn", - ], + "Bucket": Object { + "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2", }, - "Runtime": "nodejs14.x", - }, - "Type": "AWS::Lambda::Function", - }, - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10": Object { - "Properties": Object { - "AssumeRolePolicyDocument": Object { + "PolicyDocument": Object { "Statement": Array [ Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": Object { - "Service": "lambda.amazonaws.com", + "Action": "s3:*", + "Condition": Object { + "Bool": Object { + "aws:SecureTransport": "false", + }, }, - }, - Object { - "Action": "sts:AssumeRole", - "Effect": "Allow", + "Effect": "Deny", "Principal": Object { - "Service": "edgelambda.amazonaws.com", + "AWS": "*", }, - }, - ], - "Version": "2012-10-17", - }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ + "Resource": Array [ Object { - "Action": Array [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", + "Fn::GetAtt": Array [ + "CloudFrontToS3S3LoggingBucketEF5CD8B2", + "Arn", ], - "Effect": "Allow", - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - "arn:aws:logs:", - Object { - "Ref": "AWS::Region", - }, - ":", - Object { - "Ref": "AWS::AccountId", - }, - ":log-group:/aws/lambda/*", - ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "CloudFrontToS3S3LoggingBucketEF5CD8B2", + "Arn", + ], + }, + "/*", ], - }, + ], }, ], - "Version": "2012-10-17", }, - "PolicyName": "LambdaFunctionServiceRolePolicy", - }, - ], - }, - "Type": "AWS::IAM::Role", - }, - "CloudFrontToS3SetHttpSecurityHeadersVersion699208AE": Object { - "Properties": Object { - "FunctionName": Object { - "Ref": "CloudFrontToS3SetHttpSecurityHeaders9E6088E2", + ], + "Version": "2012-10-17", }, }, - "Type": "AWS::Lambda::Version", + "Type": "AWS::S3::BucketPolicy", }, "CustomResource": Object { "DeletionPolicy": "Delete", @@ -479,7 +440,7 @@ Object { "Properties": Object { "Code": Object { "S3Bucket": Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3BucketC05003CF", }, "S3Key": Object { "Fn::Join": Array [ @@ -492,7 +453,7 @@ Object { "Fn::Split": Array [ "||", Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD", }, ], }, @@ -505,7 +466,7 @@ Object { "Fn::Split": Array [ "||", Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD", }, ], }, @@ -515,6 +476,7 @@ Object { ], }, }, + "Description": "AWS CDK resource provider framework - onEvent (S3StaticWebsiteStack/CustomResourceProvider)", "Environment": Object { "Variables": Object { "USER_ON_EVENT_FUNCTION_ARN": Object { @@ -532,7 +494,7 @@ Object { "Arn", ], }, - "Runtime": "nodejs14.x", + "Runtime": "nodejs12.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", @@ -575,12 +537,28 @@ Object { Object { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": Object { - "Fn::GetAtt": Array [ - "staticContentHandlerC21DFC88", - "Arn", - ], - }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "staticContentHandlerC21DFC88", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "staticContentHandlerC21DFC88", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], }, ], "Version": "2012-10-17", @@ -692,7 +670,18 @@ Object { ], "Effect": "Allow", "Resource": Array [ - "arn:aws:s3:::wildrydes-us-east-1", + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::wildrydes-us-east-1", + ], + ], + }, "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*", ], }, @@ -713,7 +702,11 @@ Object { "Fn::Join": Array [ "", Array [ - "arn:aws:s3:::", + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::", Object { "Ref": "CloudFrontToS3S3Bucket9CE6AB04", }, 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 986ec2f36..625e47ca9 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 @@ -43,16 +43,16 @@ Object { "Description": "S3 key for asset version \\"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061ArtifactHash40CCFA64": Object { - "Description": "Artifact hash for asset \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916ArtifactHashB448546A": Object { + "Description": "Artifact hash for asset \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1": Object { - "Description": "S3 bucket for asset \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3BucketC05003CF": Object { + "Description": "S3 bucket for asset \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC": Object { - "Description": "S3 key for asset version \\"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\\"", + "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD": Object { + "Description": "S3 key for asset version \\"543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916\\"", "Type": "String", }, "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360ArtifactHash5ED5576F": Object { @@ -71,6 +71,20 @@ Object { "Resources": Object { "CognitoToApiGatewayToLambdaApiAccessLogGroup43A4A269": Object { "DeletionPolicy": "Retain", + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely", + }, + Object { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)", + }, + ], + }, + }, "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Retain", }, @@ -94,21 +108,28 @@ Object { "Type": "AWS::ApiGateway::Authorizer", }, "CognitoToApiGatewayToLambdaCognitoUserPool6EE989F1": Object { + "DeletionPolicy": "Retain", "Properties": Object { + "AccountRecoverySetting": Object { + "RecoveryMechanisms": Array [ + Object { + "Name": "verified_phone_number", + "Priority": 1, + }, + Object { + "Name": "verified_email", + "Priority": 2, + }, + ], + }, "AdminCreateUserConfig": Object { - "AllowAdminCreateUserOnly": true, + "AllowAdminCreateUserOnly": false, }, + "AutoVerifiedAttributes": Array [ + "email", + ], "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", - "SmsConfiguration": Object { - "ExternalId": "ServerlessBackendStackCognitoToApiGatewayToLambdaCognitoUserPool0C465C62", - "SnsCallerArn": Object { - "Fn::GetAtt": Array [ - "CognitoToApiGatewayToLambdaCognitoUserPoolsmsRole62C22F60", - "Arn", - ], - }, - }, "SmsVerificationMessage": "The verification code to your new account is {####}", "UserPoolAddOns": Object { "AdvancedSecurityMode": "ENFORCED", @@ -122,62 +143,34 @@ Object { }, }, "Type": "AWS::Cognito::UserPool", + "UpdateReplacePolicy": "Retain", }, "CognitoToApiGatewayToLambdaCognitoUserPoolClientC6919938": Object { "Properties": Object { + "AllowedOAuthFlows": Array [ + "implicit", + "code", + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": Array [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin", + ], + "CallbackURLs": Array [ + "https://example.com", + ], + "SupportedIdentityProviders": Array [ + "COGNITO", + ], "UserPoolId": Object { "Ref": "CognitoToApiGatewayToLambdaCognitoUserPool6EE989F1", }, }, "Type": "AWS::Cognito::UserPoolClient", }, - "CognitoToApiGatewayToLambdaCognitoUserPoolsmsRole62C22F60": Object { - "Metadata": Object { - "cfn_nag": Object { - "rules_to_suppress": Array [ - Object { - "id": "W11", - "reason": "Allowing * resource on permissions policy since its used by Cognito to send SMS messages via sns:Publish", - }, - ], - }, - }, - "Properties": Object { - "AssumeRolePolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sts:AssumeRole", - "Condition": Object { - "StringEquals": Object { - "sts:ExternalId": "ServerlessBackendStackCognitoToApiGatewayToLambdaCognitoUserPool0C465C62", - }, - }, - "Effect": "Allow", - "Principal": Object { - "Service": "cognito-idp.amazonaws.com", - }, - }, - ], - "Version": "2012-10-17", - }, - "Policies": Array [ - Object { - "PolicyDocument": Object { - "Statement": Array [ - Object { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": "*", - }, - ], - "Version": "2012-10-17", - }, - "PolicyName": "sns-publish", - }, - ], - }, - "Type": "AWS::IAM::Role", - }, "CognitoToApiGatewayToLambdaLambdaFunction555D0B9C": Object { "DependsOn": Array [ "CognitoToApiGatewayToLambdaLambdaFunctionServiceRoleDefaultPolicyCC3D84AE", @@ -194,6 +187,10 @@ Object { "id": "W89", "reason": "This is not a rule for the general case, just for specific use cases/industries", }, + Object { + "id": "W92", + "reason": "Impossible for us to define the correct concurrency for clients", + }, ], }, }, @@ -252,6 +249,9 @@ Object { ], }, "Runtime": "nodejs14.x", + "TracingConfig": Object { + "Mode": "Active", + }, }, "Type": "AWS::Lambda::Function", }, @@ -284,7 +284,11 @@ Object { "Fn::Join": Array [ "", Array [ - "arn:aws:logs:", + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", Object { "Ref": "AWS::Region", }, @@ -307,9 +311,27 @@ Object { "Type": "AWS::IAM::Role", }, "CognitoToApiGatewayToLambdaLambdaFunctionServiceRoleDefaultPolicyCC3D84AE": Object { + "Metadata": Object { + "cfn_nag": Object { + "rules_to_suppress": Array [ + Object { + "id": "W12", + "reason": "Lambda needs the following minimum required permissions to send trace data to X-Ray and access ENIs in a VPC.", + }, + ], + }, + }, "Properties": Object { "PolicyDocument": Object { "Statement": Array [ + Object { + "Action": Array [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + ], + "Effect": "Allow", + "Resource": "*", + }, Object { "Action": Array [ "dynamodb:BatchGetItem", @@ -323,6 +345,7 @@ Object { "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", + "dynamodb:DescribeTable", ], "Effect": "Allow", "Resource": Array [ @@ -531,7 +554,11 @@ Object { "Fn::Join": Array [ "", Array [ - "arn:aws:logs:", + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":logs:", Object { "Ref": "AWS::Region", }, @@ -553,7 +580,7 @@ Object { }, "Type": "AWS::IAM::Role", }, - "CognitoToApiGatewayToLambdaLambdaRestApiDeployment1E6D5E6682fe19d2bcf96aadbe65fec49213c15c": Object { + "CognitoToApiGatewayToLambdaLambdaRestApiDeployment1E6D5E66be3a2e657ed1037e23e62faacc9671b0": Object { "DependsOn": Array [ "CognitoToApiGatewayToLambdaLambdaRestApiproxyANY2839789B", "CognitoToApiGatewayToLambdaLambdaRestApiproxyOPTIONS63FED6E8", @@ -591,11 +618,11 @@ Object { "Format": "{\\"requestId\\":\\"$context.requestId\\",\\"ip\\":\\"$context.identity.sourceIp\\",\\"user\\":\\"$context.identity.user\\",\\"caller\\":\\"$context.identity.caller\\",\\"requestTime\\":\\"$context.requestTime\\",\\"httpMethod\\":\\"$context.httpMethod\\",\\"resourcePath\\":\\"$context.resourcePath\\",\\"status\\":\\"$context.status\\",\\"protocol\\":\\"$context.protocol\\",\\"responseLength\\":\\"$context.responseLength\\"}", }, "DeploymentId": Object { - "Ref": "CognitoToApiGatewayToLambdaLambdaRestApiDeployment1E6D5E6682fe19d2bcf96aadbe65fec49213c15c", + "Ref": "CognitoToApiGatewayToLambdaLambdaRestApiDeployment1E6D5E66be3a2e657ed1037e23e62faacc9671b0", }, "MethodSettings": Array [ Object { - "DataTraceEnabled": true, + "DataTraceEnabled": false, "HttpMethod": "*", "LoggingLevel": "INFO", "ResourcePath": "/*", @@ -605,6 +632,7 @@ Object { "Ref": "CognitoToApiGatewayToLambdaLambdaRestApi31103AF0", }, "StageName": "prod", + "TracingEnabled": true, }, "Type": "AWS::ApiGateway::Stage", }, @@ -758,7 +786,7 @@ Object { Object { "Ref": "CognitoToApiGatewayToLambdaLambdaRestApiDeploymentStageprod743A20E1", }, - "/*/{proxy+}", + "/*/*", ], ], }, @@ -795,7 +823,7 @@ Object { Object { "Ref": "CognitoToApiGatewayToLambdaLambdaRestApi31103AF0", }, - "/test-invoke-stage/*/{proxy+}", + "/test-invoke-stage/*/*", ], ], }, @@ -898,7 +926,7 @@ Object { "Properties": Object { "Code": Object { "S3Bucket": Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3BucketC05003CF", }, "S3Key": Object { "Fn::Join": Array [ @@ -911,7 +939,7 @@ Object { "Fn::Split": Array [ "||", Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD", }, ], }, @@ -924,7 +952,7 @@ Object { "Fn::Split": Array [ "||", Object { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC", + "Ref": "AssetParameters543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916S3VersionKeyA2C7BFCD", }, ], }, @@ -934,6 +962,7 @@ Object { ], }, }, + "Description": "AWS CDK resource provider framework - onEvent (ServerlessBackendStack/CustomResourceProvider)", "Environment": Object { "Variables": Object { "USER_ON_EVENT_FUNCTION_ARN": Object { @@ -951,7 +980,7 @@ Object { "Arn", ], }, - "Runtime": "nodejs14.x", + "Runtime": "nodejs12.x", "Timeout": 900, }, "Type": "AWS::Lambda::Function", @@ -994,12 +1023,28 @@ Object { Object { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": Object { - "Fn::GetAtt": Array [ - "updateConfigHandler59840941", - "Arn", - ], - }, + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "updateConfigHandler59840941", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "updateConfigHandler59840941", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], }, ], "Version": "2012-10-17", @@ -1029,6 +1074,9 @@ Object { "KeyType": "HASH", }, ], + "PointInTimeRecoverySpecification": Object { + "PointInTimeRecoveryEnabled": true, + }, "SSESpecification": Object { "SSEEnabled": true, }, @@ -1139,7 +1187,11 @@ Object { "Fn::Join": Array [ "", Array [ - "arn:aws:s3:::", + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":s3:::", Object { "Fn::ImportValue": "websiteBucket", }, diff --git a/source/use_cases/aws-serverless-web-app/test/integ.001-s3-static-website-deployment.expected.json b/source/use_cases/aws-serverless-web-app/test/integ.001-s3-static-website-deployment.expected.json index 670b0856a..58ca7d0d3 100644 --- a/source/use_cases/aws-serverless-web-app/test/integ.001-s3-static-website-deployment.expected.json +++ b/source/use_cases/aws-serverless-web-app/test/integ.001-s3-static-website-deployment.expected.json @@ -31,15 +31,58 @@ { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for another bucket" - }, - { - "id": "W51", - "reason": "This S3 bucket Bucket does not need a bucket policy" } ] } } }, + "CloudFrontToS3S3LoggingBucketPolicy360F3875": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3LoggingBucketEF5CD8B2", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudFrontToS3S3LoggingBucketEF5CD8B2", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, "CloudFrontToS3S3Bucket9CE6AB04": { "Type": "AWS::S3::Bucket", "Properties": { @@ -52,6 +95,19 @@ } ] }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionTransitions": [ + { + "StorageClass": "GLACIER", + "TransitionInDays": 90 + } + ], + "Status": "Enabled" + } + ] + }, "LoggingConfiguration": { "DestinationBucketName": { "Ref": "CloudFrontToS3S3LoggingBucketEF5CD8B2" @@ -79,29 +135,16 @@ "PolicyDocument": { "Statement": [ { - "Action": [ - "s3:GetObject*", - "s3:GetBucket*", - "s3:List*" - ], - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::cloudfront:user/CloudFront Origin Access Identity ", - { - "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91" - } - ] - ] + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" } }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, "Resource": [ { "Fn::GetAtt": [ @@ -131,7 +174,7 @@ "Principal": { "CanonicalUser": { "Fn::GetAtt": [ - "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91", + "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F", "S3CanonicalUserId" ] } @@ -166,113 +209,6 @@ } } }, - "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91": { - "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", - "Properties": { - "CloudFrontOriginAccessIdentityConfig": { - "Comment": "Access S3 bucket content only through CloudFront" - } - } - }, - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - } - }, - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "edgelambda.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:aws:logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:/aws/lambda/*" - ] - ] - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "LambdaFunctionServiceRolePolicy" - } - ] - } - }, - "CloudFrontToS3SetHttpSecurityHeaders9E6088E2": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "ZipFile": "exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; headers['x-xss-protection'] = [ { key: 'X-XSS-Protection', value: '1; mode=block' } ]; headers['x-frame-options'] = [ { key: 'X-Frame-Options', value: 'DENY' } ]; headers['x-content-type-options'] = [ { key: 'X-Content-Type-Options', value: 'nosniff' } ]; headers['strict-transport-security'] = [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload' } ]; headers['referrer-policy'] = [ { key: 'Referrer-Policy', value: 'same-origin' } ]; headers['content-security-policy'] = [ { key: 'Content-Security-Policy', value: \"default-src 'none'; base-uri 'self'; img-src 'self'; script-src 'self'; style-src 'self' https:; object-src 'none'; frame-ancestors 'none'; font-src 'self' https:; form-action 'self'; manifest-src 'self'; connect-src 'self'\" } ]; callback(null, response); };" - }, - "Handler": "index.handler", - "Role": { - "Fn::GetAtt": [ - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10", - "Arn" - ] - }, - "Runtime": "nodejs14.x" - }, - "DependsOn": [ - "CloudFrontToS3SetHttpSecurityHeadersServiceRole6BABDE10" - ], - "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" - } - ] - } - } - }, - "CloudFrontToS3SetHttpSecurityHeadersVersion699208AE": { - "Type": "AWS::Lambda::Version", - "Properties": { - "FunctionName": { - "Ref": "CloudFrontToS3SetHttpSecurityHeaders9E6088E2" - } - } - }, "CloudFrontToS3CloudfrontLoggingBucket8350BE9B": { "Type": "AWS::S3::Bucket", "Properties": { @@ -304,44 +240,74 @@ { "id": "W35", "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" - }, - { - "id": "W51", - "reason": "This S3 bucket is used as the access logging bucket for CloudFront Distribution" } ] } } }, - "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E": { + "CloudFrontToS3CloudfrontLoggingBucketPolicy416B82D9": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CloudFrontToS3CloudfrontLoggingBucket8350BE9B" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F": { + "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Properties": { + "CloudFrontOriginAccessIdentityConfig": { + "Comment": "Identity for S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507" + } + } + }, + "CloudFrontToS3CloudFrontDistribution241D9866": { "Type": "AWS::CloudFront::Distribution", "Properties": { "DistributionConfig": { "DefaultCacheBehavior": { - "AllowedMethods": [ - "GET", - "HEAD" - ], - "CachedMethods": [ - "GET", - "HEAD" - ], + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "Compress": true, - "ForwardedValues": { - "Cookies": { - "Forward": "none" - }, - "QueryString": false - }, - "LambdaFunctionAssociations": [ - { - "EventType": "origin-response", - "LambdaFunctionARN": { - "Ref": "CloudFrontToS3SetHttpSecurityHeadersVersion699208AE" - } - } - ], - "TargetOriginId": "origin1", + "TargetOriginId": "S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507", "ViewerProtocolPolicy": "redirect-to-https" }, "DefaultRootObject": "index.html", @@ -354,8 +320,7 @@ "CloudFrontToS3CloudfrontLoggingBucket8350BE9B", "RegionalDomainName" ] - }, - "IncludeCookies": false + } }, "Origins": [ { @@ -365,7 +330,7 @@ "RegionalDomainName" ] }, - "Id": "origin1", + "Id": "S3StaticWebsiteStackCloudFrontToS3CloudFrontDistributionOrigin1F7C9B507", "S3OriginConfig": { "OriginAccessIdentity": { "Fn::Join": [ @@ -373,18 +338,14 @@ [ "origin-access-identity/cloudfront/", { - "Ref": "CloudFrontToS3CloudFrontOriginAccessIdentity34CC1F91" + "Ref": "CloudFrontToS3CloudFrontDistributionOrigin1S3OriginB0637B8F" } ] ] } } } - ], - "PriceClass": "PriceClass_100", - "ViewerCertificate": { - "CloudFrontDefaultCertificate": true - } + ] } }, "Metadata": { @@ -441,7 +402,18 @@ ], "Effect": "Allow", "Resource": [ - "arn:aws:s3:::wildrydes-us-east-1", + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::wildrydes-us-east-1" + ] + ] + }, "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" ] }, @@ -462,7 +434,11 @@ "Fn::Join": [ "", [ - "arn:aws:s3:::", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", { "Ref": "CloudFrontToS3S3Bucket9CE6AB04" } @@ -499,49 +475,17 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3BucketE560DEC2" + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665" - } - ] - } - ] - } - ] - ] - } + "S3Key": "1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36.zip" }, - "Handler": "copy_s3_objects.on_event", "Role": { "Fn::GetAtt": [ "staticContentHandlerServiceRole3B648F21", "Arn" ] }, + "Handler": "copy_s3_objects.on_event", "Runtime": "python3.8", "Timeout": 300 }, @@ -589,12 +533,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "staticContentHandlerC21DFC88", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "staticContentHandlerC21DFC88", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "staticContentHandlerC21DFC88", + "Arn" + ] + }, + ":*" + ] + ] + } + ] } ], "Version": "2012-10-17" @@ -612,50 +572,17 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1" + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC" - } - ] - } - ] - } - ] - ] - } + "S3Key": "543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916.zip" }, - "Handler": "framework.onEvent", "Role": { "Fn::GetAtt": [ "CustomResourceProviderframeworkonEventServiceRole7EBC5835", "Arn" ] }, - "Runtime": "nodejs14.x", + "Description": "AWS CDK resource provider framework - onEvent (S3StaticWebsiteStack/CustomResourceProvider)", "Environment": { "Variables": { "USER_ON_EVENT_FUNCTION_ARN": { @@ -666,6 +593,8 @@ } } }, + "Handler": "framework.onEvent", + "Runtime": "nodejs12.x", "Timeout": 900 }, "DependsOn": [ @@ -692,32 +621,6 @@ "DeletionPolicy": "Delete" } }, - "Parameters": { - "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3BucketE560DEC2": { - "Type": "String", - "Description": "S3 bucket for asset \"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\"" - }, - "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36S3VersionKeyA9698665": { - "Type": "String", - "Description": "S3 key for asset version \"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\"" - }, - "AssetParameters1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36ArtifactHash171A5ECB": { - "Type": "String", - "Description": "Artifact hash for asset \"1726e5810ad30312b951166bf153fa8cbc793db9019a7fa8f3440a20d21f3d36\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1": { - "Type": "String", - "Description": "S3 bucket for asset \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC": { - "Type": "String", - "Description": "S3 key for asset version \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061ArtifactHash40CCFA64": { - "Type": "String", - "Description": "Artifact hash for asset \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" - } - }, "Outputs": { "websiteURL": { "Value": { @@ -727,8 +630,8 @@ "https://", { "Fn::GetAtt": [ - "CloudFrontToS3CloudFrontDistributionCFDistribution7EEEEF4E", - "RegionalDomainName" + "CloudFrontToS3CloudFrontDistribution241D9866", + "DomainName" ] } ] @@ -743,5 +646,39 @@ "Name": "websiteBucket" } } + }, + "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/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 3d68e6e39..ee678a169 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 @@ -30,7 +30,11 @@ "Fn::Join": [ "", [ - "arn:aws:logs:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", { "Ref": "AWS::Region" }, @@ -56,6 +60,14 @@ "Properties": { "PolicyDocument": { "Statement": [ + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords" + ], + "Effect": "Allow", + "Resource": "*" + }, { "Action": [ "dynamodb:BatchGetItem", @@ -68,7 +80,8 @@ "dynamodb:BatchWriteItem", "dynamodb:PutItem", "dynamodb:UpdateItem", - "dynamodb:DeleteItem" + "dynamodb:DeleteItem", + "dynamodb:DescribeTable" ], "Effect": "Allow", "Resource": [ @@ -92,6 +105,16 @@ "Ref": "CognitoToApiGatewayToLambdaLambdaFunctionServiceRole921AB2D6" } ] + }, + "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." + } + ] + } } }, "CognitoToApiGatewayToLambdaLambdaFunction555D0B9C": { @@ -99,50 +122,16 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3Bucket20EEB389" + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577" - } - ] - } - ] - } - ] - ] - } + "S3Key": "9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360.zip" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "CognitoToApiGatewayToLambdaLambdaFunctionServiceRole921AB2D6", "Arn" ] }, - "Runtime": "nodejs14.x", "Environment": { "Variables": { "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", @@ -150,6 +139,11 @@ "Ref": "LambdaToDynamoDBDynamoTable53C1442D" } } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" } }, "DependsOn": [ @@ -166,6 +160,10 @@ { "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" } ] } @@ -174,7 +172,21 @@ "CognitoToApiGatewayToLambdaApiAccessLogGroup43A4A269": { "Type": "AWS::Logs::LogGroup", "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W86", + "reason": "Retention period for CloudWatchLogs LogGroups are set to 'Never Expire' to preserve customer data indefinitely" + }, + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } }, "CognitoToApiGatewayToLambdaLambdaRestApi31103AF0": { "Type": "AWS::ApiGateway::RestApi", @@ -187,7 +199,7 @@ "Name": "LambdaRestApi" } }, - "CognitoToApiGatewayToLambdaLambdaRestApiDeployment1E6D5E6682fe19d2bcf96aadbe65fec49213c15c": { + "CognitoToApiGatewayToLambdaLambdaRestApiDeployment1E6D5E66be3a2e657ed1037e23e62faacc9671b0": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -229,17 +241,18 @@ "Format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"user\":\"$context.identity.user\",\"caller\":\"$context.identity.caller\",\"requestTime\":\"$context.requestTime\",\"httpMethod\":\"$context.httpMethod\",\"resourcePath\":\"$context.resourcePath\",\"status\":\"$context.status\",\"protocol\":\"$context.protocol\",\"responseLength\":\"$context.responseLength\"}" }, "DeploymentId": { - "Ref": "CognitoToApiGatewayToLambdaLambdaRestApiDeployment1E6D5E6682fe19d2bcf96aadbe65fec49213c15c" + "Ref": "CognitoToApiGatewayToLambdaLambdaRestApiDeployment1E6D5E66be3a2e657ed1037e23e62faacc9671b0" }, "MethodSettings": [ { - "DataTraceEnabled": true, + "DataTraceEnabled": false, "HttpMethod": "*", "LoggingLevel": "INFO", "ResourcePath": "/*" } ], - "StageName": "prod" + "StageName": "prod", + "TracingEnabled": true } }, "CognitoToApiGatewayToLambdaLambdaRestApiOPTIONS84242119": { @@ -373,7 +386,7 @@ { "Ref": "CognitoToApiGatewayToLambdaLambdaRestApiDeploymentStageprod743A20E1" }, - "/*/{proxy+}" + "/*/*" ] ] } @@ -410,7 +423,7 @@ { "Ref": "CognitoToApiGatewayToLambdaLambdaRestApi31103AF0" }, - "/test-invoke-stage/*/{proxy+}" + "/test-invoke-stage/*/*" ] ] } @@ -633,7 +646,11 @@ "Fn::Join": [ "", [ - "arn:aws:logs:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", { "Ref": "AWS::Region" }, @@ -668,70 +685,29 @@ "CognitoToApiGatewayToLambdaLambdaRestApi31103AF0" ] }, - "CognitoToApiGatewayToLambdaCognitoUserPoolsmsRole62C22F60": { - "Type": "AWS::IAM::Role", + "CognitoToApiGatewayToLambdaCognitoUserPool6EE989F1": { + "Type": "AWS::Cognito::UserPool", "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ + "AccountRecoverySetting": { + "RecoveryMechanisms": [ { - "Action": "sts:AssumeRole", - "Condition": { - "StringEquals": { - "sts:ExternalId": "ServerlessBackendStackCognitoToApiGatewayToLambdaCognitoUserPool0C465C62" - } - }, - "Effect": "Allow", - "Principal": { - "Service": "cognito-idp.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": "sns:Publish", - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" + "Name": "verified_phone_number", + "Priority": 1 }, - "PolicyName": "sns-publish" - } - ] - }, - "Metadata": { - "cfn_nag": { - "rules_to_suppress": [ { - "id": "W11", - "reason": "Allowing * resource on permissions policy since its used by Cognito to send SMS messages via sns:Publish" + "Name": "verified_email", + "Priority": 2 } ] - } - } - }, - "CognitoToApiGatewayToLambdaCognitoUserPool6EE989F1": { - "Type": "AWS::Cognito::UserPool", - "Properties": { + }, "AdminCreateUserConfig": { - "AllowAdminCreateUserOnly": true + "AllowAdminCreateUserOnly": false }, + "AutoVerifiedAttributes": [ + "email" + ], "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", - "SmsConfiguration": { - "ExternalId": "ServerlessBackendStackCognitoToApiGatewayToLambdaCognitoUserPool0C465C62", - "SnsCallerArn": { - "Fn::GetAtt": [ - "CognitoToApiGatewayToLambdaCognitoUserPoolsmsRole62C22F60", - "Arn" - ] - } - }, "SmsVerificationMessage": "The verification code to your new account is {####}", "UserPoolAddOns": { "AdvancedSecurityMode": "ENFORCED" @@ -743,25 +719,45 @@ "EmailSubject": "Verify your new account", "SmsMessage": "The verification code to your new account is {####}" } - } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" }, "CognitoToApiGatewayToLambdaCognitoUserPoolClientC6919938": { "Type": "AWS::Cognito::UserPoolClient", "Properties": { "UserPoolId": { "Ref": "CognitoToApiGatewayToLambdaCognitoUserPool6EE989F1" - } + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] } }, "CognitoToApiGatewayToLambdaCognitoAuthorizerAF023B99": { "Type": "AWS::ApiGateway::Authorizer", "Properties": { + "Name": "authorizer", "RestApiId": { "Ref": "CognitoToApiGatewayToLambdaLambdaRestApi31103AF0" }, "Type": "COGNITO_USER_POOLS", "IdentitySource": "method.request.header.Authorization", - "Name": "authorizer", "ProviderARNs": [ { "Fn::GetAtt": [ @@ -819,7 +815,11 @@ "Fn::Join": [ "", [ - "arn:aws:s3:::", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", { "Fn::ImportValue": "websiteBucket" }, @@ -844,49 +844,17 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3Bucket928903EC" + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD" - } - ] - } - ] - } - ] - ] - } + "S3Key": "3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791.zip" }, - "Handler": "update_s3_object.on_event", "Role": { "Fn::GetAtt": [ "updateConfigHandlerServiceRole3B176B96", "Arn" ] }, + "Handler": "update_s3_object.on_event", "Runtime": "python3.8", "Timeout": 300 }, @@ -934,12 +902,28 @@ { "Action": "lambda:InvokeFunction", "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "updateConfigHandler59840941", - "Arn" - ] - } + "Resource": [ + { + "Fn::GetAtt": [ + "updateConfigHandler59840941", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "updateConfigHandler59840941", + "Arn" + ] + }, + ":*" + ] + ] + } + ] } ], "Version": "2012-10-17" @@ -957,50 +941,17 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1" + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": { - "Fn::Join": [ - "", - [ - { - "Fn::Select": [ - 0, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC" - } - ] - } - ] - }, - { - "Fn::Select": [ - 1, - { - "Fn::Split": [ - "||", - { - "Ref": "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC" - } - ] - } - ] - } - ] - ] - } + "S3Key": "543c7a94b144a6259669eaf884305607b7a9abe85c43e4bfe62f9190ace37916.zip" }, - "Handler": "framework.onEvent", "Role": { "Fn::GetAtt": [ "CustomResourceProviderframeworkonEventServiceRole7EBC5835", "Arn" ] }, - "Runtime": "nodejs14.x", + "Description": "AWS CDK resource provider framework - onEvent (ServerlessBackendStack/CustomResourceProvider)", "Environment": { "Variables": { "USER_ON_EVENT_FUNCTION_ARN": { @@ -1011,6 +962,8 @@ } } }, + "Handler": "framework.onEvent", + "Runtime": "nodejs12.x", "Timeout": 900 }, "DependsOn": [ @@ -1083,6 +1036,9 @@ } ], "BillingMode": "PAY_PER_REQUEST", + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, "SSESpecification": { "SSEEnabled": true }, @@ -1121,41 +1077,37 @@ } }, "Parameters": { - "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3Bucket20EEB389": { - "Type": "String", - "Description": "S3 bucket for asset \"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\"" - }, - "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360S3VersionKeyC46EC577": { - "Type": "String", - "Description": "S3 key for asset version \"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\"" - }, - "AssetParameters9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360ArtifactHash5ED5576F": { - "Type": "String", - "Description": "Artifact hash for asset \"9a9c398189879e9ca9700ba0658086063d8ee7ccd068043c722c28478c6c4360\"" - }, - "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3Bucket928903EC": { - "Type": "String", - "Description": "S3 bucket for asset \"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\"" - }, - "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791S3VersionKey3C7BB3DD": { - "Type": "String", - "Description": "S3 key for asset version \"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\"" - }, - "AssetParameters3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791ArtifactHashF30FBC00": { - "Type": "String", - "Description": "Artifact hash for asset \"3aa519f176d0d52023f4992f8ada07849f844467dcb0d4dfb94bb3b350a1d791\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3Bucket0A1029B1": { - "Type": "String", - "Description": "S3 bucket for asset \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061S3VersionKeyFB75FDAC": { - "Type": "String", - "Description": "S3 key for asset version \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" - }, - "AssetParameters4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061ArtifactHash40CCFA64": { - "Type": "String", - "Description": "Artifact hash for asset \"4c2988a57571fd4c34de12bae67441541aeea1a59e085f95e5b708922ff45061\"" + "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/use_cases/aws-serverless-web-app/test/s3-static-site-stack.test.ts b/source/use_cases/aws-serverless-web-app/test/s3-static-site-stack.test.ts index 615cc1ec2..ed46cbfb6 100644 --- a/source/use_cases/aws-serverless-web-app/test/s3-static-site-stack.test.ts +++ b/source/use_cases/aws-serverless-web-app/test/s3-static-site-stack.test.ts @@ -11,103 +11,203 @@ * and limitations under the License. */ -import * as cdk from '@aws-cdk/core'; -import { S3StaticWebsiteStack } from '../lib/s3-static-site-stack'; -import { SynthUtils } from '@aws-cdk/assert'; -import '@aws-cdk/assert/jest'; +import * as cdk from "@aws-cdk/core"; +import { S3StaticWebsiteStack } from "../lib/s3-static-site-stack"; +import { SynthUtils } from "@aws-cdk/assert"; +import "@aws-cdk/assert/jest"; -test('default stack', () => { +test("default stack", () => { const app = new cdk.App(); - const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + const stack = new S3StaticWebsiteStack(app, "S3StaticWebsiteStack"); expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); }); -test('check s3 bucket encryption setting', () => { +test("check s3 bucket encryption setting", () => { const app = new cdk.App(); - const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + const stack = new S3StaticWebsiteStack(app, "S3StaticWebsiteStack"); expect(stack).toHaveResource("AWS::S3::Bucket", { BucketEncryption: { ServerSideEncryptionConfiguration: [ { ServerSideEncryptionByDefault: { - SSEAlgorithm: "AES256" - } - } - ] - } + SSEAlgorithm: "AES256", + }, + }, + ], + }, }); }); -test('check s3 bucket public access setting', () => { +test("check s3 bucket public access setting", () => { const app = new cdk.App(); - const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); + const stack = new S3StaticWebsiteStack(app, "S3StaticWebsiteStack"); expect(stack).toHaveResource("AWS::S3::Bucket", { PublicAccessBlockConfiguration: { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, - RestrictPublicBuckets: true - } + RestrictPublicBuckets: true, + }, }); }); -test('check CR lambda function permissions', () => { +test("check CR lambda function permissions", () => { const app = new cdk.App(); - const stack = new S3StaticWebsiteStack(app, 'S3StaticWebsiteStack'); - expect(stack).toHaveResource("AWS::IAM::Policy", { - PolicyDocument: { - Statement: [ - { - Action: [ - "s3:GetObject", - "s3:ListBucket" - ], - Effect: "Allow", - Resource: [ - "arn:aws:s3:::wildrydes-us-east-1", - "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" - ] - }, - { - Action: [ - "s3:ListBucket", - "s3:GetObject", - "s3:PutObject", - "s3:PutObjectAcl", - "s3:PutObjectVersionAcl", - "s3:DeleteObject", - "s3:DeleteObjectVersion", - "s3:CopyObject" - ], - Effect: "Allow", - Resource: [ - { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - Ref: "CloudFrontToS3S3Bucket9CE6AB04" - } + const stack = new S3StaticWebsiteStack(app, "S3StaticWebsiteStack"); + expect(stack).toHaveResourceLike("AWS::IAM::Policy",{ + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject", + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::wildrydes-us-east-1" + ] ] - ] - }, - { - "Fn::Join": [ - "", - [ - "arn:aws:s3:::", - { - Ref: "CloudFrontToS3S3Bucket9CE6AB04" - }, - "/*" + }, + "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" + ] + }, + { + "Action": [ + "s3:ListBucket", + "s3:GetObject", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:PutObjectVersionAcl", + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:CopyObject" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + } + ] ] - ] - } - ] - } - ], - Version: "2012-10-17" - } - }); -}); \ No newline at end of file + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + }, + "/*" + ] + ] + } + ] + } +] + }, + "PolicyName": "staticContentHandlerServiceRoleDefaultPolicy0F5C5865", + "Roles": [ + { + "Ref": "staticContentHandlerServiceRole3B648F21", + } + ], +}); + + // expect(stack).toHaveResourceLike("AWS::IAM::Policy",{ + // "Properties": { + // "PolicyDocument": { + // "Statement": [ + // { + // "Action": [ + // "s3:GetObject", + // "s3:ListBucket" + // ], + // "Effect": "Allow", + // "Resource": [ + // { + // "Fn::Join": [ + // "", + // [ + // "arn:", + // { + // "Ref": "AWS::Partition" + // }, + // ":s3:::wildrydes-us-east-1" + // ] + // ] + // }, + // "arn:aws:s3:::wildrydes-us-east-1/WebApplication/1_StaticWebHosting/website/*" + // ] + // }, + // { + // "Action": [ + // "s3:ListBucket", + // "s3:GetObject", + // "s3:PutObject", + // "s3:PutObjectAcl", + // "s3:PutObjectVersionAcl", + // "s3:DeleteObject", + // "s3:DeleteObjectVersion", + // "s3:CopyObject" + // ], + // "Effect": "Allow", + // "Resource": [ + // { + // "Fn::Join": [ + // "", + // [ + // "arn:", + // { + // "Ref": "AWS::Partition" + // }, + // ":s3:::", + // { + // "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + // } + // ] + // ] + // }, + // { + // "Fn::Join": [ + // "", + // [ + // "arn:aws:s3:::", + // { + // "Ref": "CloudFrontToS3S3Bucket9CE6AB04" + // }, + // "/*" + // ] + // ] + // } + // ] + // } + // ], + // "Version": "2012-10-17" + // }, + // "PolicyName": "staticContentHandlerServiceRoleDefaultPolicy0F5C5865", + // "Roles": [ + // { + // "Ref": "staticContentHandlerServiceRole3B648F21" + // } + // ] + // } + // }); +}); diff --git a/source/use_cases/license-header.js b/source/use_cases/license-header.js index 0ff8f7478..f2c7f3da0 100644 --- a/source/use_cases/license-header.js +++ b/source/use_cases/license-header.js @@ -1,5 +1,5 @@ /** - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 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 From bcb7c6351ffa9b8ef5f5e7790522c5b1fe87dd9a Mon Sep 17 00:00:00 2001 From: mickychetta <45010053+mickychetta@users.noreply.github.com> Date: Thu, 28 Apr 2022 07:05:37 -0700 Subject: [PATCH 25/34] feat(aws-fargate-ssmstringparameter): New Construct (#653) * created README * created aws-fargate-ssmstringparameter construct * revert package.json version number * added task definition check tests * fixed conflicting string name Co-authored-by: biffgaut <78155736+biffgaut@users.noreply.github.com> --- .../.eslintignore | 4 + .../aws-fargate-ssmstringparameter/.gitignore | 15 + .../aws-fargate-ssmstringparameter/.npmignore | 21 + .../aws-fargate-ssmstringparameter/README.md | 128 ++ .../architecture.png | Bin 0 -> 141654 bytes .../lib/index.ts | 200 +++ .../package.json | 105 ++ .../test/fargate-ssmstringparameter.test.ts | 826 +++++++++++ .../integ.existing-resources.expected.json | 1211 +++++++++++++++++ .../test/integ.existing-resources.ts | 61 + .../test/integ.new-resources.expected.json | 1186 ++++++++++++++++ .../test/integ.new-resources.ts | 43 + 12 files changed, 3800 insertions(+) create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/fargate-ssmstringparameter.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.existing-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.existing-resources.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.new-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.new-resources.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.eslintignore new file mode 100644 index 000000000..e6f7801ea --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.eslintignore @@ -0,0 +1,4 @@ +lib/*.js +test/*.js +*.d.ts +coverage diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.gitignore b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.npmignore b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/README.md new file mode 100644 index 000000000..155aba9bb --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/README.md @@ -0,0 +1,128 @@ +# aws-fargate-ssmstringparameter module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **Reference Documentation**:| https://docs.aws.amazon.com/solutions/latest/constructs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png) Python|`aws_solutions_constructs.aws_fargate_ssmstringparameter`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-fargate-ssmstringparameter`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.fargatessmstringparameter`| + +This AWS Solutions Construct implements an AWS Fargate service that can read/write to an AWS Systems Manager String Parameter + +Here is a minimal deployable pattern definition: + +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { FargateToSsmstringparameter, FargateToSsmstringparameterProps } from '@aws-solutions-constructs/aws-fargate-ssmstringparameter'; + +const constructProps: FargateToSsmstringparameterProps = { + publicApi: true, + ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", + stringParameterProps: { stringValue: "test-string-value" } +}; + +new FargateToSsmstringparameter(stack, 'test-construct', constructProps); +``` + +Python +``` python +from aws_solutions_constructs.aws_fargate_ssmstringparameter import FargateToSsmstringparameter, FargateToSsmstringparameterProps +from aws_cdk import ( + Stack, + aws_ssm as ssm +) +from constructs import Construct + +FargateToSsmstringparameter(self, 'test_construct', + public_api=True, + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", + string_parameter_props=ssm.StringParameterProps( + string_value="test-string-value")) +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.ssm.*; +import software.amazon.awsconstructs.services.fargatessmstringparameter.*; + +new FargateToSsmstringparameter(this, "test-construct", new FargateToSsmstringparameterProps.Builder() + .publicApi(true) + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") + .stringParameterProps(new StringParameterProps.Builder() + .stringValue("test-string-value") + .build()) + .build()); +``` + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| publicApi | `boolean` | Whether the construct is deploying a private or public API. This has implications for the VPC. | +| vpcProps? | [`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcProps.html) | Optional custom properties for a VPC the construct will create. This VPC will be used by any Private Hosted Zone the construct creates (that's why loadBalancerProps and privateHostedZoneProps can't include a VPC). Providing both this and existingVpc is an error. | +| existingVpc? | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | An existing VPC in which to deploy the construct. Providing both this and vpcProps is an error. If the client provides an existing load balancer and/or existing Private Hosted Zone, those constructs must exist in this VPC. | +| clusterProps? | [`ecs.ClusterProps`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ClusterProps.html) | Optional properties to create a new ECS cluster. To provide an existing cluster, use the cluster attribute of fargateServiceProps. | +| ecrRepositoryArn? | `string` | The arn of an ECR Repository containing the image to use to generate the containers. Either this or the image property of containerDefinitionProps must be provided. format: arn:aws:ecr:*region*:*account number*:repository/*Repository Name* | +| ecrImageVersion? | `string` | The version of the image to use from the repository. Defaults to 'Latest' | +| containerDefinitionProps? | [`ecs.ContainerDefinitionProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinitionProps.html) | Optional props to define the container created for the Fargate Service (defaults found in fargate-defaults.ts) | +| fargateTaskDefinitionProps? | [`ecs.FargateTaskDefinitionProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateTaskDefinitionProps.html) | Optional props to define the Fargate Task Definition for this construct (defaults found in fargate-defaults.ts) | +| fargateServiceProps? | [`ecs.FargateServiceProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateServiceProps.html) | Optional values to override default Fargate Task definition properties (fargate-defaults.ts). The construct will default to launching the service is the most isolated subnets available (precedence: Isolated, Private and Public). Override those and other defaults here. | +| existingFargateServiceObject? | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html) | A Fargate Service already instantiated (probably by another Solutions Construct). If this is specified, then no props defining a new service can be provided, including: ecrImageVersion, containerDefinitionProps, fargateTaskDefinitionProps, ecrRepositoryArn, fargateServiceProps, clusterProps | +| existingContainerDefinitionObject? | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinition.html) | A container definition already instantiated as part of a Fargate service. This must be the container in the existingFargateServiceObject | +|existingStringParameterObj?|[`ssm.StringParameter`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ssm.StringParameter.html)|Existing instance of SSM String parameter object, providing both this and `stringParameterProps` will cause an error| +|stringParameterProps?|[`ssm.StringParameterProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ssm.StringParameterProps.html)|Optional user provided props to override the default props for SSM String parameter. If existingStringParameterObj is not set stringParameterProps is required. The only supported [`ssm.StringParameterProps.type`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ssm.StringParameterProps.html#type) is [`STRING`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ssm.ParameterType.html#string) if a different value is provided it will be overridden.| +|stringParameterPermissions?|`string`|Optional SSM String parameter permissions to grant to the Fargate service. One of the following may be specified: "Read", "ReadWrite". +|stringParameterEnvironmentVariableName?|`string`|Optional Name for the SSM parameter name environment variable set for the container.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| vpc | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | The VPC used by the construct (whether created by the construct or provided by the client) | +| service | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html) | The AWS Fargate service used by this construct (whether created by this construct or passed to this construct at initialization) | +| container | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinition.html) | The container associated with the AWS Fargate service in the service property. | +|stringParameter|[`ssm.StringParameter`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ssm.StringParameter.html)|Returns an instance of `ssm.StringParameter` created by the construct| + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### AWS Fargate Service +* Sets up an AWS Fargate service + * Uses the existing service if provided + * Creates a new service if none provided. + * Service will run in isolated subnets if available, then private subnets if available and finally public subnets + * Adds environment variables to the container with the ARN and Name of the SSM parameter + * Add permissions to the container IAM role allowing it to read/write to the SSM parameter + +### AWS SSM String Parameter +* Sets up an AWS SSM String Parameter + * Uses an existing parameter if one is provided, otherwise creates a new one +* Adds an Interface Endpoint to the VPC for SSM parameter (the service by default runs in Isolated or Private subnets) + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/architecture.png b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..c21afb9e2127a4bb2893105fb9ed929a3561875b GIT binary patch literal 141654 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

Jww(~HugF8|>8*W0wc;_1hzOUGU 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®Z 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 Y?dLrIZm>$034EyDM%>E3c|Z7n05*+V?f?J) literal 0 HcmV?d00001 diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/lib/index.ts new file mode 100644 index 000000000..2130389d1 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/lib/index.ts @@ -0,0 +1,200 @@ +/** + * Copyright 2022 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. + */ + +import * as ec2 from "@aws-cdk/aws-ec2"; +import * as ssm from "@aws-cdk/aws-ssm"; +// Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate +import { Construct } from "@aws-cdk/core"; +import * as defaults from "@aws-solutions-constructs/core"; +import * as ecs from "@aws-cdk/aws-ecs"; + +export interface FargateToSsmstringparameterProps { + /** + * Whether the construct is deploying a private or public API. This has implications for the VPC deployed + * by this construct. + * + * @default - none + */ + readonly publicApi: boolean; + /** + * Optional custom properties for a VPC the construct will create. This VPC will + * be used by the new Fargate service the construct creates (that's + * why targetGroupProps can't include a VPC). Providing + * both this and existingVpc is an error. An SSM Interface + * endpoint will be included in this VPC. + * + * @default - none + */ + readonly vpcProps?: ec2.VpcProps; + /** + * An existing VPC in which to deploy the construct. Providing both this and + * vpcProps is an error. If the client provides an existing Fargate service, + * this value must be the VPC where the service is running. An SSM Interface + * endpoint will be added to this VPC. + * + * @default - none + */ + readonly existingVpc?: ec2.IVpc; + /** + * Optional properties to create a new ECS cluster + */ + readonly clusterProps?: ecs.ClusterProps; + /** + * The arn of an ECR Repository containing the image to use + * to generate the containers + * + * format: + * arn:aws:ecr:[region]:[account number]:repository/[Repository Name] + */ + readonly ecrRepositoryArn?: string; + /** + * The version of the image to use from the repository + * + * @default - 'latest' + */ + readonly ecrImageVersion?: string; + /* + * Optional props to define the container created for the Fargate Service + * + * defaults - fargate-defaults.ts + */ + readonly containerDefinitionProps?: ecs.ContainerDefinitionProps | any; + /* + * Optional props to define the Fargate Task Definition for this construct + * + * defaults - fargate-defaults.ts + */ + readonly fargateTaskDefinitionProps?: ecs.FargateTaskDefinitionProps | any; + /** + * Optional values to override default Fargate Task definition properties + * (fargate-defaults.ts). The construct will default to launching the service + * is the most isolated subnets available (precedence: Isolated, Private and + * Public). Override those and other defaults here. + * + * defaults - fargate-defaults.ts + */ + readonly fargateServiceProps?: ecs.FargateServiceProps | any; + /** + * A Fargate Service already instantiated (probably by another Solutions Construct). If + * this is specified, then no props defining a new service can be provided, including: + * existingImageObject, ecrImageVersion, containerDefintionProps, fargateTaskDefinitionProps, + * ecrRepositoryArn, fargateServiceProps, clusterProps, existingClusterInterface. If this value + * is provided, then existingContainerDefinitionObject must be provided as well. + * + * @default - none + */ + readonly existingFargateServiceObject?: ecs.FargateService; + /* + * A container definition already instantiated as part of a Fargate service. This must + * be the container in the existingFargateServiceObject. + * + * @default - None + */ + readonly existingContainerDefinitionObject?: ecs.ContainerDefinition; + /** + * Optional user provided props to override the default props for SSM String Parameter. + * + * @default - Default props are used + */ + readonly stringParameterProps?: ssm.StringParameterProps; + /** + * Optional user provided props to override the default props for SSM String Parameter. + * + * @default - None + */ + readonly existingStringParameterObj?: ssm.StringParameter; + /** + * Optional SSM String parameter permissions to grant to the Fargate service. One of the following may be specified: "Read", "ReadWrite". + * + * @default - 'Read' + */ + readonly stringParameterPermissions?: string + /** + * Optional Name for the SSM parameter name environment variable set for the container. + * + * @default - None + */ + readonly stringParameterEnvironmentVariableName?: string; +} + +export class FargateToSsmstringparameter extends Construct { + public readonly vpc: ec2.IVpc; + public readonly service: ecs.FargateService; + public readonly container: ecs.ContainerDefinition; + public readonly stringParameter: ssm.StringParameter; + + constructor(scope: Construct, id: string, props: FargateToSsmstringparameterProps) { + super(scope, id); + defaults.CheckProps(props); + defaults.CheckFargateProps(props); + + // Other permissions for constructs are accepted as arrays, turning stringParameterPermissions into + // an array to use the same validation function. + if (props.stringParameterPermissions) { + const allowedPermissions = ['READ', 'READWRITE']; + defaults.CheckListValues(allowedPermissions, [props.stringParameterPermissions.toUpperCase()], 'stringParameterPermissions'); + } + + this.vpc = defaults.buildVpc(scope, { + existingVpc: props.existingVpc, + defaultVpcProps: props.publicApi ? defaults.DefaultPublicPrivateVpcProps() : defaults.DefaultIsolatedVpcProps(), + userVpcProps: props.vpcProps, + constructVpcProps: { enableDnsHostnames: true, enableDnsSupport: true } + }); + + defaults.AddAwsServiceEndpoint(scope, this.vpc, defaults.ServiceEndpointTypes.SSM); + + if (props.existingFargateServiceObject) { + this.service = props.existingFargateServiceObject; + // CheckFargateProps confirms that the container is provided + this.container = props.existingContainerDefinitionObject!; + } else { + [this.service, this.container] = defaults.CreateFargateService( + scope, + id, + this.vpc, + props.clusterProps, + props.ecrRepositoryArn, + props.ecrImageVersion, + props.fargateTaskDefinitionProps, + props.containerDefinitionProps, + props.fargateServiceProps + ); + } + + // Setup the SSM String parameter + if (props.existingStringParameterObj) { + this.stringParameter = props.existingStringParameterObj; + } else { + if (!props.stringParameterProps) { + throw new Error("existingStringParameterObj or stringParameterProps needs to be provided."); + } + this.stringParameter = defaults.buildSsmStringParameter(this, 'stringParameter', props.stringParameterProps); + } + + this.stringParameter.grantRead(this.service.taskDefinition.taskRole); + + if (props.stringParameterPermissions) { + const _permissions = props.stringParameterPermissions.toUpperCase(); + + // Add the requested string parameter permission + if (_permissions === 'READWRITE') { + this.stringParameter.grantWrite(this.service.taskDefinition.taskRole); + } + } + + // Add environment variables + const stringParameterEnvironmentVariableName = props.stringParameterEnvironmentVariableName || 'SSM_STRING_PARAMETER_NAME'; + this.container.addEnvironment(stringParameterEnvironmentVariableName, this.stringParameter.parameterName); + } +} diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/package.json b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/package.json new file mode 100644 index 000000000..9ac6b93ac --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/package.json @@ -0,0 +1,105 @@ +{ + "name": "@aws-solutions-constructs/aws-fargate-ssmstringparameter", + "version": "0.0.0", + "description": "CDK Constructs for AWS Fargate to AWS SSM Parameter Store Integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-constructs.git", + "directory": "source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awsconstructs.services.fargatessmstringparameter", + "maven": { + "groupId": "software.amazon.awsconstructs", + "artifactId": "fargatessmstringparameter" + } + }, + "dotnet": { + "namespace": "Amazon.SolutionsConstructs.AWS.FargateSsmStringParameter", + "packageId": "Amazon.SolutionsConstructs.AWS.FargateSsmStringParameter", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-constructs.aws-fargate-ssmstringparameter", + "module": "aws_solutions_constructs.aws_fargate_ssmstringparameter" + } + } + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-ssm": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-ssm": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@types/jest": "^26.0.22", + "@aws-solutions-constructs/core": "0.0.0", + "@types/node": "^10.3.0", + "constructs": "3.2.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ], + "coverageReporters": [ + "text", + [ + "lcov", + { + "projectRoot": "../../../../" + } + ] + ] + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-ssm": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "keywords": [ + "aws", + "cdk", + "awscdk", + "AWS Solutions Constructs", + "Amazon Systems Manager", + "Amazon SSM String Parameter", + "AWS Fargate" + ] +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/fargate-ssmstringparameter.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/fargate-ssmstringparameter.test.ts new file mode 100644 index 000000000..0ab6cad1d --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/fargate-ssmstringparameter.test.ts @@ -0,0 +1,826 @@ +/** + * Copyright 2022 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. + */ + +import '@aws-cdk/assert/jest'; +import * as defaults from '@aws-solutions-constructs/core'; +import * as cdk from "@aws-cdk/core"; +import { FargateToSsmstringparameter } from "../lib"; +import * as ssm from '@aws-cdk/aws-ssm'; +import * as ecs from '@aws-cdk/aws-ecs'; + +const allowedPattern = '.*'; +const description = 'The value Foo'; +const parameterName = 'FooParameter'; +const stringValue = 'Foo'; +const clusterName = "custom-cluster-name"; +const containerName = "custom-container-name"; +const serviceName = "custom-service-name"; +const familyName = "family-name"; +const customName = 'custom-name'; + +test('New service/new parameter store, public API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + const construct = new FargateToSsmstringparameter(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: familyName }, + fargateServiceProps: { serviceName }, + stringParameterProps: { + parameterName, + stringValue + }, + }); + + expect(construct.vpc !== null); + expect(construct.service !== null); + expect(construct.container !== null); + expect(construct.stringParameter !== null); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Cluster", { + ClusterName: clusterName + }); + + expect(stack).toHaveResourceLike("AWS::SSM::Parameter", { + Name: parameterName, + Value: stringValue + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/", + { + Ref: "testconstructstringParameter4A9E7765" + } + ] + ] + } + } + ] + } + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + Family: familyName, + ContainerDefinitions: [ + { + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: containerName, + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.0.0.0/16' + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".ssm" + ] + ] + } + }); + + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SSM::Parameter', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/new parameter store, private API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + new FargateToSsmstringparameter(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + stringParameterProps: { + parameterName, + stringValue + }, + stringParameterPermissions: 'readwrite', + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "SSM_STRING_PARAMETER_NAME", + Value: { + Ref: "testconstructstringParameter4A9E7765" + } + }, + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::SSM::Parameter", { + Name: parameterName, + Value: stringValue + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.0.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/", + { + Ref: "testconstructstringParameter4A9E7765" + } + ] + ] + } + }, + { + Action: "ssm:PutParameter", + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/", + { + Ref: "testconstructstringParameter4A9E7765" + } + ] + ] + } + } + ], + Version: "2012-10-17" + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".ssm" + ] + ] + } + }); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SSM::Parameter', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/existing parameter store, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const existingParameterStore = createSsmParameterStore(stack); + + new FargateToSsmstringparameter(stack, 'test-construct', { + publicApi, + existingVpc, + existingStringParameterObj: existingParameterStore, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "SSM_STRING_PARAMETER_NAME", + Value: { + Ref: "Parameter9E1B4FBA" + } + }, + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::SSM::Parameter", { + Name: parameterName, + Value: stringValue + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/", + { + Ref: "Parameter9E1B4FBA" + } + ] + ] + } + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".ssm" + ] + ] + } + }); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::SSM::Parameter', 1); +}); + +test('Existing service/new parameter store, public API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + const existingVpc = defaults.getTestVpc(stack); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + new FargateToSsmstringparameter(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + stringParameterEnvironmentVariableName: customName, + stringParameterProps: { + parameterName, + stringValue + }, + stringParameterPermissions: 'readwrite' + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: customName, + Value: { + Ref: "testconstructstringParameter4A9E7765" + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::SSM::Parameter", { + Name: parameterName, + Value: stringValue + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/", + { + Ref: "testconstructstringParameter4A9E7765" + } + ] + ] + } + }, + { + Action: "ssm:PutParameter", + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/", + { + Ref: "testconstructstringParameter4A9E7765" + } + ] + ] + } + } + ], + Version: "2012-10-17" + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".ssm" + ] + ] + } + }); + + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::SSM::Parameter', 1); +}); + +test('Existing service/existing parameter store, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingParameterStore = createSsmParameterStore(stack); + + new FargateToSsmstringparameter(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + existingStringParameterObj: existingParameterStore + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "SSM_STRING_PARAMETER_NAME", + Value: { + Ref: "Parameter9E1B4FBA" + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::SSM::Parameter", { + Name: parameterName, + Value: stringValue + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + Effect: "Allow", + Resource: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":ssm:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":parameter/", + { + Ref: "Parameter9E1B4FBA" + } + ] + ] + } + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".ssm" + ] + ] + } + }); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::SSM::Parameter', 1); +}); + +test('Test error invalid string parameter permission', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingStringParameterObj = createSsmParameterStore(stack); + + const app = () => { + new FargateToSsmstringparameter(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + stringParameterPermissions: 'reed', + existingStringParameterObj + }); + }; + + expect(app).toThrowError('Invalid stringParameterPermissions submitted - REED'); +}); + +test('Test error no existing object or prop provided', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const app = () => { + new FargateToSsmstringparameter(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + }); + }; + + expect(app).toThrowError('existingStringParameterObj or stringParameterProps needs to be provided.'); +}); + +function createSsmParameterStore(stack: cdk.Stack) { + return new ssm.StringParameter(stack, 'Parameter', { + allowedPattern, + description, + parameterName, + stringValue, + }); +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.existing-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.existing-resources.expected.json new file mode 100644 index 000000000..a1cf37db6 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.existing-resources.expected.json @@ -0,0 +1,1211 @@ +{ + "Description": "Integration Test with new VPC, Service and SSM String Parameter", + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "172.168.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "172.168.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "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": "existing-resources/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, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/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)" + } + ] + } + } + }, + "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": "existing-resources/Vpc" + } + ] + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRAPIsecuritygroup78294485", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRDKRsecuritygroup598BA37E", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "VpcSSM173B3B5B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ssm", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesSSMsecuritygroup5CE82B38", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "Parameter9E1B4FBA": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "Foo", + "AllowedPattern": ".*", + "Description": "The value Foo", + "Name": "FooParameter" + } + }, + "existingresourcesECRAPIsecuritygroup78294485": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-ECR_API-security-group", + "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" + } + ] + } + } + }, + "existingresourcesECRDKRsecuritygroup598BA37E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-ECR_DKR-security-group", + "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" + } + ] + } + } + }, + "testclusterDF8B0D19": { + "Type": "AWS::ECS::Cluster" + }, + "testtaskdefTaskRoleB2DEF113": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testtaskdefTaskRoleDefaultPolicy5D591D1C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-east-1:", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "Parameter9E1B4FBA" + } + ] + ] + } + }, + { + "Action": "ssm:PutParameter", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-east-1:", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "Parameter9E1B4FBA" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testtaskdefTaskRoleDefaultPolicy5D591D1C", + "Roles": [ + { + "Ref": "testtaskdefTaskRoleB2DEF113" + } + ] + } + }, + "testtaskdefF924AD58": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "CUSTOM_NAME", + "Value": { + "Ref": "Parameter9E1B4FBA" + } + } + ], + "Essential": true, + "Image": "nginx", + "MemoryReservation": 512, + "Name": "test-container", + "PortMappings": [ + { + "ContainerPort": 8080, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "existingresourcestesttaskdef88B214A2", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "testtaskdefTaskRoleB2DEF113", + "Arn" + ] + } + } + }, + "testsg872EB48A": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Construct created security group", + "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" + } + ] + } + } + }, + "testserviceService2730C249": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "testclusterDF8B0D19" + }, + "DeploymentConfiguration": { + "MaximumPercent": 150, + "MinimumHealthyPercent": 75 + }, + "DesiredCount": 2, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "testsg872EB48A", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "PlatformVersion": "LATEST", + "TaskDefinition": { + "Ref": "testtaskdefF924AD58" + } + } + }, + "existingresourcesSSMsecuritygroup5CE82B38": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-SSM-security-group", + "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": { + "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-fargate-ssmstringparameter/test/integ.existing-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.existing-resources.ts new file mode 100644 index 000000000..88afd43e8 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.existing-resources.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2022 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 { Aws, App, Stack } from "@aws-cdk/core"; +import { FargateToSsmstringparameter, FargateToSsmstringparameterProps } from "../lib"; +import { generateIntegStackName, getTestVpc, CreateFargateService } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as ssm from '@aws-cdk/aws-ssm'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and SSM String Parameter'; + +const existingVpc = getTestVpc(stack); +const existingStringParameterObj = new ssm.StringParameter(stack, 'Parameter', { + allowedPattern: '.*', + description: 'The value Foo', + parameterName: 'FooParameter', + stringValue: 'Foo', +}); + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const [testService, testContainer] = CreateFargateService(stack, + 'test', + existingVpc, + undefined, + undefined, + undefined, + undefined, + { image }, +); + +const constructProps: FargateToSsmstringparameterProps = { + publicApi: true, + existingVpc, + existingStringParameterObj, + existingContainerDefinitionObject: testContainer, + existingFargateServiceObject: testService, + stringParameterEnvironmentVariableName: 'CUSTOM_NAME', + stringParameterPermissions: "readwrite" +}; + +new FargateToSsmstringparameter(stack, 'test-construct', constructProps); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.new-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.new-resources.expected.json new file mode 100644 index 000000000..d931809e0 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.new-resources.expected.json @@ -0,0 +1,1186 @@ +{ + "Description": "Integration Test with new VPC, Service and SSM String Parameter", + "Resources": { + "testconstructstringParameter4A9E7765": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "Foo", + "Name": "FooParameter" + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "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": "new-resources/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, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/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)" + } + ] + } + } + }, + "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": "new-resources/Vpc" + } + ] + } + }, + "VpcSSM173B3B5B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ssm", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesSSMsecuritygroupBA8A3B0D", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRAPIsecuritygroupE52BAE3F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRDKRsecuritygroupBA34F94F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "newresourcesSSMsecuritygroupBA8A3B0D": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-SSM-security-group", + "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" + } + ] + } + } + }, + "newresourcesECRAPIsecuritygroupE52BAE3F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_API-security-group", + "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" + } + ] + } + } + }, + "newresourcesECRDKRsecuritygroupBA34F94F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_DKR-security-group", + "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" + } + ] + } + } + }, + "testconstructcluster7B6231C5": { + "Type": "AWS::ECS::Cluster" + }, + "testconstructtaskdefTaskRoleC60414C4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testconstructtaskdefTaskRoleDefaultPolicyF34A1535": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssm:DescribeParameters", + "ssm:GetParameters", + "ssm:GetParameter", + "ssm:GetParameterHistory" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:us-east-1:", + { + "Ref": "AWS::AccountId" + }, + ":parameter/", + { + "Ref": "testconstructstringParameter4A9E7765" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testconstructtaskdefTaskRoleDefaultPolicyF34A1535", + "Roles": [ + { + "Ref": "testconstructtaskdefTaskRoleC60414C4" + } + ] + } + }, + "testconstructtaskdef8BD1F9E4": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "SSM_STRING_PARAMETER_NAME", + "Value": { + "Ref": "testconstructstringParameter4A9E7765" + } + } + ], + "Essential": true, + "Image": "nginx", + "MemoryReservation": 512, + "Name": "test-construct-container", + "PortMappings": [ + { + "ContainerPort": 8080, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "newresourcestestconstructtaskdefE4616A0D", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "testconstructtaskdefTaskRoleC60414C4", + "Arn" + ] + } + } + }, + "testconstructsgA602AA29": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Construct created security group", + "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" + } + ] + } + } + }, + "testconstructserviceService13074A8F": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "testconstructcluster7B6231C5" + }, + "DeploymentConfiguration": { + "MaximumPercent": 150, + "MinimumHealthyPercent": 75 + }, + "DesiredCount": 2, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "testconstructsgA602AA29", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "PlatformVersion": "LATEST", + "TaskDefinition": { + "Ref": "testconstructtaskdef8BD1F9E4" + } + } + } + }, + "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-fargate-ssmstringparameter/test/integ.new-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.new-resources.ts new file mode 100644 index 000000000..988769c50 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-ssmstringparameter/test/integ.new-resources.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2022 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 { Aws, App, Stack } from "@aws-cdk/core"; +import { FargateToSsmstringparameter, FargateToSsmstringparameterProps } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and SSM String Parameter'; + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const testProps: FargateToSsmstringparameterProps = { + publicApi: true, + containerDefinitionProps: { + image + }, + stringParameterProps: { + parameterName: "FooParameter", + stringValue: "Foo" + } +}; + +new FargateToSsmstringparameter(stack, 'test-construct', testProps); + +// Synth +app.synth(); From 1b843bff718dd05376f4f72ff9075db123e05288 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Thu, 28 Apr 2022 12:58:50 -0400 Subject: [PATCH 26/34] fix(aws-lambda-secretsmanager): Update docs (#673) * Update README.md * Update index.ts --- .../aws-lambda-secretsmanager/README.md | 4 ++-- .../aws-lambda-secretsmanager/lib/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md index 07244b563..77d71853e 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md @@ -90,8 +90,8 @@ new LambdaToSecretsmanager(this, "test-lambda-secretsmanager-stack", new LambdaT |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.| |secretProps?|[`secretsmanager.SecretProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-secretsmanager.SecretProps.html)|Optional user provided props to override the default props for Secrets Manager| |existingSecretObj?|[`secretsmanager.Secret`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-secretsmanager.Secret.html)|Existing instance of Secrets Manager Secret object, If this is set then the secretProps is ignored| -|grantWriteAccess?|`boolean`|Optional write access to the Secret for the Lambda function (Read-Only by default) -|secretEnvironmentVariableName?|`string`|Optional Name for the Secrets Manager secret environment variable set for the Lambda function.| +|grantWriteAccess?|`string`|Optional Access granted to the Lambda function for the secret. 'Read' or 'ReadWrite". Default is "Read" +|secretEnvironmentVariableName?|`string`|Optional Name for Lambda function environment variable containing the ARN of the secret. Default is SECRET_ARN. | |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 AWS Secrets Manager. 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.| |vpcProps?|[`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcProps.html)|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.| |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`.| diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/lib/index.ts index 1aa83aee8..40a738466 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/lib/index.ts @@ -61,9 +61,9 @@ export interface LambdaToSecretsmanagerProps { */ readonly deployVpc?: boolean; /** - * Optional Name for the Secret environment variable set for the Lambda function. + * Optional Name for Lambda function environment variable containing the ARN of the secret. * - * @default - SECRET_NAME + * @default - SECRET_ARN */ readonly secretEnvironmentVariableName?: string; /** From cd218b6900a174afa09c86f28fb0650ecfe37942 Mon Sep 17 00:00:00 2001 From: mickychetta <45010053+mickychetta@users.noreply.github.com> Date: Wed, 4 May 2022 04:25:55 -0700 Subject: [PATCH 27/34] feat(aws-fargate-secretsmanager): Create new construct (#670) * created README.md * created new construct * fixed doc typo and created cidr constant --- .../aws-fargate-secretsmanager/.eslintignore | 4 + .../aws-fargate-secretsmanager/.gitignore | 15 + .../aws-fargate-secretsmanager/.npmignore | 21 + .../aws-fargate-secretsmanager/README.md | 123 ++ .../architecture.png | Bin 0 -> 127509 bytes .../aws-fargate-secretsmanager/lib/index.ts | 197 +++ .../aws-fargate-secretsmanager/package.json | 104 ++ .../test/fargate-secretsmanager.test.ts | 640 +++++++++ .../integ.existing-resources.expected.json | 1178 ++++++++++++++++ .../test/integ.existing-resources.ts | 54 + .../test/integ.new-resources.expected.json | 1188 +++++++++++++++++ .../test/integ.new-resources.ts | 40 + .../aws-lambda-secretsmanager/README.md | 1 - 13 files changed, 3564 insertions(+), 1 deletion(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/fargate-secretsmanager.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.existing-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.existing-resources.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.new-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.new-resources.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.eslintignore new file mode 100644 index 000000000..e6f7801ea --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.eslintignore @@ -0,0 +1,4 @@ +lib/*.js +test/*.js +*.d.ts +coverage diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.gitignore b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.npmignore b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/README.md new file mode 100644 index 000000000..7b0707c86 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/README.md @@ -0,0 +1,123 @@ +# aws-fargate-secretsmanager module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +> All classes are under active development and subject to non-backward compatible changes or removal in any +> future version. These are not subject to the [Semantic Versioning](https://semver.org/) model. +> This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +| **Reference Documentation**:| https://docs.aws.amazon.com/solutions/latest/constructs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png) Python|`aws_solutions_constructs.aws_fargate_secretsmanager`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-fargate-secretsmanager`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.fargatesecretsmanager`| + +This AWS Solutions Construct implements an AWS Fargate service that can write/read to an AWS Secrets Manager + +Here is a minimal deployable pattern definition: + +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { FargateToSecretsmanager, FargateToSecretsmanagerProps } from '@aws-solutions-constructs/aws-fargate-secretsmanager'; + +const constructProps: FargateToSecretsmanagerProps = { + publicApi: true, + ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", +}; + +new FargateToSecretsmanager(stack, 'test-construct', constructProps); +``` + +Python +``` python +from aws_solutions_constructs.aws_fargate_secretsmanager import FargateToSecretsmanager, FargateToSecretsmanagerProps +from aws_cdk import ( + Stack +) +from constructs import Construct + +FargateToSecretsmanager(self, 'test_construct', + public_api=True, + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.fargatesecretsmanager.*; + +new FargateToSecretsmanager(this, "test-construct", new FargateToSecretsmanagerProps.Builder() + .publicApi(true) + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo") + .build()); +``` + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| publicApi | `boolean` | Whether the construct is deploying a private or public API. This has implications for the VPC. | +| vpcProps? | [`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcProps.html) | Optional custom properties for a VPC the construct will create. This VPC will be used by any Private Hosted Zone the construct creates (that's why loadBalancerProps and privateHostedZoneProps can't include a VPC). Providing both this and existingVpc is an error. | +| existingVpc? | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | An existing VPC in which to deploy the construct. Providing both this and vpcProps is an error. If the client provides an existing load balancer and/or existing Private Hosted Zone, those constructs must exist in this VPC. | +| clusterProps? | [`ecs.ClusterProps`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ClusterProps.html) | Optional properties to create a new ECS cluster. To provide an existing cluster, use the cluster attribute of fargateServiceProps. | +| ecrRepositoryArn? | `string` | The arn of an ECR Repository containing the image to use to generate the containers. Either this or the image property of containerDefinitionProps must be provided. format: arn:aws:ecr:*region*:*account number*:repository/*Repository Name* | +| ecrImageVersion? | `string` | The version of the image to use from the repository. Defaults to 'Latest' | +| containerDefinitionProps? | [`ecs.ContainerDefinitionProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinitionProps.html) | Optional props to define the container created for the Fargate Service (defaults found in fargate-defaults.ts) | +| fargateTaskDefinitionProps? | [`ecs.FargateTaskDefinitionProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateTaskDefinitionProps.html) | Optional props to define the Fargate Task Definition for this construct (defaults found in fargate-defaults.ts) | +| fargateServiceProps? | [`ecs.FargateServiceProps \| any`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateServiceProps.html) | Optional values to override default Fargate Task definition properties (fargate-defaults.ts). The construct will default to launching the service is the most isolated subnets available (precedence: Isolated, Private and Public). Override those and other defaults here. | +| existingFargateServiceObject? | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html) | A Fargate Service already instantiated (probably by another Solutions Construct). If this is specified, then no props defining a new service can be provided, including: ecrImageVersion, containerDefinitionProps, fargateTaskDefinitionProps, ecrRepositoryArn, fargateServiceProps, clusterProps | +| existingContainerDefinitionObject? | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinition.html) | A container definition already instantiated as part of a Fargate service. This must be the container in the existingFargateServiceObject | +|secretProps?|[`secretsmanager.SecretProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-secretsmanager.SecretProps.html)|Optional user provided props to override the default props for Secrets Manager| +|existingSecretObj?|[`secretsmanager.Secret`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-secretsmanager.Secret.html)|Existing instance of Secrets Manager Secret object, If this is set then the secretProps is ignored| +|grantWriteAccess?|`boolean`|Optional write access to the Secret for the Fargate service (Read-Only by default) +|secretEnvironmentVariableName?|`string`|Optional Name for the Secrets Manager secret environment variable set for the Fargate service.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| vpc | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.IVpc.html) | The VPC used by the construct (whether created by the construct or provided by the client) | +| service | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html) | The AWS Fargate service used by this construct (whether created by this construct or passed to this construct at initialization) | +| container | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.ContainerDefinition.html) | The container associated with the AWS Fargate service in the service property. | +|secret|[`secretsmanager.Secret`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-secretsmanager.Secret.html)|Returns an instance of `secretsmanager.Secret` created by the construct| + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### AWS Fargate Service +* Sets up an AWS Fargate service + * Uses the existing service if provided + * Creates a new service if none provided. + * Service will run in isolated subnets if available, then private subnets if available and finally public subnets + * Adds environment variables to the container with the ARN and Name of the Secrets Manager secret + * Add permissions to the container IAM role allowing it to publish to the Secrets Manager secret + +### Amazon Secrets Manager Secret +* Sets up an Amazon Secrets Manager secret + * Uses an existing secret if one is provided, otherwise creates a new one + * (default) random name + * (default) random value +* Adds an Interface Endpoint to the VPC for Secrets Manager (the service by default runs in Isolated or Private subnets) +* Retain the Secret when deleting the CloudFormation stack + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/architecture.png b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..e6451b8c605c6f485e66030b1be0804611f84f86 GIT binary patch literal 127509 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 literal 0 HcmV?d00001 diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/lib/index.ts new file mode 100644 index 000000000..15b828614 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/lib/index.ts @@ -0,0 +1,197 @@ +/** + * Copyright 2022 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. + */ + +import * as ec2 from "@aws-cdk/aws-ec2"; +import * as secretsmanager from "@aws-cdk/aws-secretsmanager"; +// Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate +import { Construct } from "@aws-cdk/core"; +import * as defaults from "@aws-solutions-constructs/core"; +import * as ecs from "@aws-cdk/aws-ecs"; + +export interface FargateToSecretsmanagerProps { + /** + * Whether the construct is deploying a private or public API. This has implications for the VPC deployed + * by this construct. + * + * @default - none + */ + readonly publicApi: boolean; + /** + * Optional custom properties for a VPC the construct will create. This VPC will + * be used by the new Fargate service the construct creates (that's + * why targetGroupProps can't include a VPC). Providing + * both this and existingVpc is an error. A Secrets Manager Interface + * endpoint will be included in this VPC. + * + * @default - none + */ + readonly vpcProps?: ec2.VpcProps; + /** + * An existing VPC in which to deploy the construct. Providing both this and + * vpcProps is an error. If the client provides an existing Fargate service, + * this value must be the VPC where the service is running. A Secrets Manager Interface + * endpoint will be added to this VPC. + * + * @default - none + */ + readonly existingVpc?: ec2.IVpc; + /** + * Optional properties to create a new ECS cluster + */ + readonly clusterProps?: ecs.ClusterProps; + /** + * The arn of an ECR Repository containing the image to use + * to generate the containers + * + * format: + * arn:aws:ecr:[region]:[account number]:repository/[Repository Name] + */ + readonly ecrRepositoryArn?: string; + /** + * The version of the image to use from the repository + * + * @default - 'latest' + */ + readonly ecrImageVersion?: string; + /* + * Optional props to define the container created for the Fargate Service + * + * defaults - fargate-defaults.ts + */ + readonly containerDefinitionProps?: ecs.ContainerDefinitionProps | any; + /* + * Optional props to define the Fargate Task Definition for this construct + * + * defaults - fargate-defaults.ts + */ + readonly fargateTaskDefinitionProps?: ecs.FargateTaskDefinitionProps | any; + /** + * Optional values to override default Fargate Task definition properties + * (fargate-defaults.ts). The construct will default to launching the service + * is the most isolated subnets available (precedence: Isolated, Private and + * Public). Override those and other defaults here. + * + * defaults - fargate-defaults.ts + */ + readonly fargateServiceProps?: ecs.FargateServiceProps | any; + /** + * A Fargate Service already instantiated (probably by another Solutions Construct). If + * this is specified, then no props defining a new service can be provided, including: + * existingImageObject, ecrImageVersion, containerDefintionProps, fargateTaskDefinitionProps, + * ecrRepositoryArn, fargateServiceProps, clusterProps, existingClusterInterface. If this value + * is provided, then existingContainerDefinitionObject must be provided as well. + * + * @default - none + */ + readonly existingFargateServiceObject?: ecs.FargateService; + /* + * A container definition already instantiated as part of a Fargate service. This must + * be the container in the existingFargateServiceObject. + * + * @default - None + */ + readonly existingContainerDefinitionObject?: ecs.ContainerDefinition; + /** + * Existing instance of Secret object, providing both this and secretProps will cause an error. + * + * @default - Default props are used + */ + readonly existingSecretObj?: secretsmanager.Secret; + /** + * Optional user-provided props to override the default props for the Secret. + * + * @default - Default props are used + */ + readonly secretProps?: secretsmanager.SecretProps; + + /** + * Optional Access granted to the Fargate service for the secret. 'Read' or 'ReadWrite + * + * @default - 'Read' + */ + readonly grantWriteAccess?: string + /** + * Optional Name for container environment variable containing the ARN of the secret. + * + * @default - SECRET_ARN + */ + readonly secretEnvironmentVariableName?: string; +} + +export class FargateToSecretsmanager extends Construct { + public readonly vpc: ec2.IVpc; + public readonly service: ecs.FargateService; + public readonly container: ecs.ContainerDefinition; + public readonly secret: secretsmanager.Secret; + + constructor(scope: Construct, id: string, props: FargateToSecretsmanagerProps) { + super(scope, id); + defaults.CheckProps(props); + defaults.CheckFargateProps(props); + + // Other permissions for constructs are accepted as arrays, turning grantWriteAccess into + // an array to use the same validation function. + if (props.grantWriteAccess) { + const allowedPermissions = ['READ', 'READWRITE']; + defaults.CheckListValues(allowedPermissions, [props.grantWriteAccess.toUpperCase()], 'grantWriteAccess'); + } + + this.vpc = defaults.buildVpc(scope, { + existingVpc: props.existingVpc, + defaultVpcProps: props.publicApi ? defaults.DefaultPublicPrivateVpcProps() : defaults.DefaultIsolatedVpcProps(), + userVpcProps: props.vpcProps, + constructVpcProps: { enableDnsHostnames: true, enableDnsSupport: true } + }); + + defaults.AddAwsServiceEndpoint(scope, this.vpc, defaults.ServiceEndpointTypes.SECRETS_MANAGER); + + if (props.existingFargateServiceObject) { + this.service = props.existingFargateServiceObject; + // CheckFargateProps confirms that the container is provided + this.container = props.existingContainerDefinitionObject!; + } else { + [this.service, this.container] = defaults.CreateFargateService( + scope, + id, + this.vpc, + props.clusterProps, + props.ecrRepositoryArn, + props.ecrImageVersion, + props.fargateTaskDefinitionProps, + props.containerDefinitionProps, + props.fargateServiceProps + ); + } + + if (props.existingSecretObj) { + this.secret = props.existingSecretObj; + } else { + this.secret = defaults.buildSecretsManagerSecret(this, 'secret', props.secretProps); + } + + // Enable read permissions for the Fargate service role by default + this.secret.grantRead(this.service.taskDefinition.taskRole); + + if (props.grantWriteAccess) { + const permission = props.grantWriteAccess.toUpperCase(); + + if (permission === 'READWRITE') { + this.secret.grantWrite(this.service.taskDefinition.taskRole); + } + } + + // Configure environment variables + const secretEnvironmentVariableName = props.secretEnvironmentVariableName || 'SECRET_ARN'; + this.container.addEnvironment(secretEnvironmentVariableName, this.secret.secretArn); + } +} diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/package.json b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/package.json new file mode 100644 index 000000000..d0778676b --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/package.json @@ -0,0 +1,104 @@ +{ + "name": "@aws-solutions-constructs/aws-fargate-secretsmanager", + "version": "0.0.0", + "description": "CDK Constructs for AWS Fargate to Amazon Secrets Manager integration", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/awslabs/aws-solutions-constructs.git", + "directory": "source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "scripts": { + "build": "tsc -b .", + "lint": "eslint -c ../eslintrc.yml --ext=.js,.ts . && tslint --project .", + "lint-fix": "eslint -c ../eslintrc.yml --ext=.js,.ts --fix .", + "test": "jest --coverage", + "clean": "tsc -b --clean", + "watch": "tsc -b -w", + "integ": "cdk-integ", + "integ-no-clean": "cdk-integ --no-clean", + "integ-assert": "cdk-integ-assert", + "jsii": "jsii", + "jsii-pacmak": "jsii-pacmak", + "build+lint+test": "npm run jsii && npm run lint && npm test && npm run integ-assert", + "snapshot-update": "npm run jsii && npm test -- -u && npm run integ-assert" + }, + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awsconstructs.services.fargatesecretsmanager", + "maven": { + "groupId": "software.amazon.awsconstructs", + "artifactId": "fargatesecretsmanager" + } + }, + "dotnet": { + "namespace": "Amazon.SolutionsConstructs.AWS.FargateSecretsmanager", + "packageId": "Amazon.SolutionsConstructs.AWS.FargateSecretsmanager", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-constructs.aws-fargate-secretsmanager", + "module": "aws_solutions_constructs.aws_fargate_secretsmanager" + } + } + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@types/jest": "^26.0.22", + "@aws-solutions-constructs/core": "0.0.0", + "@types/node": "^10.3.0", + "constructs": "3.2.0" + }, + "jest": { + "moduleFileExtensions": [ + "js" + ], + "coverageReporters": [ + "text", + [ + "lcov", + { + "projectRoot": "../../../../" + } + ] + ] + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-solutions-constructs/core": "0.0.0", + "constructs": "^3.2.0" + }, + "keywords": [ + "aws", + "cdk", + "awscdk", + "AWS Solutions Constructs", + "Amazon Secrets Manager", + "AWS Fargate" + ] +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/fargate-secretsmanager.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/fargate-secretsmanager.test.ts new file mode 100644 index 000000000..8424f2405 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/fargate-secretsmanager.test.ts @@ -0,0 +1,640 @@ +/** + * Copyright 2022 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. + */ + +import '@aws-cdk/assert/jest'; +import * as defaults from '@aws-solutions-constructs/core'; +import * as cdk from "@aws-cdk/core"; +import { FargateToSecretsmanager } from "../lib"; +import * as ecs from '@aws-cdk/aws-ecs'; +import { buildSecretsManagerSecret } from '@aws-solutions-constructs/core'; + +const clusterName = "custom-cluster-name"; +const containerName = "custom-container-name"; +const serviceName = "custom-service-name"; +const familyName = "family-name"; +const secretName = 'custom-name'; +const envName = 'CUSTOM_SECRET_ARN'; +const cidr = '172.0.0.0/16'; + +test('New service/new secret, public API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + const construct = new FargateToSecretsmanager(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr }, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: familyName }, + fargateServiceProps: { serviceName }, + secretProps: { + secretName + }, + }); + + expect(construct.vpc !== null); + expect(construct.service !== null); + expect(construct.container !== null); + expect(construct.secret !== null); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Cluster", { + ClusterName: clusterName + }); + + expect(stack).toHaveResourceLike("AWS::SecretsManager::Secret", { + Name: secretName + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + Effect: "Allow", + Resource: { + Ref: "testconstructsecret1A43460A" + } + } + ] + } + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + Family: familyName, + ContainerDefinitions: [ + { + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: containerName, + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: cidr + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".secretsmanager" + ] + ] + } + }); + + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SecretsManager::Secret', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/new secret, private API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + new FargateToSecretsmanager(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr }, + secretProps: { + secretName + }, + grantWriteAccess: 'readwrite', + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "SECRET_ARN", + Value: { + Ref: "testconstructsecret1A43460A" + } + }, + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::SecretsManager::Secret", { + Name: secretName, + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.0.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + Effect: "Allow", + Resource: { + Ref: "testconstructsecret1A43460A" + } + }, + { + Action: [ + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecret" + ], + Effect: "Allow", + Resource: { + Ref: "testconstructsecret1A43460A" + } + } + ], + Version: "2012-10-17" + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".secretsmanager" + ] + ] + } + }); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SecretsManager::Secret', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/existing secret, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const existingSecretObj = defaults.buildSecretsManagerSecret(stack, 'secret', { + secretName + }); + + new FargateToSecretsmanager(stack, 'test-construct', { + publicApi, + existingVpc, + existingSecretObj, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "SECRET_ARN", + Value: { + Ref: "secret4DA88516" + } + }, + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::SecretsManager::Secret", { + Name: secretName + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + Effect: "Allow", + Resource: { + Ref: "secret4DA88516" + } + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".secretsmanager" + ] + ] + } + }); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::SecretsManager::Secret', 1); +}); + +test('Existing service/new secret, public API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + const existingVpc = defaults.getTestVpc(stack); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + new FargateToSecretsmanager(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + secretEnvironmentVariableName: envName, + secretProps: { + secretName, + }, + grantWriteAccess: 'readwrite' + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: envName, + Value: { + Ref: "testconstructsecret1A43460A" + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::SecretsManager::Secret", { + Name: secretName, + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + Effect: "Allow", + Resource: { + Ref: "testconstructsecret1A43460A" + } + }, + { + Action: [ + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecret" + ], + Effect: "Allow", + Resource: { + Ref: "testconstructsecret1A43460A" + } + } + ], + Version: "2012-10-17" + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".secretsmanager" + ] + ] + } + }); + + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::SecretsManager::Secret', 1); +}); + +test('Existing service/existing secret, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingSecretObj = defaults.buildSecretsManagerSecret(stack, 'secret', { + secretName + }); + + new FargateToSecretsmanager(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + existingSecretObj + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "SECRET_ARN", + Value: { + Ref: "secret4DA88516" + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ] + }); + + expect(stack).toHaveResourceLike("AWS::SecretsManager::Secret", { + Name: secretName, + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + Effect: "Allow", + Resource: { + Ref: "secret4DA88516" + } + } + ] + } + }); + + expect(stack).toHaveResource("AWS::EC2::VPCEndpoint", { + ServiceName: { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + Ref: "AWS::Region" + }, + ".secretsmanager" + ] + ] + } + }); + + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); + expect(stack).toCountResources('AWS::SecretsManager::Secret', 1); +}); + +test('Test error invalid secret permission', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingSecretObj = buildSecretsManagerSecret(stack, 'secret', {}); + + const app = () => { + new FargateToSecretsmanager(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + grantWriteAccess: 'reed', + existingSecretObj + }); + }; + + expect(app).toThrowError('Invalid grantWriteAccess submitted - REED'); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.existing-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.existing-resources.expected.json new file mode 100644 index 000000000..899d61df3 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.existing-resources.expected.json @@ -0,0 +1,1178 @@ +{ + "Description": "Integration Test with new VPC, Service and a Secret", + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "172.168.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "172.168.0.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "172.168.32.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "172.168.64.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "172.168.96.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "172.168.128.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "172.168.160.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "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": "existing-resources/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, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/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)" + } + ] + } + } + }, + "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": "existing-resources/Vpc" + } + ] + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRAPIsecuritygroup78294485", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRDKRsecuritygroup598BA37E", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "VpcSECRETSMANAGERF52907C2": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.secretsmanager", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesSECRETSMANAGERsecuritygroup8010FC5B", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "secret4DA88516": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": {} + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W77", + "reason": "We allow the use of the AWS account default key aws/secretsmanager for secret encryption." + } + ] + } + } + }, + "existingresourcesECRAPIsecuritygroup78294485": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-ECR_API-security-group", + "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" + } + ] + } + } + }, + "existingresourcesECRDKRsecuritygroup598BA37E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-ECR_DKR-security-group", + "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" + } + ] + } + } + }, + "testclusterDF8B0D19": { + "Type": "AWS::ECS::Cluster" + }, + "testtaskdefTaskRoleB2DEF113": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testtaskdefTaskRoleDefaultPolicy5D591D1C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "secret4DA88516" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testtaskdefTaskRoleDefaultPolicy5D591D1C", + "Roles": [ + { + "Ref": "testtaskdefTaskRoleB2DEF113" + } + ] + } + }, + "testtaskdefF924AD58": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "SECRET_ARN", + "Value": { + "Ref": "secret4DA88516" + } + } + ], + "Essential": true, + "Image": "nginx", + "MemoryReservation": 512, + "Name": "test-container", + "PortMappings": [ + { + "ContainerPort": 8080, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "existingresourcestesttaskdef88B214A2", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "testtaskdefTaskRoleB2DEF113", + "Arn" + ] + } + } + }, + "testsg872EB48A": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Construct created security group", + "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" + } + ] + } + } + }, + "testserviceService2730C249": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "testclusterDF8B0D19" + }, + "DeploymentConfiguration": { + "MaximumPercent": 150, + "MinimumHealthyPercent": 75 + }, + "DesiredCount": 2, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "testsg872EB48A", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "PlatformVersion": "LATEST", + "TaskDefinition": { + "Ref": "testtaskdefF924AD58" + } + } + }, + "existingresourcesSECRETSMANAGERsecuritygroup8010FC5B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-SECRETS_MANAGER-security-group", + "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": { + "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-fargate-secretsmanager/test/integ.existing-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.existing-resources.ts new file mode 100644 index 000000000..27aea17fd --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.existing-resources.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2022 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 { Aws, App, Stack } from "@aws-cdk/core"; +import { FargateToSecretsmanager, FargateToSecretsmanagerProps } from "../lib"; +import { generateIntegStackName, getTestVpc, CreateFargateService } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as defaults from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and a Secret'; + +const existingVpc = getTestVpc(stack); +const existingSecretObj = defaults.buildSecretsManagerSecret(stack, 'secret', {}); + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const [testService, testContainer] = CreateFargateService(stack, + 'test', + existingVpc, + undefined, + undefined, + undefined, + undefined, + { image }, +); + +const constructProps: FargateToSecretsmanagerProps = { + publicApi: true, + existingVpc, + existingSecretObj, + existingContainerDefinitionObject: testContainer, + existingFargateServiceObject: testService, +}; + +new FargateToSecretsmanager(stack, 'test-construct', constructProps); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.new-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.new-resources.expected.json new file mode 100644 index 000000000..d1eeaa495 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.new-resources.expected.json @@ -0,0 +1,1188 @@ +{ + "Description": "Integration Test with new VPC, Service and a Secret", + "Resources": { + "testconstructsecret1A43460A": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": {} + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W77", + "reason": "We allow the use of the AWS account default key aws/secretsmanager for secret encryption." + } + ] + } + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.0.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.32.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.64.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.96.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.128.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.160.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "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": "new-resources/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, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/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)" + } + ] + } + } + }, + "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": "new-resources/Vpc" + } + ] + } + }, + "VpcSECRETSMANAGERF52907C2": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.secretsmanager", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesSECRETSMANAGERsecuritygroupD22DA6BC", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRAPIsecuritygroupE52BAE3F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRDKRsecuritygroupBA34F94F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "newresourcesSECRETSMANAGERsecuritygroupD22DA6BC": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-SECRETS_MANAGER-security-group", + "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" + } + ] + } + } + }, + "newresourcesECRAPIsecuritygroupE52BAE3F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_API-security-group", + "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" + } + ] + } + } + }, + "newresourcesECRDKRsecuritygroupBA34F94F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_DKR-security-group", + "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" + } + ] + } + } + }, + "testconstructcluster7B6231C5": { + "Type": "AWS::ECS::Cluster" + }, + "testconstructtaskdefTaskRoleC60414C4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testconstructtaskdefTaskRoleDefaultPolicyF34A1535": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "testconstructsecret1A43460A" + } + }, + { + "Action": [ + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "testconstructsecret1A43460A" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testconstructtaskdefTaskRoleDefaultPolicyF34A1535", + "Roles": [ + { + "Ref": "testconstructtaskdefTaskRoleC60414C4" + } + ] + } + }, + "testconstructtaskdef8BD1F9E4": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "SECRET_ARN", + "Value": { + "Ref": "testconstructsecret1A43460A" + } + } + ], + "Essential": true, + "Image": "nginx", + "MemoryReservation": 512, + "Name": "test-construct-container", + "PortMappings": [ + { + "ContainerPort": 8080, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "newresourcestestconstructtaskdefE4616A0D", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "testconstructtaskdefTaskRoleC60414C4", + "Arn" + ] + } + } + }, + "testconstructsgA602AA29": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Construct created security group", + "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" + } + ] + } + } + }, + "testconstructserviceService13074A8F": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "testconstructcluster7B6231C5" + }, + "DeploymentConfiguration": { + "MaximumPercent": 150, + "MinimumHealthyPercent": 75 + }, + "DesiredCount": 2, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "testconstructsgA602AA29", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "PlatformVersion": "LATEST", + "TaskDefinition": { + "Ref": "testconstructtaskdef8BD1F9E4" + } + } + } + }, + "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-fargate-secretsmanager/test/integ.new-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.new-resources.ts new file mode 100644 index 000000000..383020581 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-secretsmanager/test/integ.new-resources.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2022 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 { Aws, App, Stack } from "@aws-cdk/core"; +import { FargateToSecretsmanager, FargateToSecretsmanagerProps } from "../lib"; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and a Secret'; + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const testProps: FargateToSecretsmanagerProps = { + publicApi: true, + containerDefinitionProps: { + image + }, + grantWriteAccess: 'readwrite' +}; + +new FargateToSecretsmanager(stack, 'test-construct', testProps); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md index 77d71853e..bacb326c1 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-secretsmanager/README.md @@ -118,7 +118,6 @@ Out of the box implementation of the Construct without any override will set the ### Amazon SecretsManager Secret * Enable read-only access for the associated AWS Lambda Function -* Enable server-side encryption using a default KMS key for the account and region * Creates a new Secret * (default) random name * (default) random value From 14c50ae86e84b05d1395293a001c4baa5d5f9fce Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Fri, 6 May 2022 17:27:49 -0400 Subject: [PATCH 28/34] feat(aws-lambda-elasticachmemcached): New Construct (#675) * Interface Design * Initial implementation push * lint issue * cfn_nag on test resources * cfn_nag suppression * Add Python and Java min deployment * Results of self-review * Reponse to Code Review --- .../.eslintignore | 5 + .../.gitignore | 15 + .../.npmignore | 21 + .../aws-lambda-elasticachememcached/README.md | 120 ++++ .../architecture.png | Bin 0 -> 70415 bytes .../lib/index.ts | 157 +++++ .../package.json | 97 +++ .../integ.existingResources.expected.json | 622 +++++++++++++++++ .../test/integ.existingResources.ts | 58 ++ .../test/integ.newResources.expected.json | 638 ++++++++++++++++++ .../test/integ.newResources.ts | 37 + .../test/integ.withClientProps.expected.json | 638 ++++++++++++++++++ .../test/integ.withClientProps.ts | 43 ++ .../test/lambda-elasticachememcached.test.ts | 366 ++++++++++ .../test/lambda/index.js | 8 + .../@aws-solutions-constructs/core/index.ts | 2 + .../core/lib/elasticache-defaults.ts | 28 + .../core/lib/elasticache-helper.ts | 100 +++ .../core/lib/lambda-helper.ts | 10 +- .../core/lib/security-group-helper.ts | 33 + .../core/lib/utils.ts | 6 +- .../core/package.json | 1 + .../core/test/elasticache-defaults.test.ts | 35 + .../core/test/elasticache-helper.test.ts | 110 +++ .../core/test/security-group-helper.test.ts | 35 + .../core/test/test-helper.ts | 39 +- 26 files changed, 3219 insertions(+), 5 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.existingResources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.existingResources.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.newResources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.newResources.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.withClientProps.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.withClientProps.ts create mode 100755 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/lambda-elasticachememcached.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/lambda/index.js create mode 100644 source/patterns/@aws-solutions-constructs/core/lib/elasticache-defaults.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/lib/elasticache-helper.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/test/elasticache-defaults.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/test/elasticache-helper.test.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.eslintignore new file mode 100644 index 000000000..0819e2e65 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.eslintignore @@ -0,0 +1,5 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/lambda/index.js \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.gitignore b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.gitignore @@ -0,0 +1,15 @@ +lib/*.js +test/*.js +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.npmignore b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/README.md new file mode 100644 index 000000000..889dd024d --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/README.md @@ -0,0 +1,120 @@ +# aws-lambda-elasticachememcached module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +--- + + +| **Reference Documentation**:| https://docs.aws.amazon.com/solutions/latest/constructs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png) Python|`aws_solutions_constructs.aws_lambda_elasticachememcached`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-lambda-elasticachememcached`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.lambdaelasticachememcached`| + +This AWS Solutions Construct implements an AWS Lambda function connected to an Amazon Elasticache Memcached cluster. + +Here is a minimal deployable pattern definition : + +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { LambdaToElasticachememcached } from '@aws-solutions-constructs/aws-lambda-elasticachememcached'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +new LambdaToElasticachememcached(this, 'LambdaToElasticachememcachedPattern', { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`lambda`) + } +}); +``` + +Python +```python +from aws_solutions_constructs.aws_lambda_elasticachememcached import LambdaToElasticachememcached +from aws_cdk import ( + aws_lambda as _lambda, + Stack +) +from constructs import Construct + +LambdaToElasticachememcached(self, 'LambdaToCachePattern', + lambda_function_props=_lambda.FunctionProps( + code=_lambda.Code.from_asset('lambda'), + runtime=_lambda.Runtime.PYTHON_3_9, + handler='index.handler' + ) + ) +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awscdk.services.lambda.*; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awsconstructs.services.lambdaelasticachememcached.*; + +new LambdaToElasticachememcached(this, "LambdaToCachePattern", new LambdaToElasticachememcachedProps.Builder() + .lambdaFunctionProps(new FunctionProps.Builder() + .runtime(Runtime.NODEJS_14_X) + .code(Code.fromAsset("lambda")) + .handler("index.handler") + .build()) + .build()); +``` + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|existingLambdaObj?|[`lambda.Function`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.Function.html)|Existing instance of Lambda Function object, providing both this and `lambdaFunctionProps` will cause an error.| +|lambdaFunctionProps?|[`lambda.FunctionProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.FunctionProps.html)|Optional user provided props to override the default props for the Lambda function.| +|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.| +|vpcProps?|[`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ec2.VpcProps.html)|Optional user provided properties to override the default properties for the new VPC. `subnetConfiguration` is set by the pattern, so any values for those properties supplied here will be overrriden. | +| cacheEndpointEnvironmentVariableName?| string | Lambda function environment variable name for the cache Endpoint. Defaults to CACHE_ENDPOINT | +| cacheProps? | [`cache.CfnCacheClusterProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticache.CfnCacheClusterProps.html) | Optional user provided props to override the default props for the Elasticache Cluster. Providing both this and `existingCache` will cause an error. | +| existingCache? | [`cache.CfnCacheCluster`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticache.CfnCacheCluster.html#attrconfigurationendpointport) | Existing instance of Elasticache Cluster object, providing both this and `cacheProps` will cause an error. If you provide this, you must provide the associated VPC in existingVpc. | + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|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 used 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. This may be a VPC created by the pattern or the VPC supplied to the pattern constructor.| +| cache | [`cache.CfnCacheCluster`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticache.CfnCacheCluster.html#attrconfigurationendpointport) | The Elasticache Memcached cluster used by the construct. | + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### AWS Lambda Function +* Configure limited privilege access IAM role for Lambda function +* Enable reusing connections with Keep-Alive for NodeJs Lambda function +* Enable X-Ray Tracing +* Attached to self referencing security group to grant access to cache +* Set Environment Variables + * (default) CACHE_ENDPOINT + * AWS_NODEJS_CONNECTION_REUSE_ENABLED (for Node 10.x and higher functions) + +### Amazon Elasticache Memcached Cluster +* Creates multi node, cross-az cluster by default + * 2 cache nodes, type: cache.t3.medium +* Self referencing security group attached to cluster endpoint + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/architecture.png b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..bf4e806999c9e6db98c33bfd503b12bed7d9096c GIT binary patch literal 70415 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", + "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-lambda-elasticachememcached/test/integ.existingResources.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.existingResources.ts new file mode 100644 index 000000000..cb9bdd8f9 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.existingResources.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2022 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 { LambdaToElasticachememcached, LambdaToElasticachememcachedProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +// import * as ec2 from '@aws-cdk/aws-ec2'; +import { generateIntegStackName, getTestVpc, CreateTestCache, addCfnSuppressRules, buildSecurityGroup } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test with new resourcesfor aws-lambda-elasticachememcached'; + +const testVpc = getTestVpc(stack, false); + +// const testSG = new ec2.SecurityGroup(stack, 'test-sg', { +// vpc: testVpc, +// }); +// addCfnSuppressRules(testSG, [{ id: "W40", reason: "Test Resource" }]); +// addCfnSuppressRules(testSG, [{ id: "W5", reason: "Test Resource" }]); +// addCfnSuppressRules(testSG, [{ id: "W36", reason: "Test Resource" }]); +const testSG = buildSecurityGroup(stack, 'test-sg', { vpc: testVpc }, [], []); + +const testFunction = new lambda.Function(stack, 'test-function', { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + vpc: testVpc, + securityGroups: [testSG], +}); +addCfnSuppressRules(testFunction, [{ id: "W58", reason: "Test Resource" }]); +addCfnSuppressRules(testFunction, [{ id: "W92", reason: "Test Resource" }]); + +const testCache = CreateTestCache(stack, 'test-cache', testVpc); + +// Definitions +const props: LambdaToElasticachememcachedProps = { + existingVpc: testVpc, + existingLambdaObj: testFunction, + existingCache: testCache, +}; + +new LambdaToElasticachememcached(stack, 'test', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.newResources.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.newResources.expected.json new file mode 100644 index 000000000..c48755395 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.newResources.expected.json @@ -0,0 +1,638 @@ +{ + "Description": "Integration Test with new resourcesfor aws-lambda-elasticachememcached", + "Resources": { + "testtestcachesg9F6CF9E2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "newResources/test/test-cachesg", + "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" + } + ] + } + } + }, + "testtestingress291C0179": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "TCP", + "Description": "Self referencing rule to control access to Elasticache memcached cluster", + "FromPort": 11222, + "GroupId": { + "Fn::GetAtt": [ + "testtestcachesg9F6CF9E2", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "testtestcachesg9F6CF9E2", + "GroupId" + ] + }, + "ToPort": 11222 + }, + "DependsOn": [ + "testtestcachesg9F6CF9E2" + ] + }, + "testecsubnetgrouptest868C53AE": { + "Type": "AWS::ElastiCache::SubnetGroup", + "Properties": { + "Description": "Solutions Constructs generated Cache Subnet Group", + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ], + "CacheSubnetGroupName": "test-subnet-group" + } + }, + "testtestcluster57FB8D14": { + "Type": "AWS::ElastiCache::CacheCluster", + "Properties": { + "CacheNodeType": "cache.t3.medium", + "Engine": "memcached", + "NumCacheNodes": 2, + "AZMode": "cross-az", + "CacheSubnetGroupName": "test-subnet-group", + "ClusterName": "test-cdk-cluster", + "Port": 11222, + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testtestcachesg9F6CF9E2", + "GroupId" + ] + } + ] + }, + "DependsOn": [ + "testecsubnetgrouptest868C53AE" + ] + }, + "testLambdaFunctionServiceRoleA03EDA2B": { + "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" + } + ] + } + }, + "testLambdaFunctionServiceRoleDefaultPolicy4F560EE3": { + "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": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testLambdaFunctionServiceRoleDefaultPolicy4F560EE3", + "Roles": [ + { + "Ref": "testLambdaFunctionServiceRoleA03EDA2B" + } + ] + }, + "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." + } + ] + } + } + }, + "testReplaceDefaultSecurityGroupsecuritygroupAC4F969B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "newResources/test/ReplaceDefaultSecurityGroup-security-group", + "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" + } + ] + } + } + }, + "testLambdaFunction1BF7CD84": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "c1b23d6af38c04acb744bda25a3dc7f4394daea942c67eaff40911a707a3c37a.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testLambdaFunctionServiceRoleA03EDA2B", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "CACHE_ENDPOINT": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testtestcluster57FB8D14", + "ConfigurationEndpoint.Address" + ] + }, + ":", + { + "Fn::GetAtt": [ + "testtestcluster57FB8D14", + "ConfigurationEndpoint.Port" + ] + } + ] + ] + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testtestcachesg9F6CF9E2", + "GroupId" + ] + }, + { + "Fn::GetAtt": [ + "testReplaceDefaultSecurityGroupsecuritygroupAC4F969B", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }, + "DependsOn": [ + "testLambdaFunctionServiceRoleDefaultPolicy4F560EE3", + "testLambdaFunctionServiceRoleA03EDA2B" + ], + "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" + } + ] + } + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "newResources/Vpc" + } + ] + } + }, + "VpcisolatedSubnet1SubnetE62B1B9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "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": "newResources/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableE442650B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "newResources/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableAssociationD259E31A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + } + }, + "VpcisolatedSubnet2Subnet39217055": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "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": "newResources/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTable334F9764": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "newResources/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTableAssociation25A4716F": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet2RouteTable334F9764" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + } + }, + "VpcisolatedSubnet3Subnet44F2537D": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "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": "newResources/Vpc/isolatedSubnet3" + } + ] + } + }, + "VpcisolatedSubnet3RouteTableA2F6BBC0": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "newResources/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": "newResources/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, + "Tags": [ + { + "Key": "Name", + "Value": "newResources/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)" + } + ] + } + } + }, + "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": "newResources/Vpc" + } + ] + } + } + }, + "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-lambda-elasticachememcached/test/integ.newResources.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.newResources.ts new file mode 100644 index 000000000..f452bf7d0 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.newResources.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2022 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 { LambdaToElasticachememcached, LambdaToElasticachememcachedProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test with new resourcesfor aws-lambda-elasticachememcached'; + +// Definitions +const props: LambdaToElasticachememcachedProps = { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`${__dirname}/lambda`) + } +}; + +new LambdaToElasticachememcached(stack, 'test', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.withClientProps.expected.json b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.withClientProps.expected.json new file mode 100644 index 000000000..e25b5ec4d --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.withClientProps.expected.json @@ -0,0 +1,638 @@ +{ + "Description": "Integration Test with new resourcesfor aws-lambda-elasticachememcached", + "Resources": { + "testtestcachesg9F6CF9E2": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "withClientProps/test/test-cachesg", + "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" + } + ] + } + } + }, + "testtestingress291C0179": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "TCP", + "Description": "Self referencing rule to control access to Elasticache memcached cluster", + "FromPort": 11222, + "GroupId": { + "Fn::GetAtt": [ + "testtestcachesg9F6CF9E2", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "testtestcachesg9F6CF9E2", + "GroupId" + ] + }, + "ToPort": 11222 + }, + "DependsOn": [ + "testtestcachesg9F6CF9E2" + ] + }, + "testecsubnetgrouptest868C53AE": { + "Type": "AWS::ElastiCache::SubnetGroup", + "Properties": { + "Description": "Solutions Constructs generated Cache Subnet Group", + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ], + "CacheSubnetGroupName": "test-subnet-group" + } + }, + "testtestcluster57FB8D14": { + "Type": "AWS::ElastiCache::CacheCluster", + "Properties": { + "CacheNodeType": "cache.t3.medium", + "Engine": "memcached", + "NumCacheNodes": 2, + "AZMode": "single-az", + "CacheSubnetGroupName": "test-subnet-group", + "ClusterName": "test-cdk-cluster", + "Port": 11222, + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testtestcachesg9F6CF9E2", + "GroupId" + ] + } + ] + }, + "DependsOn": [ + "testecsubnetgrouptest868C53AE" + ] + }, + "testLambdaFunctionServiceRoleA03EDA2B": { + "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" + } + ] + } + }, + "testLambdaFunctionServiceRoleDefaultPolicy4F560EE3": { + "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": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testLambdaFunctionServiceRoleDefaultPolicy4F560EE3", + "Roles": [ + { + "Ref": "testLambdaFunctionServiceRoleA03EDA2B" + } + ] + }, + "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." + } + ] + } + } + }, + "testReplaceDefaultSecurityGroupsecuritygroupAC4F969B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "withClientProps/test/ReplaceDefaultSecurityGroup-security-group", + "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" + } + ] + } + } + }, + "testLambdaFunction1BF7CD84": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "c1b23d6af38c04acb744bda25a3dc7f4394daea942c67eaff40911a707a3c37a.zip" + }, + "Role": { + "Fn::GetAtt": [ + "testLambdaFunctionServiceRoleA03EDA2B", + "Arn" + ] + }, + "Environment": { + "Variables": { + "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", + "CACHE_ENDPOINT": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testtestcluster57FB8D14", + "ConfigurationEndpoint.Address" + ] + }, + ":", + { + "Fn::GetAtt": [ + "testtestcluster57FB8D14", + "ConfigurationEndpoint.Port" + ] + } + ] + ] + } + } + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x", + "TracingConfig": { + "Mode": "Active" + }, + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testtestcachesg9F6CF9E2", + "GroupId" + ] + }, + { + "Fn::GetAtt": [ + "testReplaceDefaultSecurityGroupsecuritygroupAC4F969B", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + }, + { + "Ref": "VpcisolatedSubnet2Subnet39217055" + }, + { + "Ref": "VpcisolatedSubnet3Subnet44F2537D" + } + ] + } + }, + "DependsOn": [ + "testLambdaFunctionServiceRoleDefaultPolicy4F560EE3", + "testLambdaFunctionServiceRoleA03EDA2B" + ], + "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" + } + ] + } + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "192.68.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "withClientProps/Vpc" + } + ] + } + }, + "VpcisolatedSubnet1SubnetE62B1B9B": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "192.68.0.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "withClientProps/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableE442650B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "withClientProps/Vpc/isolatedSubnet1" + } + ] + } + }, + "VpcisolatedSubnet1RouteTableAssociationD259E31A": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet1RouteTableE442650B" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet1SubnetE62B1B9B" + } + } + }, + "VpcisolatedSubnet2Subnet39217055": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "192.68.64.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "withClientProps/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTable334F9764": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "withClientProps/Vpc/isolatedSubnet2" + } + ] + } + }, + "VpcisolatedSubnet2RouteTableAssociation25A4716F": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcisolatedSubnet2RouteTable334F9764" + }, + "SubnetId": { + "Ref": "VpcisolatedSubnet2Subnet39217055" + } + } + }, + "VpcisolatedSubnet3Subnet44F2537D": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "192.68.128.0/18", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "isolated" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Isolated" + }, + { + "Key": "Name", + "Value": "withClientProps/Vpc/isolatedSubnet3" + } + ] + } + }, + "VpcisolatedSubnet3RouteTableA2F6BBC0": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "withClientProps/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": "withClientProps/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, + "Tags": [ + { + "Key": "Name", + "Value": "withClientProps/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)" + } + ] + } + } + }, + "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": "withClientProps/Vpc" + } + ] + } + } + }, + "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-lambda-elasticachememcached/test/integ.withClientProps.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.withClientProps.ts new file mode 100644 index 000000000..82269d117 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/integ.withClientProps.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2022 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 { LambdaToElasticachememcached, LambdaToElasticachememcachedProps } from "../lib"; +import * as lambda from '@aws-cdk/aws-lambda'; +import { generateIntegStackName } from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename)); +stack.templateOptions.description = 'Integration Test with new resourcesfor aws-lambda-elasticachememcached'; + +// Definitions +const props: LambdaToElasticachememcachedProps = { + lambdaFunctionProps: { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(`${__dirname}/lambda`) + }, + cacheProps: { + azMode: "single-az" + }, + vpcProps: { + cidr: '192.68.0.0/16' + } +}; + +new LambdaToElasticachememcached(stack, 'test', props); + +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/lambda-elasticachememcached.test.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/lambda-elasticachememcached.test.ts new file mode 100755 index 000000000..5f8a95f22 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/lambda-elasticachememcached.test.ts @@ -0,0 +1,366 @@ +/** + * Copyright 2022 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 { expect as expectCDK, haveResource } from '@aws-cdk/assert'; +// import { LambdaToElasticachememcached, LambdaToElasticachememcachedProps } from "../lib"; +// import * as lambda from '@aws-cdk/aws-lambda'; +// import * as cdk from "@aws-cdk/core"; +import "@aws-cdk/assert/jest"; +import * as defaults from "@aws-solutions-constructs/core"; +import * as cdk from "@aws-cdk/core"; +import * as lambda from "@aws-cdk/aws-lambda"; +import { LambdaToElasticachememcached } from "../lib"; + +const testPort = 12321; +const testFunctionName = "something-unique"; +const testClusterName = "something-else"; + +test("When provided a VPC, does not create a second VPC", () => { + const stack = new cdk.Stack(); + + const existingVpc = defaults.getTestVpc(stack); + new LambdaToElasticachememcached(stack, "testStack", { + existingVpc, + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + }); + + expect(stack).toCountResources("AWS::EC2::VPC", 1); +}); + +test("When provided an existingCache, does not create a second cache", () => { + const stack = new cdk.Stack(); + + const existingVpc = defaults.getTestVpc(stack); + const existingCache = defaults.CreateTestCache(stack, "test-cache", existingVpc, testPort); + + new LambdaToElasticachememcached(stack, "testStack", { + existingVpc, + existingCache, + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + }); + + expect(stack).toCountResources("AWS::ElastiCache::CacheCluster", 1); + expect(stack).toHaveResourceLike("AWS::ElastiCache::CacheCluster", { + Port: testPort, + }); +}); + +test("When provided an existingFunction, does not create a second function", () => { + const stack = new cdk.Stack(); + + const existingVpc = defaults.getTestVpc(stack); + const existingFunction = new lambda.Function(stack, "test-function", { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + vpc: existingVpc, + functionName: testFunctionName, + }); + + new LambdaToElasticachememcached(stack, "testStack", { + existingVpc, + existingLambdaObj: existingFunction, + }); + + expect(stack).toCountResources("AWS::Lambda::Function", 1); + expect(stack).toHaveResourceLike("AWS::Lambda::Function", { + FunctionName: testFunctionName, + }); +}); + +test("Test custom environment variable name", () => { + const stack = new cdk.Stack(); + + const testEnvironmentVariableName = "CUSTOM_CLUSTER_NAME"; + + new LambdaToElasticachememcached(stack, "test-construct", { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + cacheEndpointEnvironmentVariableName: testEnvironmentVariableName, + }); + + expect(stack).toHaveResource("AWS::Lambda::Function", { + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", + CUSTOM_CLUSTER_NAME: { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testconstructtestconstructclusterCF9DF48A", + "ConfigurationEndpoint.Address", + ], + }, + ":", + { + "Fn::GetAtt": [ + "testconstructtestconstructclusterCF9DF48A", + "ConfigurationEndpoint.Port", + ], + }, + ], + ], + }, + }, + }, + }); +}); + +test("Test setting custom function properties", () => { + const stack = new cdk.Stack(); + + new LambdaToElasticachememcached(stack, "test-cache", { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + functionName: testFunctionName, + }, + }); + + expect(stack).toHaveResourceLike("AWS::Lambda::Function", { + FunctionName: testFunctionName, + }); +}); + +test("Test setting custom cache properties", () => { + const stack = new cdk.Stack(); + + new LambdaToElasticachememcached(stack, "test-cache", { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + cacheProps: { + clusterName: testClusterName, + }, + }); + + expect(stack).toHaveResourceLike("AWS::ElastiCache::CacheCluster", { + ClusterName: testClusterName, + }); +}); +test("Test setting custom VPC properties", () => { + const stack = new cdk.Stack(); + const testCidrBlock = "192.168.0.0/16"; + + new LambdaToElasticachememcached(stack, "test-cache", { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + vpcProps: { + cidr: testCidrBlock, + }, + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: testCidrBlock, + }); +}); +test("Test all default values", () => { + const stack = new cdk.Stack(); + + new LambdaToElasticachememcached(stack, "test-cache", { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + }); + + expect(stack).toCountResources("AWS::Lambda::Function", 1); + expect(stack).toCountResources("AWS::ElastiCache::CacheCluster", 1); + expect(stack).toCountResources("AWS::EC2::VPC", 1); + + expect(stack).toHaveResourceLike("AWS::Lambda::Function", { + Environment: { + Variables: { + AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1", + CACHE_ENDPOINT: { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "testcachetestcachecluster27D08FAD", + "ConfigurationEndpoint.Address", + ], + }, + ":", + { + "Fn::GetAtt": [ + "testcachetestcachecluster27D08FAD", + "ConfigurationEndpoint.Port", + ], + }, + ], + ], + }, + }, + }, + Handler: ".handler", + Runtime: "nodejs14.x", + }); + + // All values taken from elasticache-defaults.ts + expect(stack).toHaveResourceLike("AWS::ElastiCache::CacheCluster", { + CacheNodeType: "cache.t3.medium", + Engine: "memcached", + NumCacheNodes: 2, + Port: 11222, + AZMode: "cross-az", + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + EnableDnsHostnames: true, + EnableDnsSupport: true, + }); +}); + +test('Test for the proper self referencing security group', () => { + const stack = new cdk.Stack(); + + new LambdaToElasticachememcached(stack, "test-cache", { + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + cacheProps: { + port: 22223 + } + }); + + expect(stack).toHaveResourceLike("AWS::EC2::SecurityGroupIngress", { + IpProtocol: "TCP", + FromPort: 22223, + ToPort: 22223, + GroupId: { + "Fn::GetAtt": [ + "testcachetestcachecachesg74A03DA4", + "GroupId" + ] + }, + SourceSecurityGroupId: { + "Fn::GetAtt": [ + "testcachetestcachecachesg74A03DA4", + "GroupId" + ] + }, + }); +}); +// test('', () => {}); +test("Test error from existingCache and no VPC", () => { + const stack = new cdk.Stack(); + + const existingVpc = defaults.getTestVpc(stack); + const existingCache = defaults.CreateTestCache(stack, "test-cache", existingVpc); + + const app = () => { + new LambdaToElasticachememcached(stack, "testStack", { + existingCache, + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + }); + }; + + expect(app).toThrowError( + "If providing an existing Cache or Lambda Function, you must also supply the associated existingVpc" + ); +}); + +test("Test error from existing function and no VPC", () => { + const stack = new cdk.Stack(); + + const existingVpc = defaults.getTestVpc(stack); + const existingFunction = new lambda.Function(stack, "test-function", { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + vpc: existingVpc, + }); + + const app = () => { + new LambdaToElasticachememcached(stack, "testStack", { + existingLambdaObj: existingFunction, + }); + }; + + expect(app).toThrowError( + "If providing an existing Cache or Lambda Function, you must also supply the associated existingVpc" + ); +}); + +test("Test error from existingCache and cacheProps", () => { + const stack = new cdk.Stack(); + + const existingVpc = defaults.getTestVpc(stack); + const existingCache = defaults.CreateTestCache(stack, "test-cache", existingVpc); + + const app = () => { + new LambdaToElasticachememcached(stack, "testStack", { + existingCache, + existingVpc, + cacheProps: { + numCacheNodes: 4, + }, + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + }); + }; + + expect(app).toThrowError("Cannot specify existingCache and cacheProps"); +}); + +test("Test error from trying to launch Redis", () => { + const stack = new cdk.Stack(); + + const app = () => { + new LambdaToElasticachememcached(stack, "testStack", { + cacheProps: { + numCacheNodes: 4, + engine: "redis", + }, + lambdaFunctionProps: { + code: lambda.Code.fromAsset(`${__dirname}/lambda`), + runtime: lambda.Runtime.NODEJS_14_X, + handler: ".handler", + }, + }); + }; + + expect(app).toThrowError("This construct can only launch memcached clusters"); +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/lambda/index.js b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/lambda/index.js new file mode 100644 index 000000000..93b955782 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-elasticachememcached/test/lambda/index.js @@ -0,0 +1,8 @@ +exports.handler = async function (event) { + console.log(`request:${JSON.stringify(event, undefined, 2)}`); + return { + statusCode: 200, + headers: { "Content-Type": "text/plain" }, + body: `Hello, AWS Solutions Constructs! You've hit ${event.path}`, + }; +}; diff --git a/source/patterns/@aws-solutions-constructs/core/index.ts b/source/patterns/@aws-solutions-constructs/core/index.ts index 41602954a..5e1aee4e8 100644 --- a/source/patterns/@aws-solutions-constructs/core/index.ts +++ b/source/patterns/@aws-solutions-constructs/core/index.ts @@ -16,6 +16,8 @@ export * from './lib/alb-helper'; export * from './lib/apigateway-defaults'; export * from './lib/apigateway-helper'; export * from './lib/dynamodb-table-defaults'; +export * from './lib/elasticache-defaults'; +export * from './lib/elasticache-helper'; export * from './lib/fargate-defaults'; export * from './lib/fargate-helper'; export * from './lib/iot-topic-rule-defaults'; diff --git a/source/patterns/@aws-solutions-constructs/core/lib/elasticache-defaults.ts b/source/patterns/@aws-solutions-constructs/core/lib/elasticache-defaults.ts new file mode 100644 index 000000000..c5282665b --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/lib/elasticache-defaults.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2022 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. + */ + +export function GetDefaultCachePort() { + // Best practice not to use default port 11211 + return 11222; +} + +export function GetMemcachedDefaults(id: string, port: number) { + return { + clusterName: `${id}-cdk-cluster`, + cacheNodeType: "cache.t3.medium", + engine: "memcached", + numCacheNodes: 2, + port, + azMode: 'cross-az' + }; +} diff --git a/source/patterns/@aws-solutions-constructs/core/lib/elasticache-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/elasticache-helper.ts new file mode 100644 index 000000000..fbcd82364 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/lib/elasticache-helper.ts @@ -0,0 +1,100 @@ +/** + * Copyright 2022 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. + */ + +import * as ec2 from "@aws-cdk/aws-ec2"; +import * as cache from "@aws-cdk/aws-elasticache"; +import { Construct } from "@aws-cdk/core"; +import { GetDefaultCachePort, GetMemcachedDefaults } from './elasticache-defaults'; +import { consolidateProps } from './utils'; + +export interface ObtainMemcachedClusterProps { + readonly cachePort?: any, + readonly cacheSecurityGroupId: string, + readonly cacheProps?: cache.CfnCacheClusterProps | any, + readonly existingCache?: cache.CfnCacheCluster, + readonly vpc?: ec2.IVpc, +} + +export function obtainMemcachedCluster( + scope: Construct, + id: string, + props: ObtainMemcachedClusterProps +) { + + if (props.existingCache) { + props.existingCache.vpcSecurityGroupIds?.push(props.cacheSecurityGroupId); + return props.existingCache; + } else { + if (!props.cachePort) { + throw Error('props.cachePort required for new caches'); + } + + // Create the subnet group from all the isolated subnets in the VPC + const subnetGroup = createCacheSubnetGroup(scope, props.vpc!, id); + + const defaultProps = GetMemcachedDefaults(id, props.cachePort); + const requiredConstructProps = { + vpcSecurityGroupIds: [props.cacheSecurityGroupId], + cacheSubnetGroupName: subnetGroup.cacheSubnetGroupName, + }; + const consolidatedProps = consolidateProps( + defaultProps, + props.cacheProps, + requiredConstructProps, + true + ); + + const newCache = new cache.CfnCacheCluster( + scope, + `${id}-cluster`, + consolidatedProps + ); + newCache.addDependsOn(subnetGroup); + return newCache; + } + +} + +export function createCacheSubnetGroup( + construct: Construct, + vpc: ec2.IVpc, + id: string +): cache.CfnSubnetGroup { + + // Memcached has no auth, all access control is + // network based, so, at least initially, we will + // only launch it in isolated subnets. + const subnetIds: string[] = []; + vpc.isolatedSubnets.forEach((subnet) => { + subnetIds.push(subnet.subnetId); + }); + + return new cache.CfnSubnetGroup(construct, `ec-subnetgroup-${id}`, { + description: "Solutions Constructs generated Cache Subnet Group", + subnetIds, + cacheSubnetGroupName: `${id}-subnet-group`, + }); +} + +export function getCachePort( + clientCacheProps?: cache.CfnCacheClusterProps | any, + existingCache?: cache.CfnCacheCluster +): any { + if (existingCache) { + return existingCache.attrConfigurationEndpointPort!; + } else if (clientCacheProps?.port) { + return clientCacheProps.port; + } else { + return GetDefaultCachePort(); + } +} diff --git a/source/patterns/@aws-solutions-constructs/core/lib/lambda-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/lambda-helper.ts index 4403a6979..272cb2545 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/lambda-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/lambda-helper.ts @@ -53,6 +53,14 @@ export function buildLambdaFunction(scope: Construct, props: BuildLambdaFunction } } else { if (props.vpc) { + const levelOneFunction: lambda.CfnFunction = props.existingLambdaObj.node.defaultChild as lambda.CfnFunction; + if (props.lambdaFunctionProps?.securityGroups) { + let ctr = 20; + props.lambdaFunctionProps?.securityGroups.forEach(sg => { + // TODO: Discuss with someone why I can't get R/O access to VpcConfigSecurityGroupIds + levelOneFunction.addOverride(`Properties.VpcConfig.SecurityGroupIds.${ctr++}`, sg.securityGroupId); + }); + } if (!props.existingLambdaObj.isBoundToVpc) { throw Error('A Lambda function must be bound to a VPC upon creation, it cannot be added to a VPC in a subsequent construct'); } @@ -128,7 +136,7 @@ export function deployLambdaFunction(scope: Construct, finalLambdaFunctionProps = overrideProps(finalLambdaFunctionProps, { securityGroups: [ lambdaSecurityGroup ], vpc, - }); + }, true); } const lambdafunction = new lambda.Function(scope, _functionId, finalLambdaFunctionProps); diff --git a/source/patterns/@aws-solutions-constructs/core/lib/security-group-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/security-group-helper.ts index 7ad7ca8e6..592019eb6 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/security-group-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/security-group-helper.ts @@ -55,3 +55,36 @@ export function buildSecurityGroup( return newSecurityGroup; } + +export function CreateSelfReferencingSecurityGroup(scope: Construct, id: string, vpc: ec2.IVpc, cachePort: any) { + const newCacheSG = new ec2.SecurityGroup(scope, `${id}-cachesg`, { + vpc, + allowAllOutbound: true, + }); + const selfReferenceRule = new ec2.CfnSecurityGroupIngress( + scope, + `${id}-ingress`, + { + groupId: newCacheSG.securityGroupId, + sourceSecurityGroupId: newCacheSG.securityGroupId, + ipProtocol: "TCP", + fromPort: cachePort, + toPort: cachePort, + description: 'Self referencing rule to control access to Elasticache memcached cluster', + } + ); + selfReferenceRule.node.addDependency(newCacheSG); + + addCfnSuppressRules(newCacheSG, [ + { + 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", + }, + ]); + return newCacheSG; +} diff --git a/source/patterns/@aws-solutions-constructs/core/lib/utils.ts b/source/patterns/@aws-solutions-constructs/core/lib/utils.ts index 7cfe7da7b..b84cf6b37 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/utils.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/utils.ts @@ -164,15 +164,15 @@ export function addCfnSuppressRules(resource: cdk.Resource | cdk.CfnResource, ru * 2) clientProps value * 3) defaultProps value */ -export function consolidateProps(defaultProps: object, clientProps?: object, constructProps?: object): any { +export function consolidateProps(defaultProps: object, clientProps?: object, constructProps?: object, concatArray: boolean = false): any { let result: object = defaultProps; if (clientProps) { - result = overrideProps(result, clientProps); + result = overrideProps(result, clientProps, concatArray); } if (constructProps) { - result = overrideProps(result, constructProps); + result = overrideProps(result, constructProps, concatArray); } return result; diff --git a/source/patterns/@aws-solutions-constructs/core/package.json b/source/patterns/@aws-solutions-constructs/core/package.json index c9c0e5179..55c416e5e 100644 --- a/source/patterns/@aws-solutions-constructs/core/package.json +++ b/source/patterns/@aws-solutions-constructs/core/package.json @@ -55,6 +55,7 @@ "@aws-cdk/aws-cloudfront": "0.0.0", "@aws-cdk/aws-cloudfront-origins": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-elasticache": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2-targets": "0.0.0", "@aws-cdk/aws-glue": "0.0.0", diff --git a/source/patterns/@aws-solutions-constructs/core/test/elasticache-defaults.test.ts b/source/patterns/@aws-solutions-constructs/core/test/elasticache-defaults.test.ts new file mode 100644 index 000000000..bf8d12cfa --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/test/elasticache-defaults.test.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2022 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. + */ + +import "@aws-cdk/assert/jest"; +import { GetDefaultCachePort, GetMemcachedDefaults } from "../lib/elasticache-defaults"; + +test("Test GetDefaultCachePort()", () => { + const defaultPort = GetDefaultCachePort(); + + expect(defaultPort).toEqual(11222); +}); + +test("Test GetMemcachedDefaults()", () => { + const testPort = 22222; + const testId = 'test'; + + const props = GetMemcachedDefaults(testId, testPort); + + expect(props.port).toEqual(testPort); + expect(props.clusterName).toEqual(`${testId}-cdk-cluster`); + expect(props.engine).toEqual("memcached"); + expect(props.cacheNodeType).toEqual("cache.t3.medium"); + expect(props.numCacheNodes).toEqual(2); + expect(props.azMode).toEqual('cross-az'); +}); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/test/elasticache-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/elasticache-helper.test.ts new file mode 100644 index 000000000..e00059ecd --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/core/test/elasticache-helper.test.ts @@ -0,0 +1,110 @@ +/** + * Copyright 2022 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. + */ + +import "@aws-cdk/assert/jest"; +import { CreateTestCache, getTestVpc } from "./test-helper"; +import * as cdk from '@aws-cdk/core'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import { getCachePort, obtainMemcachedCluster } from "../lib/elasticache-helper"; +import { GetDefaultCachePort } from "../lib/elasticache-defaults"; + +test("Test returning existing Cache", () => { + const stack = new cdk.Stack(); + + const testVpc = getTestVpc(stack, false); + const existingCache = CreateTestCache(stack, 'test', testVpc); + + const securityGroup = new ec2.SecurityGroup(stack, 'test-sg', { + vpc: testVpc + }); + const obtainedCache = obtainMemcachedCluster(stack, 'test-cache', { + existingCache, + cacheSecurityGroupId: securityGroup.securityGroupId + }); + + expect(obtainedCache).toBe(existingCache); +}); + +test("Test create cache with no client props", () => { + const stack = new cdk.Stack(); + + const testVpc = getTestVpc(stack, false); + + const securityGroup = new ec2.SecurityGroup(stack, 'test-sg', { + vpc: testVpc + }); + obtainMemcachedCluster(stack, 'test-cache', { + vpc: testVpc, + cacheSecurityGroupId: securityGroup.securityGroupId, + cachePort: 11111, + }); + + expect(stack).toHaveResourceLike("AWS::ElastiCache::CacheCluster", { + Port: 11111, + AZMode: 'cross-az', + Engine: 'memcached', + }); +}); + +test("Test create cache with client props", () => { + const stack = new cdk.Stack(); + + const testVpc = getTestVpc(stack, false); + + const securityGroup = new ec2.SecurityGroup(stack, 'test-sg', { + vpc: testVpc + }); + obtainMemcachedCluster(stack, 'test-cache', { + vpc: testVpc, + cacheSecurityGroupId: securityGroup.securityGroupId, + cachePort: 12321, + cacheProps: { + azMode: 'single-az', + clusterName: 'test-name' + } + }); + + expect(stack).toHaveResourceLike("AWS::ElastiCache::CacheCluster", { + Port: 12321, + AZMode: 'single-az', + Engine: 'memcached', + ClusterName: 'test-name' + }); +}); + +test("Test GetCachePort() with existing cache", () => { + + const stack = new cdk.Stack(); + + const testVpc = getTestVpc(stack, false); + const existingCache = CreateTestCache(stack, 'test', testVpc, 32123); + + const port = getCachePort(undefined, existingCache); + + // Since the port from the existing cache is a token, + // we can't check it directly, but we can ensure + // the default port was replaced + expect(port).not.toEqual(GetDefaultCachePort()); +}); + +test("Test GetCachePort() with clientCacheProps", () => { + const clientPort = 32123; + + const port = getCachePort({ port: clientPort }); + expect(port).toEqual(clientPort); +}); +test("Test GetCachePort() with default port", () => { + + const port = getCachePort(); + expect(port).toEqual(GetDefaultCachePort()); +}); diff --git a/source/patterns/@aws-solutions-constructs/core/test/security-group-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/security-group-helper.test.ts index e1472bc3f..139e9b2b2 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/security-group-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/security-group-helper.test.ts @@ -119,3 +119,38 @@ test("Test deployment with egress rule", () => { ], }); }); + +test("Test self referencing security group", () => { + const testPort = 33333; + // Stack + const stack = new Stack(); + + const vpc = new ec2.Vpc(stack, "test-vpc", {}); + + // Helper declaration + defaults.CreateSelfReferencingSecurityGroup( + stack, + "testsg", + vpc, + testPort, + ); + + expect(stack).toHaveResourceLike("AWS::EC2::SecurityGroupIngress", { + IpProtocol: "TCP", + FromPort: testPort, + ToPort: testPort, + GroupId: { + "Fn::GetAtt": [ + "testsgcachesg72A723EA", + "GroupId" + ] + }, + SourceSecurityGroupId: { + "Fn::GetAtt": [ + "testsgcachesg72A723EA", + "GroupId" + ] + }, + }); + +}); diff --git a/source/patterns/@aws-solutions-constructs/core/test/test-helper.ts b/source/patterns/@aws-solutions-constructs/core/test/test-helper.ts index c4ed8f74f..189e58dd4 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/test-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/test-helper.ts @@ -17,9 +17,13 @@ import { Construct, RemovalPolicy, Stack } from "@aws-cdk/core"; import { buildVpc } from '../lib/vpc-helper'; import { DefaultPublicPrivateVpcProps, DefaultIsolatedVpcProps } from '../lib/vpc-defaults'; import { overrideProps, addCfnSuppressRules } from "../lib/utils"; +import { createCacheSubnetGroup } from "../lib/elasticache-helper"; import * as path from 'path'; +import * as cache from '@aws-cdk/aws-elasticache'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as acm from '@aws-cdk/aws-certificatemanager'; import { CfnFunction } from "@aws-cdk/aws-lambda"; +import { GetDefaultCachePort } from "../lib/elasticache-defaults"; export const fakeEcrRepoArn = 'arn:aws:ecr:us-east-1:123456789012:repository/fake-repo'; @@ -106,4 +110,37 @@ export function suppressAutoDeleteHandlerWarnings(stack: Stack) { } }); -} \ No newline at end of file +} + +export function CreateTestCache(scope: Construct, id: string, vpc: ec2.IVpc, port?: number) { + const cachePort = port ?? GetDefaultCachePort(); + + // Create the subnet group from all the isolated subnets in the VPC + const subnetGroup = createCacheSubnetGroup(scope, vpc, id); + const emptySG = new ec2.SecurityGroup(scope, `${id}-cachesg`, { + vpc, + allowAllOutbound: true, + }); + addCfnSuppressRules(emptySG, [{ id: "W40", reason: "Test Resource" }]); + addCfnSuppressRules(emptySG, [{ id: "W5", reason: "Test Resource" }]); + addCfnSuppressRules(emptySG, [{ id: "W36", reason: "Test Resource" }]); + + const cacheProps = { + clusterName: `${id}-cdk-cluster`, + cacheNodeType: "cache.t3.medium", + engine: "memcached", + numCacheNodes: 2, + port: cachePort, + azMode: "cross-az", + vpcSecurityGroupIds: [emptySG.securityGroupId], + cacheSubnetGroupName: subnetGroup.cacheSubnetGroupName, + }; + + const newCache = new cache.CfnCacheCluster( + scope, + `${id}-cluster`, + cacheProps + ); + newCache.addDependsOn(subnetGroup); + return newCache; +} From ddf9d1efe05eb6dde00f8ce1f979873aed5b61d1 Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Automation Date: Sat, 7 May 2022 13:06:08 +0000 Subject: [PATCH 29/34] chore(release): 1.153.0 --- CHANGELOG.md | 15 +++++++++++++++ source/lerna.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de95a4e3..4d57268da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ 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.153.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.152.0...v1.153.0) (2022-05-07) + + +### Features + +* **aws-fargate-secretsmanager:** Create new construct ([#670](https://github.com/awslabs/aws-solutions-constructs/issues/670)) ([cd218b6](https://github.com/awslabs/aws-solutions-constructs/commit/cd218b6900a174afa09c86f28fb0650ecfe37942)) +* **aws-fargate-ssmstringparameter:** New Construct ([#653](https://github.com/awslabs/aws-solutions-constructs/issues/653)) ([bcb7c63](https://github.com/awslabs/aws-solutions-constructs/commit/bcb7c6351ffa9b8ef5f5e7790522c5b1fe87dd9a)) +* **aws-lambda-elasticachmemcached:** New Construct ([#675](https://github.com/awslabs/aws-solutions-constructs/issues/675)) ([14c50ae](https://github.com/awslabs/aws-solutions-constructs/commit/14c50ae86e84b05d1395293a001c4baa5d5f9fce)) +* **aws-s3-stepfunctions:** Changed escape hatch to eventBridgeEnabled prop ([#666](https://github.com/awslabs/aws-solutions-constructs/issues/666)) ([bc2f733](https://github.com/awslabs/aws-solutions-constructs/commit/bc2f733879a5363407729e1f236302c9361ff652)) + + +### Bug Fixes + +* **aws-lambda-secretsmanager:** Update docs ([#673](https://github.com/awslabs/aws-solutions-constructs/issues/673)) ([1b843bf](https://github.com/awslabs/aws-solutions-constructs/commit/1b843bff718dd05376f4f72ff9075db123e05288)) + ## [1.152.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.151.0...v1.152.0) (2022-04-10) * Upgraded all patterns to CDK v1.152.0 diff --git a/source/lerna.json b/source/lerna.json index d2d706160..8bedd9003 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "1.152.0" + "version": "1.153.0" } From 4e2fa51c2f69dd23ad1b10046b942f17fd454fae Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 7 May 2022 09:14:03 -0400 Subject: [PATCH 30/34] chore(changelog): Updated CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d57268da..ef5d72f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. See [standa ## [1.153.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.152.0...v1.153.0) (2022-05-07) +* Upgraded all patterns to CDK v1.153.0 ### Features @@ -12,10 +13,10 @@ All notable changes to this project will be documented in this file. See [standa * **aws-lambda-elasticachmemcached:** New Construct ([#675](https://github.com/awslabs/aws-solutions-constructs/issues/675)) ([14c50ae](https://github.com/awslabs/aws-solutions-constructs/commit/14c50ae86e84b05d1395293a001c4baa5d5f9fce)) * **aws-s3-stepfunctions:** Changed escape hatch to eventBridgeEnabled prop ([#666](https://github.com/awslabs/aws-solutions-constructs/issues/666)) ([bc2f733](https://github.com/awslabs/aws-solutions-constructs/commit/bc2f733879a5363407729e1f236302c9361ff652)) - ### Bug Fixes * **aws-lambda-secretsmanager:** Update docs ([#673](https://github.com/awslabs/aws-solutions-constructs/issues/673)) ([1b843bf](https://github.com/awslabs/aws-solutions-constructs/commit/1b843bff718dd05376f4f72ff9075db123e05288)) +* All Kinesis Streams constructs - update CloudWatch alarm threshold to the documented 12 hours ([#673](https://github.com/awslabs/aws-solutions-constructs/issues/663)) ## [1.152.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.151.0...v1.152.0) (2022-04-10) From 61ccb19a556864c4fa4e9e7e8b165ffce8cceecd Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Automation Date: Sat, 7 May 2022 17:04:01 +0000 Subject: [PATCH 31/34] chore(release): 1.153.1 --- CHANGELOG.md | 2 ++ source/lerna.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef5d72f59..1c2d397dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ 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.153.1](https://github.com/awslabs/aws-solutions-constructs/compare/v1.153.0...v1.153.1) (2022-05-07) + ## [1.153.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.152.0...v1.153.0) (2022-05-07) * Upgraded all patterns to CDK v1.153.0 diff --git a/source/lerna.json b/source/lerna.json index 8bedd9003..0fd69395d 100644 --- a/source/lerna.json +++ b/source/lerna.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "1.153.0" + "version": "1.153.1" } From 4c66a7e51601d81390ee00c7a5fab834f83409d6 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 7 May 2022 13:09:54 -0400 Subject: [PATCH 32/34] chore(changelog): Updated CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c2d397dd..18f88f2f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ 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.153.1](https://github.com/awslabs/aws-solutions-constructs/compare/v1.153.0...v1.153.1) (2022-05-07) +## [1.153.1](https://github.com/awslabs/aws-solutions-constructs/compare/v1.153.0...v1.153.1) (2022-05-07) + +* Upgraded all patterns to CDK v1.153.1 ## [1.153.0](https://github.com/awslabs/aws-solutions-constructs/compare/v1.152.0...v1.153.0) (2022-05-07) From 40763ec7e61c17c47a4fabd6f6bacbb51f000326 Mon Sep 17 00:00:00 2001 From: AWS Solutions Constructs Automation Date: Sat, 7 May 2022 18:21:49 +0000 Subject: [PATCH 33/34] chore(release): 2.6.0 --- CHANGELOG.v2.md | 19 +++++++++++++++++++ source/lerna.v2.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.v2.md b/CHANGELOG.v2.md index 4b20b92f7..72e8d6edb 100644 --- a/CHANGELOG.v2.md +++ b/CHANGELOG.v2.md @@ -2,6 +2,25 @@ 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. +## [2.6.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.5.0...v2.6.0) (2022-05-07) + + +### Features + +* **aws-fargate-dynamodb:** create new construct ([#633](https://github.com/awslabs/aws-solutions-constructs/issues/633)) ([0b35418](https://github.com/awslabs/aws-solutions-constructs/commit/0b35418b41e24b32b6064a649d77a70f1c6d7bd8)) +* **aws-fargate-secretsmanager:** Create new construct ([#670](https://github.com/awslabs/aws-solutions-constructs/issues/670)) ([cd218b6](https://github.com/awslabs/aws-solutions-constructs/commit/cd218b6900a174afa09c86f28fb0650ecfe37942)) +* **aws-fargate-ssmstringparameter:** New Construct ([#653](https://github.com/awslabs/aws-solutions-constructs/issues/653)) ([bcb7c63](https://github.com/awslabs/aws-solutions-constructs/commit/bcb7c6351ffa9b8ef5f5e7790522c5b1fe87dd9a)) +* **aws-lambda-elasticachmemcached:** New Construct ([#675](https://github.com/awslabs/aws-solutions-constructs/issues/675)) ([14c50ae](https://github.com/awslabs/aws-solutions-constructs/commit/14c50ae86e84b05d1395293a001c4baa5d5f9fce)) +* **aws-s3-stepfunctions:** Changed escape hatch to eventBridgeEnabled prop ([#666](https://github.com/awslabs/aws-solutions-constructs/issues/666)) ([bc2f733](https://github.com/awslabs/aws-solutions-constructs/commit/bc2f733879a5363407729e1f236302c9361ff652)) +* **README.md:** add python and java minimal deployment ([#582](https://github.com/awslabs/aws-solutions-constructs/issues/582)) ([2ecd9dd](https://github.com/awslabs/aws-solutions-constructs/commit/2ecd9dd935b731d2e4705ed9c146efcad0961fd8)) + + +### Bug Fixes + +* **aws-lambda-secretsmanager:** Update docs ([#673](https://github.com/awslabs/aws-solutions-constructs/issues/673)) ([1b843bf](https://github.com/awslabs/aws-solutions-constructs/commit/1b843bff718dd05376f4f72ff9075db123e05288)) +* **Remove debug statement:** Remove extra debug statement in kinesisfirehose-s3 ([#649](https://github.com/awslabs/aws-solutions-constructs/issues/649)) ([26e9ec0](https://github.com/awslabs/aws-solutions-constructs/commit/26e9ec08257a90034b76a91ea4a3d703d13eb0a2)) +* **Sonarqube configuration:** Replace comma between constructs ([#646](https://github.com/awslabs/aws-solutions-constructs/issues/646)) ([79e1b09](https://github.com/awslabs/aws-solutions-constructs/commit/79e1b09544c2d029fb73a2b500dde5e35edbf63a)) + ## [2.5.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.4.0...v2.5.0) (2022-03-30) ### Features diff --git a/source/lerna.v2.json b/source/lerna.v2.json index d4b9b288d..a98a00f23 100644 --- a/source/lerna.v2.json +++ b/source/lerna.v2.json @@ -6,5 +6,5 @@ "./patterns/@aws-solutions-constructs/*" ], "rejectCycles": "true", - "version": "2.5.0" + "version": "2.6.0" } From ff240a603e9e2d1c994e931dd431006689dfce52 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Sat, 7 May 2022 14:24:37 -0400 Subject: [PATCH 34/34] chore(changelog): Updated CHANGELOG.v2.md --- CHANGELOG.v2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.v2.md b/CHANGELOG.v2.md index 72e8d6edb..289d58acf 100644 --- a/CHANGELOG.v2.md +++ b/CHANGELOG.v2.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. See [standa ## [2.6.0](https://github.com/awslabs/aws-solutions-constructs/compare/v2.5.0...v2.6.0) (2022-05-07) +* Includes all functionality of V1.153.1 +* Built upon underlying CDK version V2.15.0 ### Features