@aws-cdk/core#CustomResourceProvider TypeScript Examples

The following examples show how to use @aws-cdk/core#CustomResourceProvider. 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-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
        });
    }