@aws-cdk/core#Duration TypeScript Examples

The following examples show how to use @aws-cdk/core#Duration. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: webAppCloudFrontDistribution.ts    From aws-boilerplate with MIT License 6 votes vote down vote up
private createDeployment(staticFilesBucket: Bucket, distribution: CloudFrontWebDistribution, props: WebAppCloudFrontDistributionProps) {
        new BucketDeployment(this, 'DeployWebsite', {
            distribution,
            distributionPaths: ['/index.html'],
            sources: props.sources,
            destinationBucket: staticFilesBucket,
            cacheControl: [CacheControl.setPublic(), CacheControl.maxAge(Duration.hours(1))],
        });
    }
Example #2
Source File: webAppCloudFrontDistribution.ts    From aws-boilerplate with MIT License 6 votes vote down vote up
private createApiProxySourceConfig(props: WebAppCloudFrontDistributionProps): SourceConfiguration | null {
        if (!props.apiDomainName) {
            return null;
        }

        return {
            behaviors: [{
                pathPattern: '/api/*',
                allowedMethods: CloudFrontAllowedMethods.ALL,
                forwardedValues: {
                    queryString: true,
                    headers: ["Host"],
                    cookies: {forward: 'all'},
                },
                defaultTtl: Duration.seconds(0),
                minTtl: Duration.seconds(0),
                maxTtl: Duration.seconds(0),
            }],
            customOriginSource: {
                domainName: props.apiDomainName,
                originProtocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,
            },
        };
    }
Example #3
Source File: city-stack.ts    From MDDL with MIT License 6 votes vote down vote up
/**
   * Create the audit log queue for the stack.
   * @param kmsKey The encryption key for the log group
   */
  private createDeadLetterQueue(kmsKey: IKey) {
    const queue = new Queue(this, 'DeadLetterQueue', {
      encryptionMasterKey: kmsKey,
      retentionPeriod: Duration.days(10),
      fifo: true,
    })
    return {
      queue,
    }
  }
Example #4
Source File: auth-stack.ts    From MDDL with MIT License 6 votes vote down vote up
private addTriggers(userPool: UserPool) {
    const parameterPath = `/${this.stackName}/clientWebApps`
    const customMessageHandler = new Function(this, 'CustomMessageHandler', {
      code: Code.fromAsset(
        pathToApiServiceLambda('emails/cognitoCustomMessageHandler'),
      ),
      environment: {
        CLIENT_WEB_APP_PARAMETER_PATH: parameterPath,
      },
      handler: 'index.handler',
      memorySize: 256,
      timeout: Duration.seconds(10),
      runtime: Runtime.NODEJS_12_X,
    })
    customMessageHandler.addToRolePolicy(
      new PolicyStatement({
        actions: ['ssm:GetParameter'],
        resources: [
          `arn:${this.partition}:ssm:${this.region}:${this.account}:parameter${parameterPath}/*`,
        ],
      }),
    )

    userPool.addTrigger(UserPoolOperation.CUSTOM_MESSAGE, customMessageHandler)
  }
Example #5
Source File: city-stack.ts    From MDDL with MIT License 6 votes vote down vote up
/**
   * Create the email processing queue for the stack.
   * @param kmsKey The encryption key for the log group
   * @param deadLetterQueue The dead letter queue to user
   */
  private createEmailProcessorQueue(kmsKey: IKey, deadLetterQueue: Queue) {
    const queue = new Queue(this, 'EmailProcessorQueue', {
      encryptionMasterKey: kmsKey,
      fifo: true,
      contentBasedDeduplication: true,
      retentionPeriod: Duration.days(10),
      visibilityTimeout: Duration.seconds(60),
      deadLetterQueue: {
        queue: deadLetterQueue,
        maxReceiveCount: 50,
      },
    })

    return {
      queue,
    }
  }
Example #6
Source File: city-stack.ts    From MDDL with MIT License 6 votes vote down vote up
/**
   * Create the audit log queue for the stack.
   * @param kmsKey The encryption key for the log group
   * @param deadLetterQueue The dead letter queue to user
   */
  private createAuditLogQueue(kmsKey: IKey, deadLetterQueue: Queue) {
    const queue = new Queue(this, 'AuditLogQueue', {
      encryptionMasterKey: kmsKey,
      fifo: true,
      retentionPeriod: Duration.days(10),
      visibilityTimeout: Duration.seconds(60),
      deadLetterQueue: {
        queue: deadLetterQueue,
        maxReceiveCount: 50,
      },
    })

    return {
      queue,
    }
  }
Example #7
Source File: applicationMultipleTargetGroupsFargateServiceBase.ts    From aws-boilerplate with MIT License 5 votes vote down vote up
protected registerECSTargets(service: BaseService, container: ContainerDefinition, targets: ApplicationTargetProps[]): ApplicationTargetGroup {

        targets?.forEach((targetProps, index) => {
            const idSuffix = index > 0 ? index : '';
            const targetGroup = new ApplicationTargetGroup(this, `TargetGroup${idSuffix}`, {
                vpc: service.cluster.vpc,
                port: targetProps.containerPort,
                healthCheck: {
                    path: '/lbcheck',
                    protocol: ELBProtocol.HTTP,
                    interval: Duration.seconds(6),
                    timeout: Duration.seconds(5),
                    healthyThresholdCount: 2,
                    unhealthyThresholdCount: 2,
                },
                deregistrationDelay: Duration.seconds(10),
                targetType: TargetType.IP,
                targets: [
                    service.loadBalancerTarget({
                        containerName: container.containerName,
                        containerPort: targetProps.containerPort,
                        protocol: targetProps.protocol,
                    }),
                ],
            });

            this.findListener(targetProps.listener).addTargetGroups(`ECSTargetGroup${idSuffix}${container.containerName}${targetProps.containerPort}`, {
                hostHeader: targetProps.hostHeader,
                pathPattern: targetProps.pathPattern,
                priority: targetProps.priority,
                targetGroups: [targetGroup],
            });
            this.targetGroups.push(targetGroup);
        });

        if (this.targetGroups.length === 0) {
            throw new Error('At least one target group should be specified.');
        }
        return this.targetGroups[0];
    }
Example #8
Source File: mainEcsCluster.ts    From aws-boilerplate with MIT License 5 votes vote down vote up
private createPublicLoadBalancer(props: MainECSClusterProps): ApplicationLoadBalancer {
        const securityGroup = new SecurityGroup(this, "ALBSecurityGroup", {
            vpc: props.vpc,
        });
        securityGroup.addIngressRule(Peer.anyIpv4(), Port.allTraffic());

        const publicLoadBalancer = new ApplicationLoadBalancer(this, "ALB", {
            vpc: props.vpc,
            internetFacing: true,
            securityGroup: securityGroup,
            idleTimeout: Duration.seconds(30),
            vpcSubnets: {subnetType: SubnetType.PUBLIC, onePerAz: true},
        });

        const httpsListener = publicLoadBalancer.addListener("HttpsListener", {
            protocol: ApplicationProtocol.HTTPS,
            open: true,
            port: 443,
            defaultTargetGroups: [new ApplicationTargetGroup(this, 'DummyTargetGroup', {
                vpc: props.vpc,
                port: 80,
                targetGroupName: `${props.envSettings.projectEnvName}-dtg`,
                targetType: TargetType.IP,
            })]
        });

        httpsListener.addCertificates("Certificate", [
            ListenerCertificate.fromCertificateManager(props.certificate),
        ]);

        new CfnOutput(this, "PublicLoadBalancerSecurityGroupIdOutput", {
            exportName: MainECSCluster.getPublicLoadBalancerSecurityGroupIdOutputExportName(props.envSettings),
            value: securityGroup.securityGroupId,
        });

        new CfnOutput(this, "PublicLoadBalancerDnsNameOutput", {
            exportName: MainECSCluster.getLoadBalancerDnsNameOutput(props.envSettings),
            value: publicLoadBalancer.loadBalancerDnsName,
        });

        new CfnOutput(this, "PublicLoadBalancerArnOutput", {
            exportName: MainECSCluster.getLoadBalancerArnOutputExportName(props.envSettings),
            value: publicLoadBalancer.loadBalancerArn,
        });

        new CfnOutput(this, "PublicLoadBalancerCanonicalHostedZoneIdOutput", {
            exportName: MainECSCluster.getLoadBalancerCanonicalHostedZoneIdOutputExportName(props.envSettings),
            value: publicLoadBalancer.loadBalancerCanonicalHostedZoneId,
        });

        new CfnOutput(this, "PublicLoadBalancerHttpsListenerArn", {
            exportName: MainECSCluster.getLoadBalancerHttpsListenerArnOutputExportName(props.envSettings),
            value: httpsListener.listenerArn,
        })

        return publicLoadBalancer;
    }
Example #9
Source File: stack.ts    From aws-boilerplate with MIT License 5 votes vote down vote up
constructor(scope: core.App, id: string, props: MigrationsStackProps) {
        super(scope, id, props);

        const {envSettings} = props;
        const resources = new FargateServiceResources(this, "MigrationsResources", props);

        const containerName = 'migrations';
        const dbSecretArn = Fn.importValue(MainDatabase.getDatabaseSecretArnOutputExportName(envSettings));
        const taskRole = this.createTaskRole(props);

        const migrationsTaskDefinition = new FargateTaskDefinition(this, "MigrationsTaskDefinition", {
            taskRole,
            cpu: 256,
            memoryLimitMiB: 512,
        });

        const containerDef = migrationsTaskDefinition.addContainer(containerName, {
            image: ContainerImage.fromEcrRepository(resources.backendRepository, envSettings.version),
            logging: this.createAWSLogDriver(this.node.id),
            environment: {
                "CHAMBER_SERVICE_NAME": this.getChamberServiceName(envSettings),
                "CHAMBER_KMS_KEY_ALIAS": MainKmsKey.getKeyAlias(envSettings),
            },
            secrets: {
                "DB_CONNECTION": EcsSecret.fromSecretsManager(
                    Secret.fromSecretArn(this, "DbSecret", dbSecretArn))
            },
        });

        new StateMachine(this, "MigrationsStateMachine", {
            stateMachineName: `${envSettings.projectEnvName}-migrations`,
            definition: new Task(this, "MigrationsFargateTask", {
                task: new RunEcsFargateTask({
                    cluster: resources.mainCluster,
                    taskDefinition: migrationsTaskDefinition,
                    assignPublicIp: true,
                    securityGroup: resources.fargateContainerSecurityGroup,
                    containerOverrides: [
                        {
                            containerDefinition: containerDef,
                            command: ['./scripts/run_migrations.sh'],
                        }
                    ],
                    integrationPattern: ServiceIntegrationPattern.SYNC,
                }),
            }),
            timeout: Duration.minutes(5),
        });
    }
Example #10
Source File: basestack.ts    From sourcestack with MIT License 5 votes vote down vote up
frontend() {
        this.bucket = new S3.Bucket(this, Config.instance.appEnv + '-hosting-bucket', {
            bucketName: Config.instance.appEnv + '-hosting-bucket',
            websiteIndexDocument: 'index.html',
            websiteErrorDocument: 'index.html',
            removalPolicy: cdk.RemovalPolicy.DESTROY,
        });
        let oai = new OriginAccessIdentity(this, Config.instance.appEnv + '-oai');
        let distributionProps: CloudFrontWebDistributionProps = {
            comment: Config.instance.appEnv + ' distribution',
            aliasConfiguration: Config.instance.domain ? {
                names: [Config.instance.subdomain],
                acmCertRef: Config.instance.certificateArn,
                sslMethod: SSLMethod.SNI
            } : undefined,
            originConfigs: [{
                s3OriginSource: {
                    s3BucketSource: this.bucket,
                    originAccessIdentity: oai,
                },
                behaviors: [{ isDefaultBehavior: true }],
            }, {
                customOriginSource: {
                    domainName: `${this.endpoint.restApiId}.execute-api.${this.region}.${this.urlSuffix}`
                },
                originPath: `/${this.endpoint.deploymentStage.stageName}`,
                behaviors: [{
                    pathPattern: '/api/*',
                    allowedMethods: CloudFrontAllowedMethods.ALL,
                    defaultTtl: Duration.millis(0),
                    maxTtl: Duration.millis(0),
                    minTtl: Duration.millis(0),
                    forwardedValues: {
                        queryString: true,
                        cookies: {
                            forward: 'all'
                        },
                        // This doesn't work - bug in CDK?
                        // headers: ['*'],
                    }
                }]
            }]
        }
        this.distribution = new CloudFrontWebDistribution(this, Config.instance.appEnv + '-distribution', distributionProps);
        if (Config.instance.domain){
            new route53.CfnRecordSet(this, Config.instance.appEnv + '-recordset', {
                hostedZoneName: Config.instance.domain + '.',
                name: Config.instance.subdomain,
                type: 'A',
                aliasTarget: {
                    hostedZoneId: 'Z2FDTNDATAQYW2', // cloudfront.net
                    dnsName: this.distribution.domainName
                }
            })
        }
        this.bucket.addToResourcePolicy(new IAM.PolicyStatement({
            effect: IAM.Effect.ALLOW,
            resources: [
                this.bucket.bucketArn,
                this.bucket.bucketArn + '/*'
            ],
            actions: [
                's3:GetBucket*',
                's3:GetObject*',
                's3:List*',
            ],
            principals: [
                new IAM.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)
            ]
        }));

    }
Example #11
Source File: city-stack.ts    From MDDL with MIT License 5 votes vote down vote up
/**
   * Runs DB migrations using Knex
   * @param dbSecret The DB secret for accessing the database
   * @param mysqlLayer The mysql layer
   */
  private runMigrations(dbSecret: ISecret, mysqlLayer: ILayerVersion) {
    const runMigrationsFunction = new Function(this, 'RunMigrationsFunction', {
      code: Code.fromAsset(
        join(__dirname, '..', '..', 'api-service', 'dist', 'migrator'),
      ),
      handler: 'index.handler',
      runtime: Runtime.NODEJS_12_X,
      vpc: this.lambdaVpc,
      securityGroups: this.lambdaSecurityGroups,
      layers: [mysqlLayer],
      timeout: Duration.seconds(60),
      memorySize: 512,
      environment: {
        DB_HOST: this.rdsEndpoint,
        DB_USER: dbSecret.secretValueFromJson('username').toString(),
        DB_PASSWORD: dbSecret.secretValueFromJson('password').toString(),
        DB_NAME: dbSecret.secretValueFromJson('username').toString(),
      },
    })

    // create a custom resource provider
    const runMigrationsResourceProvider = new Provider(
      this,
      'RunMigrationsCustomResourceProvider',
      {
        onEventHandler: runMigrationsFunction,
        logRetention: RetentionDays.ONE_DAY,
      },
    )

    return {
      resource: new CustomResource(this, 'RunMigrationsCustomResource', {
        serviceToken: runMigrationsResourceProvider.serviceToken,
        properties: {
          // Dynamic prop to force execution each time
          Execution: Math.random().toString(36).substr(2),
        },
      }),
    }
  }
Example #12
Source File: actions-stack.ts    From hasura-cdk with MIT License 5 votes vote down vote up
constructor(scope: Construct, id: string, props: ActionsStackProps) {
        super(scope, id, props);

        const hostedZone = PublicHostedZone.fromHostedZoneAttributes(this, 'HasuraHostedZone', {
            hostedZoneId: props.hostedZoneId,
            zoneName: props.hostedZoneName,
        });

        const api = new RestApi(this, 'ActionsApi', {
            domainName: {
                domainName: props.actionsHostname,
                certificate: props.certificates.actions,
            },
            restApiName: 'Actions',
            description: 'Endpoint For Hasura Actions',
            deployOptions: {
                loggingLevel: MethodLoggingLevel.INFO,
                dataTraceEnabled: true,
            },
        });

        // API DNS record
        new ARecord(this, 'ActionsApiAliasRecord', {
            zone: hostedZone,
            recordName: props.actionsHostname,
            target: AddressRecordTarget.fromAlias(new route53_targets.ApiGateway(api)),
        });

        // Create a lambda layer to contain node_modules
        const handlerDependenciesLayer = new RetainedLambdaLayerVersion(this, 'ActionHandlerDependencies', {
            contentLocation: 'actions/dependencies-layer',
            description: 'Dependencies layer',
            compatibleRuntimes: [Runtime.NODEJS_12_X],
        });

        const actionHandler = new Function(this, 'ActionHandler', {
            functionName: `${props.appName}-ActionHandler`,
            handler: 'handler.handler',
            memorySize: 1024,
            runtime: Runtime.NODEJS_12_X,
            code: Code.fromAsset(path.join(__dirname, '../../actions/dist/')),
            timeout: Duration.seconds(4),
            layers: [handlerDependenciesLayer],
        });

        const handlerResource = api.root.addResource('handler');
        const actionHandlerIntegration = new LambdaIntegration(actionHandler);

        handlerResource.addMethod('POST', actionHandlerIntegration);
        handlerResource.addMethod('GET', actionHandlerIntegration);

    }
Example #13
Source File: city-stack.ts    From MDDL with MIT License 5 votes vote down vote up
private configureAlerts(
    alertsSnsTopicArn: string,
    api: HttpApi,
    deadLetterQueue: IQueue,
    processorQueues: IQueue[],
  ) {
    const alarms = [
      // TODO alarm for server errors from the API - needs some metric math to be a percentile
      // api
      //   .metricServerError({
      //     statistic: 'sum',
      //   })
      //   .createAlarm(this, 'ServerErrorsAlarm', {
      //     evaluationPeriods: 1,
      //     threshold: 0.2,
      //   }),

      // alarm for any messages on the dead letter queue
      deadLetterQueue
        .metricApproximateNumberOfMessagesVisible()
        .createAlarm(this, 'DeadLetterQueueMessagesVisible', {
          threshold: 1,
          evaluationPeriods: 1,
        }),

      // alarm for any queue that has had a message sitting on it for 24 hours or more
      ...processorQueues.map((q) =>
        q
          .metricApproximateAgeOfOldestMessage()
          .createAlarm(this, `ProcessingStalled${q.node.id}`, {
            threshold: Duration.days(1).toSeconds(),
            evaluationPeriods: 1,
          }),
      ),
    ]

    const alertTopic = Topic.fromTopicArn(this, 'AlertTopic', alertsSnsTopicArn)

    alarms.forEach((a) => {
      a.addAlarmAction(new SnsAction(alertTopic))
    })
  }
