@aws-cdk/core#RemovalPolicy TypeScript Examples

The following examples show how to use @aws-cdk/core#RemovalPolicy. 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: aws-serverless-wordpress.ts    From aws-serverless-wordpress with Apache License 2.0 6 votes vote down vote up
stack = new AwsServerlessWordpressStack(app, 'AwsServerlessWordpressStack', {
    terminationProtection: false,
    resourceDeletionProtection: false,
    removalPolicy: RemovalPolicy.DESTROY,
    env: {region: 'us-east-1', account: config.environment.account},
    databaseCredential: {username: config.database.username, defaultDatabaseName: config.database.defaultDatabaseName},
    domainName: config.domain.domainName,
    hostname: config.domain.hostname,
    alternativeHostname: [...config.domain.alternativeHostname],
    snsEmailSubscription: [...config.contact.email],
    whitelistIpAddress: [...config.admin.allowIpAddresses],
    certificate: {server: config.admin.serverCertificateArn, client: config.admin.clientCertificateArn},
    // This load balancer account ID should not be change if you deploy in us-east-1
    // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-logging-bucket-permissions
    loadBalancerAccountId: '127311923021'
})
Example #2
Source File: integ.default.ts    From aws-cdk-for-k3scluster with MIT License 6 votes vote down vote up
constructor() {
    const app = new App();
    const env = {
      region: process.env.CDK_DEFAULT_REGION,
      account: process.env.CDK_DEFAULT_ACCOUNT,
    };

    const stack = new Stack(app, 'testing-stack', { env });

    const vpc = k3s.VpcProvider.getOrCreate(stack);

    const cluster = new k3s.Cluster(stack, 'Cluster', {
      vpc,
      spotWorkerNodes: true,
      workerMinCapacity: 1,
      workerInstanceType: new ec2.InstanceType('m6g.medium'),
      controlPlaneInstanceType: new ec2.InstanceType('m6g.medium'),
      bucketRemovalPolicy: RemovalPolicy.DESTROY,
    });

    new CfnOutput(stack, 'EndpointURI', { value: cluster.endpointUri });
    new CfnOutput(stack, 'Region', { value: Stack.of(stack).region });
    this.stack = [stack];
  }
Example #3
Source File: cluster.test.ts    From aws-cdk-for-k3scluster with MIT License 6 votes vote down vote up
test('support m6g instance types', () => {
  // GIVEN
  const app = new App();
  const stack = new Stack(app, 'testing-stack');
  // WHEN
  new k3s.Cluster(stack, 'Cluster-test', {
    bucketRemovalPolicy: RemovalPolicy.DESTROY,
    controlPlaneInstanceType: new ec2.InstanceType('m6g.large'),
    workerInstanceType: new ec2.InstanceType('m6g.medium'),
    spotWorkerNodes: true,
  });
  // THEN
  // worker nodes ASG
  expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', {
    LaunchTemplateData: {
      ImageId: {
        Ref: 'SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter',
      },
      InstanceMarketOptions: {
        MarketType: 'spot',
        SpotOptions: {
          SpotInstanceType: 'one-time',
        },
      },
      InstanceType: 'm6g.medium',
    },
  });
  // control plane ec2
  expect(stack).toHaveResource('AWS::EC2::Instance', {
    InstanceType: 'm6g.large',
  });
});
Example #4
Source File: cluster.test.ts    From aws-cdk-for-k3scluster with MIT License 6 votes vote down vote up
test('support t4g instance types', () => {
  // GIVEN
  const app = new App();
  const stack = new Stack(app, 'testing-stack');
  // WHEN
  new k3s.Cluster(stack, 'Cluster-test', {
    bucketRemovalPolicy: RemovalPolicy.DESTROY,
    controlPlaneInstanceType: new ec2.InstanceType('t4g.large'),
    workerInstanceType: new ec2.InstanceType('t4g.medium'),
    spotWorkerNodes: true,
  });
  // THEN
  // worker nodes ASG
  expect(stack).toHaveResourceLike('AWS::EC2::LaunchTemplate', {
    LaunchTemplateData: {
      ImageId: {
        Ref: 'SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter',
      },
      InstanceMarketOptions: {
        MarketType: 'spot',
        SpotOptions: {
          SpotInstanceType: 'one-time',
        },
      },
      InstanceType: 't4g.medium',
    },
  });
  // control plane ec2
  expect(stack).toHaveResource('AWS::EC2::Instance', {
    InstanceType: 't4g.large',
  });
});
Example #5
Source File: city-stack.ts    From MDDL with MIT License 6 votes vote down vote up
/**
   * Create the audit log group for the stack. Predictably named for easier finding in the console.
   * @param kmsKey The encryption key for the log group
   */
  private createAuditLogGroup(kmsKey: IKey) {
    const logGroup = new LogGroup(this, 'AuditLogGroup', {
      logGroupName: this.stackName + '_AuditLogGroup',
      removalPolicy: RemovalPolicy.RETAIN,
      retention: RetentionDays.INFINITE,
    })
    const cfnLogGroup = logGroup.node.defaultChild as CfnLogGroup
    cfnLogGroup.addPropertyOverride('KmsKeyId', kmsKey.keyArn)
    return {
      logGroup,
    }
  }
