├── .gitignore ├── .npmignore ├── LICENSE ├── Makefile ├── README.md ├── bin ├── aws-serverless-wordpress.ts └── config.sample.toml ├── cdk.context.json ├── cdk.json ├── doc ├── architecture-diagram-v2.png └── architecture-diagram.png ├── lib ├── aws-serverless-wordpress-stack.ts ├── custom-resource │ └── index.ts └── images │ ├── docker-compose.yaml │ ├── nginx │ ├── Dockerfile │ ├── nginx.conf │ └── wordpress.conf.template │ └── wordpress │ ├── Dockerfile │ └── uploads.ini ├── package.json ├── tsconfig.json ├── yarn-error.log └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | # Parcel build directories 11 | .cache 12 | .build 13 | 14 | env/ 15 | .idea/ 16 | .vscode/ 17 | 18 | bin/config.toml 19 | lib/cert -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | region=us-east-1 2 | profile=default 3 | stack="*" 4 | clean: 5 | rm -rf node_modules **/node_modules **/*.js **/*.d.ts 6 | build: 7 | yarn run build 8 | init: 9 | yarn install 10 | deploy: build 11 | npx cdk deploy $(stack) --require-approval never --profile $(profile) 12 | destroy: 13 | npx cdk destroy $(stack) --force --profile $(profile) 14 | cdk-upgrade: 15 | yarn upgrade --scope @aws-cdk --latest 16 | yarn upgrade aws-cdk --latest 17 | yarn install 18 | easy-rsa-init: 19 | mkdir -p lib/cert 20 | cd lib/cert &&\ 21 | git clone https://github.com/OpenVPN/easy-rsa.git 22 | gen-cert: 23 | cd lib/cert/easy-rsa/easyrsa3 &&\ 24 | ./easyrsa init-pki &&\ 25 | ./easyrsa build-ca nopass &&\ 26 | ./easyrsa build-server-full server nopass &&\ 27 | ./easyrsa build-client-full client nopass &&\ 28 | cp pki/ca.crt ../../ &&\ 29 | cp pki/issued/server.crt ../../ &&\ 30 | cp pki/private/server.key ../../ &&\ 31 | cp pki/issued/client.crt ../../ &&\ 32 | cp pki/private/client.key ../../ 33 | import-cert: 34 | cd lib/cert &&\ 35 | echo "\nServer Certificate ARN:" &&\ 36 | aws acm import-certificate \ 37 | --certificate fileb://server.crt \ 38 | --private-key fileb://server.key \ 39 | --certificate-chain fileb://ca.crt \ 40 | --query 'CertificateArn' --output text --region $(region) --profile $(profile) &&\ 41 | echo "\nClient Certificate ARN:" &&\ 42 | aws acm import-certificate \ 43 | --certificate fileb://client.crt \ 44 | --private-key fileb://client.key \ 45 | --certificate-chain fileb://ca.crt \ 46 | --query 'CertificateArn' --output text --region $(region) --profile $(profile) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Serverless WordPress 2 | 3 | ## Introduction 4 | Please read the blog post for introduction and explanation. 5 | [Dev.to: Best Practices for Running WordPress on AWS using CDK](https://dev.to/aws-builders/best-practices-for-running-wordpress-on-aws-using-cdk-aj9) 6 | 7 | ### WordPress Plugin Used 8 | - W3 Total Cache 9 | - WP Offload Media Lite 10 | - ElasticPress 11 | - Multiple Domain 12 | - HumanMade - AWS-XRay (Working on making it work...) 13 | 14 | ## Architecture Diagram 15 | ![Architecture Diagram](doc/architecture-diagram-v2.png) 16 | 17 | ## Deployment - (To be update) 18 | ### Before getting started 19 | Please make sure you have/are 20 | - Using bash 21 | - Node.js installed 22 | - NPM and Yarn installed 23 | - Installed and running Docker 24 | - Installed and configured AWS CLI 25 | - Installed the latest version of AWS CDK CLI 26 | 27 | *Please be notice, this stack only can deploy into us-east-1* 28 | 0. You should have a public hosted zone in Route 53 29 | 1. Initialize the CDK project, run `make init` 30 | 2. Deploy the CDK Toolkit stack on to the target region, run `cdk bootstrap aws://AWS_ACCOUNT_ID/AWS_REGION --profile AWS_PROFILE_NAME` 31 | 3. Copy the `config.sample.toml` and rename to `config.toml` 32 | 4. Run `make easy-rsa-init gen-cert import-cert` to generate the certificate for the Client VPN 33 | 5. Modify the configuration in `config.toml` 34 | ```toml 35 | [environment] 36 | account = "YOUR_AWS_ACCOUNT_ID" # Your AWS account ID 37 | 38 | [admin] 39 | allowIpAddresses = ["0.0.0.0/0"] #Your home/office public IPv4 address, if using a specific IP add /32 like 24.222.174.192/32 40 | 41 | #Both certificate ARN should create and get from the previous steps 42 | serverCertificateArn = "arn:aws:acm:us-east-1:YOUR_AWS_ACCOUNT_ID:certificate/xxxxxxxxxxxxxxxxxxxxxxxx" 43 | clientCertificateArn = "arn:aws:acm:us-east-1:YOUR_AWS_ACCOUNT_ID:certificate/yyyyyyyyyyyyyyyyyyyyyyyy" 44 | 45 | [database] 46 | username = "wordpress" #Database username 47 | defaultDatabaseName = "wordpress" #Default database name 48 | 49 | [domain] 50 | domainName = "example.com" #Your root domain name, useually is the domain name of the created public hosted zone in Route 53 51 | hostname = "blog.example.com" #Your desire hostname for the WordPress 52 | alternativeHostname = ["*.blog.example.com"] 53 | 54 | [contact] 55 | email = ["hello@blog.example.com"] #Email address for notify any in-compliance event in AWS Config 56 | ``` 57 | 6. Run `make deploy profile=YOUR_AWS_PROFILE_NAME` 58 | 7. After the CloudFormation stack deployed, open the Session Manager in System Manager, and open a session to the created bastion host. Then run the following command. (The version of WordPress plugin may NOT be latest) 59 | ```shell script 60 | sudo su - 61 | cd /mnt/efs/wp-content/plugins &&\ 62 | curl -O https://downloads.wordpress.org/plugin/w3-total-cache.0.15.1.zip &&\ 63 | curl -O https://downloads.wordpress.org/plugin/amazon-s3-and-cloudfront.2.4.4.zip &&\ 64 | curl -O https://downloads.wordpress.org/plugin/elasticpress.zip &&\ 65 | curl -O https://downloads.wordpress.org/plugin/multiple-domain.zip &&\ 66 | curl https://codeload.github.com/humanmade/aws-xray/zip/1.2.12 -o humanmade-aws-xray-1.2.12.zip &&\ 67 | unzip '*.zip' &&\ 68 | rm -rf *.zip 69 | ``` 70 | 8. After the installation, go to the webpage and setup the database connection and plugin configuration. For the hostname of Memcached or MySQL, please check the output in CloudFormation stack. 71 | 9. For `Mutiple Domain` plugin, please enter `blog.example.com` and `admin.blog.example.com` these 2 hostname. 72 | 73 | ## References 74 | https://aws.amazon.com/tw/blogs/devops/build-a-continuous-delivery-pipeline-for-your-container-images-with-amazon-ecr-as-source/ 75 | https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/blue-green.html 76 | 77 | https://docs.aws.amazon.com/codepipeline/latest/userguide/tutorials-ecs-ecr-codedeploy.html#tutorials-ecs-ecr-codedeploy-cluster 78 | https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html 79 | https://stackoverflow.com/questions/56535632/how-do-i-link-2-containers-running-in-a-aws-ecs-task 80 | 81 | https://github.com/Monogramm/docker-wordpress 82 | https://github.com/fjudith/docker-wordpress 83 | https://github.com/humanmade/aws-xray 84 | 85 | https://pecl.php.net/package/memcached 86 | https://pecl.php.net/package/APCu 87 | 88 | https://stackoverflow.com/questions/54772120/docker-links-with-awsvpc-network-mode 89 | 90 | https://www.mgt-commerce.com/blog/aws-varnish-auto-scaling-magento/ 91 | 92 | https://downloads.wordpress.org/plugin/w3-total-cache.0.15.1.zip 93 | https://downloads.wordpress.org/plugin/wp-ses.1.4.3.zip 94 | https://downloads.wordpress.org/plugin/amazon-s3-and-cloudfront.2.4.4.zip 95 | https://downloads.wordpress.org/plugin/elasticpress.zip 96 | -------------------------------------------------------------------------------- /bin/aws-serverless-wordpress.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import fs = require('fs'); 4 | import path = require('path'); 5 | import * as cdk from '@aws-cdk/core'; 6 | import {RemovalPolicy, Tags} from '@aws-cdk/core'; 7 | import {AwsServerlessWordpressStack} from '../lib/aws-serverless-wordpress-stack'; 8 | import * as toml from 'toml'; 9 | 10 | interface IConfigEnvironment { 11 | region: string 12 | account: string 13 | } 14 | 15 | interface IConfigAdmin { 16 | allowIpAddresses: string[] 17 | serverCertificateArn: string 18 | clientCertificateArn: string 19 | } 20 | 21 | interface IConfigDatabase { 22 | username: string 23 | defaultDatabaseName: string 24 | } 25 | 26 | interface IConfigDomain { 27 | domainName: string 28 | hostname: string 29 | alternativeHostname: string 30 | } 31 | 32 | interface IConfigContact { 33 | email: string[] 34 | } 35 | 36 | interface IConfig { 37 | environment: IConfigEnvironment 38 | admin: IConfigAdmin 39 | database: IConfigDatabase 40 | domain: IConfigDomain 41 | contact: IConfigContact 42 | } 43 | 44 | const config: IConfig = toml.parse(fs.readFileSync(path.join(__dirname, 'config.toml')).toString()); 45 | 46 | const app = new cdk.App(); 47 | const stack = new AwsServerlessWordpressStack(app, 'AwsServerlessWordpressStack', { 48 | terminationProtection: false, 49 | resourceDeletionProtection: false, 50 | removalPolicy: RemovalPolicy.DESTROY, 51 | env: {region: 'us-east-1', account: config.environment.account}, 52 | databaseCredential: {username: config.database.username, defaultDatabaseName: config.database.defaultDatabaseName}, 53 | domainName: config.domain.domainName, 54 | hostname: config.domain.hostname, 55 | alternativeHostname: [...config.domain.alternativeHostname], 56 | snsEmailSubscription: [...config.contact.email], 57 | whitelistIpAddress: [...config.admin.allowIpAddresses], 58 | certificate: {server: config.admin.serverCertificateArn, client: config.admin.clientCertificateArn}, 59 | // This load balancer account ID should not be change if you deploy in us-east-1 60 | // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-logging-bucket-permissions 61 | loadBalancerAccountId: '127311923021' 62 | }); 63 | Tags.of(stack).add('aws-config:cloudformation:stack-name', stack.stackName, {excludeResourceTypes: ['AWS::ResourceGroups::Group']}); -------------------------------------------------------------------------------- /bin/config.sample.toml: -------------------------------------------------------------------------------- 1 | [environment] 2 | account = "YOUR_AWS_ACCOUNT_ID" # Your AWS account ID 3 | 4 | [admin] 5 | #Both certificate ARN should create and get through Makefile 6 | serverCertificateArn = "arn:aws:acm:us-east-1:YOUR_AWS_ACCOUNT_ID:certificate/xxxxxxxxxxxxxxxxxxxxxxxx" 7 | clientCertificateArn = "arn:aws:acm:us-east-1:YOUR_AWS_ACCOUNT_ID:certificate/yyyyyyyyyyyyyyyyyyyyyyyy" 8 | 9 | [database] 10 | username = "wordpress" #Database username 11 | defaultDatabaseName = "wordpress" #Default database name 12 | 13 | [domain] 14 | domainName = "example.com" #Your root domain name, usually is the domain name of the created public hosted zone in Route 53 15 | hostname = "blog.example.com" #Your desire hostname for the WordPress 16 | alternativeHostname = ["*.blog.example.com"] 17 | 18 | [contact] 19 | email = ["hello@blog.example.com"] #Email address for notify any in-compliance event in AWS Config 20 | -------------------------------------------------------------------------------- /cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "availability-zones:account=751225572132:region=us-east-1": [ 3 | "us-east-1a", 4 | "us-east-1b", 5 | "us-east-1c", 6 | "us-east-1d", 7 | "us-east-1e", 8 | "us-east-1f" 9 | ], 10 | "hosted-zone:account=751225572132:domainName=blog.miklet.pro:region=us-east-1": { 11 | "Id": "/hostedzone/Z03770041TSIXOAD668R", 12 | "Name": "blog.miklet.pro." 13 | }, 14 | "hosted-zone:account=163703054402:domainName=miklet.pro:region=us-east-1": { 15 | "Id": "/hostedzone/Z3EKY34IAGF1VG", 16 | "Name": "miklet.pro." 17 | }, 18 | "availability-zones:account=163703054402:region=us-east-1": [ 19 | "us-east-1a", 20 | "us-east-1b", 21 | "us-east-1c", 22 | "us-east-1d", 23 | "us-east-1e", 24 | "us-east-1f" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/aws-serverless-wordpress.ts", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /doc/architecture-diagram-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikletNg/aws-serverless-wordpress/49a6f52dba3f80c7075b16e67206d32b58d9fcff/doc/architecture-diagram-v2.png -------------------------------------------------------------------------------- /doc/architecture-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikletNg/aws-serverless-wordpress/49a6f52dba3f80c7075b16e67206d32b58d9fcff/doc/architecture-diagram.png -------------------------------------------------------------------------------- /lib/aws-serverless-wordpress-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core'; 2 | import { 3 | CfnOutput, 4 | CustomResource, 5 | CustomResourceProvider, 6 | CustomResourceProviderRuntime, 7 | Duration, 8 | RemovalPolicy, SecretValue 9 | } from '@aws-cdk/core'; 10 | import {ARecord, CnameRecord, PrivateHostedZone, PublicHostedZone, RecordTarget} from '@aws-cdk/aws-route53'; 11 | import {Certificate, CertificateValidation} from '@aws-cdk/aws-certificatemanager'; 12 | import {Bucket, BucketEncryption, StorageClass} from '@aws-cdk/aws-s3'; 13 | import { 14 | AclCidr, 15 | AclTraffic, 16 | Action, 17 | BastionHostLinux, 18 | CfnClientVpnAuthorizationRule, 19 | CfnClientVpnEndpoint, 20 | CfnClientVpnRoute, 21 | CfnClientVpnTargetNetworkAssociation, 22 | CfnFlowLog, 23 | NetworkAcl, 24 | Peer, 25 | Port, 26 | SecurityGroup, 27 | SubnetType, 28 | TrafficDirection, 29 | Vpc 30 | } from '@aws-cdk/aws-ec2'; 31 | import { 32 | AuroraCapacityUnit, 33 | DatabaseClusterEngine, 34 | ServerlessCluster 35 | } from '@aws-cdk/aws-rds'; 36 | import {Secret} from '@aws-cdk/aws-secretsmanager'; 37 | import {CfnCacheCluster, CfnSubnetGroup} from '@aws-cdk/aws-elasticache'; 38 | import {FileSystem, LifecyclePolicy, PerformanceMode, ThroughputMode} from '@aws-cdk/aws-efs'; 39 | import { 40 | CfnCluster, 41 | CfnService, 42 | Cluster, 43 | ContainerImage, 44 | FargateTaskDefinition, 45 | LogDriver, 46 | Protocol, 47 | Secret as EcsSecret 48 | } from '@aws-cdk/aws-ecs'; 49 | import { 50 | ApplicationLoadBalancer, 51 | ApplicationProtocol, 52 | ApplicationTargetGroup, 53 | CfnListener, 54 | CfnTargetGroup, 55 | ListenerAction 56 | } from '@aws-cdk/aws-elasticloadbalancingv2'; 57 | import { 58 | AccountRootPrincipal, 59 | ArnPrincipal, 60 | CfnServiceLinkedRole, 61 | ManagedPolicy, 62 | PolicyDocument, 63 | PolicyStatement, 64 | Role, 65 | ServicePrincipal 66 | } from '@aws-cdk/aws-iam'; 67 | import {RetentionDays} from '@aws-cdk/aws-logs'; 68 | import {PredefinedMetric, ScalableTarget, ServiceNamespace} from '@aws-cdk/aws-applicationautoscaling'; 69 | import {Alias} from '@aws-cdk/aws-kms'; 70 | import {DockerImageAsset} from "@aws-cdk/aws-ecr-assets"; 71 | import { 72 | CfnDistribution, 73 | CloudFrontAllowedCachedMethods, 74 | CloudFrontAllowedMethods, 75 | CloudFrontWebDistribution, 76 | HttpVersion, 77 | OriginAccessIdentity, 78 | OriginProtocolPolicy, 79 | PriceClass, 80 | ViewerCertificate, 81 | ViewerProtocolPolicy 82 | } from "@aws-cdk/aws-cloudfront"; 83 | import {CfnIPSet, CfnWebACL, CfnWebACLAssociation} from "@aws-cdk/aws-wafv2"; 84 | import {BackupPlan, BackupResource, BackupVault} from "@aws-cdk/aws-backup"; 85 | import {CloudFrontTarget, LoadBalancerTarget} from "@aws-cdk/aws-route53-targets"; 86 | import {CfnDomain, Domain, ElasticsearchVersion} from "@aws-cdk/aws-elasticsearch"; 87 | import {SnsTopic} from "@aws-cdk/aws-events-targets"; 88 | import { 89 | CfnConfigRule, 90 | CfnConfigurationRecorder, 91 | CfnDeliveryChannel, 92 | CloudFormationStackDriftDetectionCheck, 93 | ManagedRule, 94 | RuleScope 95 | } from "@aws-cdk/aws-config"; 96 | import {EmailSubscription} from "@aws-cdk/aws-sns-subscriptions"; 97 | import {Topic} from "@aws-cdk/aws-sns"; 98 | import {CfnGroup} from "@aws-cdk/aws-resourcegroups"; 99 | import path = require('path'); 100 | 101 | interface IDatabaseCredential { 102 | username: string 103 | defaultDatabaseName: string 104 | } 105 | 106 | interface ICertificate { 107 | server: string 108 | client: string 109 | } 110 | 111 | interface StackProps extends cdk.StackProps { 112 | domainName: string 113 | hostname: string 114 | alternativeHostname: string[] 115 | databaseCredential: IDatabaseCredential 116 | resourceDeletionProtection: boolean 117 | removalPolicy: RemovalPolicy 118 | snsEmailSubscription: string[] 119 | cloudFrontHashHeader?: string 120 | loadBalancerAccountId: string 121 | whitelistIpAddress: string[] 122 | certificate: ICertificate 123 | } 124 | 125 | export class AwsServerlessWordpressStack extends cdk.Stack { 126 | constructor(scope: cdk.Construct, id: string, props: StackProps) { 127 | super(scope, id, props); 128 | 129 | if (!props.cloudFrontHashHeader) props.cloudFrontHashHeader = Buffer.from(`${this.stackName}.${props.domainName}`).toString('base64'); 130 | 131 | const globalTagKey = 'aws-config:cloudformation:stack-name'; 132 | const globalTagValue = Buffer.from(this.stackName).toString('base64'); 133 | 134 | const awsManagedSnsKmsKey = Alias.fromAliasName(this, 'AwsManagedSnsKmsKey', 'alias/aws/sns'); 135 | 136 | const publicHostedZone = PublicHostedZone.fromLookup(this, 'ExistingPublicHostedZone', {domainName: props.domainName}); 137 | 138 | const acmCertificate = new Certificate(this, 'Certificate', { 139 | domainName: props.hostname, 140 | subjectAlternativeNames: props.alternativeHostname, 141 | validation: CertificateValidation.fromDns(publicHostedZone) 142 | }); 143 | 144 | const staticContentBucket = new Bucket(this, 'StaticContentBucket', { 145 | encryption: BucketEncryption.S3_MANAGED, 146 | versioned: true, 147 | removalPolicy: props.removalPolicy 148 | }); 149 | 150 | const loggingBucket = new Bucket(this, 'LoggingBucket', { 151 | encryption: BucketEncryption.S3_MANAGED, 152 | removalPolicy: props.removalPolicy, 153 | lifecycleRules: [ 154 | { 155 | enabled: true, 156 | transitions: [ 157 | { 158 | storageClass: StorageClass.INFREQUENT_ACCESS, 159 | transitionAfter: Duration.days(30) 160 | }, 161 | { 162 | storageClass: StorageClass.DEEP_ARCHIVE, 163 | transitionAfter: Duration.days(90) 164 | } 165 | ] 166 | } 167 | ] 168 | }); 169 | loggingBucket.addToResourcePolicy(new PolicyStatement({ 170 | principals: [new ServicePrincipal('delivery.logs.amazonaws.com'),], 171 | actions: ['s3:PutObject'], 172 | resources: [ 173 | `${loggingBucket.bucketArn}/vpc-flow-log/AWSLogs/${this.account}/*`, 174 | `${loggingBucket.bucketArn}/application-load-balancer/AWSLogs/${this.account}/*`, 175 | `${loggingBucket.bucketArn}/vpn-admin-application-load-balancer/AWSLogs/${this.account}/*` 176 | ], 177 | conditions: { 178 | StringEquals: { 179 | 's3:x-amz-acl': 'bucket-owner-full-control' 180 | } 181 | } 182 | })); 183 | loggingBucket.addToResourcePolicy(new PolicyStatement({ 184 | principals: [new ServicePrincipal('delivery.logs.amazonaws.com'),], 185 | actions: ['s3:GetBucketAcl'], 186 | resources: [loggingBucket.bucketArn], 187 | })); 188 | loggingBucket.addToResourcePolicy(new PolicyStatement({ 189 | principals: [new ArnPrincipal(`arn:aws:iam::${props.loadBalancerAccountId}:root`)], 190 | actions: ['s3:PutObject'], 191 | resources: [ 192 | `${loggingBucket.bucketArn}/application-load-balancer/AWSLogs/${this.account}/*`, 193 | `${loggingBucket.bucketArn}/vpn-admin-application-load-balancer/AWSLogs/${this.account}/*` 194 | ] 195 | })); 196 | loggingBucket.addToResourcePolicy(new PolicyStatement({ 197 | principals: [new AccountRootPrincipal()], 198 | actions: ['s3:GetBucketAcl', 's3:PutBucketAcl'], 199 | resources: [loggingBucket.bucketArn] 200 | })); 201 | 202 | if (props.removalPolicy === RemovalPolicy.DESTROY) { 203 | const serviceToken = CustomResourceProvider.getOrCreate(this, 'Custom::EmptyLoggingBucket', { 204 | codeDirectory: path.join(__dirname, 'custom-resource'), 205 | runtime: CustomResourceProviderRuntime.NODEJS_12, 206 | timeout: Duration.minutes(3), 207 | policyStatements: [(new PolicyStatement({ 208 | actions: ['s3:ListBucket', 's3:DeleteObject'], 209 | resources: [loggingBucket.bucketArn, `${loggingBucket.bucketArn}/*`] 210 | })).toStatementJson()], 211 | environment: { 212 | LOGGING_BUCKET_NAME: loggingBucket.bucketName 213 | } 214 | }); 215 | new CustomResource(this, 'EmptyLoggingBucket', { 216 | resourceType: 'Custom::EmptyLoggingBucket', 217 | serviceToken 218 | }); 219 | } 220 | 221 | const vpc = new Vpc(this, 'Vpc', { 222 | natGateways: 3, 223 | maxAzs: 3, 224 | cidr: '172.16.0.0/16', 225 | subnetConfiguration: [ 226 | { 227 | name: 'Public', 228 | subnetType: SubnetType.PUBLIC 229 | }, 230 | { 231 | name: 'Private', 232 | subnetType: SubnetType.PRIVATE 233 | 234 | }, 235 | { 236 | name: 'Isolated', 237 | subnetType: SubnetType.ISOLATED 238 | } 239 | ], 240 | enableDnsHostnames: true, 241 | enableDnsSupport: true 242 | }); 243 | 244 | const nacl = new NetworkAcl(this, 'NetworkAcl', {vpc}); 245 | nacl.addEntry('AllowAllHttpsFromIpv4', { 246 | ruleNumber: 100, 247 | cidr: AclCidr.anyIpv4(), 248 | traffic: AclTraffic.tcpPort(443), 249 | direction: TrafficDirection.INGRESS, 250 | ruleAction: Action.ALLOW 251 | }); 252 | nacl.addEntry('AllowAllHttpsFromIpv6', { 253 | ruleNumber: 101, 254 | cidr: AclCidr.anyIpv6(), 255 | traffic: AclTraffic.tcpPort(443), 256 | direction: TrafficDirection.INGRESS, 257 | ruleAction: Action.ALLOW 258 | }); 259 | nacl.addEntry('AllowResponseToHttpsRequestToIpv4', { 260 | ruleNumber: 100, 261 | cidr: AclCidr.anyIpv4(), 262 | traffic: AclTraffic.tcpPortRange(1024, 65535), 263 | direction: TrafficDirection.EGRESS, 264 | ruleAction: Action.ALLOW 265 | }); 266 | nacl.addEntry('AllowResponseToHttpsRequestToIpv6', { 267 | ruleNumber: 101, 268 | cidr: AclCidr.anyIpv6(), 269 | traffic: AclTraffic.tcpPortRange(1024, 65535), 270 | direction: TrafficDirection.EGRESS, 271 | ruleAction: Action.ALLOW 272 | }); 273 | 274 | new CfnFlowLog(this, 'CfnVpcFlowLog', { 275 | resourceId: vpc.vpcId, 276 | resourceType: 'VPC', 277 | trafficType: 'ALL', 278 | logDestinationType: 's3', 279 | logDestination: `${loggingBucket.bucketArn}/vpc-flow-log` 280 | }); 281 | 282 | const privateHostedZone = new PrivateHostedZone(this, 'PrivateHostedZone', { 283 | vpc, 284 | zoneName: `${props.hostname}.private` 285 | }); 286 | 287 | const applicationLoadBalancerSecurityGroup = new SecurityGroup(this, 'ApplicationLoadBalancerSecurityGroup', {vpc}); 288 | const vpnApplicationLoadBalancerSecurityGroup = new SecurityGroup(this, 'VpcApplicationLoadBalancer', {vpc}); 289 | const elastiCacheMemcachedSecurityGroup = new SecurityGroup(this, 'ElastiCacheMemcachedSecurityGroup', {vpc}); 290 | const rdsAuroraClusterSecurityGroup = new SecurityGroup(this, 'RdsAuroraClusterSecurityGroup', {vpc}); 291 | const ecsFargateServiceSecurityGroup = new SecurityGroup(this, 'EcsFargateServiceSecurityGroup', {vpc}); 292 | const efsFileSystemSecurityGroup = new SecurityGroup(this, 'EfsFileSystemSecurityGroup', {vpc}); 293 | const elasticsearchDomainSecurityGroup = new SecurityGroup(this, 'ElasticsearchDomainSecurityGroup', {vpc}); 294 | const bastionHostSecurityGroup = new SecurityGroup(this, 'BastionHostSecurityGroup', {vpc}); 295 | const clientVpnSecurityGroup = new SecurityGroup(this, 'ClientVpnSecurityGroup', {vpc}); 296 | 297 | applicationLoadBalancerSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(443)); 298 | vpnApplicationLoadBalancerSecurityGroup.addIngressRule(clientVpnSecurityGroup, Port.tcp(443)); 299 | ecsFargateServiceSecurityGroup.addIngressRule(applicationLoadBalancerSecurityGroup, Port.tcp(80)); 300 | ecsFargateServiceSecurityGroup.addIngressRule(vpnApplicationLoadBalancerSecurityGroup, Port.tcp(80)); 301 | elastiCacheMemcachedSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(11211)); 302 | rdsAuroraClusterSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(3306)); 303 | efsFileSystemSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(2049)); 304 | elasticsearchDomainSecurityGroup.addIngressRule(ecsFargateServiceSecurityGroup, Port.tcp(9300)); 305 | clientVpnSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.udp(1194)); 306 | 307 | efsFileSystemSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(2049)); 308 | elastiCacheMemcachedSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(11211)); 309 | rdsAuroraClusterSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(3306)); 310 | elasticsearchDomainSecurityGroup.addIngressRule(bastionHostSecurityGroup, Port.tcp(9300)); 311 | 312 | const clientVpn = new CfnClientVpnEndpoint(this, 'ClientVpn', { 313 | clientCidrBlock: '172.16.252.0/22', 314 | serverCertificateArn: props.certificate.server, 315 | connectionLogOptions: {enabled: false}, 316 | transportProtocol: 'udp', 317 | vpcId: vpc.vpcId, 318 | vpnPort: 1194, 319 | securityGroupIds: [clientVpnSecurityGroup.securityGroupId], 320 | authenticationOptions: [{ 321 | type: 'certificate-authentication', 322 | mutualAuthentication: {clientRootCertificateChainArn: props.certificate.client} 323 | }] 324 | }); 325 | vpc.publicSubnets.forEach((subnet, i) => { 326 | const _vpnTargetNetworkAssociation = new CfnClientVpnTargetNetworkAssociation(this, `ClientVpnPublicTargetNetworkAssociation${i}`, { 327 | clientVpnEndpointId: clientVpn.ref, 328 | subnetId: subnet.subnetId 329 | }); 330 | const _vpnPublicRoute = new CfnClientVpnRoute(this, `ClientVpnPublicRoute${i}`, { 331 | clientVpnEndpointId: clientVpn.ref, 332 | destinationCidrBlock: '0.0.0.0/0', 333 | targetVpcSubnetId: subnet.subnetId 334 | }); 335 | _vpnPublicRoute.addDependsOn(_vpnTargetNetworkAssociation); 336 | }); 337 | new CfnClientVpnAuthorizationRule(this, 'ClientVpnAuthorizationRuleForInternetAccess', { 338 | clientVpnEndpointId: clientVpn.ref, 339 | authorizeAllGroups: true, 340 | targetNetworkCidr: '0.0.0.0/0' 341 | }); 342 | new CfnClientVpnAuthorizationRule(this, 'ClientVpnAuthorizationRuleForVpcAccess', { 343 | clientVpnEndpointId: clientVpn.ref, 344 | authorizeAllGroups: true, 345 | targetNetworkCidr: vpc.vpcCidrBlock 346 | }); 347 | 348 | const rdsAuroraClusterPasswordSecret = new Secret(this, 'RdsAuroraClusterPasswordSecret', { 349 | removalPolicy: props.removalPolicy, 350 | generateSecretString: {excludeCharacters: ` ;+%{}` + `@'"\`/\\#`} 351 | }); 352 | 353 | const rdsAuroraCluster = new ServerlessCluster(this, 'RdsAuroraServerlessCluster', { 354 | vpc, 355 | vpcSubnets: {subnetType: SubnetType.ISOLATED}, 356 | securityGroups: [rdsAuroraClusterSecurityGroup], 357 | engine: DatabaseClusterEngine.AURORA_MYSQL, 358 | credentials: { 359 | username: props.databaseCredential.username, 360 | password: SecretValue.secretsManager(rdsAuroraClusterPasswordSecret.secretArn) 361 | }, 362 | defaultDatabaseName: props.databaseCredential.defaultDatabaseName, 363 | deletionProtection: props.resourceDeletionProtection, 364 | removalPolicy: props.removalPolicy, 365 | scaling: { 366 | minCapacity: AuroraCapacityUnit.ACU_1, 367 | maxCapacity: AuroraCapacityUnit.ACU_16 368 | }, 369 | backupRetention: Duration.days(7) 370 | }) 371 | 372 | const rdsAuroraClusterPrivateDnsRecord = new CnameRecord(this, 'RdsAuroraClusterPrivateDnsRecord', { 373 | zone: privateHostedZone, 374 | recordName: `database.${privateHostedZone.zoneName}`, 375 | domainName: rdsAuroraCluster.clusterEndpoint.hostname, 376 | ttl: Duration.hours(1) 377 | }); 378 | 379 | const elastiCacheMemcachedCluster = new CfnCacheCluster(this, 'ElastiCacheMemcachedCluster', { 380 | cacheNodeType: 'cache.t3.micro', 381 | engine: 'memcached', 382 | azMode: 'cross-az', 383 | numCacheNodes: 3, 384 | cacheSubnetGroupName: new CfnSubnetGroup(this, 'ElastiCacheMemcachedClusterSubnetGroup', { 385 | description: 'ElastiCacheMemcachedClusterSubnetGroup', 386 | subnetIds: vpc.isolatedSubnets.map(subnet => subnet.subnetId) 387 | }).ref, 388 | vpcSecurityGroupIds: [elastiCacheMemcachedSecurityGroup.securityGroupId] 389 | }); 390 | 391 | const elastiCacheMemcachedClusterPrivateDnsRecord = new CnameRecord(this, 'ElastiCacheMemcachedClusterPrivateDnsRecord', { 392 | zone: privateHostedZone, 393 | recordName: `cache.${privateHostedZone.zoneName}`, 394 | domainName: elastiCacheMemcachedCluster.attrConfigurationEndpointAddress, 395 | ttl: Duration.hours(1) 396 | }); 397 | 398 | const elasticsearchServiceLinkRole = new CfnServiceLinkedRole(this, 'CfnElasticsearchServiceLinkRole', { 399 | awsServiceName: 'es.amazonaws.com' 400 | }); 401 | 402 | const elasticsearchDomain = new Domain(this, 'ElasticsearchDomain', { 403 | version: ElasticsearchVersion.V7_7, 404 | capacity: {dataNodes: 3, dataNodeInstanceType: 't3.small.elasticsearch'}, 405 | zoneAwareness: {enabled: true, availabilityZoneCount: 3}, 406 | encryptionAtRest: {enabled: true}, 407 | nodeToNodeEncryption: true, 408 | ebs: {volumeSize: 10}, 409 | enforceHttps: true, 410 | vpcOptions: { 411 | subnets: vpc.isolatedSubnets, 412 | securityGroups: [elasticsearchDomainSecurityGroup] 413 | } 414 | }); 415 | (elasticsearchDomain.node.defaultChild as CfnDomain).addDependsOn(elasticsearchServiceLinkRole); 416 | 417 | const elasticsearchDomainPrivateDnsRecord = new CnameRecord(this, 'ElasticsearchDomainPrivateDnsRecord', { 418 | zone: privateHostedZone, 419 | recordName: `search.${privateHostedZone.zoneName}`, 420 | domainName: elasticsearchDomain.domainEndpoint, 421 | ttl: Duration.hours(1) 422 | }); 423 | 424 | const fileSystem = new FileSystem(this, 'FileSystem', { 425 | vpc, 426 | vpcSubnets: { 427 | subnetType: SubnetType.ISOLATED 428 | }, 429 | securityGroup: efsFileSystemSecurityGroup, 430 | performanceMode: PerformanceMode.GENERAL_PURPOSE, 431 | lifecyclePolicy: LifecyclePolicy.AFTER_30_DAYS, 432 | throughputMode: ThroughputMode.BURSTING, 433 | encrypted: true, 434 | removalPolicy: props.removalPolicy 435 | }); 436 | 437 | const fileSystemAccessPoint = fileSystem.addAccessPoint('AccessPoint'); 438 | 439 | const fileSystemEndpointPrivateDnsRecord = new CnameRecord(this, 'FileSystemEndpointPrivateDnsRecord', { 440 | zone: privateHostedZone, 441 | recordName: `nfs.${privateHostedZone.zoneName}`, 442 | domainName: `${fileSystem.fileSystemId}.efs.${this.region}.amazonaws.com`, 443 | ttl: Duration.hours(1) 444 | }); 445 | 446 | const bastionHost = new BastionHostLinux(this, 'BastionHost', { 447 | vpc, 448 | securityGroup: bastionHostSecurityGroup 449 | }); 450 | bastionHost.instance.addUserData('mkdir -p /mnt/efs'); 451 | bastionHost.instance.addUserData(`mount -t nfs4 -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport ${fileSystemEndpointPrivateDnsRecord.domainName}:/ /mnt/efs `); 452 | 453 | const ecsCluster = new Cluster(this, 'EcsCluster', { 454 | containerInsights: true, 455 | vpc 456 | }); 457 | const _ecsCluster = ecsCluster.node.defaultChild as CfnCluster; 458 | _ecsCluster.capacityProviders = ['FARGATE', 'FARGATE_SPOT']; 459 | _ecsCluster.defaultCapacityProviderStrategy = [ 460 | { 461 | capacityProvider: 'FARGATE', 462 | weight: 2, 463 | base: 3 464 | }, 465 | { 466 | capacityProvider: 'FARGATE_SPOT', 467 | weight: 1 468 | } 469 | ]; 470 | 471 | const applicationLoadBalancer = new ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', { 472 | vpc, 473 | deletionProtection: props.resourceDeletionProtection, 474 | http2Enabled: true, 475 | internetFacing: true, 476 | securityGroup: applicationLoadBalancerSecurityGroup, 477 | vpcSubnets: {subnetType: SubnetType.PUBLIC} 478 | }); 479 | applicationLoadBalancer.setAttribute('routing.http.drop_invalid_header_fields.enabled', 'true'); 480 | applicationLoadBalancer.setAttribute('access_logs.s3.enabled', 'true'); 481 | applicationLoadBalancer.setAttribute('access_logs.s3.bucket', loggingBucket.bucketName); 482 | applicationLoadBalancer.setAttribute('access_logs.s3.prefix', 'application-load-balancer'); 483 | applicationLoadBalancer.addListener('HttpListener', { 484 | port: 80, 485 | protocol: ApplicationProtocol.HTTP, 486 | defaultAction: ListenerAction.redirect({protocol: 'HTTPS', port: '443'}) 487 | }); 488 | 489 | const httpsListener = applicationLoadBalancer.addListener('HttpsListener', { 490 | port: 443, 491 | protocol: ApplicationProtocol.HTTPS, 492 | certificates: [acmCertificate] 493 | }); 494 | 495 | const vpnAdminApplicationLoadBalancer = new ApplicationLoadBalancer(this, 'VpnAdminApplicationLoadBalancer', { 496 | vpc, 497 | deletionProtection: props.resourceDeletionProtection, 498 | http2Enabled: true, 499 | internetFacing: false, 500 | securityGroup: vpnApplicationLoadBalancerSecurityGroup, 501 | vpcSubnets: {subnetType: SubnetType.PRIVATE} 502 | }); 503 | vpnAdminApplicationLoadBalancer.setAttribute('routing.http.drop_invalid_header_fields.enabled', 'true'); 504 | vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.enabled', 'true'); 505 | vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.bucket', loggingBucket.bucketName); 506 | vpnAdminApplicationLoadBalancer.setAttribute('access_logs.s3.prefix', 'vpn-admin-application-load-balancer'); 507 | vpnAdminApplicationLoadBalancer.addListener('VpnAdminHttpListener', { 508 | port: 80, 509 | protocol: ApplicationProtocol.HTTP, 510 | defaultAction: ListenerAction.redirect({protocol: 'HTTPS', port: '443'}) 511 | }); 512 | 513 | const vpnAdminHttpsListener = vpnAdminApplicationLoadBalancer.addListener('VpnAdminHttpsListener', { 514 | port: 443, 515 | protocol: ApplicationProtocol.HTTPS, 516 | certificates: [acmCertificate] 517 | }); 518 | 519 | const wordPressFargateTaskExecutionRole = new Role(this, 'WordpressFargateTaskExecutionRole', { 520 | assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), 521 | managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')] 522 | }); 523 | const wordPressFargateTaskRole = new Role(this, 'WordpressFargateTaskRole', { 524 | assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), 525 | managedPolicies: [ManagedPolicy.fromManagedPolicyArn(this, 'XRayDaemonWriteAccess', 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess')], 526 | inlinePolicies: { 527 | _: new PolicyDocument({ 528 | statements: [ 529 | new PolicyStatement({ 530 | actions: ['s3:GetBucketLocation'], 531 | resources: [staticContentBucket.bucketArn] 532 | }) 533 | ] 534 | }) 535 | } 536 | }); 537 | staticContentBucket.grantReadWrite(wordPressFargateTaskRole); 538 | 539 | const wordPressFargateTaskDefinition = new FargateTaskDefinition(this, 'WordpressFargateTaskDefinition', { 540 | memoryLimitMiB: 512, 541 | cpu: 256, 542 | executionRole: wordPressFargateTaskExecutionRole, 543 | taskRole: wordPressFargateTaskRole, 544 | }); 545 | wordPressFargateTaskDefinition.addVolume({ 546 | name: 'WordPressEfsVolume', 547 | efsVolumeConfiguration: { 548 | fileSystemId: fileSystem.fileSystemId, 549 | transitEncryption: 'ENABLED', 550 | authorizationConfig: { 551 | accessPointId: fileSystemAccessPoint.accessPointId 552 | } 553 | } 554 | }); 555 | 556 | const wordPressDockerImageAsset = new DockerImageAsset(this, 'WordPressDockerImageAsset', {directory: path.join(__dirname, 'images/wordpress')}); 557 | const nginxDockerImageAsset = new DockerImageAsset(this, 'NginxDockerImageAsset', {directory: path.join(__dirname, 'images/nginx')}); 558 | 559 | const wordPressContainer = wordPressFargateTaskDefinition.addContainer('WordPress', { 560 | image: ContainerImage.fromDockerImageAsset(wordPressDockerImageAsset), 561 | environment: { 562 | WORDPRESS_DB_HOST: rdsAuroraClusterPrivateDnsRecord.domainName, 563 | WORDPRESS_DB_USER: props.databaseCredential.username, 564 | WORDPRESS_DB_NAME: props.databaseCredential.defaultDatabaseName, 565 | }, 566 | secrets: { 567 | WORDPRESS_DB_PASSWORD: EcsSecret.fromSecretsManager(rdsAuroraClusterPasswordSecret) 568 | }, 569 | logging: LogDriver.awsLogs({ 570 | streamPrefix: `${this.stackName}WordPressContainerLog`, 571 | logRetention: RetentionDays.ONE_MONTH 572 | }) 573 | }); 574 | wordPressContainer.addMountPoints({ 575 | readOnly: false, 576 | containerPath: '/var/www/html', 577 | sourceVolume: 'WordPressEfsVolume' 578 | }); 579 | 580 | const nginxContainer = wordPressFargateTaskDefinition.addContainer('Nginx', { 581 | image: ContainerImage.fromDockerImageAsset(nginxDockerImageAsset), 582 | logging: LogDriver.awsLogs({ 583 | streamPrefix: `${this.stackName}NginxContainerLog`, 584 | logRetention: RetentionDays.ONE_MONTH 585 | }), 586 | environment: { 587 | SERVER_NAME: props.hostname, 588 | MEMCACHED_HOST: elastiCacheMemcachedClusterPrivateDnsRecord.domainName, 589 | NGINX_ENTRYPOINT_QUIET_LOGS: '1' 590 | } 591 | }); 592 | nginxContainer.addPortMappings({ 593 | hostPort: 80, 594 | containerPort: 80, 595 | protocol: Protocol.TCP 596 | }); 597 | nginxContainer.addMountPoints({ 598 | readOnly: false, 599 | containerPath: '/var/www/html', 600 | sourceVolume: 'WordPressEfsVolume' 601 | }); 602 | 603 | const xrayContainer = wordPressFargateTaskDefinition.addContainer('XRay', { 604 | image: ContainerImage.fromRegistry('amazon/aws-xray-daemon'), 605 | logging: LogDriver.awsLogs({ 606 | streamPrefix: `${this.stackName}XRayContainerLog`, 607 | logRetention: RetentionDays.ONE_MONTH 608 | }), 609 | entryPoint: ['/usr/bin/xray', '-b', '127.0.0.1:2000', '-l', 'dev', '-o'], 610 | user: '1337' 611 | }); 612 | xrayContainer.addPortMappings({ 613 | containerPort: 2000, 614 | protocol: Protocol.UDP 615 | }) 616 | 617 | const _wordPressFargateServiceTargetGroup = new CfnTargetGroup(this, 'CfnWordPressFargateServiceTargetGroup', { 618 | matcher: { 619 | httpCode: '200,301,302' 620 | }, 621 | port: 80, 622 | protocol: 'HTTP', 623 | targetGroupAttributes: [ 624 | { 625 | key: 'stickiness.enabled', 626 | value: 'true' 627 | }, 628 | { 629 | key: 'stickiness.type', 630 | value: 'lb_cookie' 631 | }, 632 | { 633 | key: 'stickiness.lb_cookie.duration_seconds', 634 | value: '604800' 635 | } 636 | ], 637 | targetType: 'ip', 638 | vpcId: vpc.vpcId, 639 | unhealthyThresholdCount: 5, 640 | healthCheckTimeoutSeconds: 45, 641 | healthCheckIntervalSeconds: 60, 642 | }); 643 | 644 | const wordPressFargateServiceTargetGroup = ApplicationTargetGroup.fromTargetGroupAttributes(this, 'WordPressFargateServiceTargetGroup', { 645 | loadBalancerArns: applicationLoadBalancer.loadBalancerArn, 646 | targetGroupArn: _wordPressFargateServiceTargetGroup.ref 647 | }); 648 | httpsListener.addTargetGroups('WordPress', {targetGroups: [wordPressFargateServiceTargetGroup]}); 649 | 650 | const _wordPressVpnAdminFargateServiceTargetGroup = new CfnTargetGroup(this, 'CfnWordPressVpnAdminFargateServiceTargetGroup', { 651 | matcher: { 652 | httpCode: '200,301,302' 653 | }, 654 | port: 80, 655 | protocol: 'HTTP', 656 | targetGroupAttributes: [ 657 | { 658 | key: 'stickiness.enabled', 659 | value: 'true' 660 | }, 661 | { 662 | key: 'stickiness.type', 663 | value: 'lb_cookie' 664 | }, 665 | { 666 | key: 'stickiness.lb_cookie.duration_seconds', 667 | value: '604800' 668 | } 669 | ], 670 | targetType: 'ip', 671 | vpcId: vpc.vpcId, 672 | unhealthyThresholdCount: 5, 673 | healthCheckTimeoutSeconds: 45, 674 | healthCheckIntervalSeconds: 60, 675 | }); 676 | 677 | const wordPressVpnAdminFargateServiceTargetGroup = ApplicationTargetGroup.fromTargetGroupAttributes(this, 'WordPressVpnAdminFargateServiceTargetGroup', { 678 | loadBalancerArns: vpnAdminApplicationLoadBalancer.loadBalancerArn, 679 | targetGroupArn: _wordPressVpnAdminFargateServiceTargetGroup.ref 680 | }); 681 | vpnAdminHttpsListener.addTargetGroups('WordPressVpnAdmin', {targetGroups: [wordPressVpnAdminFargateServiceTargetGroup]}); 682 | 683 | const _wordPressFargateService = new CfnService(this, 'CfnWordPressFargateService', { 684 | cluster: ecsCluster.clusterArn, 685 | desiredCount: 3, 686 | deploymentConfiguration: { 687 | maximumPercent: 200, 688 | minimumHealthyPercent: 50 689 | }, 690 | deploymentController: { 691 | type: 'ECS' 692 | }, 693 | healthCheckGracePeriodSeconds: 60, 694 | loadBalancers: [ 695 | { 696 | containerName: nginxContainer.containerName, 697 | containerPort: 80, 698 | targetGroupArn: wordPressFargateServiceTargetGroup.targetGroupArn 699 | }, 700 | { 701 | containerName: nginxContainer.containerName, 702 | containerPort: 80, 703 | targetGroupArn: wordPressVpnAdminFargateServiceTargetGroup.targetGroupArn 704 | } 705 | ], 706 | networkConfiguration: { 707 | awsvpcConfiguration: { 708 | assignPublicIp: 'DISABLED', 709 | securityGroups: [ecsFargateServiceSecurityGroup.securityGroupId], 710 | subnets: vpc.privateSubnets.map(subnet => subnet.subnetId) 711 | } 712 | }, 713 | platformVersion: '1.4.0', 714 | taskDefinition: wordPressFargateTaskDefinition.taskDefinitionArn 715 | }); 716 | _wordPressFargateService.addOverride('DependsOn', [ 717 | this.getLogicalId(httpsListener.node.defaultChild as CfnListener), 718 | this.getLogicalId(vpnAdminHttpsListener.node.defaultChild as CfnListener) 719 | ]); 720 | 721 | const wordPressServiceScaling = new ScalableTarget(this, 'WordPressFargateServiceScaling', { 722 | scalableDimension: 'ecs:service:DesiredCount', 723 | minCapacity: 3, 724 | maxCapacity: 300, 725 | serviceNamespace: ServiceNamespace.ECS, 726 | resourceId: `service/${ecsCluster.clusterName}/${_wordPressFargateService.attrName}` 727 | }); 728 | 729 | wordPressServiceScaling.scaleToTrackMetric('RequestCountPerTarget', { 730 | predefinedMetric: PredefinedMetric.ALB_REQUEST_COUNT_PER_TARGET, 731 | resourceLabel: `${applicationLoadBalancer.loadBalancerFullName}/${_wordPressFargateServiceTargetGroup.attrTargetGroupFullName}`, 732 | targetValue: 4096, 733 | scaleInCooldown: Duration.minutes(5), 734 | scaleOutCooldown: Duration.minutes(5) 735 | }); 736 | 737 | wordPressServiceScaling.scaleToTrackMetric('TargetResponseTime', { 738 | customMetric: applicationLoadBalancer.metricTargetResponseTime(), 739 | targetValue: 4, 740 | scaleInCooldown: Duration.minutes(3), 741 | scaleOutCooldown: Duration.minutes(3) 742 | }); 743 | 744 | const adminWhitelistIpSet = new CfnIPSet(this, 'AdminWhitelistIpSet', { 745 | addresses: [...props.whitelistIpAddress], 746 | scope: 'REGIONAL', 747 | ipAddressVersion: 'IPV4' 748 | }); 749 | 750 | const wordPressCloudFrontDistributionWafWebAcl = new CfnWebACL(this, 'WordPressCloudFrontDistributionWafWebAcl', { 751 | defaultAction: {allow: {}}, 752 | scope: 'CLOUDFRONT', 753 | visibilityConfig: { 754 | sampledRequestsEnabled: true, 755 | cloudWatchMetricsEnabled: true, 756 | metricName: 'CloudFrontDistributionWebAclMetric' 757 | }, 758 | rules: [ 759 | { 760 | name: 'RuleWithAWSManagedRulesCommonRuleSet', 761 | priority: 0, 762 | overrideAction: {none: {}}, 763 | visibilityConfig: { 764 | sampledRequestsEnabled: true, 765 | cloudWatchMetricsEnabled: true, 766 | metricName: 'CommonRuleSetMetric' 767 | }, 768 | statement: { 769 | managedRuleGroupStatement: { 770 | vendorName: 'AWS', 771 | name: 'AWSManagedRulesCommonRuleSet', 772 | excludedRules: [{name: 'SizeRestrictions_BODY'}, {name: 'GenericRFI_BODY'}, {name: 'GenericRFI_URIPATH'}, {name: 'GenericRFI_QUERYARGUMENTS'}] 773 | } 774 | } 775 | }, 776 | { 777 | name: 'RuleWithAWSManagedRulesKnownBadInputsRuleSet', 778 | priority: 1, 779 | overrideAction: {none: {}}, 780 | visibilityConfig: { 781 | sampledRequestsEnabled: true, 782 | cloudWatchMetricsEnabled: true, 783 | metricName: 'KnownBadInputsRuleSetMetric' 784 | }, 785 | statement: { 786 | managedRuleGroupStatement: { 787 | vendorName: 'AWS', 788 | name: 'AWSManagedRulesKnownBadInputsRuleSet', 789 | excludedRules: [] 790 | } 791 | } 792 | }, 793 | { 794 | name: 'RuleWithAWSManagedRulesWordPressRuleSet', 795 | priority: 2, 796 | overrideAction: {none: {}}, 797 | visibilityConfig: { 798 | sampledRequestsEnabled: true, 799 | cloudWatchMetricsEnabled: true, 800 | metricName: 'WordPressRuleSetMetric' 801 | }, 802 | statement: { 803 | managedRuleGroupStatement: { 804 | vendorName: 'AWS', 805 | name: 'AWSManagedRulesWordPressRuleSet', 806 | excludedRules: [] 807 | } 808 | } 809 | }, 810 | { 811 | name: 'RuleWithAWSManagedRulesPHPRuleSet', 812 | priority: 3, 813 | overrideAction: {none: {}}, 814 | visibilityConfig: { 815 | sampledRequestsEnabled: true, 816 | cloudWatchMetricsEnabled: true, 817 | metricName: 'PHPRuleSetMetric' 818 | }, 819 | statement: { 820 | managedRuleGroupStatement: { 821 | vendorName: 'AWS', 822 | name: 'AWSManagedRulesPHPRuleSet', 823 | excludedRules: [] 824 | } 825 | } 826 | }, 827 | { 828 | name: 'RuleWithAWSManagedRulesSQLiRuleSet', 829 | priority: 4, 830 | overrideAction: {none: {}}, 831 | visibilityConfig: { 832 | sampledRequestsEnabled: true, 833 | cloudWatchMetricsEnabled: true, 834 | metricName: 'AWSManagedRulesSQLiRuleSetMetric' 835 | }, 836 | statement: { 837 | managedRuleGroupStatement: { 838 | vendorName: 'AWS', 839 | name: 'AWSManagedRulesSQLiRuleSet', 840 | excludedRules: [] 841 | } 842 | } 843 | }, 844 | { 845 | name: 'RuleWithAWSManagedRulesAmazonIpReputationList', 846 | priority: 5, 847 | overrideAction: {none: {}}, 848 | visibilityConfig: { 849 | sampledRequestsEnabled: true, 850 | cloudWatchMetricsEnabled: true, 851 | metricName: 'AmazonIpReputationListMetric' 852 | }, 853 | statement: { 854 | managedRuleGroupStatement: { 855 | vendorName: 'AWS', 856 | name: 'AWSManagedRulesAmazonIpReputationList', 857 | excludedRules: [] 858 | } 859 | } 860 | } 861 | ] 862 | }); 863 | 864 | const applicationLoadBalancerWebAcl = new CfnWebACL(this, 'ApplicationLoadBalancerWafWebAcl', { 865 | defaultAction: {block: {}}, 866 | scope: 'REGIONAL', 867 | visibilityConfig: { 868 | sampledRequestsEnabled: true, 869 | cloudWatchMetricsEnabled: true, 870 | metricName: 'ApplicationLoadBalancerWebAclMetric' 871 | }, 872 | rules: [ 873 | { 874 | name: 'RuleToAllowNonAdminRequest', 875 | priority: 0, 876 | action: {allow: {}}, 877 | visibilityConfig: { 878 | sampledRequestsEnabled: true, 879 | cloudWatchMetricsEnabled: true, 880 | metricName: 'RuleToAllowNonAdminRequestMetric' 881 | }, 882 | statement: { 883 | andStatement: { 884 | statements: [ 885 | { 886 | notStatement: { 887 | statement: { 888 | byteMatchStatement: { 889 | fieldToMatch: {uriPath: {}}, 890 | positionalConstraint: "STARTS_WITH", 891 | searchString: '/wp-admin', 892 | textTransformations: [{type: 'NONE', priority: 0}] 893 | } 894 | } 895 | } 896 | }, 897 | { 898 | byteMatchStatement: { 899 | fieldToMatch: { 900 | singleHeader: { 901 | Name: 'X_Request_From_CloudFront' 902 | } 903 | }, 904 | positionalConstraint: 'EXACTLY', 905 | searchString: props.cloudFrontHashHeader, 906 | textTransformations: [{type: 'NONE', priority: 0}] 907 | } 908 | } 909 | ] 910 | } 911 | } 912 | }, 913 | { 914 | name: 'RuleToAllowRequestWhitelistedIpSourceToAdminPage', 915 | priority: 1, 916 | action: {allow: {}}, 917 | visibilityConfig: { 918 | sampledRequestsEnabled: true, 919 | cloudWatchMetricsEnabled: true, 920 | metricName: 'RuleToAllowRequestWhitelistedIpSourceToAdminPageMetric' 921 | }, 922 | statement: { 923 | andStatement: { 924 | statements: [ 925 | { 926 | byteMatchStatement: { 927 | fieldToMatch: { 928 | singleHeader: { 929 | Name: 'X_Request_From_CloudFront' 930 | } 931 | }, 932 | positionalConstraint: 'EXACTLY', 933 | searchString: props.cloudFrontHashHeader, 934 | textTransformations: [{type: 'NONE', priority: 0}] 935 | } 936 | }, 937 | { 938 | byteMatchStatement: { 939 | fieldToMatch: {uriPath: {}}, 940 | positionalConstraint: "STARTS_WITH", 941 | searchString: '/wp-admin', 942 | textTransformations: [{type: 'NONE', priority: 0}] 943 | } 944 | }, 945 | { 946 | ipSetReferenceStatement: { 947 | arn: adminWhitelistIpSet.attrArn, 948 | ipSetForwardedIpConfig: { 949 | headerName: 'X-Forwarded-For', 950 | position: 'ANY', 951 | fallbackBehavior: 'NO_MATCH' 952 | } 953 | } 954 | } 955 | ] 956 | } 957 | } 958 | } 959 | ] 960 | }); 961 | 962 | new CfnWebACLAssociation(this, 'ApplicationLoadBalancerWafWebAclAssociation', { 963 | resourceArn: applicationLoadBalancer.loadBalancerArn, 964 | webAclArn: applicationLoadBalancerWebAcl.attrArn 965 | }); 966 | 967 | const wordPressDistribution = new CloudFrontWebDistribution(this, 'WordPressDistribution', { 968 | originConfigs: [ 969 | { 970 | customOriginSource: { 971 | domainName: applicationLoadBalancer.loadBalancerDnsName, 972 | originProtocolPolicy: OriginProtocolPolicy.HTTPS_ONLY, 973 | originReadTimeout: Duration.minutes(1), 974 | originHeaders: { 975 | 'X_Request_From_CloudFront': props.cloudFrontHashHeader 976 | } 977 | }, 978 | behaviors: [ 979 | { 980 | isDefaultBehavior: true, 981 | forwardedValues: { 982 | queryString: true, 983 | cookies: { 984 | forward: 'whitelist', 985 | whitelistedNames: [ 986 | 'comment_*', 987 | 'wordpress_*', 988 | 'wp-settings-*' 989 | ] 990 | }, 991 | headers: [ 992 | 'Host', 993 | 'CloudFront-Forwarded-Proto', 994 | 'CloudFront-Is-Mobile-Viewer', 995 | 'CloudFront-Is-Tablet-Viewer', 996 | 'CloudFront-Is-Desktop-Viewer' 997 | ] 998 | }, 999 | cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS, 1000 | allowedMethods: CloudFrontAllowedMethods.ALL 1001 | }, 1002 | { 1003 | pathPattern: 'wp-admin/*', 1004 | forwardedValues: { 1005 | queryString: true, 1006 | cookies: { 1007 | forward: 'all' 1008 | }, 1009 | headers: ['*'] 1010 | }, 1011 | cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS, 1012 | allowedMethods: CloudFrontAllowedMethods.ALL 1013 | }, 1014 | { 1015 | pathPattern: 'wp-login.php', 1016 | forwardedValues: { 1017 | queryString: true, 1018 | cookies: { 1019 | forward: 'all' 1020 | }, 1021 | headers: ['*'] 1022 | }, 1023 | cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS, 1024 | allowedMethods: CloudFrontAllowedMethods.ALL 1025 | } 1026 | ] 1027 | } 1028 | ], 1029 | priceClass: PriceClass.PRICE_CLASS_ALL, 1030 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 1031 | httpVersion: HttpVersion.HTTP2, 1032 | defaultRootObject: '', 1033 | viewerCertificate: ViewerCertificate.fromAcmCertificate(acmCertificate, {aliases: [props.hostname]}), 1034 | webACLId: wordPressCloudFrontDistributionWafWebAcl.attrArn, 1035 | loggingConfig: { 1036 | bucket: loggingBucket, 1037 | prefix: 'wordpress-distribution' 1038 | } 1039 | }); 1040 | (wordPressDistribution.node.defaultChild as CfnDistribution).addDependsOn(wordPressCloudFrontDistributionWafWebAcl); 1041 | 1042 | const staticContentBucketOriginAccessIdentity = new OriginAccessIdentity(this, 'StaticContentBucketOriginAccessIdentity'); 1043 | staticContentBucket.grantRead(staticContentBucketOriginAccessIdentity); 1044 | 1045 | const staticContentDistribution = new CloudFrontWebDistribution(this, 'StaticContentDistribution', { 1046 | originConfigs: [ 1047 | { 1048 | s3OriginSource: { 1049 | s3BucketSource: staticContentBucket, 1050 | originAccessIdentity: staticContentBucketOriginAccessIdentity 1051 | }, 1052 | behaviors: [ 1053 | { 1054 | isDefaultBehavior: true, 1055 | forwardedValues: { 1056 | queryString: true, 1057 | cookies: { 1058 | forward: 'none' 1059 | } 1060 | }, 1061 | cachedMethods: CloudFrontAllowedCachedMethods.GET_HEAD, 1062 | allowedMethods: CloudFrontAllowedMethods.GET_HEAD 1063 | } 1064 | ] 1065 | }, 1066 | ], 1067 | priceClass: PriceClass.PRICE_CLASS_ALL, 1068 | viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 1069 | httpVersion: HttpVersion.HTTP2, 1070 | defaultRootObject: '', 1071 | viewerCertificate: ViewerCertificate.fromAcmCertificate(acmCertificate, {aliases: [`static.${props.hostname}`]}), 1072 | loggingConfig: { 1073 | bucket: loggingBucket, 1074 | prefix: 'static-content-distribution' 1075 | } 1076 | }); 1077 | 1078 | 1079 | const backupVault = new BackupVault(this, 'BackupVault', { 1080 | backupVaultName: 'AwsServerlessWordPressBackupVault', 1081 | removalPolicy: props.removalPolicy 1082 | }); 1083 | 1084 | const backupPlan = BackupPlan.dailyMonthly1YearRetention(this, 'BackupPlan', backupVault); 1085 | 1086 | backupPlan.addSelection('BackupPlanSelection', { 1087 | resources: [ 1088 | BackupResource.fromEfsFileSystem(fileSystem), 1089 | BackupResource.fromArn(this.formatArn({ 1090 | resource: 'cluster', 1091 | service: 'rds', 1092 | sep: ':', 1093 | resourceName: rdsAuroraCluster.clusterIdentifier 1094 | })) 1095 | ] 1096 | }); 1097 | 1098 | const awsConfigOnComplianceSnsTopic = new Topic(this, 'AwsConfigOnComplianceSnsTopic', {masterKey: awsManagedSnsKmsKey}); 1099 | props.snsEmailSubscription.forEach(email => awsConfigOnComplianceSnsTopic.addSubscription(new EmailSubscription(email))); 1100 | 1101 | const configurationRecorderRole = new Role(this, 'ConfigurationRole', { 1102 | assumedBy: new ServicePrincipal('config.amazonaws.com') 1103 | }); 1104 | configurationRecorderRole.addToPolicy(new PolicyStatement({ 1105 | actions: ['s3:PutObject'], 1106 | resources: [`${loggingBucket.bucketArn}/config/AWSLogs/${this.account}/*`], 1107 | conditions: { 1108 | StringLike: { 1109 | "s3:x-amz-acl": "bucket-owner-full-control" 1110 | } 1111 | } 1112 | })); 1113 | configurationRecorderRole.addToPolicy(new PolicyStatement({ 1114 | actions: ['s3:GetBucketAcl'], 1115 | resources: [loggingBucket.bucketArn] 1116 | })) 1117 | 1118 | const configurationRecorder = new CfnConfigurationRecorder(this, 'ConfigurationRecorder', { 1119 | recordingGroup: { 1120 | allSupported: true, 1121 | includeGlobalResourceTypes: true 1122 | }, 1123 | roleArn: configurationRecorderRole.roleArn 1124 | }); 1125 | 1126 | const deliveryChannel = new CfnDeliveryChannel(this, 'DeliveryChannel', { 1127 | configSnapshotDeliveryProperties: { 1128 | deliveryFrequency: 'One_Hour' 1129 | }, 1130 | s3BucketName: loggingBucket.bucketName, 1131 | s3KeyPrefix: 'config' 1132 | }); 1133 | const ruleScope = RuleScope.fromTag(globalTagKey, globalTagValue); 1134 | const awsConfigManagesRules = [ 1135 | new ManagedRule(this, 'AwsConfigManagedRuleVpcFlowLogsEnabled', { 1136 | identifier: 'VPC_FLOW_LOGS_ENABLED', 1137 | inputParameters: {trafficType: 'ALL'}, 1138 | ruleScope 1139 | }), 1140 | new ManagedRule(this, 'AwsConfigManagedRuleVpcSgOpenOnlyToAuthorizedPorts', { 1141 | identifier: 'VPC_SG_OPEN_ONLY_TO_AUTHORIZED_PORTS', 1142 | inputParameters: {authorizedTcpPorts: '443'}, 1143 | ruleScope 1144 | }), 1145 | new ManagedRule(this, 'AwsConfigManagedRuleInternetGatewayAuthorizedVpcOnly', { 1146 | identifier: 'INTERNET_GATEWAY_AUTHORIZED_VPC_ONLY', 1147 | inputParameters: {AuthorizedVpcIds: vpc.vpcId}, 1148 | ruleScope 1149 | }), 1150 | new ManagedRule(this, 'AwsConfigManagedRuleAcmCertificateExpirationCheck', { 1151 | identifier: 'ACM_CERTIFICATE_EXPIRATION_CHECK', 1152 | inputParameters: {daysToExpiration: 90}, 1153 | ruleScope 1154 | }), 1155 | new ManagedRule(this, 'AwsConfigManagedRuleAutoScalingGroupElbHealthcheckRequired', { 1156 | identifier: 'AUTOSCALING_GROUP_ELB_HEALTHCHECK_REQUIRED', 1157 | ruleScope 1158 | }), 1159 | new ManagedRule(this, 'AwsConfigManagedRuleIncomingSshDisabled', { 1160 | identifier: 'INCOMING_SSH_DISABLED', 1161 | ruleScope 1162 | }), 1163 | new ManagedRule(this, 'AwsConfigManagedRuleSnsEncryptedKms', { 1164 | identifier: 'SNS_ENCRYPTED_KMS', 1165 | ruleScope 1166 | }), 1167 | new ManagedRule(this, 'AwsConfigManagedRuleElbDeletionProtection', { 1168 | identifier: 'ELB_DELETION_PROTECTION_ENABLED', 1169 | ruleScope 1170 | }), 1171 | new ManagedRule(this, 'AwsConfigManagedRuleElbLoggingEnabled', { 1172 | identifier: 'ELB_LOGGING_ENABLED', 1173 | inputParameters: {s3BucketNames: loggingBucket.bucketName}, 1174 | ruleScope 1175 | }), 1176 | new ManagedRule(this, 'AwsConfigManagedRuleAlbHttpDropInvalidHeaderEnabled', { 1177 | identifier: 'ALB_HTTP_DROP_INVALID_HEADER_ENABLED', 1178 | ruleScope 1179 | }), 1180 | new ManagedRule(this, 'AwsConfigManagedRuleAlbHttpToHttpsRedirectionCheck', { 1181 | identifier: 'ALB_HTTP_TO_HTTPS_REDIRECTION_CHECK', 1182 | ruleScope 1183 | }), 1184 | new ManagedRule(this, 'AwsConfigManagedRuleAlbWafEnabled', { 1185 | identifier: 'ALB_WAF_ENABLED', 1186 | inputParameters: {wafWebAclIds: applicationLoadBalancerWebAcl.attrArn}, 1187 | ruleScope 1188 | }), 1189 | new ManagedRule(this, 'AwsConfigManagedRuleCloudFrontOriginAccessIdentityEnabled', { 1190 | identifier: 'CLOUDFRONT_ORIGIN_ACCESS_IDENTITY_ENABLED', 1191 | ruleScope 1192 | }), 1193 | new ManagedRule(this, 'AwsConfigManagedRuleCloudFrontViewerPolicyHttps', { 1194 | identifier: 'CLOUDFRONT_VIEWER_POLICY_HTTPS', 1195 | ruleScope 1196 | }), 1197 | new ManagedRule(this, 'AwsConfigManagedRuleEfsInBackupPlan', { 1198 | identifier: 'EFS_IN_BACKUP_PLAN', 1199 | ruleScope 1200 | }), 1201 | new ManagedRule(this, 'AwsConfigManagedRuleEfsEncryptedCheck', { 1202 | identifier: 'EFS_ENCRYPTED_CHECK', 1203 | ruleScope 1204 | }), 1205 | new ManagedRule(this, 'AwsConfigManagedRuleRdsClusterDeletionProtectionEnabled', { 1206 | identifier: 'RDS_CLUSTER_DELETION_PROTECTION_ENABLED', 1207 | ruleScope 1208 | }), 1209 | new ManagedRule(this, 'AwsConfigManagedRuleEdsInBackupPlan', { 1210 | identifier: 'RDS_IN_BACKUP_PLAN', 1211 | ruleScope 1212 | }), 1213 | new ManagedRule(this, 'AwsConfigManagedRuleS3BucketPublicReadProhibited', { 1214 | identifier: 'S3_BUCKET_PUBLIC_READ_PROHIBITED', 1215 | ruleScope 1216 | }), 1217 | new ManagedRule(this, 'AwsConfigManagedRuleS3BucketPublicWriteProhibited', { 1218 | identifier: 'S3_BUCKET_PUBLIC_WRITE_PROHIBITED', 1219 | ruleScope 1220 | }) 1221 | ] 1222 | awsConfigManagesRules.forEach(rule => { 1223 | rule.onComplianceChange('TopicEvent', {target: new SnsTopic(awsConfigOnComplianceSnsTopic)}); 1224 | (rule.node.defaultChild as CfnConfigRule).addDependsOn(configurationRecorder); 1225 | (rule.node.defaultChild as CfnConfigRule).addDependsOn(deliveryChannel); 1226 | }); 1227 | 1228 | const awsConfigCloudFormationStackDriftDetectionCheckRule = new CloudFormationStackDriftDetectionCheck(this, 'AwsConfigCloudFormationStackDriftDetectionCheck', {ownStackOnly: true}); 1229 | awsConfigCloudFormationStackDriftDetectionCheckRule.onComplianceChange('TopicEvent', {target: new SnsTopic(awsConfigOnComplianceSnsTopic)}); 1230 | (awsConfigCloudFormationStackDriftDetectionCheckRule.node.defaultChild as CfnConfigRule).addDependsOn(configurationRecorder); 1231 | (awsConfigCloudFormationStackDriftDetectionCheckRule.node.defaultChild as CfnConfigRule).addDependsOn(deliveryChannel); 1232 | 1233 | new CfnGroup(this, 'ResourceGroup', { 1234 | name: 'ServerlessWordPressResourceGroup', 1235 | resourceQuery: { 1236 | type: 'TAG_FILTERS_1_0', 1237 | query: { 1238 | resourceTypeFilters: ['AWS::AllSupported'], 1239 | tagFilters: [ 1240 | { 1241 | key: globalTagKey, 1242 | values: [globalTagValue] 1243 | } 1244 | ] 1245 | } 1246 | } 1247 | }); 1248 | 1249 | const rootDnsRecord = new ARecord(this, 'RootDnsRecord', { 1250 | zone: publicHostedZone, 1251 | recordName: props.hostname, 1252 | target: RecordTarget.fromAlias(new CloudFrontTarget(wordPressDistribution)) 1253 | }); 1254 | 1255 | const vpnAdminPublicRecord = new ARecord(this, 'VpnAdminPublicDnsRecord', { 1256 | zone: publicHostedZone, 1257 | recordName: `admin.${props.hostname}`, 1258 | target: RecordTarget.fromAlias(new LoadBalancerTarget(vpnAdminApplicationLoadBalancer)) 1259 | }); 1260 | 1261 | const vpnAdminPrivateRecord = new ARecord(this, 'VpnAdminPrivateDnsRecord', { 1262 | zone: privateHostedZone, 1263 | recordName: `admin.${privateHostedZone.zoneName}`, 1264 | target: RecordTarget.fromAlias(new LoadBalancerTarget(vpnAdminApplicationLoadBalancer)) 1265 | }); 1266 | 1267 | const staticContentDnsRecord = new ARecord(this, 'StaticContentDnsRecord', { 1268 | zone: publicHostedZone, 1269 | recordName: `static.${props.hostname}`, 1270 | target: RecordTarget.fromAlias(new CloudFrontTarget(staticContentDistribution)) 1271 | }); 1272 | 1273 | new CfnOutput(this, 'RootHostname', { 1274 | value: rootDnsRecord.domainName 1275 | }); 1276 | 1277 | new CfnOutput(this, 'VpnAdminPublicHostname', { 1278 | value: vpnAdminPublicRecord.domainName 1279 | }); 1280 | 1281 | new CfnOutput(this, 'VpnAdminPrivateHostname', { 1282 | value: vpnAdminPrivateRecord.domainName 1283 | }); 1284 | 1285 | new CfnOutput(this, 'StaticContentHostname', { 1286 | value: staticContentDnsRecord.domainName 1287 | }); 1288 | 1289 | new CfnOutput(this, 'RdsAuroraServerlessClusterPrivateHostname', { 1290 | value: rdsAuroraClusterPrivateDnsRecord.domainName 1291 | }); 1292 | 1293 | new CfnOutput(this, 'ElastiCacheMemcachedClusterPrivateHostname', { 1294 | value: elastiCacheMemcachedClusterPrivateDnsRecord.domainName 1295 | }); 1296 | 1297 | new CfnOutput(this, 'ElasticsearchDomainPrivateHostname', { 1298 | value: elasticsearchDomainPrivateDnsRecord.domainName 1299 | }); 1300 | 1301 | new CfnOutput(this, 'EfsFileSystemPrivateHostname', { 1302 | value: fileSystemEndpointPrivateDnsRecord.domainName 1303 | }); 1304 | } 1305 | } -------------------------------------------------------------------------------- /lib/custom-resource/index.ts: -------------------------------------------------------------------------------- 1 | import AWS = require('aws-sdk'); 2 | 3 | const s3 = new AWS.S3(); 4 | export const handler = async (evt: any) => { 5 | const requestType = evt.RequestType; 6 | if (requestType === 'Delete') { 7 | let objects; 8 | do { 9 | objects = await s3.listObjectsV2({Bucket: process.env.LOGGING_BUCKET_NAME!}).promise(); 10 | if (objects.Contents) { 11 | await s3.deleteObjects({ 12 | Bucket: process.env.LOGGING_BUCKET_NAME!, 13 | Delete: {Objects: [...objects.Contents.filter(_object => _object.Key).map(_object => ({Key: _object.Key!}))]} 14 | }).promise(); 15 | } 16 | } while (objects.IsTruncated || objects.NextContinuationToken) 17 | } 18 | } -------------------------------------------------------------------------------- /lib/images/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | volumes: 4 | wordpress-volume: 5 | mysql-volume: 6 | 7 | services: 8 | 9 | mysql: 10 | image: mysql:latest 11 | restart: always 12 | environment: 13 | MYSQL_DATABASE: wordpress 14 | MYSQL_USER: wordpress 15 | MYSQL_PASSWORD: P@ssw0rd 16 | MYSQL_ROOT_PASSWORD: R00tP@ssw0rd 17 | volumes: 18 | - mysq-volume:/var/lib/mysql 19 | 20 | memcached: 21 | image: memcached:latest 22 | 23 | nginx: 24 | build: nginx/ 25 | ports: 26 | - 8080:80 27 | links: 28 | - wordpress:wordpress 29 | - memcached:memcached 30 | volumes: 31 | - wordpress-volume:/var/www/html 32 | 33 | wordpress: 34 | build: wordpress/ 35 | environment: 36 | WORDPRESS_DB_HOST: mysql 37 | WORDPRESS_DB_NAME: wordpress 38 | WORDPRESS_DB_USER: wordpress 39 | WORDPRESS_DB_PASSWORD: P@ssw0rd 40 | volumes: 41 | - wordpress-volume:/var/www/html 42 | links: 43 | - mysql:mysql 44 | - memcached:memcached -------------------------------------------------------------------------------- /lib/images/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | RUN rm -f /etc/nginx/conf.d/default.conf 4 | RUN rm -f /etc/nginx/conf.d/example_ssl.conf 5 | 6 | COPY wordpress.conf.template /etc/nginx/templates/wordpress.conf.template 7 | COPY nginx.conf /etc/nginx/nginx.conf 8 | 9 | CMD ["nginx"] -------------------------------------------------------------------------------- /lib/images/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | daemon off; 2 | user nginx; 3 | worker_processes auto; 4 | 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | 8 | events { 9 | worker_connections 1024; 10 | use epoll; 11 | accept_mutex off; 12 | } 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | 19 | default_type application/octet-stream; 20 | 21 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 22 | '$status $body_bytes_sent "$http_referer" ' 23 | '"$http_user_agent" "$http_x_forwarded_for"'; 24 | 25 | access_log /var/log/nginx/access.log main; 26 | 27 | sendfile on; 28 | #tcp_nopush on; 29 | 30 | keepalive_timeout 65; 31 | 32 | client_max_body_size 0; 33 | client_body_buffer_size 128k; 34 | 35 | gzip on; 36 | gzip_http_version 1.0; 37 | gzip_comp_level 6; 38 | gzip_min_length 0; 39 | gzip_buffers 16 8k; 40 | gzip_proxied any; 41 | gzip_types text/plain text/css text/xml text/javascript application/xml application/xml+rss application/javascript application/json; 42 | gzip_disable "MSIE [1-6]\."; 43 | gzip_vary on; 44 | 45 | include /etc/nginx/conf.d/*.conf; 46 | } -------------------------------------------------------------------------------- /lib/images/nginx/wordpress.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; ## listen for ipv4; this line is default and implied 3 | listen [::]:80 default ipv6only=on; ## listen for ipv6 4 | server_name ${SERVER_NAME}; 5 | 6 | include /var/www/html/*.conf; 7 | 8 | keepalive_timeout 5 5; 9 | proxy_buffering off; 10 | 11 | # allow large uploads 12 | client_max_body_size 4G; 13 | 14 | # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486) 15 | chunked_transfer_encoding on; 16 | 17 | charset UTF-8; 18 | root /var/www/html; 19 | index index.php index.html index.htm; 20 | 21 | location / { 22 | try_files $uri $uri/ /index.php?$args; 23 | } 24 | 25 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 26 | # 27 | location ~ .php$ { 28 | try_files $uri =404; 29 | fastcgi_index index.php; 30 | fastcgi_connect_timeout 10; 31 | fastcgi_send_timeout 180; 32 | fastcgi_read_timeout 180; 33 | fastcgi_buffer_size 512k; 34 | fastcgi_buffers 4 256k; 35 | fastcgi_busy_buffers_size 512k; 36 | fastcgi_temp_file_write_size 512k; 37 | fastcgi_intercept_errors on; 38 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 39 | fastcgi_keep_conn on; 40 | 41 | fastcgi_param QUERY_STRING $query_string; 42 | fastcgi_param REQUEST_METHOD $request_method; 43 | fastcgi_param CONTENT_TYPE $content_type; 44 | fastcgi_param CONTENT_LENGTH $content_length; 45 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 46 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 47 | fastcgi_param REQUEST_URI $request_uri; 48 | fastcgi_param DOCUMENT_URI $document_uri; 49 | fastcgi_param DOCUMENT_ROOT $document_root; 50 | fastcgi_param SERVER_PROTOCOL $server_protocol; 51 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 52 | fastcgi_param SERVER_SOFTWARE nginx; 53 | fastcgi_param REMOTE_ADDR $remote_addr; 54 | fastcgi_param REMOTE_PORT $remote_port; 55 | fastcgi_param SERVER_ADDR $server_addr; 56 | fastcgi_param SERVER_PORT $server_port; 57 | fastcgi_param SERVER_NAME $server_name; 58 | fastcgi_param PATH_INFO $fastcgi_path_info; 59 | fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; 60 | fastcgi_param REDIRECT_STATUS 200; 61 | 62 | fastcgi_pass localhost:9000; 63 | } 64 | } -------------------------------------------------------------------------------- /lib/images/wordpress/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM wordpress:5.5.1-php7.3-fpm-alpine 2 | 3 | COPY uploads.ini /usr/local/etc/php/conf.d/uploads.ini 4 | 5 | RUN set -ex; \ 6 | \ 7 | apk add --no-cache --update \ 8 | bash \ 9 | bzip2 \ 10 | less \ 11 | memcached \ 12 | mysql-client \ 13 | unzip \ 14 | zip \ 15 | oniguruma \ 16 | ; \ 17 | mkdir -p /usr/src/php/ext; \ 18 | apk add --no-cache --virtual .build-deps \ 19 | $PHPIZE_DEPS \ 20 | curl-dev \ 21 | freetype-dev \ 22 | icu-dev \ 23 | jpeg-dev \ 24 | memcached-dev \ 25 | libmemcached-dev \ 26 | libpng-dev \ 27 | libpq \ 28 | libxml2-dev \ 29 | libzip-dev \ 30 | oniguruma-dev \ 31 | ; \ 32 | docker-php-ext-configure gd; \ 33 | docker-php-ext-install -j "$(nproc)" \ 34 | exif \ 35 | gd \ 36 | intl \ 37 | mbstring \ 38 | mysqli \ 39 | opcache \ 40 | pcntl \ 41 | pdo_mysql \ 42 | soap \ 43 | zip \ 44 | ; \ 45 | pecl install \ 46 | apcu \ 47 | memcached \ 48 | xhprof \ 49 | ; \ 50 | docker-php-ext-enable \ 51 | apcu \ 52 | memcached \ 53 | xhprof \ 54 | \ 55 | && chown www-data:www-data /var/www /var/www/html; \ 56 | \ 57 | runDeps="$( \ 58 | scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ 59 | | tr ',' '\n' \ 60 | | sort -u \ 61 | | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ 62 | )"; \ 63 | apk add --virtual .wordpress-phpexts-rundeps $runDeps; \ 64 | apk del .build-deps 65 | 66 | #RUN wget https://elasticache-downloads.s3.amazonaws.com/ClusterClient/PHP-7.3/latest-64bit &&\ 67 | # tar -zxvf latest-64bit &&\ 68 | # cp amazon-elasticache-cluster-client.so /usr/local/lib/php/extensions/ &&\ 69 | # echo 'extension=amazon-elasticache-cluster-client.so' >> $PHP_INI_DIR/php.ini 70 | 71 | WORKDIR /var/www/html 72 | 73 | ENTRYPOINT ["docker-entrypoint.sh"] 74 | CMD ["php-fpm"] -------------------------------------------------------------------------------- /lib/images/wordpress/uploads.ini: -------------------------------------------------------------------------------- 1 | file_uploads = On 2 | memory_limit = 500M 3 | upload_max_filesize = 500M 4 | post_max_size = 500M 5 | max_execution_time = 600 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-serverless-wordpress", 3 | "version": "0.1.0", 4 | "author": { 5 | "name": "MikletNg", 6 | "email": "mike@miklet.pro", 7 | "url": "https://miklet.pro" 8 | }, 9 | "license": "Apache-2.0", 10 | "bin": { 11 | "aws-serverless-wordpress": "bin/aws-serverless-wordpress.js" 12 | }, 13 | "scripts": { 14 | "build": "tsc", 15 | "watch": "tsc -w", 16 | "test": "jest", 17 | "cdk": "cdk" 18 | }, 19 | "devDependencies": { 20 | "@aws-cdk/assert": "1.72.0", 21 | "@types/jest": "^24.0.22", 22 | "@types/node": "10.17.5", 23 | "aws-cdk": "1.72.0", 24 | "jest": "^24.9.0", 25 | "ts-jest": "^24.1.0", 26 | "ts-node": "^8.1.0", 27 | "typescript": "~3.7.2" 28 | }, 29 | "dependencies": { 30 | "@aws-cdk/aws-applicationautoscaling": "1.72.0", 31 | "@aws-cdk/aws-backup": "1.72.0", 32 | "@aws-cdk/aws-certificatemanager": "1.72.0", 33 | "@aws-cdk/aws-cloudfront": "1.72.0", 34 | "@aws-cdk/aws-cloudwatch": "1.72.0", 35 | "@aws-cdk/aws-config": "1.72.0", 36 | "@aws-cdk/aws-ec2": "1.72.0", 37 | "@aws-cdk/aws-ecr": "1.72.0", 38 | "@aws-cdk/aws-ecr-assets": "1.72.0", 39 | "@aws-cdk/aws-ecs": "1.72.0", 40 | "@aws-cdk/aws-efs": "1.72.0", 41 | "@aws-cdk/aws-elasticache": "1.72.0", 42 | "@aws-cdk/aws-elasticloadbalancingv2": "1.72.0", 43 | "@aws-cdk/aws-elasticsearch": "1.72.0", 44 | "@aws-cdk/aws-events-targets": "1.72.0", 45 | "@aws-cdk/aws-iam": "1.72.0", 46 | "@aws-cdk/aws-kms": "1.72.0", 47 | "@aws-cdk/aws-logs": "1.72.0", 48 | "@aws-cdk/aws-rds": "1.72.0", 49 | "@aws-cdk/aws-resourcegroups": "1.72.0", 50 | "@aws-cdk/aws-route53": "1.72.0", 51 | "@aws-cdk/aws-route53-targets": "1.72.0", 52 | "@aws-cdk/aws-s3": "1.72.0", 53 | "@aws-cdk/aws-secretsmanager": "1.72.0", 54 | "@aws-cdk/aws-sns": "1.72.0", 55 | "@aws-cdk/aws-sns-subscriptions": "1.72.0", 56 | "@aws-cdk/aws-wafv2": "1.72.0", 57 | "@aws-cdk/core": "1.72.0", 58 | "@aws-cdk/region-info": "1.72.0", 59 | "@types/lodash": "^4.14.157", 60 | "@types/moment": "^2.13.0", 61 | "aws-sdk": "^2.779.0", 62 | "source-map-support": "^0.5.16", 63 | "toml": "^3.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization":false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | --------------------------------------------------------------------------------