Example #14
Source File: city-stack.ts    From MDDL with MIT License 5 votes vote down vote up
/**
   * Create a lambda function with a standard configuration
   * @param name The name for constructs
   * @param path The path to the entrypoint
   * @param props Extra properties for the function
   */
  private createLambda(
    name: string,
    path: string,
    props: {
      handler?: string
      dbSecret?: ISecret
      extraEnvironmentVariables?: EnvironmentVariables[]
      layers?: ILayerVersion[]
      extraFunctionProps?: Partial<FunctionProps>
      documentBucketPermissions?: BucketPermissions
      collectionBucketPermissions?: BucketPermissions
      auditLogSqsPermissions?: SqsPermissions
      emailProcessorSqsPermissions?: SqsPermissions
      auditLogGroupPermissions?: LogGroupPermissions
    },
  ) {
    const {
      handler = 'index.handler',
      dbSecret,
      extraEnvironmentVariables = [],
      extraFunctionProps = {},
      layers,
    } = props
    const requiresDbConnectivity = !!dbSecret
    const dbParams: { [key: string]: string } = dbSecret
      ? {
          DB_HOST: this.rdsEndpoint,
          DB_USER: dbSecret.secretValueFromJson('username').toString(),
          DB_PASSWORD: dbSecret.secretValueFromJson('password').toString(),
          DB_NAME: dbSecret.secretValueFromJson('username').toString(),
        }
      : {}
    const environment: {
      [key: string]: string
    } = {
      NODE_ENV: 'production',
      ...dbParams,
    }

    // add specified environment variables
    extraEnvironmentVariables.forEach((e) => {
      if (this.environmentVariables[e])
        environment[e] = this.environmentVariables[e]
    })

    // add common environment variables
    commonEnvironmentVariables.forEach((e) => {
      if (this.environmentVariables[e])
        environment[e] = this.environmentVariables[e]
    })

    const lambda = new Function(this, name, {
      ...extraFunctionProps,
      code: Code.fromAsset(path),
      handler,
      environment,
      memorySize: 512,
      timeout: Duration.seconds(60),
      layers,
      runtime: Runtime.NODEJS_12_X,
      vpc: requiresDbConnectivity ? this.lambdaVpc : undefined,
      securityGroups: requiresDbConnectivity
        ? this.lambdaSecurityGroups
        : undefined,
      tracing: Tracing.ACTIVE,
    })
    const cfnLambda = lambda.node.defaultChild as CfnFunction
    cfnLambda.addPropertyOverride('KmsKeyArn', this.kmsKey.keyArn)
    this.addPermissionsToLambda(lambda, props)
    return lambda
  }
Example #15
Source File: city-stack.ts    From MDDL with MIT License 4 votes vote down vote up
/**
   * Adds collection specific routes to the API
   * @param apiProps Common properties for API functions
   */
  private addCollectionRoutes(apiProps: ApiProps) {
    const { api, dbSecret, mySqlLayer, authorizer } = apiProps

    // add route and lambda to list collections documents
    this.addRoute(api, {
      name: 'GetCollectionDocuments',
      routeKey: 'GET /collections/{collectionId}/documents',
      lambdaFunction: this.createLambda(
        'GetCollectionDocuments',
        pathToApiServiceLambda('collections/getDocumentsByCollectionId'),
        {
          dbSecret,
          layers: [mySqlLayer],
          extraEnvironmentVariables: [
            ...authEnvironmentVariables,
            EnvironmentVariables.DOCUMENTS_BUCKET,
          ],
          // permission needed to create presigned urls for thumbnails
          documentBucketPermissions: {
            includeRead: true,
          },
        },
      ),
      authorizer,
      throttlingSettings: ReadRouteDefaultThrottling,
    })

    // add route and lambda to list collections grants
    this.addRoute(api, {
      name: 'GetCollectionGrants',
      routeKey: 'GET /collections/{collectionId}/grants',
      lambdaFunction: this.createLambda(
        'GetCollectionGrants',
        pathToApiServiceLambda('collections/getGrantsByCollectionId'),
        {
          dbSecret,
          layers: [mySqlLayer],
          extraEnvironmentVariables: [...authEnvironmentVariables],
        },
      ),
      authorizer,
      throttlingSettings: ReadRouteDefaultThrottling,
    })

    // add lambda to create a zip of collection documents
    const createCollectionZip = this.createLambda(
      'CreateCollectionZip',
      pathToApiServiceLambda('collections/createCollectionZip'),
      {
        dbSecret,
        layers: [mySqlLayer],
        extraEnvironmentVariables: [EnvironmentVariables.DOCUMENTS_BUCKET],
        extraFunctionProps: {
          timeout: Duration.seconds(900),
        },
        documentBucketPermissions: {
          // can read documents
          includeRead: true,
        },
        collectionBucketPermissions: {
          // can read and write collections
          includeRead: true,
          includeWrite: true,
          includeTagging: true,
        },
      },
    )
    this.environmentVariables[
      EnvironmentVariables.CREATE_COLLECTION_ZIP_FUNCTION_NAME
    ] = createCollectionZip.functionName

    // add lambda to create a collection download
    const downloadCollectionDocuments = this.createLambda(
      'DownloadCollectionDocuments',
      pathToApiServiceLambda('collections/downloadCollectionDocuments'),
      {
        dbSecret,
        layers: [mySqlLayer],
        extraEnvironmentVariables: [
          ...authEnvironmentVariables,
          EnvironmentVariables.DOCUMENTS_BUCKET,
          EnvironmentVariables.CREATE_COLLECTION_ZIP_FUNCTION_NAME,
          EnvironmentVariables.ACTIVITY_RECORD_SQS_QUEUE_URL,
        ],
        collectionBucketPermissions: {
          // can read collections
          includeRead: true,
        },
        auditLogSqsPermissions: {
          includeWrite: true,
        },
      },
    )

    createCollectionZip.grantInvoke(downloadCollectionDocuments)
    // add route to create a collection download
    this.addRoute(api, {
      name: 'DownloadCollectionDocuments',
      routeKey: 'POST /collections/{collectionId}/documents/downloads',
      lambdaFunction: downloadCollectionDocuments,
      authorizer,
      throttlingSettings: WriteRouteDefaultThrottling,
    })

    // add route and lambda to fetch a collection download
    this.addRoute(api, {
      name: 'GetDownloadForCollectionDocuments',
      routeKey:
        'GET /collections/{collectionId}/documents/downloads/{downloadId}',
      lambdaFunction: this.createLambda(
        'GetDownloadForCollectionDocuments',
        pathToApiServiceLambda('collections/getDownloadForCollectionDocuments'),
        {
          dbSecret,
          layers: [mySqlLayer],
          extraEnvironmentVariables: [
            ...authEnvironmentVariables,
            EnvironmentVariables.DOCUMENTS_BUCKET,
            EnvironmentVariables.ACTIVITY_RECORD_SQS_QUEUE_URL,
          ],
          collectionBucketPermissions: {
            // can read collections
            includeRead: true,
          },
          auditLogSqsPermissions: {
            includeWrite: true,
          },
        },
      ),
      authorizer,
      throttlingSettings: ReadRouteDefaultThrottling,
    })
  }