Example #6
Source File: cluster.test.ts    From aws-cdk-for-k3scluster with MIT License 5 votes vote down vote up
test('add s3 removalPolicy', () => {
  const app = new App();
  const stack = new Stack(app, 'testing-stack');
  new k3s.Cluster(stack, 'Cluster-s3-removalPolicy', {
    bucketRemovalPolicy: RemovalPolicy.DESTROY,
  });
  expect(stack).toHaveResource('AWS::S3::Bucket');
});
Example #7
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 #8
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
        });


    }
Example #9
Source File: pipeline-stack.ts    From aws-cross-account-cicd-pipeline with MIT No Attribution 4 votes vote down vote up
constructor(app: App, id: string, props: PipelineStackProps) {

    super(app, id, props);

    const repository = codecommit.Repository.fromRepositoryName(this, 'CodeCommitRepo', `repo-${this.account}`);

    const prodDeploymentRole = iam.Role.fromRoleArn(this, 'ProdDeploymentRole', `arn:aws:iam::${props.prodAccountId}:role/CloudFormationDeploymentRole`, {
      mutable: false
    });
    const prodCrossAccountRole = iam.Role.fromRoleArn(this, 'ProdCrossAccountRole', `arn:aws:iam::${props.prodAccountId}:role/CodePipelineCrossAccountRole`, {
      mutable: false
    });

    const prodAccountRootPrincipal = new iam.AccountPrincipal(props.prodAccountId);

    const key = new kms.Key(this, 'ArtifactKey', {
      alias: 'key/artifact-key',
    });
    key.grantDecrypt(prodAccountRootPrincipal);
    key.grantDecrypt(prodCrossAccountRole);

    const artifactBucket = new s3.Bucket(this, 'ArtifactBucket', {
      bucketName: `artifact-bucket-${this.account}`,
      removalPolicy: RemovalPolicy.DESTROY,
      encryption: s3.BucketEncryption.KMS,
      encryptionKey: key
    });
    artifactBucket.grantPut(prodAccountRootPrincipal);
    artifactBucket.grantRead(prodAccountRootPrincipal);

    const cdkBuild = new codebuild.PipelineProject(this, 'CdkBuild', {
      buildSpec: codebuild.BuildSpec.fromObject({
        version: '0.2',
        phases: {
          install: {
            commands: [
              'npm install'
            ],
          },
          build: {
            commands: [
              'npm run build',
              'npm run cdk synth -- -o dist'
            ],
          },
        },
        artifacts: {
          'base-directory': 'dist',
          files: [
            '*ApplicationStack.template.json',
          ],
        },
      }),
      environment: {
        buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_NODEJS_10_14_1,
      },
      encryptionKey: key
    });
    const lambdaBuild = new codebuild.PipelineProject(this, 'LambdaBuild', {
      buildSpec: codebuild.BuildSpec.fromObject({
        version: '0.2',
        phases: {
          install: {
            commands: [
              'cd app',
              'npm install',
            ],
          },
          build: {
            commands: 'npm run build',
          },
        },
        artifacts: {
          'base-directory': 'app',
          files: [
            'index.js',
            'node_modules/**/*',
          ],
        },
      }),
      environment: {
        buildImage: codebuild.LinuxBuildImage.UBUNTU_14_04_NODEJS_10_14_1,
      },
      encryptionKey: key
    });

    const sourceOutput = new codepipeline.Artifact();
    const cdkBuildOutput = new codepipeline.Artifact('CdkBuildOutput');
    const lambdaBuildOutput = new codepipeline.Artifact('LambdaBuildOutput');

    const pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
      pipelineName: 'CrossAccountPipeline',
      artifactBucket: artifactBucket,
      stages: [
        {
          stageName: 'Source',
          actions: [
            new codepipeline_actions.CodeCommitSourceAction({
              actionName: 'CodeCommit_Source',
              repository: repository,
              output: sourceOutput,
            }),
          ],
        },
        {
          stageName: 'Build',
          actions: [
            new codepipeline_actions.CodeBuildAction({
              actionName: 'Application_Build',
              project: lambdaBuild,
              input: sourceOutput,
              outputs: [lambdaBuildOutput],
            }),
            new codepipeline_actions.CodeBuildAction({
              actionName: 'CDK_Synth',
              project: cdkBuild,
              input: sourceOutput,
              outputs: [cdkBuildOutput],
            }),
          ],
        },
        {
          stageName: 'Deploy_Dev',
          actions: [
            new codepipeline_actions.CloudFormationCreateUpdateStackAction({
              actionName: 'Deploy',
              templatePath: cdkBuildOutput.atPath('DevApplicationStack.template.json'),
              stackName: 'DevApplicationDeploymentStack',
              adminPermissions: true,
              parameterOverrides: {
                ...props.devApplicationStack.lambdaCode.assign(lambdaBuildOutput.s3Location),
              },
              extraInputs: [lambdaBuildOutput],
            })
          ],
        },
        {
          stageName: 'Deploy_Prod',
          actions: [
            new codepipeline_actions.CloudFormationCreateUpdateStackAction({
              actionName: 'Deploy',
              templatePath: cdkBuildOutput.atPath('ProdApplicationStack.template.json'),
              stackName: 'ProdApplicationDeploymentStack',
              adminPermissions: true,
              parameterOverrides: {
                ...props.prodApplicationStack.lambdaCode.assign(lambdaBuildOutput.s3Location),
              },
              deploymentRole: prodDeploymentRole,
              capabilities: [cloudformation.CloudFormationCapabilities.ANONYMOUS_IAM],
              extraInputs: [lambdaBuildOutput],
              role: prodCrossAccountRole,
            }),
          ],
        },
      ],
    });

    pipeline.addToRolePolicy(new iam.PolicyStatement({
      actions: ['sts:AssumeRole'],
      resources: [`arn:aws:iam::${props.prodAccountId}:role/*`]
    }));

    new CfnOutput(this, 'ArtifactBucketEncryptionKeyArn', {
      value: key.keyArn,
      exportName: 'ArtifactBucketEncryptionKey'
    });

  }
