├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bin └── aws-ecs-wordpress.ts ├── cdk.json ├── docs └── architecture.png ├── jest.config.js ├── lib └── aws-ecs-wordpress-stack.ts ├── package-lock.json ├── package.json ├── test └── aws-ecs-wordpress.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = off 12 | 13 | [CHANGELOG.md] 14 | indent_size = false 15 | -------------------------------------------------------------------------------- /.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 default cache directory 11 | .parcel-cache 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alex Rhea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wordpress on Amazon ECS Fargate 2 | 3 | This repository uses the [AWS Cloud Development Kit](https://aws.amazon.com/cdk/) (CDK) to deploy a highly available Wordpress installation on [AWS Fargate](https://aws.amazon.com/fargate/) on [Amazon ECS](https://aws.amazon.com/ecs/). This will provision a VPC, ECS Cluster, EFS Filesystem, Secrets, Aurora, and Wordpress Containers. 4 | 5 | ![Wordpress Architecture](./docs/architecture.png) 6 | 7 | To deploy Wordpress we use the official [Wordpress container image](https://hub.docker.com/_/wordpress) available on Docker hub. This image is configured using a volume mount for persistent storage and a series of envrionment variables from AWS Secrets Manager. 8 | 9 | Wordpress has shared resources such as plugins, themes, and uploads that all containers need access to in order to function properly. To facilitate this storage, we provision an Amazon Elastic Filesystem (EFS) filesystem that is attached to all of the containers running. 10 | 11 | Next, we use AWS Secrets Manager to store database credentials for the Aurora MySQL Database and to generate the [Wordpress Salts](https://api.wordpress.org/secret-key/1.1/salt/). These also need to be mounted to every container. 12 | 13 | ## Usage 14 | 15 | This repository is built on AWS Cloud Development Kit (CDK). For full guidance on using the CDK, see the CDK documentation. 16 | 17 | ```bash 18 | # install the depdencies 19 | npm install 20 | 21 | # deploy the stack 22 | cdk deploy 23 | 24 | # destroy the stack 25 | cdk destroy 26 | ``` 27 | 28 | ## License 29 | 30 | This library is licensed under the MIT-0 License. See the [LICENSE file](./LICENSE). 31 | -------------------------------------------------------------------------------- /bin/aws-ecs-wordpress.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import { AwsEcsWordpressStack } from '../lib/aws-ecs-wordpress-stack'; 5 | 6 | const app = new cdk.App(); 7 | new AwsEcsWordpressStack(app, 'AwsEcsWordpressStack'); 8 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/aws-ecs-wordpress.ts", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arhea/aws-ecs-wordpress/42fa15096e219d10a914f31c0e976b9b2f8ac60e/docs/architecture.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /lib/aws-ecs-wordpress-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core'; 2 | import * as ec2 from '@aws-cdk/aws-ec2'; 3 | import * as ecs from '@aws-cdk/aws-ecs'; 4 | import * as ecsPatterns from '@aws-cdk/aws-ecs-patterns'; 5 | import * as efs from '@aws-cdk/aws-efs'; 6 | import * as rds from '@aws-cdk/aws-rds'; 7 | import * as secretsManager from '@aws-cdk/aws-secretsmanager' 8 | import * as iam from '@aws-cdk/aws-iam'; 9 | 10 | export class AwsEcsWordpressStack extends cdk.Stack { 11 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 12 | super(scope, id, props); 13 | 14 | // create the vpc 15 | const vpc = new ec2.Vpc(this, 'VPC'); 16 | 17 | // create the ecs cluster 18 | const cluster = new ecs.Cluster(this, 'Cluster', { 19 | vpc, 20 | }); 21 | 22 | // create wordpress secrets 23 | const secretDatabaseCredentials = new secretsManager.Secret(this, 'DatabaseCredentials', { 24 | generateSecretString: { 25 | passwordLength: 30, 26 | excludePunctuation: true, 27 | includeSpace: false 28 | }, 29 | removalPolicy: cdk.RemovalPolicy.DESTROY 30 | }); 31 | 32 | const secretAuthKey = new secretsManager.Secret(this, 'AuthKey', { 33 | generateSecretString: { 34 | excludeCharacters: '\'"', 35 | passwordLength: 64 36 | }, 37 | removalPolicy: cdk.RemovalPolicy.DESTROY 38 | }); 39 | 40 | const secretSecureAuthKey = new secretsManager.Secret(this, 'SecureAuthKey', { 41 | generateSecretString: { 42 | excludeCharacters: '\'"', 43 | passwordLength: 64 44 | }, 45 | removalPolicy: cdk.RemovalPolicy.DESTROY 46 | }); 47 | 48 | const secretLoggedInKey = new secretsManager.Secret(this, 'LoggedInKey', { 49 | generateSecretString: { 50 | excludeCharacters: '\'"', 51 | passwordLength: 64 52 | }, 53 | removalPolicy: cdk.RemovalPolicy.DESTROY 54 | }); 55 | 56 | const secretNonceKey = new secretsManager.Secret(this, 'NonceKey', { 57 | generateSecretString: { 58 | excludeCharacters: '\'"', 59 | passwordLength: 64 60 | }, 61 | removalPolicy: cdk.RemovalPolicy.DESTROY 62 | }); 63 | 64 | const secretAuthSalt = new secretsManager.Secret(this, 'AuthSalt', { 65 | generateSecretString: { 66 | excludeCharacters: '\'"', 67 | passwordLength: 64 68 | }, 69 | removalPolicy: cdk.RemovalPolicy.DESTROY 70 | }); 71 | 72 | const secretSecureAuthSalt = new secretsManager.Secret(this, 'SecureAuthSalt', { 73 | generateSecretString: { 74 | excludeCharacters: '\'"', 75 | passwordLength: 64 76 | }, 77 | removalPolicy: cdk.RemovalPolicy.DESTROY 78 | }); 79 | 80 | const secretLoggedInSalt = new secretsManager.Secret(this, 'LoggedInSalt', { 81 | generateSecretString: { 82 | excludeCharacters: '\'"', 83 | passwordLength: 64 84 | }, 85 | removalPolicy: cdk.RemovalPolicy.DESTROY 86 | }); 87 | 88 | const secretNonceSalt = new secretsManager.Secret(this, 'NonceSalt', { 89 | generateSecretString: { 90 | excludeCharacters: '\'"', 91 | passwordLength: 64 92 | }, 93 | removalPolicy: cdk.RemovalPolicy.DESTROY 94 | }); 95 | 96 | // create the aurora mysql database 97 | const db = new rds.DatabaseCluster(this, 'Database', { 98 | engine: rds.DatabaseClusterEngine.AURORA_MYSQL, 99 | defaultDatabaseName: 'wordpress', 100 | masterUser: { 101 | username: 'admin', 102 | password: cdk.SecretValue.secretsManager(secretDatabaseCredentials.secretArn) 103 | }, 104 | instanceProps: { 105 | vpc, 106 | vpcSubnets: { 107 | subnetType: ec2.SubnetType.PRIVATE, 108 | }, 109 | instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MEDIUM), 110 | }, 111 | removalPolicy: cdk.RemovalPolicy.DESTROY 112 | }); 113 | 114 | // create the shared file system 115 | const fsSecurityGroup = new ec2.SecurityGroup(this, 'EfsSecurityGroup', { 116 | vpc, 117 | description: 'allow access to the efs file system', 118 | allowAllOutbound: true 119 | }); 120 | 121 | const fileSystem = new efs.FileSystem(this, 'Content', { 122 | vpc, 123 | encrypted: true, 124 | securityGroup: fsSecurityGroup, 125 | performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, 126 | throughputMode: efs.ThroughputMode.BURSTING, 127 | removalPolicy: cdk.RemovalPolicy.DESTROY 128 | }); 129 | 130 | // configure the wordpress task 131 | const taskExecutionRole = new iam.Role(this, 'TaskExecutionRole', { 132 | assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), 133 | }); 134 | 135 | taskExecutionRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy')) 136 | secretDatabaseCredentials.grantRead(taskExecutionRole); 137 | secretAuthKey.grantRead(taskExecutionRole); 138 | secretSecureAuthKey.grantRead(taskExecutionRole); 139 | secretLoggedInKey.grantRead(taskExecutionRole); 140 | secretNonceKey.grantRead(taskExecutionRole); 141 | secretAuthSalt.grantRead(taskExecutionRole); 142 | secretSecureAuthSalt.grantRead(taskExecutionRole); 143 | secretLoggedInSalt.grantRead(taskExecutionRole); 144 | secretNonceSalt.grantRead(taskExecutionRole); 145 | 146 | const taskSecurityGroup = new ec2.SecurityGroup(this, 'TaskSecurityGroup', { 147 | vpc, 148 | description: 'allow access to the task', 149 | allowAllOutbound: true 150 | }); 151 | 152 | const taskDef = new ecs.FargateTaskDefinition(this, 'TaskDef', { 153 | family: 'wordpress', 154 | executionRole: taskExecutionRole, 155 | memoryLimitMiB: 1024, 156 | cpu: 512, 157 | }); 158 | 159 | // workaround for in progress support of EFS in CDK 160 | const cfnTask = taskDef.node.defaultChild as ecs.CfnTaskDefinition; 161 | 162 | cfnTask.addPropertyOverride('Volumes', [{ 163 | Name: 'wp-content', 164 | EFSVolumeConfiguration: { 165 | FilesystemId: fileSystem.fileSystemId, 166 | TransitEncryption: 'ENABLED' 167 | }, 168 | }]); 169 | 170 | // add the container to the task definition 171 | const container = taskDef.addContainer('Wordpress', { 172 | image: ecs.ContainerImage.fromRegistry('wordpress:5.5-apache'), 173 | logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'Wordpress' }), 174 | memoryLimitMiB: 1024, 175 | cpu: 512, 176 | environment: { 177 | WORDPRESS_DB_HOST: db.clusterEndpoint.socketAddress, 178 | WORDPRESS_DB_NAME: 'wordpress', 179 | WORDPRESS_DB_USER: 'admin' 180 | }, 181 | secrets: { 182 | WORDPRESS_DB_PASSWORD: ecs.Secret.fromSecretsManager(secretDatabaseCredentials), 183 | WORDPRESS_AUTH_KEY: ecs.Secret.fromSecretsManager(secretSecureAuthKey), 184 | WORDPRESS_SECURE_AUTH_KEY: ecs.Secret.fromSecretsManager(secretSecureAuthKey), 185 | WORDPRESS_LOGGED_IN_KEY: ecs.Secret.fromSecretsManager(secretLoggedInKey), 186 | WORDPRESS_NONCE_KEY: ecs.Secret.fromSecretsManager(secretNonceKey), 187 | WORDPRESS_AUTH_SALT: ecs.Secret.fromSecretsManager(secretAuthSalt), 188 | WORDPRESS_SECURE_AUTH_SALT: ecs.Secret.fromSecretsManager(secretSecureAuthSalt), 189 | WORDPRESS_LOGGED_IN_SALT: ecs.Secret.fromSecretsManager(secretLoggedInSalt), 190 | WORDPRESS_NONCE_SALT: ecs.Secret.fromSecretsManager(secretNonceSalt) 191 | } 192 | }); 193 | 194 | container.addPortMappings({ 195 | containerPort: 80 196 | }); 197 | 198 | container.addMountPoints({ 199 | sourceVolume: 'wp-content', 200 | containerPath: '/var/www/html/wp-content', 201 | readOnly: false 202 | }); 203 | 204 | // create the wordpress service 205 | const wordpress = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Service', { 206 | cluster, 207 | taskDefinition: taskDef, 208 | platformVersion: ecs.FargatePlatformVersion.VERSION1_4, 209 | }); 210 | 211 | wordpress.service.connections.addSecurityGroup(taskSecurityGroup); 212 | 213 | wordpress.service.autoScaleTaskCount({ 214 | minCapacity: 1, 215 | maxCapacity: 10, 216 | }); 217 | 218 | wordpress.targetGroup.configureHealthCheck({ 219 | enabled: true, 220 | path: '/index.php', 221 | healthyHttpCodes: '200,201,302', 222 | interval: cdk.Duration.seconds(15), 223 | timeout: cdk.Duration.seconds(10), 224 | healthyThresholdCount: 3, 225 | unhealthyThresholdCount: 2 226 | }); 227 | 228 | // configure security groups 229 | db.connections.allowFrom(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(3306), 'allow connections from within the vpc to the database') 230 | fsSecurityGroup.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(2049), 'allow access to the efs file mounts') 231 | 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-ecs-wordpress", 3 | "version": "0.1.0", 4 | "bin": { 5 | "aws-ecs-wordpress": "bin/aws-ecs-wordpress.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@aws-cdk/assert": "1.59.0", 15 | "@types/jest": "^26.0.4", 16 | "@types/node": "10.17.27", 17 | "jest": "^26.0.4", 18 | "ts-jest": "^26.1.3", 19 | "aws-cdk": "1.59.0", 20 | "ts-node": "^8.1.0", 21 | "typescript": "~3.9.6" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-ec2": "^1.59.0", 25 | "@aws-cdk/aws-ecs": "^1.59.0", 26 | "@aws-cdk/aws-ecs-patterns": "^1.59.0", 27 | "@aws-cdk/aws-iam": "^1.59.0", 28 | "@aws-cdk/aws-rds": "^1.59.0", 29 | "@aws-cdk/aws-secretsmanager": "^1.59.0", 30 | "@aws-cdk/core": "1.59.0", 31 | "source-map-support": "^0.5.16" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/aws-ecs-wordpress.test.ts: -------------------------------------------------------------------------------- 1 | import { expect as expectCDK, matchTemplate, MatchStyle } from '@aws-cdk/assert'; 2 | import * as cdk from '@aws-cdk/core'; 3 | import * as AwsEcsWordpress from '../lib/aws-ecs-wordpress-stack'; 4 | 5 | test('Empty Stack', () => { 6 | const app = new cdk.App(); 7 | // WHEN 8 | const stack = new AwsEcsWordpress.AwsEcsWordpressStack(app, 'MyTestStack'); 9 | // THEN 10 | expectCDK(stack).to(matchTemplate({ 11 | "Resources": {} 12 | }, MatchStyle.EXACT)) 13 | }); 14 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------