Example #16
Source File: backend-stack.ts    From flect-chime-sdk-demo with Apache License 2.0 4 votes vote down vote up
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        ///////////////////////////////
        //// Authentication
        ///////////////////////////////
        ////  (1) Cognito User Pool
        const userPool = new UserPool(this, `${id}_UserPool`, {
            userPoolName: `${id}_UserPool`,
            selfSignUpEnabled: true,
            autoVerify: {
                email: true,
            },
            passwordPolicy: {
                minLength: 6,
                requireSymbols: false,
            },
            signInAliases: {
                email: true,
            },
        });

        const userPoolClient = new UserPoolClient(this, id + "_UserPool_Client", {
            userPoolClientName: `${id}_UserPoolClient`,
            userPool: userPool,
            accessTokenValidity: Duration.minutes(1440),
            idTokenValidity: Duration.minutes(1440),
            refreshTokenValidity: Duration.days(30),
        });

        //// (2) Policy Statement
        const statement = new PolicyStatement({
            effect: Effect.ALLOW,
        });
        statement.addActions(
            "cognito-idp:GetUser",
            "cognito-idp:AdminGetUser",

            "chime:CreateMeeting",
            "chime:DeleteMeeting",
            "chime:GetMeeting",
            "chime:ListMeetings",
            "chime:BatchCreateAttendee",
            "chime:CreateAttendee",
            "chime:DeleteAttendee",
            "chime:GetAttendee",
            "chime:ListAttendees",
            "chime:StartMeetingTranscription",
            "chime:StopMeetingTranscription",

            "execute-api:ManageConnections",

            "ecs:RunTask",
            "ecs:DescribeTasks",
            "ecs:UpdateService",
            "ecs:DescribeServices",

            `ec2:DescribeNetworkInterfaces`,

            "iam:PassRole"
        );
        statement.addResources(userPool.userPoolArn);
        statement.addResources("arn:*:chime::*:meeting/*");
        statement.addResources("arn:aws:execute-api:*:*:**/@connections/*");
        statement.addResources("arn:aws:ecs:*");
        statement.addResources("*");
        statement.addResources("arn:aws:iam::*:*");

        //////////////////////////////////////
        //// Storage Resources (S3)
        //////////////////////////////////////
        const bucket = new s3.Bucket(this, "StaticSiteBucket", {
            bucketName: `${id}-Bucket`.toLowerCase(),
            removalPolicy: cdk.RemovalPolicy.DESTROY,
            publicReadAccess: true,
        });

        let cdn: cloudfront.CloudFrontWebDistribution;
        if (USE_CDN) {
            const oai = new cloudfront.OriginAccessIdentity(this, "my-oai");

            const myBucketPolicy = new iam.PolicyStatement({
                effect: iam.Effect.ALLOW,
                actions: ["s3:GetObject"],
                principals: [new iam.CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)],
                resources: [bucket.bucketArn + "/*"],
            });
            bucket.addToResourcePolicy(myBucketPolicy);

            // Create CloudFront WebDistribution
            cdn = new cloudfront.CloudFrontWebDistribution(this, "WebsiteDistribution", {
                viewerCertificate: {
                    aliases: [],
                    props: {
                        cloudFrontDefaultCertificate: true,
                    },
                },
                priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
                originConfigs: [
                    {
                        s3OriginSource: {
                            s3BucketSource: bucket,
                            originAccessIdentity: oai,
                        },
                        behaviors: [
                            {
                                isDefaultBehavior: true,
                                minTtl: cdk.Duration.seconds(0),
                                maxTtl: cdk.Duration.days(365),
                                defaultTtl: cdk.Duration.days(1),
                                pathPattern: "my-contents/*",
                            },
                        ],
                    },
                ],
                errorConfigurations: [
                    {
                        errorCode: 403,
                        responsePagePath: "/index.html",
                        responseCode: 200,
                        errorCachingMinTtl: 0,
                    },
                    {
                        errorCode: 404,
                        responsePagePath: "/index.html",
                        responseCode: 200,
                        errorCachingMinTtl: 0,
                    },
                ],
            });
        }

        //////////////////////////////////////
        //// Storage Resources (DynamoDB)
        //////////////////////////////////////
        //// (1) Meeting Table
        const meetingTable = new Table(this, "meetingTable", {
            tableName: `${id}_MeetingTable`,
            partitionKey: {
                name: "MeetingName",
                type: AttributeType.STRING,
            },
            readCapacity: 2,
            writeCapacity: 2,
            removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
        });

        meetingTable.addGlobalSecondaryIndex({
            indexName: "MeetingId",
            partitionKey: {
                name: "MeetingId",
                type: AttributeType.STRING,
            },
            projectionType: ProjectionType.ALL,
            readCapacity: 2,
            writeCapacity: 2,
        });

        //// (2) Attendee Table
        const attendeeTable = new Table(this, "attendeeTable", {
            tableName: `${id}_AttendeeTable`,
            partitionKey: {
                name: "AttendeeId",
                type: AttributeType.STRING,
            },
            readCapacity: 2,
            writeCapacity: 2,
            removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
        });

        //// (3) Connection Table
        const connectionTable = new Table(this, "connectionTable", {
            tableName: `${id}_ConnectionTable`,
            partitionKey: {
                name: "MeetingId",
                type: AttributeType.STRING,
            },
            sortKey: {
                name: "AttendeeId",
                type: AttributeType.STRING,
            },
            readCapacity: 2,
            writeCapacity: 2,
            removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
        });

        //// (x) SlackFederationAccounts Table
        const slackFederationAuthsTable = new Table(this, "slackFederationAuthsTable", {
            tableName: `${id}_SlackFederationAuthsTable`,
            partitionKey: {
                name: "TeamId",
                type: AttributeType.STRING,
            },
            readCapacity: 2,
            writeCapacity: 2,
            removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
        });

        /////////////////////
        /// Fargate
        /////////////////////
        const vpc = new ec2.Vpc(this, `${id}_vpc`, {
            maxAzs: 2,
            subnetConfiguration: [
                {
                    cidrMask: 24,
                    name: `${id}_pub`,
                    subnetType: ec2.SubnetType.PUBLIC,
                },
            ],
        });
        const cluster = new ecs.Cluster(this, `${id}_cluster`, { vpc });
        const logGroup = new logs.LogGroup(this, `${id}_logGroup`, {
            logGroupName: `/${id}-fargate`,
            removalPolicy: cdk.RemovalPolicy.DESTROY,
        });

        // create a task definition with CloudWatch Logs
        const logging = new ecs.AwsLogDriver({
            logGroup: logGroup,
            streamPrefix: "fargate",
        });

        const taskDefinition = new ecs.FargateTaskDefinition(this, `${id}_fargate_task`, {
            family: this.node.tryGetContext("serviceName"),
            cpu: 2048,
            // memoryLimitMiB: 5120,
            memoryLimitMiB: 8192,
        });

        if (USE_DOCKER) {
            const container = taskDefinition.addContainer("DefaultContainer", {
                containerName: `${id}_manager_container`,
                image: ecs.ContainerImage.fromAsset("lib/manager"),
                cpu: 2048,
                // memoryLimitMiB: 4096,
                memoryLimitMiB: 8192,
                logging: logging,
            });
        } else {
            const image = ecs.ContainerImage.fromRegistry(`dannadori/hmm:latest`);
            const container = taskDefinition.addContainer("DefaultContainer", {
                containerName: `${id}_manager_container`,
                image: image,
                cpu: 2048,
                memoryLimitMiB: 8192,
                // memoryLimitMiB: 5120,
                logging: logging,
            });
        }

        bucket.grantReadWrite(taskDefinition.taskRole);

        const securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
            vpc: vpc,
        });
        securityGroup.addIngressRule(ec2.Peer.ipv4("0.0.0.0/0"), ec2.Port.tcp(3000));

        ///////////////////////////////
        //// Lambda Layers
        ///////////////////////////////
        // ( - ) Lambda Layer
        const nodeModulesLayer = new lambda.LayerVersion(this, "NodeModulesLayer", {
            layerVersionName: `${id}_LambdaLayer`,
            code: lambda.AssetCode.fromAsset(`${__dirname}/layer`),
            compatibleRuntimes: [lambda.Runtime.NODEJS_14_X],
        });

        // ( - ) Utility
        const addCommonSetting = (f: lambda.Function) => {
            meetingTable.grantFullAccess(f);
            attendeeTable.grantFullAccess(f);
            connectionTable.grantFullAccess(f);
            slackFederationAuthsTable.grantFullAccess(f);
            f.addToRolePolicy(statement);

            f.addEnvironment("MEETING_TABLE_NAME", meetingTable.tableName);
            f.addEnvironment("ATTENDEE_TABLE_NAME", attendeeTable.tableName);
            f.addEnvironment("CONNECTION_TABLE_NAME", connectionTable.tableName);
            f.addEnvironment("SLACK_FEDERATION_AUTHS_TABLE_NAME", slackFederationAuthsTable.tableName);

            f.addEnvironment("USER_POOL_ID", userPool.userPoolId);
            f.addEnvironment("VPC_ID", vpc.vpcId);
            f.addEnvironment("SUBNET_ID", vpc.publicSubnets[0].subnetId);
            f.addEnvironment("CLUSTER_ARN", cluster.clusterArn);
            f.addEnvironment("TASK_DIFINITION_ARN_MANAGER", taskDefinition.taskDefinitionArn);
            f.addEnvironment("BUCKET_DOMAIN_NAME", bucket.bucketDomainName);
            f.addEnvironment("MANAGER_CONTAINER_NAME", `${id}_manager_container`);
            f.addEnvironment("BUCKET_ARN", bucket.bucketArn);
            f.addEnvironment("BUCKET_NAME", bucket.bucketName);
            f.addEnvironment("SECURITY_GROUP_NAME", securityGroup.securityGroupName);
            f.addEnvironment("SECURITY_GROUP_ID", securityGroup.securityGroupId);

            f.addEnvironment("USERPOOL_ID", userPool.userPoolId);
            f.addEnvironment("USERPOOL_CLIENT_ID", userPoolClient.userPoolClientId);

            // Slack-Chime Connector (slack access)
            f.addEnvironment("SLACK_CLIENT_ID", SLACK_CLIENT_ID);
            f.addEnvironment("SLACK_CLIENT_SECRET", SLACK_CLIENT_SECRET);
            f.addEnvironment("SLACK_SIGNING_SECRET", SLACK_SIGNING_SECRET);
            f.addEnvironment("SLACK_STATE_SECRET", SLACK_STATE_SECRET);
            // Slack-Chime Connector (db access)
            f.addEnvironment("SLACK_APP_DB_PASSWORD", SLACK_APP_DB_PASSWORD);
            f.addEnvironment("SLACK_APP_DB_SALT", SLACK_APP_DB_SALT);
            f.addEnvironment("SLACK_APP_DB_SECRET", SLACK_APP_DB_SECRET);

            //// DEMO URL
            if (USE_CDN) {
                f.addEnvironment("DEMO_ENDPOINT", `https://${cdn!.distributionDomainName}`);
            } else {
                f.addEnvironment("DEMO_ENDPOINT", `https://${bucket.bucketDomainName}`);
            }

            f.addLayers(nodeModulesLayer);
        };

        // ( - ) auth
        const lambdaFuncRestAPIAuth = new lambda.Function(this, "ChimeRESTAPIAuth", {
            code: lambda.Code.fromAsset(`${__dirname}/dist`),
            handler: "rest_auth.authorize",
            runtime: lambda.Runtime.NODEJS_14_X,
            timeout: Duration.seconds(5),
            memorySize: 256,
        });
        addCommonSetting(lambdaFuncRestAPIAuth);

        const restAPIRole = new Role(this, `ChimeRESTAPIRole`, {
            assumedBy: new ServicePrincipal("apigateway.amazonaws.com"),
        });
        const restAPIPolicy = new PolicyStatement({
            effect: Effect.ALLOW,
            resources: [lambdaFuncRestAPIAuth.functionArn],
            actions: ["lambda:InvokeFunction"],
        });
        restAPIRole.addToPolicy(restAPIPolicy);

        //// (1) Function For RestAPI
        const lambdaFunctionForRestAPI: lambda.Function = new lambda.Function(this, "funcHelloWorld", {
            functionName: `${id}_getRoot`,
            runtime: lambda.Runtime.NODEJS_14_X,
            code: lambda.Code.fromAsset(`${__dirname}/dist`),
            handler: "index.handler",
            memorySize: 256,
            timeout: cdk.Duration.seconds(5),
        });
        addCommonSetting(lambdaFunctionForRestAPI);

        //// (2) Function For SlackFederation API
        const lambdaFunctionForSlackFederationRestAPI: lambda.Function = new lambda.Function(this, "funcSlackFederation", {
            functionName: `${id}_slackFederationRoot`,
            runtime: lambda.Runtime.NODEJS_14_X,
            code: lambda.Code.fromAsset(`${__dirname}/dist/federation/slack`),
            handler: "slack.handler",
            memorySize: 256,
            timeout: cdk.Duration.seconds(3),
        });
        addCommonSetting(lambdaFunctionForSlackFederationRestAPI);

        ///////////////////////////////
        //// API Gateway
        ///////////////////////////////
        //// ( - ) Utility
        // https://github.com/aws/aws-cdk/issues/906
        const addCorsOptions = (apiResource: IResource) => {
            let origin;
            if (FRONTEND_LOCAL_DEV) {
                origin = "'https://localhost:3000'";
                // origin = "'https://192.168.0.4:3000'";
            } else {
                if (USE_CDN) {
                    origin = `'https://${cdn!.distributionDomainName}'`;
                } else {
                    origin = `'https://${bucket.bucketDomainName}'`;
                }
            }
            apiResource.addMethod(
                "OPTIONS",
                new MockIntegration({
                    integrationResponses: [
                        {
                            statusCode: "200",
                            responseParameters: {
                                "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,X-Flect-Access-Token'",
                                "method.response.header.Access-Control-Allow-Origin": origin,
                                "method.response.header.Access-Control-Allow-Credentials": "'true'",
                                "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE,PATCH,HEAD'",
                            },
                        },
                    ],
                    passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
                    requestTemplates: {
                        "application/json": '{"statusCode": 200}',
                    },
                }),
                {
                    methodResponses: [
                        {
                            statusCode: "200",
                            responseParameters: {
                                "method.response.header.Access-Control-Allow-Headers": true,
                                "method.response.header.Access-Control-Allow-Methods": true,
                                "method.response.header.Access-Control-Allow-Credentials": true,
                                "method.response.header.Access-Control-Allow-Origin": true,
                            },
                        },
                    ],
                }
            );
        };
        //// ( - ) Rest API
        const restApi: RestApi = new RestApi(this, "ChimeAPI", {
            restApiName: `${id}_restApi`,
        });
        //lambdaFunctionPostAttendeeOperation.addEnvironment("RESTAPI_ENDPOINT", restApi.url) ///// Exception(Circular dependency between resources) -> generate from request context in lambda.

        //// ( - ) Authorizer
        const authorizer2 = new CfnAuthorizer(this, "apiAuthorizerLambda", {
            name: `${id}_authorizerLamda`,
            type: "TOKEN",
            // identitySource: "method.request.header.Authorization",
            identitySource: "method.request.header.X-Flect-Access-Token",
            restApiId: restApi.restApiId,
            authorizerCredentials: restAPIRole.roleArn,
            authorizerUri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${lambdaFuncRestAPIAuth.functionArn}/invocations`,
            authorizerResultTtlInSeconds: 0,
        });

        //// (1) Get Root
        const root = restApi.root;
        addCorsOptions(root);
        const addRoot = root.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_getRoot`,
            // authorizationType: AuthorizationType.CUSTOM,
            // authorizer: {
            //   authorizerId: authorizer2.ref,
            // },
        });

        //// (2) Meeting
        const apiMeetings = restApi.root.addResource("meetings");
        const apiMeeting = apiMeetings.addResource("{meetingName}");
        addCorsOptions(apiMeetings);
        addCorsOptions(apiMeeting);
        //// (2-1) Get Meetings
        // apiMeetings.addMethod("GET", new LambdaIntegration(lambdaFunctionGetMeetings), {
        apiMeetings.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_getMeetings`,
            authorizationType: AuthorizationType.CUSTOM,
            authorizer: {
                authorizerId: authorizer2.ref,
            },
        });

        //// (2-2) Post Meeting
        // apiMeetings.addMethod("POST", new LambdaIntegration(lambdaFunctionPostMeeting), {
        apiMeetings.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_postMeeting`,
            authorizationType: AuthorizationType.CUSTOM,
            authorizer: {
                authorizerId: authorizer2.ref,
            },
        });

        //// (2-3) Delete Meeting
        // apiMeeting.addMethod("DELETE", new LambdaIntegration(lambdaFunctionDeleteMeeting), {
        apiMeeting.addMethod("DELETE", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_deleteMeeting`,
            authorizationType: AuthorizationType.CUSTOM,
            authorizer: {
                authorizerId: authorizer2.ref,
            },
        });

        //// (2-4) Get Meeting
        // apiMeeting.addMethod("GET", new LambdaIntegration(lambdaFunctionGetMeeting), {
        apiMeeting.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_getMeeting`,
            authorizationType: AuthorizationType.CUSTOM,
            authorizer: {
                authorizerId: authorizer2.ref,
            },
        });

        ///// (3) Attendee
        const apiAttendees = apiMeeting.addResource("attendees");
        const apiAttendee = apiAttendees.addResource("{attendeeId}");
        addCorsOptions(apiAttendees);
        addCorsOptions(apiAttendee);

        //// (3-1) Get Attendee
        // apiAttendee.addMethod("GET", new LambdaIntegration(lambdaFunctionGetAttendee), {
        apiAttendee.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_getAttendee`,
            authorizationType: AuthorizationType.CUSTOM,
            authorizer: {
                authorizerId: authorizer2.ref,
            },
        });

        //// (3-2) Post Attendee
        // apiAttendees.addMethod("POST", new LambdaIntegration(lambdaFunctionPostAttendee), {
        apiAttendees.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_postAttendee`,
            authorizationType: AuthorizationType.CUSTOM,
            authorizer: {
                authorizerId: authorizer2.ref,
            },
        });
        //// (3-3) List Attendees
        // apiAttendees.addMethod("GET", new LambdaIntegration(lambdaFunctionGetAttendees), {
        apiAttendees.addMethod("GET", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_getAttendees`,
            authorizationType: AuthorizationType.CUSTOM,
            authorizer: {
                authorizerId: authorizer2.ref,
            },
        });

        ///// (4) Attendee Operations // Operation under Meeting
        const apiAttendeeOperations = apiAttendee.addResource("operations");
        const apiAttendeeOperation = apiAttendeeOperations.addResource("{operation}");
        addCorsOptions(apiAttendeeOperations);
        addCorsOptions(apiAttendeeOperation);

        //// (4-1) Post Attendee Operation
        // apiAttendeeOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionPostAttendeeOperation), {
        apiAttendeeOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_postAttendeeOperation`,
            authorizationType: AuthorizationType.CUSTOM,
            authorizer: {
                authorizerId: authorizer2.ref,
            },
        });
        // lambdaFunctionPostAttendeeOperation.addEnvironment("RESTAPI_ENDPOINT", restApi.url) ///// Exception(Circular dependency between resources) -> generate from request context in lambda.

        //// (5) Log
        const apiLogs = restApi.root.addResource("logs");
        addCorsOptions(apiLogs);
        //// (5-1) Post Log
        // apiLogs.addMethod("POST", new LambdaIntegration(lambdaFunctionPostLog), {
        apiLogs.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_postLog`,
        });

        //// (a) Operation  // Global Operation(before signin)
        const apiOperations = restApi.root.addResource("operations");
        const apiOperation = apiOperations.addResource("{operation}");
        addCorsOptions(apiOperations);
        addCorsOptions(apiOperation);
        //// (a-1) Post Onetime Code Signin Request
        // apiOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionPostOperation), {
        apiOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionForRestAPI), {
            operationName: `${id}_postOperation`,
        });

        ///// (b) slack federation
        const apiSlack = restApi.root.addResource("slack");
        const apiSlackOAuthRedirect = apiSlack.addResource("oauth_redirect");
        const apiSlackInstall = apiSlack.addResource("install");
        const apiSlackEvents = apiSlack.addResource("events");
        const apiSlackApi = apiSlack.addResource("api");
        const apiSlackOperation = apiSlackApi.addResource("{operation}");
        addCorsOptions(apiSlackOAuthRedirect);
        addCorsOptions(apiSlackInstall);
        addCorsOptions(apiSlackEvents);
        addCorsOptions(apiSlackOperation);
        apiSlackOAuthRedirect.addMethod("GET", new LambdaIntegration(lambdaFunctionForSlackFederationRestAPI), {
            operationName: `${id}_slackOAuthRedirect`,
        });
        apiSlackInstall.addMethod("GET", new LambdaIntegration(lambdaFunctionForSlackFederationRestAPI), {
            operationName: `${id}_slackInstall`,
        });
        apiSlackEvents.addMethod("POST", new LambdaIntegration(lambdaFunctionForSlackFederationRestAPI), {
            operationName: `${id}_slackEvents`,
        });
        apiSlackOperation.addMethod("POST", new LambdaIntegration(lambdaFunctionForSlackFederationRestAPI), {
            operationName: `${id}_slackApi`,
        });

        /////////
        // WebSocket
        // https://github.com/aws-samples/aws-cdk-examples/pull/325/files
        ////////

        // API Gateway
        const webSocketApi = new v2.CfnApi(this, "ChimeMessageAPI", {
            name: "ChimeMessageAPI",
            protocolType: "WEBSOCKET",
            routeSelectionExpression: "$request.body.action",
        });

        //// Lambda Function
        // (1) connect
        const lambdaFuncMessageConnect = new lambda.Function(this, "ChimeMessageAPIConnect", {
            code: lambda.Code.fromAsset(`${__dirname}/dist`),
            handler: "message.connect",
            runtime: lambda.Runtime.NODEJS_14_X,
            timeout: Duration.seconds(30),
            memorySize: 256,
        });
        addCommonSetting(lambdaFuncMessageConnect);

        // (2) disconnect
        const lambdaFuncMessageDisconnect = new lambda.Function(this, "ChimeMessageAPIDisconnect", {
            code: lambda.Code.fromAsset(`${__dirname}/dist`),
            handler: "message.disconnect",
            runtime: lambda.Runtime.NODEJS_14_X,
            timeout: Duration.seconds(30),
            memorySize: 256,
        });
        addCommonSetting(lambdaFuncMessageDisconnect);

        // (3) message
        const lambdaFuncMessageMessage = new lambda.Function(this, "ChimeMessageAPIMessage", {
            code: lambda.Code.fromAsset(`${__dirname}/dist`),
            handler: "message.message",
            runtime: lambda.Runtime.NODEJS_14_X,
            timeout: Duration.seconds(30),
            memorySize: 256,
        });
        addCommonSetting(lambdaFuncMessageMessage);

        // (4) auth
        const lambdaFuncMessageAuth = new lambda.Function(this, "ChimeMessageAPIAuth", {
            code: lambda.Code.fromAsset(`${__dirname}/dist`),
            handler: "message.authorize",
            runtime: lambda.Runtime.NODEJS_14_X,
            timeout: Duration.seconds(30),
            memorySize: 256,
        });
        addCommonSetting(lambdaFuncMessageAuth);

        const policy = new PolicyStatement({
            effect: Effect.ALLOW,
            resources: [lambdaFuncMessageConnect.functionArn, lambdaFuncMessageDisconnect.functionArn, lambdaFuncMessageMessage.functionArn, lambdaFuncMessageAuth.functionArn],
            actions: ["lambda:InvokeFunction"],
        });

        const role = new Role(this, `ChimeMessageAPIRole`, {
            assumedBy: new ServicePrincipal("apigateway.amazonaws.com"),
        });
        role.addToPolicy(policy);

        const messageAuthorizer = new v2.CfnAuthorizer(this, "messageAuthorizer", {
            name: `${id}_authorizer`,
            authorizerType: "REQUEST",
            identitySource: [],
            apiId: webSocketApi.ref,
            authorizerUri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${lambdaFuncMessageAuth.functionArn}/invocations`,
            authorizerCredentialsArn: role.roleArn,
        });

        //// Integration
        const connectIntegration = new v2.CfnIntegration(this, "ChimeMessageAPIConnectIntegration", {
            apiId: webSocketApi.ref,
            integrationType: "AWS_PROXY",
            integrationUri: "arn:aws:apigateway:" + this.region + ":lambda:path/2015-03-31/functions/" + lambdaFuncMessageConnect.functionArn + "/invocations",
            credentialsArn: role.roleArn,
        });

        const disconnectIntegration = new v2.CfnIntegration(this, "ChimeMessageAPIDisconnectIntegration", {
            apiId: webSocketApi.ref,
            integrationType: "AWS_PROXY",
            integrationUri: "arn:aws:apigateway:" + this.region + ":lambda:path/2015-03-31/functions/" + lambdaFuncMessageDisconnect.functionArn + "/invocations",
            credentialsArn: role.roleArn,
        });

        const messageIntegration = new v2.CfnIntegration(this, "ChimeMessageAPIMessageIntegration", {
            apiId: webSocketApi.ref,
            integrationType: "AWS_PROXY",
            integrationUri: "arn:aws:apigateway:" + this.region + ":lambda:path/2015-03-31/functions/" + lambdaFuncMessageMessage.functionArn + "/invocations",
            credentialsArn: role.roleArn,
        });

        // const messageAuthorizer = new CfnAuthorizer(this, 'cfnMessageAuth', {
        //   name: `${id}_messageAuthorizer`,
        //   type: "TOKEN",
        //   restApiId: restApi.restApiId,
        //   identitySource: 'method.request.header.Authorization',
        //   authorizerCredentials: role.roleArn,
        //   authorizerUri: `arn:aws:apigateway:${this.region}:lambda:path/2015-03-31/functions/${lambdaFuncMessageAuth.functionArn}/invocations`,
        // })

        //// Route
        const connectRoute = new v2.CfnRoute(this, "connectRoute", {
            apiId: webSocketApi.ref,
            routeKey: "$connect",
            authorizationType: AuthorizationType.CUSTOM,
            //authorizationType: "NONE",
            target: "integrations/" + connectIntegration.ref,
            authorizerId: messageAuthorizer.ref,
        });

        const disconnectRoute = new v2.CfnRoute(this, "disconnectRoute", {
            apiId: webSocketApi.ref,
            routeKey: "$disconnect",
            authorizationType: "NONE",
            target: "integrations/" + disconnectIntegration.ref,
        });

        const messageRoute = new v2.CfnRoute(this, "messageRoute", {
            apiId: webSocketApi.ref,
            routeKey: "sendmessage",
            authorizationType: "NONE",
            target: "integrations/" + messageIntegration.ref,
        });

        //// Deploy
        const deployment = new v2.CfnDeployment(this, "ChimeMessageAPIDep", {
            apiId: webSocketApi.ref,
        });

        const stage = new v2.CfnStage(this, `ChimeMessageAPIStage`, {
            apiId: webSocketApi.ref,
            autoDeploy: true,
            deploymentId: deployment.ref,
            stageName: "Prod",
        });

        const dependencies = new ConcreteDependable();
        dependencies.add(connectRoute);
        dependencies.add(disconnectRoute);
        dependencies.add(messageRoute);
        deployment.node.addDependency(dependencies);

        /////////////////////////////////////////////
        /// add env info after api gateway
        ////////////////////////////////////////////
        // lambdaFunctionForSlackFederationRestAPI.addEnvironment("RESTAPI_ENDPOINT", restApi.url); /// Exception(Circular dependency between resources) -> generate from request context in lambda.

        ///////////////////////////////
        //// Output
        ///////////////////////////////
        new CfnOutput(this, "UserPoolId", {
            description: "UserPoolId",
            value: userPool.userPoolId,
        });

        new CfnOutput(this, "UserPoolClientId", {
            description: "UserPoolClientId",
            value: userPoolClient.userPoolClientId,
        });

        new CfnOutput(this, "Bucket", {
            description: "Bucket",
            value: bucket.bucketName,
        });

        new CfnOutput(this, "BucketWebsiteDomainName", {
            description: "BucketWebsiteDomainName",
            value: bucket.bucketWebsiteDomainName,
        });

        new CfnOutput(this, "BucketDomainName", {
            description: "BucketDomainName",
            value: bucket.bucketDomainName,
        });

        new CfnOutput(this, "RestAPIEndpoint", {
            description: "RestAPIEndpoint",
            value: restApi.url,
        });

        new CfnOutput(this, "WebSocketEndpoint", {
            description: "WebSocketEndpoint",
            value: webSocketApi.attrApiEndpoint,
        });

        if (USE_CDN) {
            new CfnOutput(this, "CDNComainName", {
                description: "CDNComainName",
                value: cdn!.distributionDomainName,
            });
        }

        //// DEMO URL
        if (USE_CDN) {
            new CfnOutput(this, "DemoEndpoint", {
                description: "DemoEndpoint",
                value: `https://${cdn!.distributionDomainName}`,
            });
            new CfnOutput(this, "DistributionId", {
                description: "DistributionId",
                value: cdn!.distributionId,
            });
        } else {
            new CfnOutput(this, "DemoEndpoint", {
                description: "DemoEndpoint",
                value: `https://${bucket.bucketDomainName}`,
            });
        }

        // new cdk.CfnOutput(this, "AmongLoadBalancerDNS", {
        //   value: lb_among.loadBalancerDnsName
        // });
        // new cdk.CfnOutput(this, "AmongServiceArn", {
        //   value: ecsService_among.serviceArn
        // });
        // new cdk.CfnOutput(this, "AmongServiceName", {
        //   value: ecsService_among.serviceName
        // });
    }
