@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 |
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
});
}