From 5c86f3a711c45c8991b66369b7b5054d5e9229e1 Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Wed, 19 Jan 2022 13:31:19 -0500 Subject: [PATCH] feat(aws-fargate-sns): New Construct (#574) * Initial upload * cfn_nag issue * README updates and fixes * Additional tests, Self review items * Better variable names * respond to comments * Respond to comments --- .../aws-alb-fargate/README.md | 20 +- .../aws-alb-fargate/lib/index.ts | 2 +- .../aws-alb-fargate/test/alb-fargate.test.ts | 21 +- .../aws-fargate-sns/.eslintignore | 4 + .../aws-fargate-sns/.gitignore | 15 + .../aws-fargate-sns/.npmignore | 21 + .../aws-fargate-sns/README.md | 109 ++ .../aws-fargate-sns/architecture.png | Bin 0 -> 70491 bytes .../aws-fargate-sns/lib/index.ts | 182 +++ .../aws-fargate-sns/package.json | 104 ++ .../aws-fargate-sns/test/fargate-sns.test.ts | 366 +++++ .../integ.existing-resources.expected.json | 1179 +++++++++++++++ .../test/integ.existing-resources.ts | 57 + .../test/integ.new-resources.expected.json | 1271 +++++++++++++++++ .../test/integ.new-resources.ts | 39 + .../core/lib/fargate-helper.ts | 42 +- .../core/test/fargate-helper.test.ts | 14 +- .../core/test/test-helper.ts | 10 +- 18 files changed, 3402 insertions(+), 54 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/fargate-sns.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.existing-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.existing-resources.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.new-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.new-resources.ts 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 8ec496e91..9ec754c03 100644 --- a/source/patterns/@aws-solutions-constructs/aws-alb-fargate/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-alb-fargate/README.md @@ -27,6 +27,8 @@ This AWS Solutions Construct implements an an Application Load Balancer to an AW Here is a minimal deployable pattern definition in 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, @@ -34,7 +36,7 @@ Here is a minimal deployable pattern definition in Typescript: "arn:aws:acm:us-east-1:123456789012:certificate/11112222-3333-1234-1234-123456789012" ); - const props: AlbToLambdaProps = { + const props: AlbToFargateProps = { ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", ecrImageVersion: "latest", listenerProps: { @@ -72,14 +74,14 @@ _Parameters_ | 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 | -| clusterProps | [ecs.ClusterProps]() | 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]() | Optional props to define the container created for the Fargate Service (defaults found in fargate-defaults.ts) | -| fargateTaskDefinitionProps | [ecs.FargateTaskDefinitionProps /| any]() | Optional props to define the Fargate Task Definition for this construct (defaults found in fargate-defaults.ts) | -| fargateServiceProps | [ecs.FargateServiceProps /| any]() | Optional properties to override default values for the Fargate service. Service will set up in the Public or Isolated subnets of the VPC by default, override that (e.g. - choose Private subnets) by setting vpcSubnets on this object. | -| existingFargateServiceObject | [ecs.FargateService]() | 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 | -| existingContainerDefinitionObject | [ecs.ContainerDefinition]() | A container definition already instantiated as part of a Fargate service. This much be the container in the existingFargateServiceObject | +| 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 properties to override default values for the Fargate service. Service will set up in the Public or Isolated subnets of the VPC by default, override that (e.g. - choose Private subnets) by setting vpcSubnets on this object. | +| 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: existingImageObject, ecrImageVersion, containerDefintionProps, fargateTaskDefinitionProps, ecrRepositoryArn, fargateServiceProps, clusterProps, existingClusterInterface | +| 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 | ## Pattern Properties diff --git a/source/patterns/@aws-solutions-constructs/aws-alb-fargate/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-alb-fargate/lib/index.ts index de4c233f8..9b2c75be4 100644 --- a/source/patterns/@aws-solutions-constructs/aws-alb-fargate/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-alb-fargate/lib/index.ts @@ -175,7 +175,7 @@ export class AlbToFargate extends Construct { existingVpc: props.existingVpc, defaultVpcProps: props.publicApi ? defaults.DefaultPublicPrivateVpcProps() : defaults.DefaultIsolatedVpcProps(), userVpcProps: props.vpcProps, - constructVpcProps: props.publicApi ? {} : { enableDnsHostnames: true, enableDnsSupport: true } + constructVpcProps: { enableDnsHostnames: true, enableDnsSupport: true } }); // Set up the ALB diff --git a/source/patterns/@aws-solutions-constructs/aws-alb-fargate/test/alb-fargate.test.ts b/source/patterns/@aws-solutions-constructs/aws-alb-fargate/test/alb-fargate.test.ts index 7082d8456..575ec4db0 100644 --- a/source/patterns/@aws-solutions-constructs/aws-alb-fargate/test/alb-fargate.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-alb-fargate/test/alb-fargate.test.ts @@ -17,7 +17,6 @@ import * as elb from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cdk from "@aws-cdk/core"; import '@aws-cdk/assert/jest'; import * as defaults from '@aws-solutions-constructs/core'; -import { fakeEcrRepoArn } from '../../core/test/fargate-helper.test'; test('Test new vpc, load balancer, service', () => { // An environment with region is required to enable logging on an ALB @@ -26,7 +25,7 @@ test('Test new vpc, load balancer, service', () => { }); const testProps: AlbToFargateProps = { publicApi: true, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, listenerProps: { protocol: 'HTTP' }, @@ -56,7 +55,7 @@ test('Test new load balancer, service, existing vpc', () => { const testProps: AlbToFargateProps = { existingVpc: defaults.getTestVpc(stack), publicApi: true, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, listenerProps: { protocol: 'HTTP' }, @@ -94,7 +93,7 @@ test('Test new service, existing load balancer, vpc', () => { const testProps: AlbToFargateProps = { existingVpc, publicApi: true, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, existingLoadBalancerObj: existingAlb, listenerProps: { protocol: 'HTTP' @@ -134,7 +133,7 @@ test('Test existing load balancer, vpc, service', () => { 'test', existingVpc, undefined, - fakeEcrRepoArn); + defaults.fakeEcrRepoArn); const existingAlb = new elb.ApplicationLoadBalancer(stack, 'test-alb', { vpc: existingVpc, @@ -181,7 +180,7 @@ test('Test add a second target with rules', () => { const testProps: AlbToFargateProps = { existingVpc: defaults.getTestVpc(stack), publicApi: true, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, listenerProps: { protocol: 'HTTP' }, @@ -238,7 +237,7 @@ test('Test new vpc, load balancer, service - custom Service Props', () => { const testProps: AlbToFargateProps = { publicApi: true, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, listenerProps: { protocol: 'HTTP' }, @@ -272,7 +271,7 @@ test('Test new vpc, load balancer, service - custom VPC Props', () => { const testProps: AlbToFargateProps = { publicApi: true, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, listenerProps: { protocol: 'HTTP' }, @@ -305,7 +304,7 @@ test('Test new vpc, load balancer, service - custom LoadBalancer and targetGroup const testProps: AlbToFargateProps = { publicApi: true, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, listenerProps: { protocol: 'HTTP' }, @@ -347,7 +346,7 @@ test('Test HTTPS API with new vpc, load balancer, service', () => { const testProps: AlbToFargateProps = { publicApi: true, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, listenerProps: { protocol: 'HTTPS', certificates: [ fakeCert ] @@ -389,7 +388,7 @@ test('Test HTTPS API with new vpc, load balancer, service and private API', () = const testProps: AlbToFargateProps = { publicApi: false, - ecrRepositoryArn: fakeEcrRepoArn, + ecrRepositoryArn: defaults.fakeEcrRepoArn, listenerProps: { protocol: 'HTTPS', certificates: [ fakeCert ] diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/.eslintignore new file mode 100644 index 000000000..e6f7801ea --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/.eslintignore @@ -0,0 +1,4 @@ +lib/*.js +test/*.js +*.d.ts +coverage diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/.gitignore b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/.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-sns/.npmignore b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/.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-sns/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/README.md new file mode 100644 index 000000000..aeb83c8a2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/README.md @@ -0,0 +1,109 @@ +# aws-fargate-sns 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_sns`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-fargate-sns`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.fargatesns`| + +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: + +``` typescript + 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 = { + publicApi: true, + ecrRepositoryArn: "arn of a repo in ECR in your account", + }); + + new FargateToSns(stack, 'test-construct', props); +``` + +## Initializer + +``` text +new FargateToSns(scope: Construct, id: string, props: FargateToSnsProps); +``` + +_Parameters_ + +* scope [`Construct`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_core.Construct.html) +* id `string` +* props [`FargateToSnsProps`](#pattern-construct-props) + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| publicApi | boolean | Whether the construct is deploying a private or public API. This has implications for the VPC and ALB. | +| 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. | +| 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: existingImageObject, ecrImageVersion, containerDefintionProps, fargateTaskDefinitionProps, ecrRepositoryArn, fargateServiceProps, clusterProps, existingClusterInterface | +| 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 | +|existingTopicObj?|[sns.Topic](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-sns.Topic.html)|Existing instance of SNS Topic object, providing both this and `topicProps` will cause an error.| +|topicProps?|[sns.TopicProps](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.TopicProps.html)|Optional user provided properties to override the default properties for the SNS topic.| +|topicArnEnvironmentVariableName?|string|Optional Name for the SNS topic arn environment variable set for the container.| +|topicNameEnvironmentVariableName?|string|Optional Name for the SNS topic 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 providedb 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. | +|snsTopic|[`sns.Topic`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.Topic.html)|Returns an instance of the SNS topic created by the pattern.| + +## 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 SNS topic + * Add permissions to the container IAM role allowing it to publish to the SNS topic + +### Amazon SNS Topic +* Sets up an Amazon SNS topic + * Uses an existing topic if one is provided, otherwise creates a new one +* Adds an Interface Endpoint to the VPC for SNS (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-sns/architecture.png b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..e7acdda7678f0b7a92f19a91f9fe9d076d1b05db GIT binary patch literal 70491 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 P0Y8#ra-yZe2LAsCk~o)< literal 0 HcmV?d00001 diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/lib/index.ts new file mode 100644 index 000000000..c507202b4 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/lib/index.ts @@ -0,0 +1,182 @@ +/** + * 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 sns from "@aws-cdk/aws-sns"; +// 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 FargateToSnsProps { + /** + * 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 SNS 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 SNS Interface + * endpoint will be added to this VPC. + * + * @default - none + */ + readonly existingVpc?: ec2.IVpc; + /** + * 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 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; + /** + * Existing instance of SNS Topic object, providing both this and topicProps will cause an error.. + * + * @default - Default props are used + */ + readonly existingTopicObject?: sns.Topic; + /** + * Optional user provided properties to override the default properties for the SNS topic. + * + * @default - Default properties are used. + */ + readonly topicProps?: sns.TopicProps; + /** + * Optional Name for the SNS topic arn environment variable set for the container. + * + * @default - None + */ + readonly topicArnEnvironmentVariableName?: string; + /** + * Optional Name for the SNS topic name environment variable set for the container. + * + * @default - None + */ + readonly topicNameEnvironmentVariableName?: string; + /* + * 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; +} + +export class FargateToSns extends Construct { + public readonly snsTopic: sns.Topic; + public readonly service: ecs.FargateService; + public readonly vpc: ec2.IVpc; + public readonly container: ecs.ContainerDefinition; + + constructor(scope: Construct, id: string, props: FargateToSnsProps) { + super(scope, id); + defaults.CheckProps(props); + defaults.CheckFargateProps(props); + + 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.SNS); + + 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 SNS topic + [this.snsTopic] = defaults.buildTopic(this, { + existingTopicObj: props.existingTopicObject, + topicProps: props.topicProps, + }); + + this.snsTopic.grantPublish(this.service.taskDefinition.taskRole); + + const topicArnEnvironmentVariableName = props.topicArnEnvironmentVariableName || 'SNS_TOPIC_ARN'; + this.container.addEnvironment(topicArnEnvironmentVariableName, this.snsTopic.topicArn); + const topicNameEnvironmentVariableName = props.topicNameEnvironmentVariableName || 'SNS_TOPIC_NAME'; + this.container.addEnvironment(topicNameEnvironmentVariableName, this.snsTopic.topicName); + + } +} diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/package.json b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/package.json new file mode 100644 index 000000000..d04e5f3f3 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/package.json @@ -0,0 +1,104 @@ +{ + "name": "@aws-solutions-constructs/aws-fargate-sns", + "version": "0.0.0", + "description": "CDK Constructs for AWS Fargate to Amazon SNS 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-sns" + }, + "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.fargatesns", + "maven": { + "groupId": "software.amazon.awsconstructs", + "artifactId": "fargatesns" + } + }, + "dotnet": { + "namespace": "Amazon.SolutionsConstructs.AWS.FargateSns", + "packageId": "Amazon.SolutionsConstructs.AWS.FargateSns", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-constructs.aws-fargate-sns", + "module": "aws_solutions_constructs.aws_fargate_sns" + } + } + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-sns": "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-sns": "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-sns": "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 SNS", + "AWS Fargate" + ] +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/fargate-sns.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/fargate-sns.test.ts new file mode 100644 index 000000000..cc1c78ec1 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/fargate-sns.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. + */ + +import '@aws-cdk/assert/jest'; +import * as defaults from '@aws-solutions-constructs/core'; +import * as cdk from "@aws-cdk/core"; +import { FargateToSns } from "../lib"; +import * as sns from '@aws-cdk/aws-sns'; +import * as ecs from '@aws-cdk/aws-ecs'; + +test('New service/new topic, public API, new VPC', () => { + + // An environment with region is required to enable logging on an ALB + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + const publicApi = true; + const clusterName = "custom-cluster-name"; + const containerName = "custom-container-name"; + const serviceName = "custom-service-name"; + const topicName = "custom-topic-name"; + + new FargateToSns(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + clusterProps: { clusterName }, + containerDefinitionProps: { containerName }, + fargateTaskDefinitionProps: { family: 'family-name' }, + fargateServiceProps: { serviceName }, + topicProps: { topicName }, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName + }); + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + Family: 'family-name' + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Cluster", { + ClusterName: clusterName + }); + + expect(stack).toHaveResourceLike("AWS::SNS::Topic", { + TopicName: topicName + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + 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' + }); + // Confirm we created a Public/Private VPC + expect(stack).toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SNS::Topic', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/new topic, private API, new VPC', () => { + + // An environment with region is required to enable logging on an ALB + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + const publicApi = false; + + new FargateToSns(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' } + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::SNS::Topic", { + + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.0.0.0/16' + }); + // Confirm we created an Isolated VPC + expect(stack).not.toHaveResourceLike('AWS::EC2::InternetGateway', {}); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SNS::Topic', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/existing topic, private API, existing VPC', () => { + // An environment with region is required to enable logging on an ALB + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + const publicApi = false; + const topicName = 'custom-topic-name'; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const existingTopic = new sns.Topic(stack, 'MyTopic', { + topicName + }); + + new FargateToSns(stack, 'test-construct', { + publicApi, + existingVpc, + existingTopicObject: existingTopic, + 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::SNS::Topic", { + TopicName: topicName + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SNS::Topic', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('Existing service/new topic, public API, existing VPC', () => { + // An environment with region is required to enable logging on an ALB + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + const publicApi = true; + const serviceName = 'custom-name'; + + const existingVpc = defaults.getTestVpc(stack); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + new FargateToSns(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + topicArnEnvironmentVariableName: 'CUSTOM_ARN', + topicNameEnvironmentVariableName: 'CUSTOM_NAME', + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: 'CUSTOM_ARN', + Value: { + Ref: "testconstructSnsTopic44188529" + } + }, + { + Name: 'CUSTOM_NAME', + Value: { + "Fn::GetAtt": [ + "testconstructSnsTopic44188529", + "TopicName" + ] + } + } + ], + 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::SNS::Topic", { + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SNS::Topic', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +// Test existing service/existing topic, private API, new VPC +test('Existing service/existing topic, private API, existing VPC', () => { + // An environment with region is required to enable logging on an ALB + const stack = new cdk.Stack(undefined, undefined, { + env: { account: "123456789012", region: 'us-east-1' }, + }); + const publicApi = false; + const serviceName = 'custom-name'; + const topicName = 'custom-topic-name'; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName }); + + const existingTopic = new sns.Topic(stack, 'MyTopic', { + topicName + }); + + new FargateToSns(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + existingTopicObject: existingTopic + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: serviceName, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "SNS_TOPIC_ARN", + Value: { + Ref: "MyTopic86869434" + } + }, + { + Name: "SNS_TOPIC_NAME", + Value: { + "Fn::GetAtt": [ + "MyTopic86869434", + "TopicName" + ] + } + } + ], + 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::SNS::Topic", { + TopicName: topicName + }); + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.0.0/16' + }); + expect(stack).toCountResources('AWS::EC2::VPC', 1); + expect(stack).toCountResources('AWS::SNS::Topic', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.existing-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.existing-resources.expected.json new file mode 100644 index 000000000..4479fc2ff --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.existing-resources.expected.json @@ -0,0 +1,1179 @@ +{ + "Description": "Integration Test with new VPC, Service and Topic", + "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" + } + }, + "VpcSNS5B664381": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.sns", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesSNSsecuritygroup2696BE98", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "testtopicB3D54793": { + "Type": "AWS::SNS::Topic", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W47", + "reason": "Stub topic for placehoder in Integration test" + } + ] + } + } + }, + "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": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "testtopicB3D54793" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testtaskdefTaskRoleDefaultPolicy5D591D1C", + "Roles": [ + { + "Ref": "testtaskdefTaskRoleB2DEF113" + } + ] + } + }, + "testtaskdefF924AD58": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "CUSTOM_ARN", + "Value": { + "Ref": "testtopicB3D54793" + } + }, + { + "Name": "CUSTOM_NAME", + "Value": { + "Fn::GetAtt": [ + "testtopicB3D54793", + "TopicName" + ] + } + } + ], + "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" + } + } + }, + "existingresourcesSNSsecuritygroup2696BE98": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "existing-resources/existing-resources-SNS-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-sns/test/integ.existing-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.existing-resources.ts new file mode 100644 index 000000000..f51989de8 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/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 { FargateToSns, FargateToSnsProps } from "../lib"; +import { generateIntegStackName, getTestVpc, CreateFargateService, addCfnSuppressRules } from '@aws-solutions-constructs/core'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sns from '@aws-cdk/aws-sns'; + +// 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 Topic'; + +const existingVpc = getTestVpc(stack); +const existingTopic = new sns.Topic(stack, 'test-topic', {}); +addCfnSuppressRules(existingTopic, [ { id: "W47", reason: "Stub topic for placehoder in Integration test" } ]); + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const [testService, testContainer] = CreateFargateService(stack, + 'test', + existingVpc, + undefined, + undefined, + undefined, + undefined, + { image }, +); + +const testProps: FargateToSnsProps = { + publicApi: true, + existingVpc, + existingTopicObject: existingTopic, + existingContainerDefinitionObject: testContainer, + existingFargateServiceObject: testService, + topicArnEnvironmentVariableName: 'CUSTOM_ARN', + topicNameEnvironmentVariableName: 'CUSTOM_NAME', +}; + +new FargateToSns(stack, 'test-construct', testProps); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.new-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.new-resources.expected.json new file mode 100644 index 000000000..e9ab2f25f --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.new-resources.expected.json @@ -0,0 +1,1271 @@ +{ + "Description": "Integration Test with new VPC, Service and Topic", + "Resources": { + "testconstructSnsTopic44188529": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":kms:us-east-1:", + { + "Ref": "AWS::AccountId" + }, + ":alias/aws/sns" + ] + ] + } + } + }, + "testconstructSnsTopicPolicy72FFD530": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "SNS:Publish", + "SNS:RemovePermission", + "SNS:SetTopicAttributes", + "SNS:DeleteTopic", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:Receive", + "SNS:AddPermission", + "SNS:Subscribe" + ], + "Condition": { + "StringEquals": { + "AWS:SourceOwner": { + "Ref": "AWS::AccountId" + } + } + }, + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": { + "Ref": "testconstructSnsTopic44188529" + }, + "Sid": "TopicOwnerOnlyAccess" + }, + { + "Action": [ + "SNS:Publish", + "SNS:RemovePermission", + "SNS:SetTopicAttributes", + "SNS:DeleteTopic", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:Receive", + "SNS:AddPermission", + "SNS:Subscribe" + ], + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": { + "Ref": "testconstructSnsTopic44188529" + }, + "Sid": "HttpsOnly" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "testconstructSnsTopic44188529" + } + ] + } + }, + "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" + } + ] + } + }, + "VpcSNS5B664381": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.sns", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesSNSsecuritygroup4422F7B8", + "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" + } + }, + "newresourcesSNSsecuritygroup4422F7B8": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-SNS-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": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "testconstructSnsTopic44188529" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testconstructtaskdefTaskRoleDefaultPolicyF34A1535", + "Roles": [ + { + "Ref": "testconstructtaskdefTaskRoleC60414C4" + } + ] + } + }, + "testconstructtaskdef8BD1F9E4": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "SNS_TOPIC_ARN", + "Value": { + "Ref": "testconstructSnsTopic44188529" + } + }, + { + "Name": "SNS_TOPIC_NAME", + "Value": { + "Fn::GetAtt": [ + "testconstructSnsTopic44188529", + "TopicName" + ] + } + } + ], + "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-sns/test/integ.new-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.new-resources.ts new file mode 100644 index 000000000..d0b629907 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-sns/test/integ.new-resources.ts @@ -0,0 +1,39 @@ +/** + * 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 { FargateToSns, FargateToSnsProps } 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 Topic'; + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const testProps: FargateToSnsProps = { + publicApi: true, + containerDefinitionProps: { + image + }, +}; + +new FargateToSns(stack, 'test-construct', testProps); + +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/core/lib/fargate-helper.ts b/source/patterns/@aws-solutions-constructs/core/lib/fargate-helper.ts index b4026ccf7..bd6a5d495 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/fargate-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/fargate-helper.ts @@ -45,12 +45,12 @@ export function CreateFargateService( defaults.ServiceEndpointTypes.S3 ); - const containerDefintionConstructProps: any = {}; - const fargateServiceDefintionConstructProps: any = {}; + const constructContainerDefintionProps: any = {}; + const constructFargateServiceDefinitionProps: any = {}; if (!clientFargateServiceProps?.cluster) { // Construct Fargate Service - fargateServiceDefintionConstructProps.cluster = CreateCluster( + constructFargateServiceDefinitionProps.cluster = CreateCluster( scope, `${id}-cluster`, constructVpc, @@ -60,7 +60,7 @@ export function CreateFargateService( // Set up the Fargate service if (!clientContainerDefinitionProps?.image) { - containerDefintionConstructProps.image = CreateImage( + constructContainerDefintionProps.image = CreateImage( scope, id, ecrRepositoryArn, @@ -69,21 +69,22 @@ export function CreateFargateService( } // Create the Fargate Service - fargateServiceDefintionConstructProps.taskDefinition = CreateTaskDefinition( + let newContainerDefinition; + [constructFargateServiceDefinitionProps.taskDefinition, newContainerDefinition] = CreateTaskDefinition( scope, id, clientFargateTaskDefinitionProps, clientContainerDefinitionProps, - containerDefintionConstructProps + constructContainerDefintionProps ); if (!clientFargateServiceProps?.vpcSubnets) { if (constructVpc.isolatedSubnets.length) { - fargateServiceDefintionConstructProps.vpcSubnets = { + constructFargateServiceDefinitionProps.vpcSubnets = { subnets: constructVpc.isolatedSubnets, }; } else { - fargateServiceDefintionConstructProps.vpcSubnets = { + constructFargateServiceDefinitionProps.vpcSubnets = { subnets: constructVpc.privateSubnets, }; } @@ -117,7 +118,7 @@ export function CreateFargateService( const fargateServiceProps = defaults.consolidateProps( defaultFargateServiceProps, clientFargateServiceProps, - fargateServiceDefintionConstructProps + constructFargateServiceDefinitionProps ); const newService = new ecs.FargateService( @@ -125,12 +126,8 @@ export function CreateFargateService( `${id}-service`, fargateServiceProps, ); - // We just created this container, so there should never be a situation where it doesn't exist - const newContainer = newService.taskDefinition.findContainer( - `${id}-container` - ) as ecs.ContainerDefinition; - return [newService, newContainer]; + return [newService, newContainerDefinition]; } function CreateCluster( @@ -172,8 +169,8 @@ function CreateTaskDefinition( id: string, clientFargateTaskDefinitionProps?: ecs.FargateTaskDefinitionProps, clientContainerDefinitionProps?: ecs.ContainerDefinitionProps, - constructContainerDefintionProps?: ecs.ContainerDefinitionProps -): ecs.FargateTaskDefinition { + constructContainerDefinitionProps?: ecs.ContainerDefinitionProps +): [ecs.FargateTaskDefinition, ecs.ContainerDefinition] { const taskDefinitionProps = defaults.consolidateProps( defaults.DefaultFargateTaskDefinitionProps(), clientFargateTaskDefinitionProps @@ -184,15 +181,16 @@ function CreateTaskDefinition( taskDefinitionProps ); + const defaultContainerDefinitionProps = defaults.consolidateProps(defaults.DefaultContainerDefinitionProps(), { + containerName: `${id}-container`, + }); const containerDefinitionProps = defaults.consolidateProps( - defaults.DefaultContainerDefinitionProps(), + defaultContainerDefinitionProps, clientContainerDefinitionProps, - defaults.consolidateProps({}, constructContainerDefintionProps, { - containerName: `${id}-container`, - }) + constructContainerDefinitionProps, ); - taskDefinition.addContainer(`${id}-container`, containerDefinitionProps); - return taskDefinition; + const containerDefinition = taskDefinition.addContainer(`${id}-container`, containerDefinitionProps); + return [taskDefinition, containerDefinition]; } export function CheckFargateProps(props: any) { diff --git a/source/patterns/@aws-solutions-constructs/core/test/fargate-helper.test.ts b/source/patterns/@aws-solutions-constructs/core/test/fargate-helper.test.ts index 1b3fa69b1..db717819e 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/fargate-helper.test.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/fargate-helper.test.ts @@ -19,8 +19,6 @@ import * as ecs from "@aws-cdk/aws-ecs"; import * as ecr from "@aws-cdk/aws-ecr"; import '@aws-cdk/assert/jest'; -export const fakeEcrRepoArn = 'arn:aws:ecr:us-east-1:123456789012:repository/fake-repo'; - test('Test with all defaults', () => { const stack = new Stack(); @@ -29,7 +27,7 @@ test('Test with all defaults', () => { 'test', testVpc, undefined, - fakeEcrRepoArn); + defaults.fakeEcrRepoArn); expect(stack).toHaveResource("AWS::ECS::Service", { Cluster: { @@ -108,7 +106,7 @@ test('Test with all defaults in isolated VPC', () => { 'test', testVpc, undefined, - fakeEcrRepoArn); + defaults.fakeEcrRepoArn); expect(stack).toHaveResource("AWS::ECS::Service", { Cluster: { @@ -220,7 +218,7 @@ test('Test with custom container definition', () => { 'test', testVpc, undefined, - fakeEcrRepoArn, + defaults.fakeEcrRepoArn, undefined, { cpu: 256, memoryLimitMiB: 512 } ); @@ -240,7 +238,7 @@ test('Test with custom cluster props', () => { 'test', testVpc, { clusterName }, - fakeEcrRepoArn, + defaults.fakeEcrRepoArn, undefined, ); @@ -258,7 +256,7 @@ test('Test with custom Fargate Service props', () => { 'test', testVpc, undefined, - fakeEcrRepoArn, + defaults.fakeEcrRepoArn, undefined, undefined, undefined, @@ -287,7 +285,7 @@ test('Test with custom security group', () => { 'test', testVpc, undefined, - fakeEcrRepoArn, + defaults.fakeEcrRepoArn, undefined, undefined, undefined, 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 86575e61c..94716f610 100644 --- a/source/patterns/@aws-solutions-constructs/core/test/test-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/test/test-helper.ts @@ -15,11 +15,13 @@ import { Bucket, BucketProps, BucketEncryption } from "@aws-cdk/aws-s3"; import { Construct, RemovalPolicy, Stack } from "@aws-cdk/core"; import { buildVpc } from '../lib/vpc-helper'; -import { DefaultPublicPrivateVpcProps } from '../lib/vpc-defaults'; +import { DefaultPublicPrivateVpcProps, DefaultIsolatedVpcProps } from '../lib/vpc-defaults'; import { overrideProps, addCfnSuppressRules } from "../lib/utils"; import * as path from 'path'; import * as acm from '@aws-cdk/aws-certificatemanager'; +export const fakeEcrRepoArn = 'arn:aws:ecr:us-east-1:123456789012:repository/fake-repo'; + // Creates a bucket used for testing - minimal properties, destroyed after test export function CreateScrapBucket(scope: Construct, props?: BucketProps | any) { const defaultProps = { @@ -72,9 +74,11 @@ export function generateIntegStackName(filename: string): string { } // Helper Functions -export function getTestVpc(stack: Stack) { +export function getTestVpc(stack: Stack, publicFacing: boolean = true) { return buildVpc(stack, { - defaultVpcProps: DefaultPublicPrivateVpcProps(), + defaultVpcProps: publicFacing ? + DefaultPublicPrivateVpcProps() : + DefaultIsolatedVpcProps(), constructVpcProps: { enableDnsHostnames: true, enableDnsSupport: true,