Example #17
Source File: data-store-stack.ts    From MDDL with MIT License 4 votes vote down vote up
constructor(scope: Construct, id: string, props: Props) {
    super(scope, id, props)

    // read out config and set defaults
    const { vpcConfig = {}, rdsConfig = {}, providedKmsKey } = props
    const {
      cidrBlock = '10.0.0.0/16',
      maxAzs = 2,
      natGatewaysCount = 2,
    } = vpcConfig
    const {
      backupRetentionDays = 30,
      minCapacity = 1,
      maxCapacity = 8,
    } = rdsConfig

    let kmsKey: IKey
    if (providedKmsKey) {
      // import key
      kmsKey = Key.fromKeyArn(this, 'ProvidedKey', providedKmsKey.keyArn)
    } else {
      // create new KMS key for the data store to use
      kmsKey = new Key(this, 'Key', {
        description: `KMS Key for ${this.stackName} stack`,
        enableKeyRotation: true,
      })

      // allow RDS to use the KMS key
      // https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.Keys.html
      kmsKey.addToResourcePolicy(
        new PolicyStatement({
          actions: ['kms:Decrypt', 'kms:GenerateDataKey*'],
          resources: ['*'],
          principals: [new AnyPrincipal()],
          conditions: {
            StringEquals: {
              'kms:ViaService': `rds.${this.region}.amazonaws.com`,
              'kms:CallerAccount': this.account,
            },
          },
        }),
      )
    }

    // create the VPC
    this.vpc = new Vpc(this, 'Vpc', {
      cidr: cidrBlock,
      maxAzs: maxAzs,
      enableDnsHostnames: false,
      enableDnsSupport: true,
      natGateways: natGatewaysCount,
      subnetConfiguration: [
        {
          cidrMask: 28,
          name: 'isolated',
          subnetType: SubnetType.ISOLATED,
        },
        {
          cidrMask: 28,
          name: 'private',
          subnetType: SubnetType.PRIVATE,
        },
        {
          cidrMask: 28,
          name: 'public',
          subnetType: SubnetType.PUBLIC,
        },
      ],
    })

    // create the root DB credentials
    const rdsRootCredentialsSecret = new Secret(
      this,
      'RdsClusterCredentialsSecret',
      {
        secretName: `${this.stackName}-rds-root-credentials`,
        generateSecretString: {
          secretStringTemplate: JSON.stringify({
            username: 'root',
          }),
          excludePunctuation: true,
          includeSpace: false,
          generateStringKey: 'password',
          excludeCharacters: '"@/\\',
          passwordLength: 30,
        },
        encryptionKey: kmsKey,
      },
    )

    // configure RDS subnet group
    const rdsSubnetGroup = new CfnDBSubnetGroup(this, 'RdsSubnetGroup', {
      dbSubnetGroupDescription: `Subnet group for RDS ${this.stackName}`,
      subnetIds: this.vpc.isolatedSubnets.map((s) => s.subnetId),
    })

    // configure RDS security group
    const rdsSecurityGroup = new SecurityGroup(this, 'RdsSecurityGroup', {
      vpc: this.vpc,
      allowAllOutbound: false,
      description: `${this.stackName} security group for RDS`,
    })

    // create the DB cluster
    const rdsPort = 3306
    const databaseName = 'root'
    const rdsCluster = new CfnDBCluster(this, 'RdsCluster', {
      engineMode: 'serverless',
      engine: 'aurora-mysql',
      engineVersion: '5.7.mysql_aurora.2.07.1',
      enableHttpEndpoint: true,
      databaseName,
      kmsKeyId: kmsKey.keyId,
      masterUsername: rdsRootCredentialsSecret
        .secretValueFromJson('username')
        .toString(),
      masterUserPassword: rdsRootCredentialsSecret
        .secretValueFromJson('password')
        .toString(),
      backupRetentionPeriod: backupRetentionDays,
      scalingConfiguration: {
        maxCapacity,
        minCapacity,
        autoPause: false,
      },
      deletionProtection: true,
      dbSubnetGroupName: rdsSubnetGroup.ref,
      dbClusterParameterGroupName: 'default.aurora-mysql5.7',
      // The following three lines cause errors on updates, so they are commented out.
      //  If you need custom values for them, uncomment before first deployment.
      // preferredMaintenanceWindow: maintenanceWindowWeekly,
      // preferredBackupWindow: backupWindowDaily,
      // port: rdsPort,
      storageEncrypted: true,
      vpcSecurityGroupIds: [rdsSecurityGroup.securityGroupId],
    })
    rdsCluster.applyRemovalPolicy(RemovalPolicy.SNAPSHOT)
    rdsCluster.node.addDependency(this.vpc, kmsKey)
    this.rdsEndpoint = rdsCluster.attrEndpointAddress

    // add database networking
    this.rdsAccessSecurityGroup = new SecurityGroup(
      this,
      'RdsAccessSecurityGroup',
      {
        vpc: this.vpc,
        description: `${this.stackName} security group for RDS Access`,
      },
    )
    rdsSecurityGroup.addIngressRule(
      this.rdsAccessSecurityGroup,
      Port.tcp(rdsPort),
    )
    rdsSecurityGroup.addEgressRule(
      this.rdsAccessSecurityGroup,
      Port.tcp(rdsPort),
    )
    this.rdsAccessSecurityGroup.addIngressRule(
      rdsSecurityGroup,
      Port.tcp(rdsPort),
    )

    const { layer: mysqlLayer } = this.addMysqlLayer()

    // configure function used by the city stacks to create a new DB and user within this cluster
    this.createDbUserFunction = new Function(this, 'CreateDbUserFunction', {
      code: Code.fromAsset(path.join('build', 'create-db-and-user.zip')),
      handler: 'index.handler',
      runtime: Runtime.NODEJS_12_X,
      vpc: this.vpc,
      timeout: Duration.seconds(60),
      securityGroups: [this.rdsAccessSecurityGroup],
      layers: [mysqlLayer],
      environment: {
        DB_HOST: rdsCluster.attrEndpointAddress,
        DB_USER: rdsRootCredentialsSecret
          .secretValueFromJson('username')
          .toString(),
        DB_PASSWORD: rdsRootCredentialsSecret
          .secretValueFromJson('password')
          .toString(),
        DB_DEFAULT_DATABASE: databaseName,
      },
    })
  }