Example #10
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 #11
Source File: hasura-stack.ts    From hasura-cdk with MIT License 4 votes vote down vote up
constructor(scope: Construct, id: string, props: HasuraStackProps) {
        super(scope, id, props);

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


        const hasuraDatabaseName = props.appName;

        const hasuraDatabase = new DatabaseInstance(this, 'HasuraDatabase', {
            instanceIdentifier: props.appName,
            databaseName: hasuraDatabaseName,
            engine: DatabaseInstanceEngine.POSTGRES,
            instanceType: InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.MICRO),
            masterUsername: 'syscdk',
            storageEncrypted: true,
            allocatedStorage: 20,
            maxAllocatedStorage: 100,
            vpc: props.vpc,
            deletionProtection: false,
            multiAz: props.multiAz,
            removalPolicy: RemovalPolicy.DESTROY,
        });

        const hasuraUsername = 'hasura';

        const hasuraUserSecret = new DatabaseSecret(this, 'HasuraDatabaseUser', {
            username: hasuraUsername,
            masterSecret: hasuraDatabase.secret,

        });
        hasuraUserSecret.attach(hasuraDatabase); // Adds DB connections information in the secret

        // Output the Endpoint Address so it can be used in post-deploy
        new CfnOutput(this, 'HasuraDatabaseUserSecretArn', {
            value: hasuraUserSecret.secretArn,
        });

        new CfnOutput(this, 'HasuraDatabaseMasterSecretArn', {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            value: hasuraDatabase.secret!.secretArn,
        });

        const hasuraDatabaseUrlSecret = new Secret(this, 'HasuraDatabaseUrlSecret', {
            secretName: `${props.appName}-HasuraDatabaseUrl`,
        });


        new CfnOutput(this, 'HasuraDatabaseUrlSecretArn', {
            value: hasuraDatabaseUrlSecret.secretArn,
        });

        const hasuraAdminSecret = new Secret(this, 'HasuraAdminSecret', {
            secretName: `${props.appName}-HasuraAdminSecret`,
        });

        new CfnOutput(this, 'HasuraAdminSecretArn', {
            value: hasuraAdminSecret.secretArn,
        });

        const hasuraJwtSecret = new Secret(this, 'HasuraJwtSecret', {
            secretName: `${props.appName}-HasuraJWTSecret`,
        });

        new CfnOutput(this, 'HasuraJwtSecretArn', {
            value: hasuraJwtSecret.secretArn,
        });


        // Create a load-balanced Fargate service and make it public
        const fargate = new ApplicationLoadBalancedFargateService(this, 'HasuraFargateService', {
            serviceName: props.appName,
            vpc: props.vpc,
            cpu: 256,
            desiredCount: props.multiAz ? 2 : 1,
            taskImageOptions: {
                image: ContainerImage.fromRegistry('hasura/graphql-engine:v1.2.1'),
                containerPort: 8080,
                enableLogging: true,
                environment: {
                    HASURA_GRAPHQL_ENABLE_CONSOLE: 'true',
                    HASURA_GRAPHQL_PG_CONNECTIONS: '100',
                    HASURA_GRAPHQL_LOG_LEVEL: 'debug',
                },
                secrets: {
                    HASURA_GRAPHQL_DATABASE_URL: ECSSecret.fromSecretsManager(hasuraDatabaseUrlSecret),
                    HASURA_GRAPHQL_ADMIN_SECRET: ECSSecret.fromSecretsManager(hasuraAdminSecret),
                    HASURA_GRAPHQL_JWT_SECRET: ECSSecret.fromSecretsManager(hasuraJwtSecret),
                },
            },
            memoryLimitMiB: 512,
            publicLoadBalancer: true, // Default is false
            certificate: props.certificates.hasura,
            domainName: props.hasuraHostname,
            domainZone: hostedZone,
            assignPublicIp: true,
        });

        fargate.targetGroup.configureHealthCheck({
            enabled: true,
            path: '/healthz',
            healthyHttpCodes: '200',
        });

        hasuraDatabase.connections.allowFrom(fargate.service, new Port({
            protocol: Protocol.TCP,
            stringRepresentation: 'Postgres Port',
            fromPort: 5432,
            toPort: 5432,
        }));
    }
