From a34aa74932653ed107bb0777b4f88fc3c80dccf6 Mon Sep 17 00:00:00 2001 From: mickychetta Date: Wed, 12 Oct 2022 18:48:29 +0000 Subject: [PATCH 1/6] created README --- .../aws-fargate-opensearch/README.md | 141 ++++++++++++++++++ .../aws-fargate-opensearch/architecture.png | Bin 0 -> 65541 bytes .../aws-fargate-opensearch/package.json | 105 +++++++++++++ 3 files changed, 246 insertions(+) create mode 100755 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/package.json diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md new file mode 100755 index 000000000..24b4a5243 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md @@ -0,0 +1,141 @@ +# aws-fargate-opensearch 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_opensearch`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-fargate-opensearch`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.fargateopensearch`| + +## Overview +This AWS Solutions Construct implements an AWS Fargate service that can write/read to an Amazon OpenSearch Service domain. + +Here is a minimal deployable pattern definition: + +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import { FargateToOpenSearch, FargateToOpenSearchProps } from '@aws-solutions-constructs/aws-fargate-opensearch'; + +const constructProps: FargateToOpenSearchProps = { + publicApi: true, + ecrRepositoryArn: "arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", + openSearchDomainName: 'testdomain', + // TODO: Ensure the Cognito domain name is globally unique + cognitoDomainName: 'globallyuniquedomain' + Aws.ACCOUNT_ID +}; + +new FargateToOpenSearch(this, 'test-construct', constructProps); +``` + +Python +``` python +from aws_solutions_constructs.aws_fargate_opensearch import FargateToOpenSearch, FargateToOpenSearchProps +from aws_cdk import ( + Stack +) +from constructs import Construct + +FargateToOpenSearch(self, 'test_construct', + public_api=True, + ecr_repository_arn="arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo", + open_search_domain_name='testdomain', + # TODO: Ensure the Cognito domain name is globally unique + cognito_domain_name='globallyuniquedomain' + Aws.ACCOUNT_ID) +``` + +Java +``` java +import software.constructs.Construct; + +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; +import software.amazon.awsconstructs.services.fargateopensearch.*; + +new FargateToOpenSearch(this, "test_construct", new FargateToOpenSearchProps.Builder() + .publicApi(true) + .ecrRepositoryArn("arn:aws:ecr:us-east-1:123456789012:repository/your-ecr-repo" + .openSearchDomainName("testdomain") + // TODO: Ensure the Cognito domain name is globally unique + .cognitoDomainName("globallyuniquedomain" + Aws.ACCOUNT_ID) + .build()); +``` +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| publicApi | `boolean` | Whether the construct is deploying a private or public API. This has implications for the VPC. | +| vpcProps? | [`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.VpcProps.html) | Optional custom properties for a VPC the construct will create. This VPC will be used by any Private Hosted Zone the construct creates (that's why loadBalancerProps and privateHostedZoneProps can't include a VPC). Providing both this and existingVpc is an error. | +| existingVpc? | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.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/v2/docs/aws-cdk-lib.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/v2/docs/aws-cdk-lib.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/v2/docs/aws-cdk-lib.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/v2/docs/aws-cdk-lib.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/v2/docs/aws-cdk-lib.aws_ecs.FargateService.html) | A Fargate Service already instantiated (probably by another Solutions Construct). If this is specified, then no props defining a new service can be provided, including: ecrImageVersion, containerDefinitionProps, fargateTaskDefinitionProps, ecrRepositoryArn, fargateServiceProps, clusterProps.| +| existingContainerDefinitionObject? | [`ecs.ContainerDefinition`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.ContainerDefinition.html) | A container definition already instantiated as part of a Fargate service. This must be the container in the existingFargateServiceObject.| +|openSearchDomainProps?|[`opensearchservice.CfnDomainProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomainProps.html)|Optional user provided props to override the default props for the OpenSearch Service.| +|openSearchDomainName|`string`|Domain name for the OpenSearch Service.| +|cognitoDomainName?|`string`|Optional Amazon Cognito domain name. If omitted the Amazon Cognito domain will default to the OpenSearch Service domain name.| +|createCloudWatchAlarms?|`boolean`|Whether to create the recommended CloudWatch alarms.| +|domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch Service domain endpoint environment variable set for the Lambda function.| + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +| vpc | [`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html) | The VPC used by the construct (whether created by the construct or provided by the client). | +| service | [`ecs.FargateService`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.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/v2/docs/aws-cdk-lib.aws_ecs.ContainerDefinition.html) | The container associated with the AWS Fargate service in the service property. | +|userPool|[`cognito.UserPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html)|Returns an instance of `cognito.UserPool` created by the construct.| +|userPoolClient|[`cognito.UserPoolClient`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html)|Returns an instance of `cognito.UserPoolClient` created by the construct.| +|identityPool|[`cognito.CfnIdentityPool`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.CfnIdentityPool.html)|Returns an instance of `cognito.CfnIdentityPool` created by the construct.| +|openSearchDomain|[`opensearchservice.CfnDomain`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_opensearchservice.CfnDomain.html)|Returns an instance of `opensearch.CfnDomain` created by the construct.| +|openSearchRole|[`iam.Role`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html)|Returns an instance of `iam.Role` created by the construct for `opensearch.CfnDomain`.| +|cloudWatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of `cloudwatch.Alarm` created by the construct.| + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### AWS Fargate Service +* Sets up an AWS Fargate service + * Uses the existing service if provided + * Creates a new service if none provided + * Service will run in isolated subnets if available, then private subnets if available and finally public subnets + * Adds environment variables to the container with the OpenSearch Service domain endpoint + * Add permissions to the container IAM role allowing it to write/read to OpenSearch Service domain endpoint + +### Amazon Cognito +* Set password policy for User Pools +* Enforce the advanced security mode for User Pools + +### Amazon OpenSearch Service +* Deploy best practices CloudWatch Alarms for the OpenSearch Service domain +* Secure the OpenSearch Service dashboard access with Cognito User Pools +* Enable server-side encryption for OpenSearch Service domain using AWS managed KMS Key +* Enable node-to-node encryption for the OpenSearch Service domain +* Configure the cluster for the OpenSearch Service domain + +## 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-opensearch/architecture.png b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..f811a428d5f0620f6036cfe1d2bb5f5a3c12d835 GIT binary patch literal 65541 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
DWI7nj{|q`5 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 literal 0 HcmV?d00001 diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/package.json b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/package.json new file mode 100644 index 000000000..fec924f4e --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/package.json @@ -0,0 +1,105 @@ +{ + "name": "@aws-solutions-constructs/aws-fargate-opensearch", + "version": "0.0.0", + "description": "CDK Constructs for AWS Fargate to Amazon OpenSearch Service", + "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-opensearch" + }, + "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.fargateopensearch", + "maven": { + "groupId": "software.amazon.awsconstructs", + "artifactId": "fargateopensearch" + } + }, + "dotnet": { + "namespace": "Amazon.SolutionsConstructs.AWS.FargateOpenSearch", + "packageId": "Amazon.SolutionsConstructs.AWS.FargateOpenSearch", + "signAssembly": true, + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-solutions-constructs.aws-fargate-opensearch", + "module": "aws_solutions_constructs.aws_fargate_opensearch" + } + } + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-opensearchservice": "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/awsopensearchservice": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-cdk/aws-iam": "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-opensearchservice": "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 OpenSearch Service", + "AWS Fargate" + ] +} \ No newline at end of file From e29750c3520f641ae0694b567d98e5a041255236 Mon Sep 17 00:00:00 2001 From: mickychetta <45010053+mickychetta@users.noreply.github.com> Date: Wed, 12 Oct 2022 14:53:11 -0400 Subject: [PATCH 2/6] Update README.md --- .../@aws-solutions-constructs/aws-fargate-opensearch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md index 24b4a5243..51b69189e 100755 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md @@ -95,7 +95,7 @@ new FargateToOpenSearch(this, "test_construct", new FargateToOpenSearchProps.Bui |openSearchDomainName|`string`|Domain name for the OpenSearch Service.| |cognitoDomainName?|`string`|Optional Amazon Cognito domain name. If omitted the Amazon Cognito domain will default to the OpenSearch Service domain name.| |createCloudWatchAlarms?|`boolean`|Whether to create the recommended CloudWatch alarms.| -|domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch Service domain endpoint environment variable set for the Lambda function.| +|domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch Service domain endpoint environment variable set for the Lambda function. Defaults is `DOMAIN_ENDPOINT`.| ## Pattern Properties From 1d7e35746822e00b00d4c6a9498a384a873d767c Mon Sep 17 00:00:00 2001 From: mickychetta <45010053+mickychetta@users.noreply.github.com> Date: Wed, 12 Oct 2022 14:53:37 -0400 Subject: [PATCH 3/6] Update README.md --- .../@aws-solutions-constructs/aws-fargate-opensearch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md index 51b69189e..02d718681 100755 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md @@ -95,7 +95,7 @@ new FargateToOpenSearch(this, "test_construct", new FargateToOpenSearchProps.Bui |openSearchDomainName|`string`|Domain name for the OpenSearch Service.| |cognitoDomainName?|`string`|Optional Amazon Cognito domain name. If omitted the Amazon Cognito domain will default to the OpenSearch Service domain name.| |createCloudWatchAlarms?|`boolean`|Whether to create the recommended CloudWatch alarms.| -|domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch Service domain endpoint environment variable set for the Lambda function. Defaults is `DOMAIN_ENDPOINT`.| +|domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch Service domain endpoint environment variable set for the Lambda function. Default is `DOMAIN_ENDPOINT`.| ## Pattern Properties From f3ea038d9ae62bbe701df8f21df572f381f78c2a Mon Sep 17 00:00:00 2001 From: mickychetta <45010053+mickychetta@users.noreply.github.com> Date: Wed, 12 Oct 2022 14:55:08 -0400 Subject: [PATCH 4/6] Update README.md --- .../@aws-solutions-constructs/aws-fargate-opensearch/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md index 02d718681..fcf0888ab 100755 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/README.md @@ -121,7 +121,7 @@ Out of the box implementation of the Construct without any override will set the * 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 OpenSearch Service domain endpoint - * Add permissions to the container IAM role allowing it to write/read to OpenSearch Service domain endpoint + * Add permissions to the container IAM role allowing it to write/read to the OpenSearch Service domain endpoint ### Amazon Cognito * Set password policy for User Pools From 5ecfbad075f762011b9581b7952697655441ab89 Mon Sep 17 00:00:00 2001 From: mickychetta Date: Wed, 19 Oct 2022 22:45:00 +0000 Subject: [PATCH 5/6] created aws-fargate-opensearch construct --- .../aws-fargate-opensearch/.eslintignore | 4 + .../aws-fargate-opensearch/.gitignore | 15 + .../aws-fargate-opensearch/.npmignore | 21 + .../aws-fargate-opensearch/lib/index.ts | 210 +++ .../test/fargate-opensearch.test.ts | 725 ++++++++ .../integ.existing-resources.expected.json | 1567 +++++++++++++++++ .../test/integ.existing-resources.ts | 55 + .../test/integ.new-resources.expected.json | 1567 +++++++++++++++++ .../test/integ.new-resources.ts | 41 + .../aws-lambda-opensearch/README.md | 2 +- .../aws-lambda-opensearch/lib/index.ts | 4 - .../core/lib/fargate-helper.ts | 8 + .../core/lib/input-validation.ts | 7 + 13 files changed, 4221 insertions(+), 5 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/fargate-opensearch.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.new-resources.expected.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.new-resources.ts diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.eslintignore new file mode 100644 index 000000000..e6f7801ea --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.eslintignore @@ -0,0 +1,4 @@ +lib/*.js +test/*.js +*.d.ts +coverage diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.gitignore b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.gitignore new file mode 100644 index 000000000..6773cabd2 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.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-opensearch/.npmignore b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/.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-opensearch/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/lib/index.ts new file mode 100644 index 000000000..89ca0c3fa --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/lib/index.ts @@ -0,0 +1,210 @@ +/** + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +import * as defaults from "@aws-solutions-constructs/core"; +import * as ecs from "aws-cdk-lib/aws-ecs"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; +import * as opensearch from "aws-cdk-lib/aws-opensearchservice"; +// Note: To ensure CDKv2 compatibility, keep the import statement for Construct separate +import { Construct } from "constructs"; + +export interface FargateToOpenSearchProps { + /** + * 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. + * + * @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. + * + * @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; + /* + * A container definition already instantiated as part of a Fargate service. This must + * be the container in the existingFargateServiceObject. + * + * @default - None + */ + readonly existingContainerDefinitionObject?: ecs.ContainerDefinition; + /** + * Optional user provided props to override the default props for the OpenSearch Service. + * + * @default - Default props are used + */ + readonly openSearchDomainProps?: opensearch.CfnDomainProps; + /** + * Domain name for the OpenSearch Service. + * + * @default - None + */ + readonly openSearchDomainName: string; + /** + * Optional Amazon Cognito domain name. If omitted the Amazon Cognito domain will default to the OpenSearch Service domain name. + * + * @default - the OpenSearch Service domain name + */ + readonly cognitoDomainName?: string; + /** + * Whether to create recommended CloudWatch alarms + * + * @default - Alarms are created + */ + readonly createCloudWatchAlarms?: boolean; + /** + * Optional Name for the container environment variable set to the domain endpoint. + * + * @default - DOMAIN_ENDPOINT + */ + readonly domainEndpointEnvironmentVariableName?: string; +} + +export class FargateToOpenSearch extends Construct { + public readonly vpc: ec2.IVpc; + public readonly service: ecs.FargateService; + public readonly container: ecs.ContainerDefinition; + public readonly userPool: cognito.UserPool; + public readonly userPoolClient: cognito.UserPoolClient; + public readonly identityPool: cognito.CfnIdentityPool; + public readonly openSearchDomain: opensearch.CfnDomain; + public readonly openSearchRole: iam.Role; + public readonly cloudWatchAlarms?: cloudwatch.Alarm[]; + + constructor(scope: Construct, id: string, props: FargateToOpenSearchProps) { + 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 } + }); + + 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 + ); + } + + let cognitoAuthorizedRole: iam.Role; + + [this.userPool, this.userPoolClient, this.identityPool, cognitoAuthorizedRole] = + defaults.buildCognitoForSearchService(this, props.cognitoDomainName ?? props.openSearchDomainName); + + let securityGroupIds; + + if (this.vpc) { + securityGroupIds = defaults.getServiceVpcSecurityGroupIds(this.service); + } + + const buildOpenSearchProps: defaults.BuildOpenSearchProps = { + userpool: this.userPool, + identitypool: this.identityPool, + cognitoAuthorizedRoleARN: cognitoAuthorizedRole.roleArn, + vpc: this.vpc, + openSearchDomainName: props.openSearchDomainName, + clientDomainProps: props.openSearchDomainProps, + securityGroupIds + }; + + [this.openSearchDomain, this.openSearchRole] = defaults.buildOpenSearch(this, buildOpenSearchProps); + + if (props.createCloudWatchAlarms === undefined || props.createCloudWatchAlarms) { + this.cloudWatchAlarms = defaults.buildOpenSearchCWAlarms(this); + } + + // Add environment variables + const bucketArnEnvironmentVariableName = props.domainEndpointEnvironmentVariableName || 'DOMAIN_ENDPOINT'; + this.container.addEnvironment(bucketArnEnvironmentVariableName, this.openSearchDomain.attrDomainEndpoint); + + } +} diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/fargate-opensearch.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/fargate-opensearch.test.ts new file mode 100644 index 000000000..95f2b4540 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/fargate-opensearch.test.ts @@ -0,0 +1,725 @@ +/** + * 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-lib"; +import { FargateToOpenSearch } from "../lib"; +import * as ecs from 'aws-cdk-lib/aws-ecs'; + +const DOMAIN_NAME = "solutions-construct-domain"; +const COGNITO_DOMAIN_NAME = "cogn-solutions-construct-domain"; +const CLUSTER_NAME = "custom-cluster-name"; +const CONTAINER_NAME = "custom-container-name"; +const SERVICE_NAME = "custom-service-name"; +const FAMILY_NAME = "family-name"; +const CUSTOM_ENV_NAME = 'CUSTOM_DOMAIN_ENDPOINT'; + +const deployStackWithNewResources = (stack: cdk.Stack, publicApi: boolean) => { + return new FargateToOpenSearch(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + clusterProps: { clusterName: CLUSTER_NAME }, + containerDefinitionProps: { CONTAINER_NAME }, + fargateTaskDefinitionProps: { family: FAMILY_NAME }, + fargateServiceProps: { serviceName: SERVICE_NAME }, + openSearchDomainName: DOMAIN_NAME, + }); +}; + +test('Test domain and cognito domain name', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + new FargateToOpenSearch(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + clusterProps: { clusterName: CLUSTER_NAME }, + containerDefinitionProps: { CONTAINER_NAME }, + fargateTaskDefinitionProps: { family: FAMILY_NAME }, + fargateServiceProps: { servieName: SERVICE_NAME }, + openSearchDomainName: DOMAIN_NAME, + cognitoDomainName: COGNITO_DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + DomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::Cognito::UserPoolDomain", { + Domain: COGNITO_DOMAIN_NAME + }); +}); + +test('Check construct property', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + const construct = deployStackWithNewResources(stack, publicApi); + + expect(construct.vpc !== null); + expect(construct.service !== null); + expect(construct.container !== null); + expect(construct.userPool !== null); + expect(construct.userPoolClient !== null); + expect(construct.identityPool !== null); + expect(construct.openSearchDomain !== null); + expect(construct.openSearchRole !== null); + expect(construct.cloudWatchAlarms !== null); +}); + +test('Test cognito dashboard role IAM policy', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + deployStackWithNewResources(stack, publicApi); + + expect(stack).toHaveResourceLike("AWS::IAM::Policy", { + PolicyDocument: { + Statement: [ + { + Action: [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + Effect: "Allow", + Resource: [ + { + "Fn::GetAtt": [ + "testconstructCognitoUserPoolA4991355", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":identitypool/", + { + Ref: "testconstructCognitoIdentityPool51EFD08D" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + Ref: "AWS::Region" + }, + ":", + { + Ref: "AWS::AccountId" + }, + ":domain/solutions-construct-domain" + ] + ] + } + ] + }, + { + Action: "iam:PassRole", + Condition: { + StringLike: { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + Effect: "Allow", + Resource: { + "Fn::GetAtt": [ + "testconstructCognitoDashboardConfigureRoleFA66EB70", + "Arn" + ] + } + } + ], + Version: "2012-10-17" + } + }); +}); + +test('Test custom environment variable name', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + new FargateToOpenSearch(stack, 'test-construct', { + publicApi, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + vpcProps: { cidr: '172.0.0.0/16' }, + clusterProps: { clusterName: CLUSTER_NAME }, + containerDefinitionProps: { CONTAINER_NAME }, + fargateTaskDefinitionProps: { family: FAMILY_NAME }, + fargateServiceProps: { serviceName: SERVICE_NAME }, + openSearchDomainName: DOMAIN_NAME, + domainEndpointEnvironmentVariableName: CUSTOM_ENV_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: CUSTOM_ENV_NAME, + Value: { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ], + Family: FAMILY_NAME + }); + +}); + +test('New service/new domain, public API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + deployStackWithNewResources(stack, publicApi); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + ServiceName: SERVICE_NAME + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + DomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Cluster", { + ClusterName: CLUSTER_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "DOMAIN_ENDPOINT", + Value: { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ], + Family: FAMILY_NAME + }); + + 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::OpenSearchService::Domain', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/new domain, private API, new VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + deployStackWithNewResources(stack, publicApi); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + DomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + ServiceName: SERVICE_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Cluster", { + ClusterName: CLUSTER_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "DOMAIN_ENDPOINT", + Value: { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ], + Family: FAMILY_NAME + }); + + 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::OpenSearchService::Domain', 1); + expect(stack).toCountResources('AWS::ECS::Service', 1); +}); + +test('New service/new domain, public API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + const existingVpc = defaults.getTestVpc(stack, publicApi); + + new FargateToOpenSearch(stack, 'test-construct', { + publicApi, + existingVpc, + openSearchDomainName: DOMAIN_NAME, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + DomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "DOMAIN_ENDPOINT", + Value: { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ], + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.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::ECS::Service', 1); + expect(stack).toCountResources('AWS::OpenSearchService::Domain', 1); +}); + +test('New service/new domain, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + const existingVpc = defaults.getTestVpc(stack, publicApi); + + new FargateToOpenSearch(stack, 'test-construct', { + publicApi, + existingVpc, + openSearchDomainName: DOMAIN_NAME, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + DomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + LaunchType: 'FARGATE', + DesiredCount: 2, + DeploymentConfiguration: { + MaximumPercent: 150, + MinimumHealthyPercent: 75 + }, + PlatformVersion: ecs.FargatePlatformVersion.LATEST, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "DOMAIN_ENDPOINT", + Value: { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + Essential: true, + Image: { + "Fn::Join": [ + "", + [ + "123456789012.dkr.ecr.us-east-1.", + { + Ref: "AWS::URLSuffix" + }, + "/fake-repo:latest" + ] + ] + }, + MemoryReservation: 512, + Name: "test-construct-container", + PortMappings: [ + { + ContainerPort: 8080, + Protocol: "tcp" + } + ] + } + ], + }); + + expect(stack).toHaveResourceLike("AWS::EC2::VPC", { + CidrBlock: '172.168.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::ECS::Service', 1); + expect(stack).toCountResources('AWS::OpenSearchService::Domain', 1); +}); + +test('Existing service/new domain, public API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = true; + + const existingVpc = defaults.getTestVpc(stack); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName: SERVICE_NAME }); + + new FargateToOpenSearch(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + openSearchDomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + DomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: SERVICE_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "DOMAIN_ENDPOINT", + Value: { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + 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::EC2::VPC", { + CidrBlock: '172.168.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::ECS::Service', 1); + expect(stack).toCountResources('AWS::OpenSearchService::Domain', 1); +}); + +test('Existing service/new domain, private API, existing VPC', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const existingVpc = defaults.getTestVpc(stack, publicApi); + + const [testService, testContainer] = defaults.CreateFargateService(stack, + 'test', + existingVpc, + undefined, + defaults.fakeEcrRepoArn, + undefined, + undefined, + undefined, + { serviceName: SERVICE_NAME }); + + new FargateToOpenSearch(stack, 'test-construct', { + publicApi, + existingFargateServiceObject: testService, + existingContainerDefinitionObject: testContainer, + existingVpc, + openSearchDomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::OpenSearchService::Domain", { + DomainName: DOMAIN_NAME + }); + + expect(stack).toHaveResourceLike("AWS::ECS::Service", { + ServiceName: SERVICE_NAME, + }); + + expect(stack).toHaveResourceLike("AWS::ECS::TaskDefinition", { + ContainerDefinitions: [ + { + Environment: [ + { + Name: "DOMAIN_ENDPOINT", + Value: { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + 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::EC2::VPC", { + CidrBlock: '172.168.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::ECS::Service', 1); + expect(stack).toCountResources('AWS::OpenSearchService::Domain', 1); +}); + +test('Check error for using OpenSearch VPC prop parameter', () => { + const stack = new cdk.Stack(); + const publicApi = false; + + const app = () => { + new FargateToOpenSearch(stack, 'test-construct', { + publicApi, + openSearchDomainName: DOMAIN_NAME, + ecrRepositoryArn: defaults.fakeEcrRepoArn, + openSearchDomainProps: { + vpcOptions: { securityGroupIds: ['fake-sg-id'] } + } + }); + }; + + expect(app).toThrowError("Error - Define VPC using construct parameters not the OpenSearch Service props"); +}); diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.expected.json new file mode 100644 index 000000000..6162b0439 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.expected.json @@ -0,0 +1,1567 @@ +{ + "Description": "Integration Test with new VPC, Service and OpenSearch Service domain", + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "172.168.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "172.168.0.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet1" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1RouteTableAssociation97140677" + ] + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "172.168.32.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet2" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTableAssociationDD5762D8" + ] + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "172.168.64.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PublicSubnet3" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02" + ] + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "172.168.96.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "172.168.128.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "172.168.160.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcFlowLogIAMRole6A475D41": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "vpc-flow-logs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcFlowLogIAMRoleDefaultPolicy406FB995": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogLogGroup7B5C56B9", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VpcFlowLogIAMRoleDefaultPolicy406FB995", + "Roles": [ + { + "Ref": "VpcFlowLogIAMRole6A475D41" + } + ] + } + }, + "VpcFlowLogLogGroup7B5C56B9": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "VpcFlowLog8FF33A73": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "Vpc8378EB38" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "VpcFlowLogLogGroup7B5C56B9" + }, + "Tags": [ + { + "Key": "Name", + "Value": "existing-resources/Vpc" + } + ] + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRAPIsecuritygroup78294485", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "existingresourcesECRDKRsecuritygroup598BA37E", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "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" + } + } + }, + "testtaskdefF924AD58": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "CUSTOM_NAME", + "Value": { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + "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" + } + } + }, + "testconstructCognitoUserPoolA4991355": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testconstructCognitoUserPoolClient36ACF012": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "testconstructCognitoUserPoolA4991355" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "testconstructCognitoIdentityPool51EFD08D": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "testconstructCognitoUserPoolClient36ACF012" + }, + "ProviderName": { + "Fn::GetAtt": [ + "testconstructCognitoUserPoolA4991355", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "testconstructUserPoolDomain6B64F32F": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "cogn-solution-constructs", + "UserPoolId": { + "Ref": "testconstructCognitoUserPoolA4991355" + } + }, + "DependsOn": [ + "testconstructCognitoUserPoolA4991355" + ] + }, + "testconstructCognitoAuthorizedRole7BA83790": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "testconstructCognitoIdentityPool51EFD08D" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/cogn-solution-constructs/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "testconstructIdentityPoolRoleMapping1391CBDD": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "testconstructCognitoIdentityPool51EFD08D" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "testconstructCognitoAuthorizedRole7BA83790", + "Arn" + ] + } + } + } + }, + "testconstructCognitoDashboardConfigureRoleFA66EB70": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testconstructCognitoDashboardConfigureRolePolicy3EC6F1C4": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testconstructCognitoUserPoolA4991355", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "testconstructCognitoIdentityPool51EFD08D" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solution-constructs" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testconstructCognitoDashboardConfigureRoleFA66EB70", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testconstructCognitoDashboardConfigureRolePolicy3EC6F1C4", + "Roles": [ + { + "Ref": "testconstructCognitoDashboardConfigureRoleFA66EB70" + } + ] + } + }, + "testconstructOpenSearchDomainD2A5B104": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "testconstructCognitoAuthorizedRole7BA83790", + "Arn" + ] + } + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solution-constructs/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "ClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 3 + }, + "ZoneAwarenessEnabled": true + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "testconstructCognitoIdentityPool51EFD08D" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testconstructCognitoDashboardConfigureRoleFA66EB70", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "testconstructCognitoUserPoolA4991355" + } + }, + "DomainName": "solution-constructs", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "OpenSearch_1.3", + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + }, + "VPCOptions": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testsg872EB48A", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only" + }, + { + "id": "W90", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + } + ] + } + } + }, + "testconstructStatusRedAlarmFBEA96DF": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testconstructStatusYellowAlarm3B8C3640": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testconstructFreeStorageSpaceTooLowAlarm08294658": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 20000 + } + }, + "testconstructIndexWritesBlockedTooHighAlarm06094A18": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testconstructAutomatedSnapshotFailureTooHighAlarm29E550A3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testconstructCPUUtilizationTooHighAlarmD32179B7": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testconstructJVMMemoryPressureTooHighAlarmFD4175A0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testconstructMasterCPUUtilizationTooHighAlarmAA50D0A7": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "testconstructMasterJVMMemoryPressureTooHighAlarm5AE37D64": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + } + }, + "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-opensearch/test/integ.existing-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.ts new file mode 100644 index 000000000..8067f8c1e --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.ts @@ -0,0 +1,55 @@ +/** + * 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-lib"; +import { FargateToOpenSearch, FargateToOpenSearchProps } from "../lib"; +import { generateIntegStackName, getTestVpc, CreateFargateService } from '@aws-solutions-constructs/core'; +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import * as defaults from '@aws-solutions-constructs/core'; + +// Setup +const app = new App(); +const stack = new Stack(app, generateIntegStackName(__filename), { + env: { account: Aws.ACCOUNT_ID, region: 'us-east-1' }, +}); +stack.templateOptions.description = 'Integration Test with new VPC, Service and OpenSearch Service domain'; + +const existingVpc = getTestVpc(stack); +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const [testService, testContainer] = CreateFargateService(stack, + 'test', + existingVpc, + undefined, + undefined, + undefined, + undefined, + { image }, +); + +const testProps: FargateToOpenSearchProps = { + publicApi: true, + existingVpc, + openSearchDomainName: 'solution-constructs', + cognitoDomainName: 'cogn-solution-constructs', + existingContainerDefinitionObject: testContainer, + existingFargateServiceObject: testService, + domainEndpointEnvironmentVariableName: 'CUSTOM_NAME', +}; + +new FargateToOpenSearch(stack, 'test-construct', testProps); + +defaults.suppressAutoDeleteHandlerWarnings(stack); +// Synth +app.synth(); \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.new-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.new-resources.expected.json new file mode 100644 index 000000000..d8b77ef9b --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.new-resources.expected.json @@ -0,0 +1,1567 @@ +{ + "Description": "Integration Test with new VPC, Service and OpenSearch Service domain", + "Resources": { + "testconstructCognitoUserPoolA4991355": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "UserPoolAddOns": { + "AdvancedSecurityMode": "ENFORCED" + }, + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "testconstructCognitoUserPoolClient36ACF012": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "testconstructCognitoUserPoolA4991355" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "testconstructCognitoIdentityPool51EFD08D": { + "Type": "AWS::Cognito::IdentityPool", + "Properties": { + "AllowUnauthenticatedIdentities": false, + "CognitoIdentityProviders": [ + { + "ClientId": { + "Ref": "testconstructCognitoUserPoolClient36ACF012" + }, + "ProviderName": { + "Fn::GetAtt": [ + "testconstructCognitoUserPoolA4991355", + "ProviderName" + ] + }, + "ServerSideTokenCheck": true + } + ] + } + }, + "testconstructUserPoolDomain6B64F32F": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "solution-constructs", + "UserPoolId": { + "Ref": "testconstructCognitoUserPoolA4991355" + } + }, + "DependsOn": [ + "testconstructCognitoUserPoolA4991355" + ] + }, + "testconstructCognitoAuthorizedRole7BA83790": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "cognito-identity.amazonaws.com:aud": { + "Ref": "testconstructCognitoIdentityPool51EFD08D" + } + }, + "ForAnyValue:StringLike": { + "cognito-identity.amazonaws.com:amr": "authenticated" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "cognito-identity.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solution-constructs/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CognitoAccessPolicy" + } + ] + } + }, + "testconstructIdentityPoolRoleMapping1391CBDD": { + "Type": "AWS::Cognito::IdentityPoolRoleAttachment", + "Properties": { + "IdentityPoolId": { + "Ref": "testconstructCognitoIdentityPool51EFD08D" + }, + "Roles": { + "authenticated": { + "Fn::GetAtt": [ + "testconstructCognitoAuthorizedRole7BA83790", + "Arn" + ] + } + } + } + }, + "testconstructCognitoDashboardConfigureRoleFA66EB70": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "es.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testconstructCognitoDashboardConfigureRolePolicy3EC6F1C4": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cognito-idp:DescribeUserPool", + "cognito-idp:CreateUserPoolClient", + "cognito-idp:DeleteUserPoolClient", + "cognito-idp:DescribeUserPoolClient", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:AdminUserGlobalSignOut", + "cognito-idp:ListUserPoolClients", + "cognito-identity:DescribeIdentityPool", + "cognito-identity:UpdateIdentityPool", + "cognito-identity:SetIdentityPoolRoles", + "cognito-identity:GetIdentityPoolRoles", + "es:UpdateDomainConfig" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "testconstructCognitoUserPoolA4991355", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:cognito-identity:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identitypool/", + { + "Ref": "testconstructCognitoIdentityPool51EFD08D" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solution-constructs" + ] + ] + } + ] + }, + { + "Action": "iam:PassRole", + "Condition": { + "StringLike": { + "iam:PassedToService": "cognito-identity.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "testconstructCognitoDashboardConfigureRoleFA66EB70", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "testconstructCognitoDashboardConfigureRolePolicy3EC6F1C4", + "Roles": [ + { + "Ref": "testconstructCognitoDashboardConfigureRoleFA66EB70" + } + ] + } + }, + "testconstructOpenSearchDomainD2A5B104": { + "Type": "AWS::OpenSearchService::Domain", + "Properties": { + "AccessPolicies": { + "Statement": [ + { + "Action": "es:ESHttp*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "testconstructCognitoAuthorizedRole7BA83790", + "Arn" + ] + } + }, + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:es:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":domain/solution-constructs/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "ClusterConfig": { + "DedicatedMasterCount": 3, + "DedicatedMasterEnabled": true, + "InstanceCount": 3, + "ZoneAwarenessConfig": { + "AvailabilityZoneCount": 3 + }, + "ZoneAwarenessEnabled": true + }, + "CognitoOptions": { + "Enabled": true, + "IdentityPoolId": { + "Ref": "testconstructCognitoIdentityPool51EFD08D" + }, + "RoleArn": { + "Fn::GetAtt": [ + "testconstructCognitoDashboardConfigureRoleFA66EB70", + "Arn" + ] + }, + "UserPoolId": { + "Ref": "testconstructCognitoUserPoolA4991355" + } + }, + "DomainName": "solution-constructs", + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10 + }, + "EncryptionAtRestOptions": { + "Enabled": true + }, + "EngineVersion": "OpenSearch_1.3", + "NodeToNodeEncryptionOptions": { + "Enabled": true + }, + "SnapshotOptions": { + "AutomatedSnapshotStartHour": 1 + }, + "VPCOptions": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "testconstructsgA602AA29", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W28", + "reason": "The OpenSearch Service domain is passed dynamically as as parameter and explicitly specified to ensure that IAM policies are configured to lockdown access to this specific OpenSearch Service instance only" + }, + { + "id": "W90", + "reason": "This is not a rule for the general case, just for specific use cases/industries" + } + ] + } + } + }, + "testconstructStatusRedAlarmFBEA96DF": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one primary shard and its replicas are not allocated to a node. ", + "MetricName": "ClusterStatus.red", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testconstructStatusYellowAlarm3B8C3640": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "At least one replica shard is not allocated to a node.", + "MetricName": "ClusterStatus.yellow", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testconstructFreeStorageSpaceTooLowAlarm08294658": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "A node in your cluster is down to 20 GiB of free storage space.", + "MetricName": "FreeStorageSpace", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Minimum", + "Threshold": 20000 + } + }, + "testconstructIndexWritesBlockedTooHighAlarm06094A18": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Your cluster is blocking write requests.", + "MetricName": "ClusterIndexWritesBlocked", + "Namespace": "AWS/ES", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testconstructAutomatedSnapshotFailureTooHighAlarm29E550A3": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "An automated snapshot failed. This failure is often the result of a red cluster health status.", + "MetricName": "AutomatedSnapshotFailure", + "Namespace": "AWS/ES", + "Period": 60, + "Statistic": "Maximum", + "Threshold": 1 + } + }, + "testconstructCPUUtilizationTooHighAlarmD32179B7": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "100% CPU utilization is not uncommon, but sustained high usage is problematic. Consider using larger instance types or adding instances.", + "MetricName": "CPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testconstructJVMMemoryPressureTooHighAlarmFD4175A0": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "JVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 80 + } + }, + "testconstructMasterCPUUtilizationTooHighAlarmAA50D0A7": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "AlarmDescription": "Average CPU utilization over last 45 minutes too high. Consider using larger instance types for your dedicated master nodes.", + "MetricName": "MasterCPUUtilization", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "testconstructMasterJVMMemoryPressureTooHighAlarm5AE37D64": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmDescription": "Average JVM memory pressure over last 15 minutes too high. Consider scaling vertically.", + "MetricName": "MasterJVMMemoryPressure", + "Namespace": "AWS/ES", + "Period": 900, + "Statistic": "Average", + "Threshold": 50 + } + }, + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.0.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet1" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1RouteTableAssociation97140677" + ] + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.32.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet2" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet2DefaultRoute97F91067", + "VpcPublicSubnet2RouteTableAssociationDD5762D8" + ] + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.64.0/19", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W33", + "reason": "Allow Public Subnets to have MapPublicIpOnLaunch set to true" + } + ] + } + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PublicSubnet3" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet3DefaultRoute4697774F", + "VpcPublicSubnet3RouteTableAssociation1F1EDF02" + ] + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "CidrBlock": "10.0.96.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "CidrBlock": "10.0.128.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "CidrBlock": "10.0.160.0/19", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "VpcFlowLogIAMRole6A475D41": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "vpc-flow-logs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcFlowLogIAMRoleDefaultPolicy406FB995": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogLogGroup7B5C56B9", + "Arn" + ] + } + }, + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "VpcFlowLogIAMRoleDefaultPolicy406FB995", + "Roles": [ + { + "Ref": "VpcFlowLogIAMRole6A475D41" + } + ] + } + }, + "VpcFlowLogLogGroup7B5C56B9": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain", + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W84", + "reason": "By default CloudWatchLogs LogGroups data is encrypted using the CloudWatch server-side encryption keys (AWS Managed Keys)" + } + ] + } + } + }, + "VpcFlowLog8FF33A73": { + "Type": "AWS::EC2::FlowLog", + "Properties": { + "ResourceId": { + "Ref": "Vpc8378EB38" + }, + "ResourceType": "VPC", + "TrafficType": "ALL", + "DeliverLogsPermissionArn": { + "Fn::GetAtt": [ + "VpcFlowLogIAMRole6A475D41", + "Arn" + ] + }, + "LogDestinationType": "cloud-watch-logs", + "LogGroupName": { + "Ref": "VpcFlowLogLogGroup7B5C56B9" + }, + "Tags": [ + { + "Key": "Name", + "Value": "new-resources/Vpc" + } + ] + } + }, + "VpcECRAPI9A3B6A2B": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.api", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRAPIsecuritygroupE52BAE3F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcECRDKR604E039F": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": "com.amazonaws.us-east-1.ecr.dkr", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "PrivateDnsEnabled": true, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "newresourcesECRDKRsecuritygroupBA34F94F", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "VpcEndpointType": "Interface" + } + }, + "VpcS3A5408339": { + "Type": "AWS::EC2::VPCEndpoint", + "Properties": { + "ServiceName": { + "Fn::Join": [ + "", + [ + "com.amazonaws.", + { + "Ref": "AWS::Region" + }, + ".s3" + ] + ] + }, + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "RouteTableIds": [ + { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + } + ], + "VpcEndpointType": "Gateway" + } + }, + "newresourcesECRAPIsecuritygroupE52BAE3F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_API-security-group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": { + "Fn::GetAtt": [ + "Vpc8378EB38", + "CidrBlock" + ] + }, + "Description": { + "Fn::Join": [ + "", + [ + "from ", + { + "Fn::GetAtt": [ + "Vpc8378EB38", + "CidrBlock" + ] + }, + ":443" + ] + ] + }, + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W5", + "reason": "Egress of 0.0.0.0/0 is default and generally considered OK" + }, + { + "id": "W40", + "reason": "Egress IPProtocol of -1 is default and generally considered OK" + } + ] + } + } + }, + "newresourcesECRDKRsecuritygroupBA34F94F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "new-resources/new-resources-ECR_DKR-security-group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": { + "Fn::GetAtt": [ + "Vpc8378EB38", + "CidrBlock" + ] + }, + "Description": { + "Fn::Join": [ + "", + [ + "from ", + { + "Fn::GetAtt": [ + "Vpc8378EB38", + "CidrBlock" + ] + }, + ":443" + ] + ] + }, + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "Metadata": { + "cfn_nag": { + "rules_to_suppress": [ + { + "id": "W5", + "reason": "Egress of 0.0.0.0/0 is default and generally considered OK" + }, + { + "id": "W40", + "reason": "Egress IPProtocol of -1 is default and generally considered OK" + } + ] + } + } + }, + "testconstructcluster7B6231C5": { + "Type": "AWS::ECS::Cluster" + }, + "testconstructtaskdefTaskRoleC60414C4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "testconstructtaskdef8BD1F9E4": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "DOMAIN_ENDPOINT", + "Value": { + "Fn::GetAtt": [ + "testconstructOpenSearchDomainD2A5B104", + "DomainEndpoint" + ] + } + } + ], + "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-opensearch/test/integ.new-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.new-resources.ts new file mode 100644 index 000000000..e102ed50f --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.new-resources.ts @@ -0,0 +1,41 @@ +/** + * 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-lib"; +import { FargateToOpenSearch, FargateToOpenSearchProps } from "../lib"; +import { generateIntegStackName, suppressAutoDeleteHandlerWarnings } from '@aws-solutions-constructs/core'; +import * as ecs from 'aws-cdk-lib/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 OpenSearch Service domain'; + +const image = ecs.ContainerImage.fromRegistry('nginx'); + +const testProps: FargateToOpenSearchProps = { + publicApi: true, + containerDefinitionProps: { + image + }, + openSearchDomainName: 'solution-constructs', +}; + +new FargateToOpenSearch(stack, 'test-construct', testProps); + +suppressAutoDeleteHandlerWarnings(stack); +// Synth +app.synth(); diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md index 350bf3b32..3654eff0c 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/README.md @@ -105,7 +105,7 @@ new LambdaToOpenSearch(this, "sample", |openSearchDomainName|`string`|Domain name for the OpenSearch Service.| |cognitoDomainName?|`string`|Optional Amazon Cognito domain name. If omitted the Amazon Cognito domain will default to the OpenSearch Service domain name.| |createCloudWatchAlarms?|`boolean`|Whether to create the recommended CloudWatch alarms.| -|domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch domain endpoint environment variable set for the Lambda function.| +|domainEndpointEnvironmentVariableName?|`string`|Optional name for the OpenSearch domain endpoint environment variable set for the Lambda function. Default is `DOMAIN_ENDPOINT`.| |existingVpc?|[`ec2.IVpc`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.IVpc.html)|An optional, existing VPC into which this pattern should be deployed. When deployed in a VPC, the Lambda function will use ENIs in the VPC to access network resources. If an existing VPC is provided, the `deployVpc` property cannot be `true`. This uses `ec2.IVpc` to allow clients to supply VPCs that exist outside the stack using the [`ec2.Vpc.fromLookup()`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html#static-fromwbrlookupscope-id-options) method.| |vpcProps?|[`ec2.VpcProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.VpcProps.html)|Optional user provided properties to override the default properties for the new VPC. `enableDnsHostnames`, `enableDnsSupport`, `natGateways` and `subnetConfiguration` are set by the pattern, so any values for those properties supplied here will be overridden. If `deployVpc` is not `true` then this property will be ignored.| |deployVpc?|`boolean`|Whether to create a new VPC based on `vpcProps` into which to deploy this pattern. Setting this to true will deploy the minimal, most private VPC to run the pattern:
  • One isolated subnet in each Availability Zone used by the CDK program
  • `enableDnsHostnames` and `enableDnsSupport` will both be set to true