Example #18
Source File: aws-serverless-wordpress-stack.ts    From aws-serverless-wordpress with Apache License 2.0 4 votes vote down vote up
constructor(scope: cdk.Construct, id: string, props: StackProps) {
        super(scope, id, props);

        if (!props.cloudFrontHashHeader) props.cloudFrontHashHeader = Buffer.from(`${this.stackName}.${props.domainName}`).toString('base64');

        const globalTagKey = 'aws-config:cloudformation:stack-name';
        const globalTagValue = Buffer.from(this.stackName).toString('base64');

        const awsManagedSnsKmsKey = Alias.fromAliasName(this, 'AwsManagedSnsKmsKey', 'alias/aws/sns');

        const publicHostedZone = PublicHostedZone.fromLookup(this, 'ExistingPublicHostedZone', {domainName: props.domainName});

        const acmCertificate = new Certificate(this, 'Certificate', {
            domainName: props.hostname,
            subjectAlternativeNames: props.alternativeHostname,
            validation: CertificateValidation.fromDns(publicHostedZone)
        });

        const staticContentBucket = new Bucket(this, 'StaticContentBucket', {
            encryption: BucketEncryption.S3_MANAGED,
            versioned: true,
            removalPolicy: props.removalPolicy
        });

        const loggingBucket = new Bucket(this, 'LoggingBucket', {
            encryption: BucketEncryption.S3_MANAGED,
            removalPolicy: props.removalPolicy,
            lifecycleRules: [
                {
                    enabled: true,
                    transitions: [
                        {
                            storageClass: StorageClass.INFREQUENT_ACCESS,
                            transitionAfter: Duration.days(30)
                        },
                        {
                            storageClass: StorageClass.DEEP_ARCHIVE,
                            transitionAfter: Duration.days(90)
                        }
                    ]
                }
            ]
        });
        loggingBucket.addToResourcePolicy(new PolicyStatement({
            principals: [new ServicePrincipal('delivery.logs.amazonaws.com'),],
            actions: ['s3:PutObject'],
            resources: [
                `${loggingBucket.bucketArn}/vpc-flow-log/AWSLogs/${this.account}/*`,
                `${loggingBucket.bucketArn}/application-load-balancer/AWSLogs/${this.account}/*`,
                `${loggingBucket.bucketArn}/vpn-admin-application-load-balancer/AWSLogs/${this.account}/*`
            ],
            conditions: {
                StringEquals: {
                    's3:x-amz-acl': 'bucket-owner-full-control'
                }
            }
        }));
        loggingBucket.addToResourcePolicy(new PolicyStatement({
            principals: [new ServicePrincipal('delivery.logs.amazonaws.com'),],
            actions: ['s3:GetBucketAcl'],
            resources: [loggingBucket.bucketArn],
        }));
        loggingBucket.addToResourcePolicy(new PolicyStatement({
            principals: [new ArnPrincipal(`arn:aws:iam::${props.loadBalancerAccountId}:root`)],
            actions: ['s3:PutObject'],
            resources: [
                `${loggingBucket.bucketArn}/application-load-balancer/AWSLogs/${this.account}/*`,
                `${loggingBucket.bucketArn}/vpn-admin-application-load-balancer/AWSLogs/${this.account}/*`
            ]
        }));
        loggingBucket.addToResourcePolicy(new PolicyStatement({
            principals: [new AccountRootPrincipal()],
            actions: ['s3:GetBucketAcl', 's3:PutBucketAcl'],
            resources: [loggingBucket.bucketArn]
        }));

        if (props.removalPolicy === RemovalPolicy.DESTROY) {
            const serviceToken = CustomResourceProvider.getOrCreate(this, 'Custom::EmptyLoggingBucket', {
                codeDirectory: path.join(__dirname, 'custom-resource'),
                runtime: CustomResourceProviderRuntime.NODEJS_12,
                timeout: Duration.minutes(3),
                policyStatements: [(new PolicyStatement({
                    actions: ['s3:ListBucket', 's3:DeleteObject'],
                    resources: [loggingBucket.bucketArn, `${loggingBucket.bucketArn}/*`]
                })).toStatementJson()],
                environment: {
                    LOGGING_BUCKET_NAME: loggingBucket.bucketName
                }
            });
            new CustomResource(this, 'EmptyLoggingBucket', {
                resourceType: 'Custom::EmptyLoggingBucket',
                serviceToken
            });
        }

        const vpc = new Vpc(this, 'Vpc', {
            natGateways: 3,
            maxAzs: 3,
            cidr: '172.16.0.0/16',
            subnetConfiguration: [
                {
                    name: 'Public',
                    subnetType: SubnetType.PUBLIC
                },
                {
                    name: 'Private',
                    subnetType: SubnetType.PRIVATE

                },
                {
                    name: 'Isolated',
                    subnetType: SubnetType.ISOLATED
                }
            ],
            enableDnsHostnames: true,
            enableDnsSupport: true
        });

        const nacl = new NetworkAcl(this, 'NetworkAcl', {vpc});
        nacl.addEntry('AllowAllHttpsFromIpv4', {
            ruleNumber: 100,
            cidr: AclCidr.anyIpv4(),
            traffic: AclTraffic.tcpPort(443),
            direction: TrafficDirection.INGRESS,
            ruleAction: Action.ALLOW
        });
        nacl.addEntry('AllowAllHttpsFromIpv6', {
            ruleNumber: 101,
            cidr: AclCidr.anyIpv6(),
            traffic: AclTraffic.tcpPort(443),
            direction: TrafficDirection.INGRESS,
            ruleAction: Action.ALLOW
        });
        nacl.addEntry('AllowResponseToHttpsRequestToIpv4', {
            ruleNumber: 100,
            cidr: AclCidr.anyIpv4(),
            traffic: AclTraffic.tcpPortRange(1024, 65535),
            direction: TrafficDirection.EGRESS,
            ruleAction: Action.ALLOW
        });
        nacl.addEntry('AllowResponseToHttpsRequestToIpv6', {
            ruleNumber: 101,
            cidr: AclCidr.anyIpv6(),
            traffic: AclTraffic.tcpPortRange(1024, 65535),
            direction: TrafficDirection.EGRESS,
            ruleAction: Action.ALLOW
        });

        new CfnFlowLog(this, 'CfnVpcFlowLog', {
            resourceId: vpc.vpcId,
            resourceType: 'VPC',
            trafficType: 'ALL',
            logDestinationType: 's3',
            logDestination: `${loggingBucket.bucketArn}/vpc-flow-log`
        });

        const privateHostedZone = new PrivateHostedZone(this, 'PrivateHostedZone', {
            vpc,
            zoneName: `${props.hostname}.private`
        });

        const applicationLoadBalancerSecurityGroup = new SecurityGroup(this, 'ApplicationLoadBalancerSecurityGroup', {vpc});
        const vpnApplicationLoadBalancerSecurityGroup = new SecurityGroup(this, 'VpcApplicationLoadBalancer', {vpc});
        const elastiCacheMemcachedSecurityGroup = new SecurityGroup(this, 'ElastiCacheMemcachedSecurityGroup', {vpc});
        const rdsAuroraClusterSecurityGroup = new SecurityGroup(this, 'RdsAuroraClusterSecurityGroup', {vpc});
        const ecsFargateServiceSecurityGroup = new SecurityGroup(this, 'EcsFargateServiceSecurityGroup', {vpc});
        const efsFileSystemSecurityGroup = new SecurityGroup(this, 'EfsFileSystemSecurityGroup', {vpc});
        const elasticsearchDomainSecurityGroup = new SecurityGroup(this, 'ElasticsearchDomainSecurityGroup', {vpc});
        const bastionHostSecurityGroup = new SecurityGroup(this, 'BastionHostSecurityGroup', {vpc});
        const clientVpnSecurityGroup = new SecurityGroup(this, 'ClientVpnSecurityGroup', {vpc});

        applicationLoadBalancerSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(443));
        vpnApplicationLoadBalancerSecurityGroup.addIngressRule(clientVpnSecurityGroup, Port.tcp(443));
        ecsFargateServiceSecurityGroup.addIngressRule(applicationLoadBalancerSecurityGroup, Port.tcp(80));
        ecsFargateServiceSecurityGroup.addIngressRule(vpnApplicationLoadBalancerSecurityGroup, Port.tcp(80));
        elastiCacheMemcachedSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(11211));
        rdsAuroraClusterSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(3306));
        efsFileSystemSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(2049));
        elasticsearchDomainSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(9300));
        clientVpnSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.udp(1194));

        efsFileSystemSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(2049));
        elastiCacheMemcachedSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(11211));
        rdsAuroraClusterSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(3306));
        elasticsearchDomainSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(9300));

        const clientVpn = new CfnClientVpnEndpoint(this, 'ClientVpn', {
            clientCidrBlock: '172.16.252.0/22',
            serverCertificateArn: props.certificate.server,
            connectionLogOptions: {enabled: false},
            transportProtocol: 'udp',
            vpcId: vpc.vpcId,
            vpnPort: 1194,
            securityGroupIds: [clientVpnSecurityGroup.securityGroupId],
            authenticationOptions: [{
                type: 'certificate-authentication',
                mutualAuthentication: {clientRootCertificateChainArn: props.certificate.client}
            }]
        });
        vpc.publicSubnets.forEach((subnet, i) => {
            const _vpnTargetNetworkAssociation = new CfnClientVpnTargetNetworkAssociation(this, `ClientVpnPublicTargetNetworkAssociation${i}`, {
                clientVpnEndpointId: clientVpn.ref,
                subnetId: subnet.subnetId
            });
            const _vpnPublicRoute = new CfnClientVpnRoute(this, `ClientVpnPublicRoute${i}`, {
                clientVpnEndpointId: clientVpn.ref,
                destinationCidrBlock: '0.0.0.0/0',
                targetVpcSubnetId: subnet.subnetId
            });
            _vpnPublicRoute.addDependsOn(_vpnTargetNetworkAssociation);
        });
        new CfnClientVpnAuthorizationRule(this, 'ClientVpnAuthorizationRuleForInternetAccess', {
            clientVpnEndpointId: clientVpn.ref,
            authorizeAllGroups: true,
            targetNetworkCidr: '0.0.0.0/0'
        });
        new CfnClientVpnAuthorizationRule(this, 'ClientVpnAuthorizationRuleForVpcAccess', {
            clientVpnEndpointId: clientVpn.ref,
            authorizeAllGroups: true,
            targetNetworkCidr: vpc.vpcCidrBlock
        });

        const rdsAuroraClusterPasswordSecret = new Secret(this, 'RdsAuroraClusterPasswordSecret', {
            removalPolicy: props.removalPolicy,
            generateSecretString: {excludeCharacters: ` ;+%{}` + `@'"\`/\\#`}
        });

        const rdsAuroraCluster = new ServerlessCluster(this, 'RdsAuroraServerlessCluster', {
            vpc,
            vpcSubnets: {subnetType: SubnetType.ISOLATED},
            securityGroups: [rdsAuroraClusterSecurityGroup],
            engine: DatabaseClusterEngine.AURORA_MYSQL,
            credentials: {
                username: props.databaseCredential.username,
                password: SecretValue.secretsManager(rdsAuroraClusterPasswordSecret.secretArn)
            },
            defaultDatabaseName: props.databaseCredential.defaultDatabaseName,
            deletionProtection: props.resourceDeletionProtection,
            removalPolicy: props.removalPolicy,
            scaling: {
                minCapacity: AuroraCapacityUnit.ACU_1,
                maxCapacity: AuroraCapacityUnit.ACU_16
            },
            backupRetention: Duration.days(7)
        })

        const rdsAuroraClusterPrivateDnsRecord = new CnameRecord(this, 'RdsAuroraClusterPrivateDnsRecord', {
            zone: privateHostedZone,
            recordName: `database.${privateHostedZone.zoneName}`,
            domainName: rdsAuroraCluster.clusterEndpoint.hostname,
            ttl: Duration.hours(1)
        });

        const elastiCacheMemcachedCluster = new CfnCacheCluster(this, 'ElastiCacheMemcachedCluster', {
            cacheNodeType: 'cache.t3.micro',
            engine: 'memcached',
            azMode: 'cross-az',
            numCacheNodes: 3,
            cacheSubnetGroupName: new CfnSubnetGroup(this, 'ElastiCacheMemcachedClusterSubnetGroup', {
                description: 'ElastiCacheMemcachedClusterSubnetGroup',
                subnetIds: vpc.isolatedSubnets.map(subnet => subnet.subnetId)
            }).ref,
            vpcSecurityGroupIds: [elastiCacheMemcachedSecurityGroup.securityGroupId]
        });

        const elastiCacheMemcachedClusterPrivateDnsRecord = new CnameRecord(this, 'ElastiCacheMemcachedClusterPrivateDnsRecord', {
            zone: privateHostedZone,
            recordName: `cache.${privateHostedZone.zoneName}`,
            domainName: elastiCacheMemcachedCluster.attrConfigurationEndpointAddress,
            ttl: Duration.hours(1)
        });

        const elasticsearchServiceLinkRole = new CfnServiceLinkedRole(this, 'CfnElasticsearchServiceLinkRole', {
            awsServiceName: 'es.amazonaws.com'
        });

        const elasticsearchDomain = new Domain(this, 'ElasticsearchDomain', {
            version: ElasticsearchVersion.V7_7,
            capacity: {dataNodes: 3, dataNodeInstanceType: 't3.small.elasticsearch'},
            zoneAwareness: {enabled: true, availabilityZoneCount: 3},
            encryptionAtRest: {enabled: true},
            nodeToNodeEncryption: true,
            ebs: {volumeSize: 10},
            enforceHttps: true,
            vpcOptions: {
                subnets: vpc.isolatedSubnets,
                securityGroups: [elasticsearchDomainSecurityGroup]
            }
        });
        (elasticsearchDomain.node.defaultChild as CfnDomain).addDependsOn(elasticsearchServiceLinkRole);

        const elasticsearchDomainPrivateDnsRecord = new CnameRecord(this, 'ElasticsearchDomainPrivateDnsRecord', {
            zone: privateHostedZone,
            recordName: `search.${privateHostedZone.zoneName}`,
            domainName: elasticsearchDomain.domainEndpoint,
            ttl: Duration.hours(1)
        });

        const fileSystem = new FileSystem(this, 'FileSystem', {
            vpc,
            vpcSubnets: {
                subnetType: SubnetType.ISOLATED
            },
            securityGroup: efsFileSystemSecurityGroup,
            performanceMode: PerformanceMode.GENERAL_PURPOSE,
            lifecyclePolicy: LifecyclePolicy.AFTER_30_DAYS,
            throughputMode: ThroughputMode.BURSTING,
            encrypted: true,
            removalPolicy: props.removalPolicy
        });

        const fileSystemAccessPoint = fileSystem.addAccessPoint('AccessPoint');

        const fileSystemEndpointPrivateDnsRecord = new CnameRecord(this, 'FileSystemEndpointPrivateDnsRecord', {
            zone: privateHostedZone,
            recordName: `nfs.${privateHostedZone.zoneName}`,
            domainName: `${fileSystem.fileSystemId}.efs.${this.region}.amazonaws.com`,
            ttl: Duration.hours(1)
        });

        const bastionHost = new BastionHostLinux(this, 'BastionHost', {
            vpc,
            securityGroup: bastionHostSecurityGroup
        });
        bastionHost.instance.addUserData('mkdir -p /mnt/efs');
        bastionHost.instance.addUserData(`mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport ${fileSystemEndpointPrivateDnsRecord.domainName}:/ /mnt/efs `);

        const ecsCluster = new Cluster(this, 'EcsCluster', {
            containerInsights: true,
            vpc
        });
        const _ecsCluster = ecsCluster.node.defaultChild as CfnCluster;
        _ecsCluster.capacityProviders = ['FARGATE', 'FARGATE_SPOT'];
        _ecsCluster.defaultCapacityProviderStrategy = [
            {
                capacityProvider: 'FARGATE',
                weight: 2,
                base: 3
            },
            {
                capacityProvider: 'FARGATE_SPOT',
                weight: 1
            }
        ];

        const applicationLoadBalancer = new ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', {
            vpc,
            deletionProtection: props.resourceDeletionProtection,
            http2Enabled: true,
            internetFacing: true,
            securityGroup: applicationLoadBalancerSecurityGroup,
            vpcSubnets: {subnetType: SubnetType.PUBLIC}
        });
        applicationLoadBalancer.setAttribute('routing.http.drop_invalid_header_fields.enabled', 'true');
        applicationLoadBalancer.setAttribute('access_logs.s3.enabled', 'true');
        applicationLoadBalancer.setAttribute('access_logs.s3.bucket', loggingBucket.bucketName);
        applicationLoadBalancer.setAttribute('access_logs.s3.prefix', 'application-load-balancer');
        applicationLoadBalancer.addListener('HttpListener', {
            port: 80,
            protocol: ApplicationProtocol.HTTP,
            defaultAction: ListenerAction.redirect({protocol: 'HTTPS', port: '443'})
        });

        const httpsListener = applicationLoadBalancer.addListener('HttpsListener', {
            port: 443,
            protocol: ApplicationProtocol.HTTPS,
            certificates: [acmCertificate]
        });

        const vpnAdminApplicationLoadBalancer = new ApplicationLoadBalancer(this, 'VpnAdminApplicationLoadBalancer', {
            vpc,
            deletionProtection: props.resourceDeletionProtection,
            http2Enabled: true,
            internetFacing: false,
            securityGroup: vpnApplicationLoadBalancerSecurityGroup,
            vpcSubnets: {subnetType: SubnetType.PRIVATE}
        });
        vpnAdminApplicationLoadBalancer.setAttribute('routing.http.drop_invalid_header_fields.enabled', 'true');
        vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.enabled', 'true');
        vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.bucket', loggingBucket.bucketName);
        vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.prefix', 'vpn-admin-application-load-balancer');
        vpnAdminApplicationLoadBalancer.addListener('VpnAdminHttpListener', {
            port: 80,
            protocol: ApplicationProtocol.HTTP,
            defaultAction: ListenerAction.redirect({protocol: 'HTTPS', port: '443'})
        });

        const vpnAdminHttpsListener = vpnAdminApplicationLoadBalancer.addListener('VpnAdminHttpsListener', {
            port: 443,
            protocol: ApplicationProtocol.HTTPS,
            certificates: [acmCertificate]
        });

        const wordPressFargateTaskExecutionRole = new Role(this, 'WordpressFargateTaskExecutionRole', {
            assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),
            managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')]
        });
        const wordPressFargateTaskRole = new Role(this, 'WordpressFargateTaskRole', {
            assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),
            managedPolicies: [ManagedPolicy.fromManagedPolicyArn(this, 'XRayDaemonWriteAccess', 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess')],
            inlinePolicies: {
                _: new PolicyDocument({
                    statements: [
                        new PolicyStatement({
                            actions: ['s3:GetBucketLocation'],
                            resources: [staticContentBucket.bucketArn]
                        })
                    ]
                })
            }
        });
        staticContentBucket.grantReadWrite(wordPressFargateTaskRole);

        const wordPressFargateTaskDefinition = new FargateTaskDefinition(this, 'WordpressFargateTaskDefinition', {
            memoryLimitMiB: 512,
            cpu: 256,
            executionRole: wordPressFargateTaskExecutionRole,
            taskRole: wordPressFargateTaskRole,
        });
        wordPressFargateTaskDefinition.addVolume({
            name: 'WordPressEfsVolume',
            efsVolumeConfiguration: {
                fileSystemId: fileSystem.fileSystemId,
                transitEncryption: 'ENABLED',
                authorizationConfig: {
                    accessPointId: fileSystemAccessPoint.accessPointId
                }
            }
        });

        const wordPressDockerImageAsset = new DockerImageAsset(this, 'WordPressDockerImageAsset', {directory: path.join(__dirname, 'images/wordpress')});
        const nginxDockerImageAsset = new DockerImageAsset(this, 'NginxDockerImageAsset', {directory: path.join(__dirname, 'images/nginx')});

        const wordPressContainer = wordPressFargateTaskDefinition.addContainer('WordPress', {
            image: ContainerImage.fromDockerImageAsset(wordPressDockerImageAsset),
            environment: {
                WORDPRESS_DB_HOST: rdsAuroraClusterPrivateDnsRecord.domainName,
                WORDPRESS_DB_USER: props.databaseCredential.username,
                WORDPRESS_DB_NAME: props.databaseCredential.defaultDatabaseName,
            },
            secrets: {
                WORDPRESS_DB_PASSWORD: EcsSecret.fromSecretsManager(rdsAuroraClusterPasswordSecret)
            },
            logging: LogDriver.awsLogs({
                streamPrefix: `${this.stackName}WordPressContainerLog`,
                logRetention: RetentionDays.ONE_MONTH
            })
        });
        wordPressContainer.addMountPoints({
            readOnly: false,
            containerPath: '/var/www/html',
            sourceVolume: 'WordPressEfsVolume'
        });

        const nginxContainer = wordPressFargateTaskDefinition.addContainer('Nginx', {
            image: ContainerImage.fromDockerImageAsset(nginxDockerImageAsset),
            logging: LogDriver.awsLogs({
                streamPrefix: `${this.stackName}NginxContainerLog`,
                logRetention: RetentionDays.ONE_MONTH
            }),
            environment: {
                SERVER_NAME: props.hostname,
                MEMCACHED_HOST: elastiCacheMemcachedClusterPrivateDnsRecord.domainName,
                NGINX_ENTRYPOINT_QUIET_LOGS: '1'
            }
        });
        nginxContainer.addPortMappings({
            hostPort: 80,
            containerPort: 80,
            protocol: Protocol.TCP
        });
        nginxContainer.addMountPoints({
            readOnly: false,
            containerPath: '/var/www/html',
            sourceVolume: 'WordPressEfsVolume'
        });

        const xrayContainer = wordPressFargateTaskDefinition.addContainer('XRay', {
            image: ContainerImage.fromRegistry('amazon/aws-xray-daemon'),
            logging: LogDriver.awsLogs({
                streamPrefix: `${this.stackName}XRayContainerLog`,
                logRetention: RetentionDays.ONE_MONTH
            }),
            entryPoint: ['/usr/bin/xray', '-b', '127.0.0.1:2000', '-l', 'dev', '-o'],
            user: '1337'
        });
        xrayContainer.addPortMappings({
            containerPort: 2000,
            protocol: Protocol.UDP
        })

        const _wordPressFargateServiceTargetGroup = new CfnTargetGroup(this, 'CfnWordPressFargateServiceTargetGroup', {
            matcher: {
                httpCode: '200,301,302'
            },
            port: 80,
            protocol: 'HTTP',
            targetGroupAttributes: [
                {
                    key: 'stickiness.enabled',
                    value: 'true'
                },
                {
                    key: 'stickiness.type',
                    value: 'lb_cookie'
                },
                {
                    key: 'stickiness.lb_cookie.duration_seconds',
                    value: '604800'
                }
            ],
            targetType: 'ip',
            vpcId: vpc.vpcId,
            unhealthyThresholdCount: 5,
            healthCheckTimeoutSeconds: 45,
            healthCheckIntervalSeconds: 60,
        });

        const wordPressFargateServiceTargetGroup = ApplicationTargetGroup.fromTargetGroupAttributes(this, 'WordPressFargateServiceTargetGroup', {
            loadBalancerArns: applicationLoadBalancer.loadBalancerArn,
            targetGroupArn: _wordPressFargateServiceTargetGroup.ref
        });
        httpsListener.addTargetGroups('WordPress', {targetGroups: [wordPressFargateServiceTargetGroup]});

        const _wordPressVpnAdminFargateServiceTargetGroup = new CfnTargetGroup(this, 'CfnWordPressVpnAdminFargateServiceTargetGroup', {
            matcher: {
                httpCode: '200,301,302'
            },
            port: 80,
            protocol: 'HTTP',
            targetGroupAttributes: [
                {
                    key: 'stickiness.enabled',
                    value: 'true'
                },
                {
                    key: 'stickiness.type',
                    value: 'lb_cookie'
                },
                {
                    key: 'stickiness.lb_cookie.duration_seconds',
                    value: '604800'
                }
            ],
            targetType: 'ip',
            vpcId: vpc.vpcId,
            unhealthyThresholdCount: 5,
            healthCheckTimeoutSeconds: 45,
            healthCheckIntervalSeconds: 60,
        });

        const wordPressVpnAdminFargateServiceTargetGroup = ApplicationTargetGroup.fromTargetGroupAttributes(this, 'WordPressVpnAdminFargateServiceTargetGroup', {
            loadBalancerArns: vpnAdminApplicationLoadBalancer.loadBalancerArn,
            targetGroupArn: _wordPressVpnAdminFargateServiceTargetGroup.ref
        });
        vpnAdminHttpsListener.addTargetGroups('WordPressVpnAdmin', {targetGroups: [wordPressVpnAdminFargateServiceTargetGroup]});

        const _wordPressFargateService = new CfnService(this, 'CfnWordPressFargateService', {
            cluster: ecsCluster.clusterArn,
            desiredCount: 3,
            deploymentConfiguration: {
                maximumPercent: 200,
                minimumHealthyPercent: 50
            },
            deploymentController: {
                type: 'ECS'
            },
            healthCheckGracePeriodSeconds: 60,
            loadBalancers: [
                {
                    containerName: nginxContainer.containerName,
                    containerPort: 80,
                    targetGroupArn: wordPressFargateServiceTargetGroup.targetGroupArn
                },
                {
                    containerName: nginxContainer.containerName,
                    containerPort: 80,
                    targetGroupArn: wordPressVpnAdminFargateServiceTargetGroup.targetGroupArn
                }
            ],
            networkConfiguration: {
                awsvpcConfiguration: {
                    assignPublicIp: 'DISABLED',
                    securityGroups: [ecsFargateServiceSecurityGroup.securityGroupId],
                    subnets: vpc.privateSubnets.map(subnet => subnet.subnetId)
                }
            },
            platformVersion: '1.4.0',
            taskDefinition: wordPressFargateTaskDefinition.taskDefinitionArn
        });
        _wordPressFargateService.addOverride('DependsOn', [
            this.getLogicalId(httpsListener.node.defaultChild as CfnListener),
            this.getLogicalId(vpnAdminHttpsListener.node.defaultChild as CfnListener)
        ]);

        const wordPressServiceScaling = new ScalableTarget(this, 'WordPressFargateServiceScaling', {
            scalableDimension: 'ecs:service:DesiredCount',
            minCapacity: 3,
            maxCapacity: 300,
            serviceNamespace: ServiceNamespace.ECS,
            resourceId: `service/${ecsCluster.clusterName}/${_wordPressFargateService.attrName}`
        });

        wordPressServiceScaling.scaleToTrackMetric('RequestCountPerTarget', {
            predefinedMetric: PredefinedMetric.ALB_REQUEST_COUNT_PER_TARGET,
            resourceLabel: `${applicationLoadBalancer.loadBalancerFullName}/${_wordPressFargateServiceTargetGroup.attrTargetGroupFullName}`,
            targetValue: 4096,
            scaleInCooldown: Duration.minutes(5),
            scaleOutCooldown: Duration.minutes(5)
        });

        wordPressServiceScaling.scaleToTrackMetric('TargetResponseTime', {
            customMetric: applicationLoadBalancer.metricTargetResponseTime(),
            targetValue: 4,
            scaleInCooldown: Duration.minutes(3),
            scaleOutCooldown: Duration.minutes(3)
        });

        const adminWhitelistIpSet = new CfnIPSet(this, 'AdminWhitelistIpSet', {
            addresses: [...props.whitelistIpAddress],
            scope: 'REGIONAL',
            ipAddressVersion: 'IPV4'
        });

        const wordPressCloudFrontDistributionWafWebAcl = new CfnWebACL(this, 'WordPressCloudFrontDistributionWafWebAcl', {
            defaultAction: {allow: {}},
            scope: 'CLOUDFRONT',
            visibilityConfig: {
                sampledRequestsEnabled: true,
                cloudWatchMetricsEnabled: true,
                metricName: 'CloudFrontDistributionWebAclMetric'
            },
            rules: [
                {
                    name: 'RuleWithAWSManagedRulesCommonRuleSet',
                    priority: 0,
                    overrideAction: {none: {}},
                    visibilityConfig: {
                        sampledRequestsEnabled: true,
                        cloudWatchMetricsEnabled: true,
                        metricName: 'CommonRuleSetMetric'
                    },
                    statement: {
                        managedRuleGroupStatement: {
                            vendorName: 'AWS',
                            name: 'AWSManagedRulesCommonRuleSet',
                            excludedRules: [{name: 'SizeRestrictions_BODY'}, {name: 'GenericRFI_BODY'}, {name: 'GenericRFI_URIPATH'}, {name: 'GenericRFI_QUERYARGUMENTS'}]
                        }
                    }
                },
                {
                    name: 'RuleWithAWSManagedRulesKnownBadInputsRuleSet',
                    priority: 1,
                    overrideAction: {none: {}},
                    visibilityConfig: {
                        sampledRequestsEnabled: true,
                        cloudWatchMetricsEnabled: true,
                        metricName: 'KnownBadInputsRuleSetMetric'
                    },
                    statement: {
                        managedRuleGroupStatement: {
                            vendorName: 'AWS',
                            name: 'AWSManagedRulesKnownBadInputsRuleSet',
                            excludedRules: []
                        }
                    }
                },
                {
                    name: 'RuleWithAWSManagedRulesWordPressRuleSet',
                    priority: 2,
                    overrideAction: {none: {}},
                    visibilityConfig: {
                        sampledRequestsEnabled: true,
                        cloudWatchMetricsEnabled: true,
                        metricName: 'WordPressRuleSetMetric'
                    },
                    statement: {
                        managedRuleGroupStatement: {
                            vendorName: 'AWS',
                            name: 'AWSManagedRulesWordPressRuleSet',
                            excludedRules: []
                        }
                    }
                },
                {
                    name: 'RuleWithAWSManagedRulesPHPRuleSet',
                    priority: 3,
                    overrideAction: {none: {}},
                    visibilityConfig: {
                        sampledRequestsEnabled: true,
                        cloudWatchMetricsEnabled: true,
                        metricName: 'PHPRuleSetMetric'
                    },
                    statement: {
                        managedRuleGroupStatement: {
                            vendorName: 'AWS',
                            name: 'AWSManagedRulesPHPRuleSet',
                            excludedRules: []
                        }
                    }
                },
                {
                    name: 'RuleWithAWSManagedRulesSQLiRuleSet',
                    priority: 4,
                    overrideAction: {none: {}},
                    visibilityConfig: {
                        sampledRequestsEnabled: true,
                        cloudWatchMetricsEnabled: true,
                        metricName: 'AWSManagedRulesSQLiRuleSetMetric'
                    },
                    statement: {
                        managedRuleGroupStatement: {
                            vendorName: 'AWS',
                            name: 'AWSManagedRulesSQLiRuleSet',
                            excludedRules: []
                        }
                    }
                },
                {
                    name: 'RuleWithAWSManagedRulesAmazonIpReputationList',
                    priority: 5,
                    overrideAction: {none: {}},
                    visibilityConfig: {
                        sampledRequestsEnabled: true,
                        cloudWatchMetricsEnabled: true,
                        metricName: 'AmazonIpReputationListMetric'
                    },
                    statement: {
                        managedRuleGroupStatement: {
                            vendorName: 'AWS',
                            name: 'AWSManagedRulesAmazonIpReputationList',
                            excludedRules: []
                        }
                    }
                }
            ]
        });

        const applicationLoadBalancerWebAcl = new CfnWebACL(this, 'ApplicationLoadBalancerWafWebAcl', {
            defaultAction: {block: {}},
            scope: 'REGIONAL',
            visibilityConfig: {
                sampledRequestsEnabled: true,
                cloudWatchMetricsEnabled: true,
                metricName: 'ApplicationLoadBalancerWebAclMetric'
            },
            rules: [
                {
                    name: 'RuleToAllowNonAdminRequest',
                    priority: 0,
                    action: {allow: {}},
                    visibilityConfig: {
                        sampledRequestsEnabled: true,
                        cloudWatchMetricsEnabled: true,
                        metricName: 'RuleToAllowNonAdminRequestMetric'
                    },
                    statement: {
                        andStatement: {
                            statements: [
                                {
                                    notStatement: {
                                        statement: {
                                            byteMatchStatement: {
                                                fieldToMatch: {uriPath: {}},
                                                positionalConstraint: "STARTS_WITH",
                                                searchString: '/wp-admin',
                                                textTransformations: [{type: 'NONE', priority: 0}]
                                            }
                                        }
                                    }
                                },
                                {
                                    byteMatchStatement: {
                                        fieldToMatch: {
                                            singleHeader: {
                                                Name: 'X_Request_From_CloudFront'
                                            }
                                        },
                                        positionalConstraint: 'EXACTLY',
                                        searchString: props.cloudFrontHashHeader,
                                        textTransformations: [{type: 'NONE', priority: 0}]
                                    }
                                }
                            ]
                        }
                    }
                },
                {
                    name: 'RuleToAllowRequestWhitelistedIpSourceToAdminPage',
                    priority: 1,
                    action: {allow: {}},
                    visibilityConfig: {
                        sampledRequestsEnabled: true,
                        cloudWatchMetricsEnabled: true,
                        metricName: 'RuleToAllowRequestWhitelistedIpSourceToAdminPageMetric'
                    },
                    statement: {
                        andStatement: {
                            statements: [
                                {
                                    byteMatchStatement: {
                                        fieldToMatch: {
                                            singleHeader: {
                                                Name: 'X_Request_From_CloudFront'
                                            }
                                        },
                                        positionalConstraint: 'EXACTLY',
                                        searchString: props.cloudFrontHashHeader,
                                        textTransformations: [{type: 'NONE', priority: 0}]
                                    }
                                },
                                {
                                    byteMatchStatement: {
                                        fieldToMatch: {uriPath: {}},
                                        positionalConstraint: "STARTS_WITH",
                                        searchString: '/wp-admin',
                                        textTransformations: [{type: 'NONE', priority: 0}]
                                    }
                                },
                                {
                                    ipSetReferenceStatement: {
                                        arn: adminWhitelistIpSet.attrArn,
                                        ipSetForwardedIpConfig: {
                                            headerName: 'X-Forwarded-For',
                                            position: 'ANY',
                                            fallbackBehavior: 'NO_MATCH'
                                        }
                                    }
                                }
                            ]
                        }
                    }
                }
            ]
        });

        new CfnWebACLAssociation(this, 'ApplicationLoadBalancerWafWebAclAssociation', {
            resourceArn: applicationLoadBalancer.loadBalancerArn,
            webAclArn: applicationLoadBalancerWebAcl.attrArn
        });

        const wordPressDistribution = new CloudFrontWebDistribution(this, 'WordPressDistribution', {
            originConfigs: [
                {
                    customOriginSource: {
                        domainName: applicationLoadBalancer.loadBalancerDnsName,
                        originProtocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,
                        originReadTimeout: Duration.minutes(1),
                        originHeaders: {
                            'X_Request_From_CloudFront': props.cloudFrontHashHeader
                        }
                    },
                    behaviors: [
                        {
                            isDefaultBehavior: true,
                            forwardedValues: {
                                queryString: true,
                                cookies: {
                                    forward: 'whitelist',
                                    whitelistedNames: [
                                        'comment_*',
                                        'wordpress_*',
                                        'wp-settings-*'
                                    ]
                                },
                                headers: [
                                    'Host',
                                    'CloudFront-Forwarded-Proto',
                                    'CloudFront-Is-Mobile-Viewer',
                                    'CloudFront-Is-Tablet-Viewer',
                                    'CloudFront-Is-Desktop-Viewer'
                                ]
                            },
                            cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
                            allowedMethods: CloudFrontAllowedMethods.ALL
                        },
                        {
                            pathPattern: 'wp-admin/*',
                            forwardedValues: {
                                queryString: true,
                                cookies: {
                                    forward: 'all'
                                },
                                headers: ['*']
                            },
                            cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
                            allowedMethods: CloudFrontAllowedMethods.ALL
                        },
                        {
                            pathPattern: 'wp-login.php',
                            forwardedValues: {
                                queryString: true,
                                cookies: {
                                    forward: 'all'
                                },
                                headers: ['*']
                            },
                            cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
                            allowedMethods: CloudFrontAllowedMethods.ALL
                        }
                    ]
                }
            ],
            priceClass: PriceClass.PRICE_CLASS_ALL,
            viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
            httpVersion: HttpVersion.HTTP2,
            defaultRootObject: '',
            viewerCertificate: ViewerCertificate.fromAcmCertificate(acmCertificate, {aliases: [props.hostname]}),
            webACLId: wordPressCloudFrontDistributionWafWebAcl.attrArn,
            loggingConfig: {
                bucket: loggingBucket,
                prefix: 'wordpress-distribution'
            }
        });
        (wordPressDistribution.node.defaultChild as CfnDistribution).addDependsOn(wordPressCloudFrontDistributionWafWebAcl);

        const staticContentBucketOriginAccessIdentity = new OriginAccessIdentity(this, 'StaticContentBucketOriginAccessIdentity');
        staticContentBucket.grantRead(staticContentBucketOriginAccessIdentity);

        const staticContentDistribution = new CloudFrontWebDistribution(this, 'StaticContentDistribution', {
            originConfigs: [
                {
                    s3OriginSource: {
                        s3BucketSource: staticContentBucket,
                        originAccessIdentity: staticContentBucketOriginAccessIdentity
                    },
                    behaviors: [
                        {
                            isDefaultBehavior: true,
                            forwardedValues: {
                                queryString: true,
                                cookies: {
                                    forward: 'none'
                                }
                            },
                            cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD,
                            allowedMethods: CloudFrontAllowedMethods.GET_HEAD
                        }
                    ]
                },
            ],
            priceClass: PriceClass.PRICE_CLASS_ALL,
            viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
            httpVersion: HttpVersion.HTTP2,
            defaultRootObject: '',
            viewerCertificate: ViewerCertificate.fromAcmCertificate(acmCertificate, {aliases: [`static.${props.hostname}`]}),
            loggingConfig: {
                bucket: loggingBucket,
                prefix: 'static-content-distribution'
            }
        });


        const backupVault = new BackupVault(this, 'BackupVault', {
            backupVaultName: 'AwsServerlessWordPressBackupVault',
            removalPolicy: props.removalPolicy
        });

        const backupPlan = BackupPlan.dailyMonthly1YearRetention(this, 'BackupPlan', backupVault);

        backupPlan.addSelection('BackupPlanSelection', {
            resources: [
                BackupResource.fromEfsFileSystem(fileSystem),
                BackupResource.fromArn(this.formatArn({
                    resource: 'cluster',
                    service: 'rds',
                    sep: ':',
                    resourceName: rdsAuroraCluster.clusterIdentifier
                }))
            ]
        });

        const awsConfigOnComplianceSnsTopic = new Topic(this, 'AwsConfigOnComplianceSnsTopic', {masterKey: awsManagedSnsKmsKey});
        props.snsEmailSubscription.forEach(email => awsConfigOnComplianceSnsTopic.addSubscription(new EmailSubscription(email)));

        const configurationRecorderRole = new Role(this, 'ConfigurationRole', {
            assumedBy: new ServicePrincipal('config.amazonaws.com')
        });
        configurationRecorderRole.addToPolicy(new PolicyStatement({
            actions: ['s3:PutObject'],
            resources: [`${loggingBucket.bucketArn}/config/AWSLogs/${this.account}/*`],
            conditions: {
                StringLike: {
                    "s3:x-amz-acl": "bucket-owner-full-control"
                }
            }
        }));
        configurationRecorderRole.addToPolicy(new PolicyStatement({
            actions: ['s3:GetBucketAcl'],
            resources: [loggingBucket.bucketArn]
        }))

        const configurationRecorder = new CfnConfigurationRecorder(this, 'ConfigurationRecorder', {
            recordingGroup: {
                allSupported: true,
                includeGlobalResourceTypes: true
            },
            roleArn: configurationRecorderRole.roleArn
        });

        const deliveryChannel = new CfnDeliveryChannel(this, 'DeliveryChannel', {
            configSnapshotDeliveryProperties: {
                deliveryFrequency: 'One_Hour'
            },
            s3BucketName: loggingBucket.bucketName,
            s3KeyPrefix: 'config'
        });
        const ruleScope = RuleScope.fromTag(globalTagKey, globalTagValue);
        const awsConfigManagesRules = [
            new ManagedRule(this, 'AwsConfigManagedRuleVpcFlowLogsEnabled', {
                identifier: 'VPC_FLOW_LOGS_ENABLED',
                inputParameters: {trafficType: 'ALL'},
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleVpcSgOpenOnlyToAuthorizedPorts', {
                identifier: 'VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS',
                inputParameters: {authorizedTcpPorts: '443'},
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleInternetGatewayAuthorizedVpcOnly', {
                identifier: 'INTERNET_GATEWAY_AUTHORIZED_VPC_ONLY',
                inputParameters: {AuthorizedVpcIds: vpc.vpcId},
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleAcmCertificateExpirationCheck', {
                identifier: 'ACM_CERTIFICATE_EXPIRATION_CHECK',
                inputParameters: {daysToExpiration: 90},
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleAutoScalingGroupElbHealthcheckRequired', {
                identifier: 'AUTOSCALING_GROUP_ELB_HEALTHCHECK_REQUIRED',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleIncomingSshDisabled', {
                identifier: 'INCOMING_SSH_DISABLED',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleSnsEncryptedKms', {
                identifier: 'SNS_ENCRYPTED_KMS',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleElbDeletionProtection', {
                identifier: 'ELB_DELETION_PROTECTION_ENABLED',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleElbLoggingEnabled', {
                identifier: 'ELB_LOGGING_ENABLED',
                inputParameters: {s3BucketNames: loggingBucket.bucketName},
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleAlbHttpDropInvalidHeaderEnabled', {
                identifier: 'ALB_HTTP_DROP_INVALID_HEADER_ENABLED',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleAlbHttpToHttpsRedirectionCheck', {
                identifier: 'ALB_HTTP_TO_HTTPS_REDIRECTION_CHECK',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleAlbWafEnabled', {
                identifier: 'ALB_WAF_ENABLED',
                inputParameters: {wafWebAclIds: applicationLoadBalancerWebAcl.attrArn},
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleCloudFrontOriginAccessIdentityEnabled', {
                identifier: 'CLOUDFRONT_ORIGIN_ACCESS_IDENTITY_ENABLED',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleCloudFrontViewerPolicyHttps', {
                identifier: 'CLOUDFRONT_VIEWER_POLICY_HTTPS',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleEfsInBackupPlan', {
                identifier: 'EFS_IN_BACKUP_PLAN',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleEfsEncryptedCheck', {
                identifier: 'EFS_ENCRYPTED_CHECK',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleRdsClusterDeletionProtectionEnabled', {
                identifier: 'RDS_CLUSTER_DELETION_PROTECTION_ENABLED',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleEdsInBackupPlan', {
                identifier: 'RDS_IN_BACKUP_PLAN',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleS3BucketPublicReadProhibited', {
                identifier: 'S3_BUCKET_PUBLIC_READ_PROHIBITED',
                ruleScope
            }),
            new ManagedRule(this, 'AwsConfigManagedRuleS3BucketPublicWriteProhibited', {
                identifier: 'S3_BUCKET_PUBLIC_WRITE_PROHIBITED',
                ruleScope
            })
        ]
        awsConfigManagesRules.forEach(rule => {
            rule.onComplianceChange('TopicEvent', {target: new SnsTopic(awsConfigOnComplianceSnsTopic)});
            (rule.node.defaultChild as CfnConfigRule).addDependsOn(configurationRecorder);
            (rule.node.defaultChild as CfnConfigRule).addDependsOn(deliveryChannel);
        });

        const awsConfigCloudFormationStackDriftDetectionCheckRule = new CloudFormationStackDriftDetectionCheck(this, 'AwsConfigCloudFormationStackDriftDetectionCheck', {ownStackOnly: true});
        awsConfigCloudFormationStackDriftDetectionCheckRule.onComplianceChange('TopicEvent', {target: new SnsTopic(awsConfigOnComplianceSnsTopic)});
        (awsConfigCloudFormationStackDriftDetectionCheckRule.node.defaultChild as CfnConfigRule).addDependsOn(configurationRecorder);
        (awsConfigCloudFormationStackDriftDetectionCheckRule.node.defaultChild as CfnConfigRule).addDependsOn(deliveryChannel);

        new CfnGroup(this, 'ResourceGroup', {
            name: 'ServerlessWordPressResourceGroup',
            resourceQuery: {
                type: 'TAG_FILTERS_1_0',
                query: {
                    resourceTypeFilters: ['AWS::AllSupported'],
                    tagFilters: [
                        {
                            key: globalTagKey,
                            values: [globalTagValue]
                        }
                    ]
                }
            }
        });

        const rootDnsRecord = new ARecord(this, 'RootDnsRecord', {
            zone: publicHostedZone,
            recordName: props.hostname,
            target: RecordTarget.fromAlias(new CloudFrontTarget(wordPressDistribution))
        });

        const vpnAdminPublicRecord = new ARecord(this, 'VpnAdminPublicDnsRecord', {
            zone: publicHostedZone,
            recordName: `admin.${props.hostname}`,
            target: RecordTarget.fromAlias(new LoadBalancerTarget(vpnAdminApplicationLoadBalancer))
        });

        const vpnAdminPrivateRecord = new ARecord(this, 'VpnAdminPrivateDnsRecord', {
            zone: privateHostedZone,
            recordName: `admin.${privateHostedZone.zoneName}`,
            target: RecordTarget.fromAlias(new LoadBalancerTarget(vpnAdminApplicationLoadBalancer))
        });

        const staticContentDnsRecord = new ARecord(this, 'StaticContentDnsRecord', {
            zone: publicHostedZone,
            recordName: `static.${props.hostname}`,
            target: RecordTarget.fromAlias(new CloudFrontTarget(staticContentDistribution))
        });

        new CfnOutput(this, 'RootHostname', {
            value: rootDnsRecord.domainName
        });

        new CfnOutput(this, 'VpnAdminPublicHostname', {
            value: vpnAdminPublicRecord.domainName
        });

        new CfnOutput(this, 'VpnAdminPrivateHostname', {
            value: vpnAdminPrivateRecord.domainName
        });

        new CfnOutput(this, 'StaticContentHostname', {
            value: staticContentDnsRecord.domainName
        });

        new CfnOutput(this, 'RdsAuroraServerlessClusterPrivateHostname', {
            value: rdsAuroraClusterPrivateDnsRecord.domainName
        });

        new CfnOutput(this, 'ElastiCacheMemcachedClusterPrivateHostname', {
            value: elastiCacheMemcachedClusterPrivateDnsRecord.domainName
        });

        new CfnOutput(this, 'ElasticsearchDomainPrivateHostname', {
            value: elasticsearchDomainPrivateDnsRecord.domainName
        });

        new CfnOutput(this, 'EfsFileSystemPrivateHostname', {
            value: fileSystemEndpointPrivateDnsRecord.domainName
        });
    }