Example #12
Source File: auth-stack.ts    From MDDL with MIT License 4 votes vote down vote up
/**
   * Configure and add the user pool resource
   * @param userPoolName The name of the user pool
   * @param emailSender Details on how to send emails (optional)
   */
  private addUserPool(userPoolName: string, emailSender?: EmailSender) {
    const userPool = new UserPool(this, 'UserPool', {
      accountRecovery: AccountRecovery.EMAIL_ONLY,
      signInAliases: {
        email: true,
      },
      emailSettings: emailSender
        ? {
            from: `${emailSender.name} <${emailSender.address}>`,
          }
        : undefined,
      userVerification: {
        emailStyle: VerificationEmailStyle.LINK,
        emailSubject: 'Please verify your email address',
      },
      selfSignUpEnabled: true,
      passwordPolicy: {
        minLength: 6,
        requireDigits: true,
        requireLowercase: false,
        requireUppercase: true,
        requireSymbols: true,
      },
      standardAttributes: {
        email: {
          mutable: true,
          required: true,
        },
        givenName: {
          mutable: true,
          required: true,
        },
        familyName: {
          mutable: true,
          required: true,
        },
        locale: {
          mutable: true,
          required: false,
        },
        timezone: {
          mutable: true,
          required: false,
        },
      },
      customAttributes: {
        features: new StringAttribute({
          maxLen: 255,
          mutable: true,
          minLen: 0,
        }),
      },
      signInCaseSensitive: false,
      userPoolName,
    })

    // manual override to retain the user pool
    const cfnUserPool = userPool.node.defaultChild as CfnUserPool
    cfnUserPool.applyRemovalPolicy(RemovalPolicy.RETAIN)

    // manual overrides to set the config to use a specific identity for sending emails
    if (emailSender) {
      const { address } = emailSender
      cfnUserPool.addPropertyOverride(
        'EmailConfiguration.EmailSendingAccount',
        'DEVELOPER',
      )
      cfnUserPool.addPropertyOverride(
        'EmailConfiguration.SourceArn',
        `arn:aws:ses:${this.region}:${this.account}:identity/${address}`,
      )
    }

    // CSS for hosted login
    const uiCustomization = this.deployUiCustomization
      ? new CfnUserPoolUICustomizationAttachment(
          this,
          'UserPoolUICustomizationAttachment',
          {
            clientId: 'ALL',
            userPoolId: userPool.userPoolId,
            css: getCognitoHostedLoginCss(),
          },
        )
      : undefined

    return {
      userPool,
      uiCustomization,
    }
  }
Example #13
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 #14
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,
      },
    })
  }