If this property is `true` then `existingVpc` cannot be specified. Defaults to `false`.| diff --git a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts index dd152dfbc..0d8f39865 100644 --- a/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-lambda-opensearch/lib/index.ts @@ -117,10 +117,6 @@ export class LambdaToOpenSearch extends Construct { throw new Error("Error - Define VPC using construct parameters not Lambda function props"); } - if (props.openSearchDomainProps?.vpcOptions) { - throw new Error("Error - Define VPC using construct parameters not the OpenSearch Service props"); - } - if (props.deployVpc || props.existingVpc) { this.vpc = defaults.buildVpc(scope, { defaultVpcProps: defaults.DefaultIsolatedVpcProps(), 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 fb2340f25..e0c6dc06d 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/fargate-helper.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/fargate-helper.ts @@ -262,3 +262,11 @@ export function CheckFargateProps(props: any) { throw new Error(errorMessages); } } + +export function getServiceVpcSecurityGroupIds(service: ecs.FargateService): string[] { + const securityGroupIds: string[] = []; + + service.connections.securityGroups.forEach(element => securityGroupIds.push(element.securityGroupId)); + + return securityGroupIds; +} \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts b/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts index d0a9971d7..99b2b7fa6 100644 --- a/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts +++ b/source/patterns/@aws-solutions-constructs/core/lib/input-validation.ts @@ -24,6 +24,7 @@ import * as glue from 'aws-cdk-lib/aws-glue'; import * as sagemaker from 'aws-cdk-lib/aws-sagemaker'; import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; import * as kms from "aws-cdk-lib/aws-kms"; +import * as opensearch from "aws-cdk-lib/aws-opensearchservice"; export interface VerifiedProps { readonly dynamoTableProps?: dynamodb.TableProps, @@ -76,6 +77,8 @@ export interface VerifiedProps { readonly existingLoggingBucketObj?: s3.IBucket; readonly loggingBucketProps?: s3.BucketProps; readonly logS3AccessLogs?: boolean; + + readonly openSearchDomainProps?: opensearch.CfnDomainProps; } export function CheckProps(propsObject: VerifiedProps | any) { @@ -202,6 +205,10 @@ export function CheckProps(propsObject: VerifiedProps | any) { errorFound = true; } + if (propsObject.openSearchDomainProps?.vpcOptions) { + throw new Error("Error - Define VPC using construct parameters not the OpenSearch Service props"); + } + if (errorFound) { throw new Error(errorMessages); } From 63818a65526c931a1bb42a1bf9044523b68c8be7 Mon Sep 17 00:00:00 2001 From: mickychetta Date: Mon, 31 Oct 2022 02:23:54 +0000 Subject: [PATCH 6/6] Fixed variable names --- .../aws-fargate-opensearch/lib/index.ts | 4 ++-- .../aws-fargate-opensearch/test/fargate-opensearch.test.ts | 2 +- .../test/integ.existing-resources.expected.json | 2 +- .../aws-fargate-opensearch/test/integ.existing-resources.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/lib/index.ts index 89ca0c3fa..801c5a551 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/lib/index.ts @@ -203,8 +203,8 @@ export class FargateToOpenSearch extends Construct { } // Add environment variables - const bucketArnEnvironmentVariableName = props.domainEndpointEnvironmentVariableName || 'DOMAIN_ENDPOINT'; - this.container.addEnvironment(bucketArnEnvironmentVariableName, this.openSearchDomain.attrDomainEndpoint); + const domainEndpointEnvironmentVariableName = props.domainEndpointEnvironmentVariableName || 'DOMAIN_ENDPOINT'; + this.container.addEnvironment(domainEndpointEnvironmentVariableName, this.openSearchDomain.attrDomainEndpoint); } } diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/fargate-opensearch.test.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/fargate-opensearch.test.ts index 95f2b4540..85bbd8d95 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/fargate-opensearch.test.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/fargate-opensearch.test.ts @@ -63,7 +63,7 @@ test('Test domain and cognito domain name', () => { }); }); -test('Check construct property', () => { +test('Check construct properties', () => { const stack = new cdk.Stack(); const publicApi = true; diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.expected.json b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.expected.json index 6162b0439..7979e0e49 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.expected.json +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.expected.json @@ -1,5 +1,5 @@ { - "Description": "Integration Test with new VPC, Service and OpenSearch Service domain", + "Description": "Integration Test with existing VPC, Service and OpenSearch Service domain", "Resources": { "Vpc8378EB38": { "Type": "AWS::EC2::VPC", diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.ts index 8067f8c1e..dcd772c04 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-opensearch/test/integ.existing-resources.ts @@ -23,7 +23,7 @@ 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 OpenSearch Service domain'; +stack.templateOptions.description = 'Integration Test with existing VPC, Service and OpenSearch Service domain'; const existingVpc = getTestVpc(stack); const image = ecs.ContainerImage.fromRegistry('nginx');