Example #19
Source File: city-stack.ts    From MDDL with MIT License 4 votes vote down vote up
/**
   * Add hosting for a web app
   * @param appName The name of the web app
   * @param hostedDomainConfig The configuration for its hosting domain (optional)
   * @param hostedZone The hosted zone (optional)
   */
  private addHosting(
    appName: string,
    hostedDomainConfig?: HostedDomain,
    hostedZone?: IHostedZone,
  ) {
    //Create Certificate
    let viewerCertificate: ViewerCertificate | undefined
    if (hostedDomainConfig) {
      const certificate = Certificate.fromCertificateArn(
        this,
        `${appName}Certificate`,
        hostedDomainConfig.certificateArn,
      )
      viewerCertificate = ViewerCertificate.fromAcmCertificate(certificate, {
        aliases: [hostedDomainConfig.domain],
        securityPolicy: SecurityPolicyProtocol.TLS_V1_2_2019,
      })
    }

    // Random part included for easier update if needed
    const bucketName = `${this.stackName}-${appName}-AEBE24AF`.toLowerCase()
    this.bucketNames[appName] = bucketName

    // Create App Bucket
    const bucket = new Bucket(this, `${appName}Bucket`, {
      blockPublicAccess: {
        blockPublicAcls: true,
        blockPublicPolicy: true,
        ignorePublicAcls: true,
        restrictPublicBuckets: true,
      },
      bucketName,
    })

    // Create App Origin Access Identity
    const originAccessIdentity = new OriginAccessIdentity(
      this,
      `${appName}OriginAccessIdentity`,
      {
        comment: appName,
      },
    )
    bucket.grantRead(originAccessIdentity)

    // Create App CloudFront Distribution
    const cloudFrontDistribution = new CloudFrontWebDistribution(
      this,
      `${appName}CloudFrontWebDistribution`,
      {
        defaultRootObject: 'index.html',
        errorConfigurations: [
          {
            errorCode: 403,
            responseCode: 200,
            responsePagePath: '/index.html',
          },
          {
            errorCode: 404,
            responseCode: 200,
            responsePagePath: '/index.html',
          },
        ],
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: bucket,
              originAccessIdentity: originAccessIdentity,
            },
            behaviors: [
              ...['/index.html', '/sw.js'].map(
                (shortCachePathPattern) =>
                  ({
                    maxTtl: Duration.minutes(5),
                    minTtl: Duration.minutes(5),
                    defaultTtl: Duration.minutes(5),
                    pathPattern: shortCachePathPattern,
                    compress: true,
                  } as any),
              ),
              {
                isDefaultBehavior: true,
                compress: true,
              },
            ],
          },
        ],
        viewerCertificate,
      },
    )
    cloudFrontDistribution.node.addDependency(bucket, originAccessIdentity)

    // Create Domain Record
    if (hostedDomainConfig && hostedZone) {
      const { domain } = hostedDomainConfig
      const aliasRecord = new ARecord(this, `${appName}AliasRecord`, {
        zone: hostedZone,
        recordName: domain,
        target: RecordTarget.fromAlias(
          new CloudFrontTarget(cloudFrontDistribution),
        ),
      })
      aliasRecord.node.addDependency(cloudFrontDistribution)
    }

    return {
      domain: hostedDomainConfig
        ? hostedDomainConfig.domain
        : cloudFrontDistribution.distributionDomainName,
    }
  }
Example #20
Source File: city-stack.ts    From MDDL with MIT License 4 votes vote down vote up
/**
   * Create the bucket for storing uploads
   * @param kmsKey The encryption key for the bucket
   * @param corsOrigins CORS origins for the bucket policy
   */
  private createUploadsBucket(kmsKey: IKey, corsOrigins: string[]) {
    const bucket = new Bucket(this, 'DocumentsBucket', {
      blockPublicAccess: {
        blockPublicAcls: true,
        blockPublicPolicy: true,
        ignorePublicAcls: true,
        restrictPublicBuckets: true,
      },
      encryptionKey: kmsKey,
      removalPolicy: RemovalPolicy.RETAIN,
      cors: [
        {
          allowedMethods: [HttpMethods.POST, HttpMethods.GET],
          allowedOrigins: corsOrigins,
          maxAge: 3000,
          allowedHeaders: [
            'x-amz-*',
            'content-type',
            'content-disposition',
            'content-length',
          ],
        },
      ],
    })
    bucket.addLifecycleRule({
      expiration: Duration.days(14),
      prefix: CityStack.documentsBucketCollectionsPrefix,
    })

    // this will mean uploads will only be accepted over HTTPS
    bucket.addToResourcePolicy(
      new PolicyStatement({
        sid: 'DenyRequestsOverInsecureTransport',
        effect: Effect.DENY,
        actions: ['s3:*'],
        principals: [new AnyPrincipal()],
        resources: [bucket.arnForObjects('*')],
        conditions: {
          Bool: {
            'aws:SecureTransport': false,
          },
        },
      }),
    )

    // this will mean server side encryption either:
    // 1. Cannot be specified in the request (which means the default bucket encryption will be used - specified by the KMS key above)
    // 2. Or the aws:kms type must be specified, and if a key is specified, it must be the KMS key for the stack
    // This is to work around multipart uploads streamed from the bucket itself which maintain the same encryption headers as the object streamed
    // All API PutObject actions do not explicitly set a KMS key.
    bucket.addToResourcePolicy(
      new PolicyStatement({
        sid: 'DenySpecifiedNonKmsEncryptionHeader',
        effect: Effect.DENY,
        principals: [new AnyPrincipal()],
        actions: ['s3:PutObject'],
        resources: [bucket.arnForObjects('*')],
        conditions: {
          StringNotEqualsIfExists: {
            's3:x-amz-server-side-encryption': 'aws:kms',
            's3:x-amz-server-side-encryption-aws-kms-key-id': kmsKey.keyArn,
          },
        },
      }),
    )

    // this will mean requests must be signed with Signature V4 which is the latest supported algorithm
    bucket.addToResourcePolicy(
      new PolicyStatement({
        sid: 'DenyRequestsNotUsingSignatureV4',
        effect: Effect.DENY,
        principals: [new AnyPrincipal()],
        actions: ['s3:*'],
        resources: [bucket.arnForObjects('*')],
        conditions: {
          StringNotEquals: {
            's3:signatureversion': 'AWS4-HMAC-SHA256',
          },
        },
      }),
    )

    return {
      bucket,
    }
  }
Example #21
Source File: index.ts    From aws-cdk-dynamodb-seeder with Apache License 2.0 4 votes vote down vote up
constructor(scope: Construct, id: string, props: Props) {
    super(scope, id);
    if (!props.setup || !Array.isArray(props.setup)) throw new Error('setup value must be an array of JSON objects');
    this.props = props;

    const destinationBucket = new Bucket(this, 'acds-bucket', {
      removalPolicy: RemovalPolicy.DESTROY,
    });
    tmp.setGracefulCleanup();
    tmp.dir((err, dir) => {
      if (err) throw err;
      this.writeTempFile(dir, 'setup.json', props.setup);
      if (props.teardown) {
        this.writeTempFile(dir, 'teardown.json', props.teardown);
      }
      new BucketDeployment(this, id, {
        sources: [Source.asset(dir)],
        destinationBucket,
        retainOnDelete: false,
      });
    });

    const fn = new Function(this, 'handler', {
      runtime: Runtime.NODEJS_12_X,
      handler: 'index.handler',
      timeout: Duration.seconds(900),
      code: Code.fromInline(`
console.log('function loaded');

const AWS = require('aws-sdk');
const s3 = new AWS.S3();

const writeTypeFromAction = (action) => {
  if (action === "Put")
    return "Item";
  if (action === "Delete")
    return "Key";
}

const run = async (filename, action) => {
  console.log('reading from s3');
  const data = await s3.getObject({
    Bucket: "${destinationBucket.bucketName}", 
    Key: filename
  }).promise();
  console.log('finished reading from s3');
  
  console.log('transforming seed data');
  const seed = JSON.parse(data.Body.toString());
  console.log('finished transforming seed data');
  
  const documentClient = new AWS.DynamoDB.DocumentClient({
    convertEmptyValues: true
  });
  console.log('sending data to dynamodb');
  do {
    const requests = [];
    const batch = seed.splice(0, 25);
    for (let i = 0; i < batch.length; i++) {
      requests.push({
        [action + "Request"]: {
          [writeTypeFromAction(action)]: batch[i]
        }
      });
    }
    await documentClient.batchWrite({
      RequestItems: {
        '${props.table.tableName}': [...requests]
      }
    }).promise();
  }
  while (seed.length > 0);
  console.log('finished sending data to dynamodb');
}

exports.handler = async (event) => {
  if (event.mode === "delete")
    await run("teardown.json", "Delete");
  if (event.mode === "create" || event.mode === "update")
    await run("setup.json", "Put");
}`),
    });
    destinationBucket.grantRead(fn);
    props.table.grantWriteData(fn);

    const onEvent = new AwsCustomResource(this, 'on-event', {
      onCreate: {
        ...this.callLambdaOptions(),
        parameters: {
          FunctionName: fn.functionArn,
          InvokeArgs: JSON.stringify({
            mode: 'create',
          }),
        },
      },
      onDelete: props.teardown
        ? {
            ...this.callLambdaOptions(),
            parameters: {
              FunctionName: fn.functionArn,
              InvokeArgs: JSON.stringify({
                mode: 'delete',
              }),
            },
          }
        : undefined,
      onUpdate: props.refreshOnUpdate
        ? {
            ...this.callLambdaOptions(),
            parameters: {
              FunctionName: fn.functionArn,
              InvokeArgs: JSON.stringify({
                mode: 'update',
              }),
            },
          }
        : undefined,
      policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
    });
    fn.grantInvoke(onEvent);
  }
Example #22
Source File: stack.ts    From keycloak-on-aws with Apache License 2.0 4 votes vote down vote up
constructor(scope: Construct, id: string, props: KeycloakStackProps = {}) {
    super(scope, id, props);

    const dbMsg = props.auroraServerless ? 'using aurora serverless' : 'rds mysql';
    const vpcMsg = props.fromExistingVPC ? 'existing vpc' : 'new vpc';

    this.setDescription(`(SO8021) - Deploy keycloak ${dbMsg} with ${vpcMsg}. template version: ${process.env.VERSION}`);

    const certificateArnParam = this.makeParam('CertificateArn', {
      type: 'String',
      description: 'Certificate Arn for Application Load Balancer',
      minLength: 5,
    });

    this.addGroupParam({ 'Application Load Balancer Settings': [certificateArnParam] });

    this._keycloakSettings.certificateArn = certificateArnParam.valueAsString;

    if (!props.auroraServerless) {
      const databaseInstanceType = this.makeParam('DatabaseInstanceType', {
        type: 'String',
        description: 'Instance type to be used for the core instances',
        allowedValues: INSTANCE_TYPES,
        default: 'r5.large',
      });
      this.addGroupParam({ 'Database Instance Settings': [databaseInstanceType] });
      this._keycloakSettings.databaseInstanceType = new ec2.InstanceType(databaseInstanceType.valueAsString);
    }

    if (props.fromExistingVPC) {
      const vpcIdParam = this.makeParam('VpcId', {
        type: 'AWS::EC2::VPC::Id',
        description: 'Your VPC Id',
      });
      const pubSubnetsParam = this.makeParam('PubSubnets', {
        type: 'List<AWS::EC2::Subnet::Id>',
        description: 'Public subnets (Choose two)',
      });
      const privSubnetsParam = this.makeParam('PrivSubnets', {
        type: 'List<AWS::EC2::Subnet::Id>',
        description: 'Private subnets (Choose two)',
      });
      const dbSubnetsParam = this.makeParam('DBSubnets', {
        type: 'List<AWS::EC2::Subnet::Id>',
        description: 'Database subnets (Choose two)',
      });
      this.addGroupParam({ 'VPC Settings': [vpcIdParam, pubSubnetsParam, privSubnetsParam, dbSubnetsParam] });

      const azs = ['a', 'b'];
      const vpc = ec2.Vpc.fromVpcAttributes(this, 'VpcAttr', {
        vpcId: vpcIdParam.valueAsString,
        vpcCidrBlock: Aws.NO_VALUE,
        availabilityZones: azs,
        publicSubnetIds: azs.map((_, index) => Fn.select(index, pubSubnetsParam.valueAsList)),
        privateSubnetIds: azs.map((_, index) => Fn.select(index, privSubnetsParam.valueAsList)),
        isolatedSubnetIds: azs.map((_, index) => Fn.select(index, dbSubnetsParam.valueAsList)),
      });

      Object.assign(this._keycloakSettings, {
        vpc,
        publicSubnets: { subnets: vpc.publicSubnets },
        privateSubnets: { subnets: vpc.privateSubnets },
        databaseSubnets: { subnets: vpc.isolatedSubnets },
      });
    }

    const minContainersParam = this.makeParam('MinContainers', {
      type: 'Number',
      description: 'minimum containers count',
      default: 2,
      minValue: 2,
    });
    const maxContainersParam = this.makeParam('MaxContainers', {
      type: 'Number',
      description: 'maximum containers count',
      default: 10,
      minValue: 2,
    });
    const targetCpuUtilizationParam = this.makeParam('AutoScalingTargetCpuUtilization', {
      type: 'Number',
      description: 'Auto scaling target cpu utilization',
      default: 75,
      minValue: 0,
    });
    this.addGroupParam({ 'AutoScaling Settings': [minContainersParam, maxContainersParam, targetCpuUtilizationParam] });

    const javaOptsParam = this.makeParam('JavaOpts', {
      type: 'String',
      description: 'JAVA_OPTS environment variable',
    });
    this.addGroupParam({ 'Environment variable': [javaOptsParam] });

    new KeyCloak(this, 'KeyCloak', {
      vpc: this._keycloakSettings.vpc,
      publicSubnets: this._keycloakSettings.publicSubnets,
      privateSubnets: this._keycloakSettings.privateSubnets,
      databaseSubnets: this._keycloakSettings.databaseSubnets,
      certificateArn: this._keycloakSettings.certificateArn,
      auroraServerless: props.auroraServerless,
      databaseInstanceType: this._keycloakSettings.databaseInstanceType,
      stickinessCookieDuration: Duration.days(7),
      nodeCount: minContainersParam.valueAsNumber,
      autoScaleTask: {
        min: minContainersParam.valueAsNumber,
        max: maxContainersParam.valueAsNumber,
        targetCpuUtilization: targetCpuUtilizationParam.valueAsNumber,
      },
      env: {
        JAVA_OPTS: javaOptsParam.valueAsString,
      },
    });
  }
Example #23
Source File: ecs-service.ts    From amazon-efs-integrations with MIT No Attribution 4 votes vote down vote up
static create(
    serviceType: ServiceType,
    cluster: Cluster,
    fileSystem?: FileSystem,
    efsAccessPoints?: EfsAccessPoints,
    props?: ApplicationLoadBalancedEc2ServiceProps | ApplicationLoadBalancedFargateServiceProps,
  ) {
    let service: ApplicationLoadBalancedEc2Service | ApplicationLoadBalancedFargateService;

    const containerImage = 'coderaiser/cloudcmd:14.3.10-alpine';
    if (serviceType === ServiceType.EC2) {
      /*
        Initial task definition with the 80:80 port mapping is needed here to ensure the ALB security
        group is created properly. This is not necessary for the Fargate service.
        @todo: Remove when CDK supports EFS for ECS fully.
      */
      const initialEc2TaskDefinition = new Ec2TaskDefinition(cluster, 'Ec2ServiceInitialTaskDefinition', {})
      const container = initialEc2TaskDefinition.addContainer('cloudcmd', {
        image: ContainerImage.fromRegistry(containerImage),
        logging: LogDriver.awsLogs({streamPrefix: 'ecs'}),
        memoryLimitMiB: 512,
      })
      container.addPortMappings({
        containerPort: 80,
        hostPort: 80,
        protocol: Protocol.TCP
      })
      service = new ApplicationLoadBalancedEc2Service(cluster.stack, 'Ec2Service', {
        cluster,
        desiredCount: 2,
        memoryLimitMiB: 512,
        taskDefinition: initialEc2TaskDefinition,
        ...props,
      });
    } else {
      service = new ApplicationLoadBalancedFargateService(cluster.stack, 'FargateService', {
        cluster,
        desiredCount: 2,
        platformVersion: FargatePlatformVersion.VERSION1_4,
        taskImageOptions: {
          containerName: 'cloudcmd',
          image: ContainerImage.fromRegistry(containerImage),
        },
        ...props
      });
    }

    // Required to run Cloud Commander from behind the ALB
    service.targetGroup.enableCookieStickiness(Duration.minutes(5));

    let mountPoints: MountPoint[] = [];
    let volumes: Volume[] = [];

    if (efsAccessPoints) {
      efsAccessPoints.forEach((ap: AwsCustomResource, name: string) => {
        // Don't add the "EcsPrivate" AP on the Fargate task (and vice-versa)
        if (
          (serviceType === ServiceType.EC2 && name === 'FargatePrivate') ||
          (serviceType === ServiceType.FARGATE && name === 'EcsPrivate')
        ) {
          return
        }

        // Drop the "ecs" or "fargate" suffix if it's the "private" access point
        const containerPath = '/files' + (
          (name === 'FargatePrivate' || name === 'EcsPrivate') ? '/private' : ap.getResponseField('RootDirectory.Path')
        );

        mountPoints.push({containerPath, sourceVolume: name})
        volumes.push({
          name,
          efsVolumeConfiguration: {
            fileSystemId: fileSystem!.fileSystemId,
            authorizationConfig: {
              iam: 'ENABLED',
              accessPointId: ap.getResponseField('AccessPointId'),
            },
            transitEncryption: 'ENABLED',
          }
        })
      });
    } else {
      // Whether it's a "Bind Mount" or a root EFS filesystem mount, we'll be using "/files" as the mount point
      mountPoints = [{
        containerPath: '/files',
        sourceVolume: 'files',
      }];

      // `efsVolumeConfiguration` if we're mounting the root EFS filesystem.
      if (fileSystem) {
        volumes = [{
          efsVolumeConfiguration: {
            fileSystemId: fileSystem.fileSystemId,
            transitEncryption: 'ENABLED',
          },
          name: 'files',
        }];
      } else {
        volumes = [{name: 'files'}];
      }
    }

    /*
      This JSON structure represents the final desired task definition, which includes the
      EFS volume configurations. This is a stop-gap measure that will be replaced when this
      capability is fully supported in CloudFormation and CDK.
    */
    const customTaskDefinitionJson = {
      containerDefinitions: [
        {
          command: [
            '--no-keys-panel',
            '--one-file-panel',
            '--port=80',
            '--root=/files',
          ],
          essential: true,
          image: containerImage,
          logConfiguration: {
            logDriver: service.taskDefinition.defaultContainer?.logDriverConfig?.logDriver,
            options: service.taskDefinition.defaultContainer?.logDriverConfig?.options,
          },
          memory: 512,
          mountPoints,
          name: service.taskDefinition.defaultContainer?.containerName,
          portMappings: [
            {
              containerPort: 80,
              hostPort: 80,
              protocol: 'tcp',
            },
          ],
        },
      ],
      cpu: '256',
      executionRoleArn: service.taskDefinition.executionRole?.roleArn,
      family: service.taskDefinition.family,
      memory: '1024',
      networkMode: serviceType === ServiceType.EC2 ? NetworkMode.BRIDGE : NetworkMode.AWS_VPC,
      requiresCompatibilities: [
        serviceType.toUpperCase(),
      ],
      taskRoleArn: service.taskDefinition.taskRole.roleArn,
      volumes,
    };

    /*
      We use `AwsCustomResource` to create a new task definition revision with EFS volume
      configurations, which is available in the AWS SDK.
    */
    const createOrUpdateCustomTaskDefinition = {
      action: 'registerTaskDefinition',
      outputPath: 'taskDefinition.taskDefinitionArn',
      parameters: customTaskDefinitionJson,
      physicalResourceId: PhysicalResourceId.fromResponse('taskDefinition.taskDefinitionArn'),
      service: 'ECS',
    };
    const customTaskDefinition = new AwsCustomResource(service, 'Custom' + serviceType + 'TaskDefinition', {
      onCreate: createOrUpdateCustomTaskDefinition,
      onUpdate: createOrUpdateCustomTaskDefinition,
      policy: AwsCustomResourcePolicy.fromSdkCalls({
        resources: AwsCustomResourcePolicy.ANY_RESOURCE,
      }),
    });
    service.taskDefinition.executionRole?.grantPassRole(customTaskDefinition.grantPrincipal);
    service.taskDefinition.taskRole.grantPassRole(customTaskDefinition.grantPrincipal);

    /*
      Finally, we'll update the ECS service to use the new task definition revision
      that we just created above.
    */
    (service.service.node.tryFindChild('Service') as CfnService)?.addPropertyOverride(
      'TaskDefinition',
      customTaskDefinition.getResponseField('taskDefinition.taskDefinitionArn'),
    );

    return service;
  }
Example #24
Source File: blue-green-using-ecs-stack.ts    From ecs-codepipeline-demo with Apache License 2.0 4 votes vote down vote up
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // =============================================================================
        // ECR and CodeCommit repositories for the Blue/ Green deployment
        // =============================================================================

        // ECR repository for the docker images
        const ecrRepo = new ecr.Repository(this, 'demoAppEcrRepo', {
            imageScanOnPush: true
        });

        // CodeCommit repository for storing the source code
        const codeRepo = new codeCommit.Repository(this, "demoAppCodeRepo", {
            repositoryName: BlueGreenUsingEcsStack.ECS_APP_NAME,
            description: "Demo application hosted on NGINX"
        });

        // =============================================================================
        // CODE BUILD and ECS TASK ROLES for the Blue/ Green deployment
        // =============================================================================

        // IAM role for the Code Build project
        const codeBuildServiceRole = new iam.Role(this, "codeBuildServiceRole", {
            assumedBy: new ServicePrincipal('codebuild.amazonaws.com')
        });

        const inlinePolicyForCodeBuild = new iam.PolicyStatement({
            effect: Effect.ALLOW,
            actions: [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:InitiateLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:CompleteLayerUpload",
                "ecr:PutImage"
            ],
            resources: ["*"]
        });

        codeBuildServiceRole.addToPolicy(inlinePolicyForCodeBuild);

        // ECS task role
        const ecsTaskRole = new iam.Role(this, "ecsTaskRoleForWorkshop", {
            assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com')
        });
        ecsTaskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy"));


        // =============================================================================
        // CODE DEPLOY APPLICATION for the Blue/ Green deployment
        // =============================================================================

        // Creating the code deploy application
        const codeDeployApplication = new codeDeploy.EcsApplication(this, "demoAppCodeDeploy");

        // Creating the code deploy service role
        const codeDeployServiceRole = new iam.Role(this, "codeDeployServiceRole", {
            assumedBy: new ServicePrincipal('codedeploy.amazonaws.com')
        });
        codeDeployServiceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AWSCodeDeployRoleForECS"));

        // IAM role for custom lambda function
        const customLambdaServiceRole = new iam.Role(this, "codeDeployCustomLambda", {
            assumedBy: new ServicePrincipal('lambda.amazonaws.com')
        });

        const inlinePolicyForLambda = new iam.PolicyStatement({
            effect: Effect.ALLOW,
            actions: [
                "iam:PassRole",
                "sts:AssumeRole",
                "codedeploy:List*",
                "codedeploy:Get*",
                "codedeploy:UpdateDeploymentGroup",
                "codedeploy:CreateDeploymentGroup",
                "codedeploy:DeleteDeploymentGroup"
            ],
            resources: ["*"]
        });

        customLambdaServiceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'))
        customLambdaServiceRole.addToPolicy(inlinePolicyForLambda);

        // Custom resource to create the deployment group
        const createDeploymentGroupLambda = new lambda.Function(this, 'createDeploymentGroupLambda', {
            code: lambda.Code.fromAsset(
                path.join(__dirname, 'custom_resources'),
                {
                    exclude: ["**", "!create_deployment_group.py"]
                }),
            runtime: lambda.Runtime.PYTHON_3_8,
            handler: 'create_deployment_group.handler',
            role: customLambdaServiceRole,
            description: "Custom resource to create deployment group",
            memorySize: 128,
            timeout: cdk.Duration.seconds(60)
        });

        // =============================================================================
        // VPC, ECS Cluster, ELBs and Target groups for the Blue/ Green deployment
        // =============================================================================

        // Creating the VPC
        const vpc = new ec2.Vpc(this, 'vpcForECSCluster');

        // Creating a ECS cluster
        const cluster = new ecs.Cluster(this, 'ecsClusterForWorkshop', {vpc});

        // Creating an application load balancer, listener and two target groups for Blue/Green deployment
        const alb = new elb.ApplicationLoadBalancer(this, "alb", {
            vpc: vpc,
            internetFacing: true
        });
        const albProdListener = alb.addListener('albProdListener', {
            port: 80
        });
        const albTestListener = alb.addListener('albTestListener', {
            port: 8080
        });

        albProdListener.connections.allowDefaultPortFromAnyIpv4('Allow traffic from everywhere');
        albTestListener.connections.allowDefaultPortFromAnyIpv4('Allow traffic from everywhere');

        // Target group 1
        const blueGroup = new elb.ApplicationTargetGroup(this, "blueGroup", {
            vpc: vpc,
            protocol: ApplicationProtocol.HTTP,
            port: 80,
            targetType: TargetType.IP,
            healthCheck: {
                path: "/",
                timeout: Duration.seconds(10),
                interval: Duration.seconds(15),
                healthyHttpCodes: "200,404"
            }
        });

        // Target group 2
        const greenGroup = new elb.ApplicationTargetGroup(this, "greenGroup", {
            vpc: vpc,
            protocol: ApplicationProtocol.HTTP,
            port: 80,
            targetType: TargetType.IP,
            healthCheck: {
                path: "/",
                timeout: Duration.seconds(10),
                interval: Duration.seconds(15),
                healthyHttpCodes: "200,404"
            }
        });

        // Registering the blue target group with the production listener of load balancer
        albProdListener.addTargetGroups("blueTarget", {
            targetGroups: [blueGroup]
        });

        // Registering the green target group with the test listener of load balancer
        albTestListener.addTargetGroups("greenTarget", {
            targetGroups: [greenGroup]
        });

        // ================================================================================================
        // CloudWatch Alarms for 5XX errors
        const blue4xxMetric = new cloudWatch.Metric({
            namespace: 'AWS/ApplicationELB',
            metricName: 'HTTPCode_Target_4XX_Count',
            dimensions: {
                TargetGroup: blueGroup.targetGroupFullName,
                LoadBalancer: alb.loadBalancerFullName
            },
            statistic: cloudWatch.Statistic.SUM,
            period: Duration.minutes(1)
        });
        const blueGroupAlarm = new cloudWatch.Alarm(this, "blue5xxErrors", {
            alarmName: "Blue_5xx_Alarm",
            alarmDescription: "CloudWatch Alarm for the 4xx errors of Blue target group",
            metric: blue4xxMetric,
            threshold: 1,
            evaluationPeriods: 1
        });

        const green4xxMetric = new cloudWatch.Metric({
            namespace: 'AWS/ApplicationELB',
            metricName: 'HTTPCode_Target_4XX_Count',
            dimensions: {
                TargetGroup: greenGroup.targetGroupFullName,
                LoadBalancer: alb.loadBalancerFullName
            },
            statistic: cloudWatch.Statistic.SUM,
            period: Duration.minutes(1)
        });
        const greenGroupAlarm = new cloudWatch.Alarm(this, "green4xxErrors", {
            alarmName: "Green_4xx_Alarm",
            alarmDescription: "CloudWatch Alarm for the 4xx errors of Green target group",
            metric: green4xxMetric,
            threshold: 1,
            evaluationPeriods: 1
        });

        // ================================================================================================


        // ================================================================================================
        // DUMMY TASK DEFINITION for the initial service creation
        // This is required for the service being made available to create the CodeDeploy Deployment Group
        // ================================================================================================
        const sampleTaskDefinition = new ecs.FargateTaskDefinition(this, "sampleTaskDefn", {
            family: BlueGreenUsingEcsStack.DUMMY_TASK_FAMILY_NAME,
            cpu: 256,
            memoryLimitMiB: 1024,
            taskRole: ecsTaskRole,
            executionRole: ecsTaskRole
        });
        const sampleContainerDefn = sampleTaskDefinition.addContainer("sampleAppContainer", {
            image: ecs.ContainerImage.fromRegistry(BlueGreenUsingEcsStack.DUMMY_CONTAINER_IMAGE),
            logging: new ecs.AwsLogDriver({
                logGroup: new log.LogGroup(this, "sampleAppLogGroup", {
                    logGroupName: BlueGreenUsingEcsStack.DUMMY_APP_LOG_GROUP_NAME,
                    removalPolicy: RemovalPolicy.DESTROY
                }),
                streamPrefix: BlueGreenUsingEcsStack.DUMMY_APP_NAME
            }),
            dockerLabels: {
                name: BlueGreenUsingEcsStack.DUMMY_APP_NAME
            }
        });
        sampleContainerDefn.addPortMappings({
            containerPort: 80,
            protocol: Protocol.TCP
        });

        // ================================================================================================
        // ECS task definition using ECR image
        // Will be used by the CODE DEPLOY for Blue/Green deployment
        // ================================================================================================
        const taskDefinition = new ecs.FargateTaskDefinition(this, "appTaskDefn", {
            family: BlueGreenUsingEcsStack.ECS_TASK_FAMILY_NAME,
            cpu: 256,
            memoryLimitMiB: 1024,
            taskRole: ecsTaskRole,
            executionRole: ecsTaskRole
        });
        const containerDefinition = taskDefinition.addContainer("demoAppContainer", {
            image: ContainerImage.fromEcrRepository(ecrRepo, "latest"),
            logging: new ecs.AwsLogDriver({
                logGroup: new log.LogGroup(this, "demoAppLogGroup", {
                    logGroupName: BlueGreenUsingEcsStack.ECS_APP_LOG_GROUP_NAME,
                    removalPolicy: RemovalPolicy.DESTROY
                }),
                streamPrefix: BlueGreenUsingEcsStack.ECS_APP_NAME
            }),
            dockerLabels: {
                name: BlueGreenUsingEcsStack.ECS_APP_NAME
            }
        });
        containerDefinition.addPortMappings({
            containerPort: 80,
            protocol: Protocol.TCP
        });

        // =============================================================================
        // ECS SERVICE for the Blue/ Green deployment
        // =============================================================================
        const demoAppService = new ecs.FargateService(this, "demoAppService", {
            cluster: cluster,
            taskDefinition: sampleTaskDefinition,
            healthCheckGracePeriod: Duration.seconds(10),
            desiredCount: 3,
            deploymentController: {
                type: DeploymentControllerType.CODE_DEPLOY
            },
            serviceName: BlueGreenUsingEcsStack.ECS_APP_NAME
        });

        demoAppService.connections.allowFrom(alb, Port.tcp(80))
        demoAppService.connections.allowFrom(alb, Port.tcp(8080))
        demoAppService.attachToApplicationTargetGroup(blueGroup);

        // =============================================================================
        // CODE DEPLOY - Deployment Group CUSTOM RESOURCE for the Blue/ Green deployment
        // =============================================================================

        new CustomResource(this, 'customEcsDeploymentGroup', {
            serviceToken: createDeploymentGroupLambda.functionArn,
            properties: {
                ApplicationName: codeDeployApplication.applicationName,
                DeploymentGroupName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_GROUP_NAME,
                DeploymentConfigName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_CONFIG_NAME,
                ServiceRoleArn: codeDeployServiceRole.roleArn,
                BlueTargetGroup: blueGroup.targetGroupName,
                GreenTargetGroup: greenGroup.targetGroupName,
                ProdListenerArn: albProdListener.listenerArn,
                TestListenerArn: albTestListener.listenerArn,
                EcsClusterName: cluster.clusterName,
                EcsServiceName: demoAppService.serviceName,
                TerminationWaitTime: BlueGreenUsingEcsStack.ECS_TASKSET_TERMINATION_WAIT_TIME,
                BlueGroupAlarm: blueGroupAlarm.alarmName,
                GreenGroupAlarm: greenGroupAlarm.alarmName,
            }
        });

        const ecsDeploymentGroup = codeDeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(this, "ecsDeploymentGroup", {
            application: codeDeployApplication,
            deploymentGroupName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_GROUP_NAME,
            deploymentConfig: EcsDeploymentConfig.fromEcsDeploymentConfigName(this, "ecsDeploymentConfig", BlueGreenUsingEcsStack.ECS_DEPLOYMENT_CONFIG_NAME)
        });


        // =============================================================================
        // CODE BUILD PROJECT for the Blue/ Green deployment
        // =============================================================================

        // Creating the code build project
        const demoAppCodeBuild = new codeBuild.Project(this, "demoAppCodeBuild", {
            role: codeBuildServiceRole,
            description: "Code build project for the demo application",
            environment: {
                buildImage: codeBuild.LinuxBuildImage.STANDARD_4_0,
                computeType: ComputeType.SMALL,
                privileged: true,
                environmentVariables: {
                    REPOSITORY_URI: {
                        value: ecrRepo.repositoryUri,
                        type: BuildEnvironmentVariableType.PLAINTEXT
                    },
                    TASK_EXECUTION_ARN: {
                        value: ecsTaskRole.roleArn,
                        type: BuildEnvironmentVariableType.PLAINTEXT
                    },
                    TASK_FAMILY: {
                        value: BlueGreenUsingEcsStack.ECS_TASK_FAMILY_NAME,
                        type: BuildEnvironmentVariableType.PLAINTEXT
                    }
                }
            },
            source: codeBuild.Source.codeCommit({
                repository: codeRepo
            })
        });

        // =============================================================================
        // CODE PIPELINE for Blue/Green ECS deployment
        // =============================================================================

        const codePipelineServiceRole = new iam.Role(this, "codePipelineServiceRole", {
            assumedBy: new ServicePrincipal('codepipeline.amazonaws.com')
        });

        const inlinePolicyForCodePipeline = new iam.PolicyStatement({
            effect: Effect.ALLOW,
            actions: [
                "iam:PassRole",
                "sts:AssumeRole",
                "codecommit:Get*",
                "codecommit:List*",
                "codecommit:GitPull",
                "codecommit:UploadArchive",
                "codecommit:CancelUploadArchive",
                "codebuild:BatchGetBuilds",
                "codebuild:StartBuild",
                "codedeploy:CreateDeployment",
                "codedeploy:Get*",
                "codedeploy:RegisterApplicationRevision",
                "s3:Get*",
                "s3:List*",
                "s3:PutObject"
            ],
            resources: ["*"]
        });

        codePipelineServiceRole.addToPolicy(inlinePolicyForCodePipeline);

        const sourceArtifact = new codePipeline.Artifact('sourceArtifact');
        const buildArtifact = new codePipeline.Artifact('buildArtifact');

        // S3 bucket for storing the code pipeline artifacts
        const demoAppArtifactsBucket = new s3.Bucket(this, "demoAppArtifactsBucket", {
            encryption: BucketEncryption.S3_MANAGED,
            blockPublicAccess: BlockPublicAccess.BLOCK_ALL
        });

        // S3 bucket policy for the code pipeline artifacts
        const denyUnEncryptedObjectUploads = new iam.PolicyStatement({
            effect: Effect.DENY,
            actions: ["s3:PutObject"],
            principals: [new AnyPrincipal()],
            resources: [demoAppArtifactsBucket.bucketArn.concat("/*")],
            conditions: {
                StringNotEquals: {
                    "s3:x-amz-server-side-encryption": "aws:kms"
                }
            }
        });

        const denyInsecureConnections = new iam.PolicyStatement({
            effect: Effect.DENY,
            actions: ["s3:*"],
            principals: [new AnyPrincipal()],
            resources: [demoAppArtifactsBucket.bucketArn.concat("/*")],
            conditions: {
                Bool: {
                    "aws:SecureTransport": "false"
                }
            }
        });

        demoAppArtifactsBucket.addToResourcePolicy(denyUnEncryptedObjectUploads);
        demoAppArtifactsBucket.addToResourcePolicy(denyInsecureConnections);

        // Code Pipeline - CloudWatch trigger event is created by CDK
        new codePipeline.Pipeline(this, "ecsBlueGreen", {
            role: codePipelineServiceRole,
            artifactBucket: demoAppArtifactsBucket,
            stages: [
                {
                    stageName: 'Source',
                    actions: [
                        new codePipelineActions.CodeCommitSourceAction({
                            actionName: 'Source',
                            repository: codeRepo,
                            output: sourceArtifact,
                        }),
                    ]
                },
                {
                    stageName: 'Build',
                    actions: [
                        new codePipelineActions.CodeBuildAction({
                            actionName: 'Build',
                            project: demoAppCodeBuild,
                            input: sourceArtifact,
                            outputs: [buildArtifact]
                        })
                    ]
                },
                {
                    stageName: 'Deploy',
                    actions: [
                        new codePipelineActions.CodeDeployEcsDeployAction({
                            actionName: 'Deploy',
                            deploymentGroup: ecsDeploymentGroup,
                            appSpecTemplateInput: buildArtifact,
                            taskDefinitionTemplateInput: buildArtifact,
                        })
                    ]
                }
            ]
        });

        // =============================================================================
        // Export the outputs
        // =============================================================================
        new CfnOutput(this, "ecsBlueGreenCodeRepo", {
            description: "Demo app code commit repository",
            exportName: "ecsBlueGreenDemoAppRepo",
            value: codeRepo.repositoryCloneUrlHttp
        });

        new CfnOutput(this, "ecsBlueGreenLBDns", {
            description: "Load balancer DNS",
            exportName: "ecsBlueGreenLBDns",
            value: alb.loadBalancerDnsName
        });


    }