Traditional WordPress hosting on shared servers or single instances has limitations when it comes to scalability, reliability, and performance. Modern content management platforms need to handle traffic spikes, ensure high availability, and provide seamless scaling. This post explores building a production-ready WordPress platform using AWS ECS Fargate, diving into architectural decisions, tradeoffs, and implementation details.
The Challenge: WordPress at Scale
WordPress powers over 40% of websites globally, but scaling it beyond a single server presents several challenges:
- Traffic Variability: Content sites experience unpredictable traffic patterns
- Database Bottlenecks: MySQL becomes the limiting factor at scale
- File Storage: Shared file systems for uploads and media
- Session Management: Handling user sessions across multiple instances
- Plugin Compatibility: Ensuring third-party plugins work in containerized environments
- Security: Protecting against vulnerabilities and attacks
- Cost Optimization: Balancing performance with operational costs
Let’s explore how containerized WordPress on ECS Fargate addresses these challenges.
Architecture Overview
Our scalable WordPress platform uses a microservices approach with these AWS components:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ CloudFront │ │ ALB │ │ECS Fargate │ │ RDS │
│ (CDN) │───▶│ (Load Bal.) │───▶│ WordPress │───▶│ MySQL │
└─────────────┘ └──────────────┘ │ Container │ │ (Multi-AZ) │
└─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ EFS │
│(File System)│
└─────────────┘
Core Components
- ECS Fargate: Serverless containers for WordPress application
- Application Load Balancer: Distributes traffic across multiple containers
- RDS MySQL: Managed database with Multi-AZ deployment
- EFS: Shared file system for WordPress uploads and plugins
- CloudFront: Global CDN for static content delivery
- Secrets Manager: Secure storage for database credentials
CDK Infrastructure Implementation
VPC and Network Setup
The foundation starts with a properly configured network:
1// lib/ecs-wordpress-stack.ts
2export class EcsWordpressStack extends cdk.Stack {
3 constructor(scope: Construct, id: string, props?: cdk.StackProps) {
4 super(scope, id, props);
5
6 // VPC with public and private subnets across multiple AZs
7 const vpc = new ec2.Vpc(this, 'WordPressVpc', {
8 maxAzs: 2,
9 natGateways: 2, // One per AZ for high availability
10 subnetConfiguration: [
11 {
12 cidrMask: 24,
13 name: 'PublicSubnet',
14 subnetType: ec2.SubnetType.PUBLIC,
15 },
16 {
17 cidrMask: 24,
18 name: 'PrivateSubnet',
19 subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
20 },
21 {
22 cidrMask: 28,
23 name: 'IsolatedSubnet',
24 subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
25 },
26 ],
27 });
28
29 // Security group for ECS tasks
30 const ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', {
31 vpc,
32 description: 'Security group for ECS WordPress tasks',
33 allowAllOutbound: true,
34 });
35
36 // Security group for RDS
37 const rdsSecurityGroup = new ec2.SecurityGroup(this, 'RdsSecurityGroup', {
38 vpc,
39 description: 'Security group for RDS MySQL instance',
40 allowAllOutbound: false,
41 });
42
43 // Allow ECS to connect to RDS on MySQL port
44 rdsSecurityGroup.addIngressRule(
45 ecsSecurityGroup,
46 ec2.Port.tcp(3306),
47 'Allow ECS tasks to connect to MySQL'
48 );
49
50 // Security group for EFS
51 const efsSecurityGroup = new ec2.SecurityGroup(this, 'EfsSecurityGroup', {
52 vpc,
53 description: 'Security group for EFS file system',
54 allowAllOutbound: false,
55 });
56
57 // Allow ECS to connect to EFS on NFS port
58 efsSecurityGroup.addIngressRule(
59 ecsSecurityGroup,
60 ec2.Port.tcp(2049),
61 'Allow ECS tasks to connect to EFS'
62 );
63 }
64}
Key Design Decisions:
- Multi-AZ deployment ensures high availability
- Private subnets for ECS tasks provide security isolation
- Isolated subnets for RDS eliminate internet access
- Security groups implement least-privilege access
EFS File System for Persistent Storage
WordPress requires shared storage for uploads, themes, and plugins:
1// EFS File System with encryption
2const fileSystem = new efs.FileSystem(this, 'WordPressFileSystem', {
3 vpc,
4 securityGroup: efsSecurityGroup,
5 encrypted: true,
6 lifecyclePolicy: efs.LifecyclePolicy.AFTER_30_DAYS,
7 performanceMode: efs.PerformanceMode.GENERAL_PURPOSE,
8 throughputMode: efs.ThroughputMode.BURSTING,
9 removalPolicy: cdk.RemovalPolicy.DESTROY,
10});
11
12// EFS Access Points for better security and performance
13const accessPoint = new efs.AccessPoint(this, 'WordPressAccessPoint', {
14 fileSystem,
15 path: '/var/www/html',
16 creationInfo: {
17 ownerGid: '33', // www-data group
18 ownerUid: '33', // www-data user
19 permissions: '755',
20 },
21 posixUser: {
22 gid: 33,
23 uid: 33,
24 },
25});
26
27// Create mount targets in each AZ
28const mountTargets = vpc.privateSubnets.map((subnet, index) => {
29 return new efs.MountTarget(this, `MountTarget${index}`, {
30 fileSystem,
31 subnet,
32 securityGroup: efsSecurityGroup,
33 });
34});
RDS MySQL Database
Managed MySQL database with high availability:
1// Database subnet group for RDS
2const dbSubnetGroup = new rds.SubnetGroup(this, 'DbSubnetGroup', {
3 vpc,
4 description: 'Subnet group for WordPress RDS instance',
5 subnets: {
6 subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
7 },
8});
9
10// RDS MySQL instance
11const database = new rds.DatabaseInstance(this, 'WordPressDatabase', {
12 engine: rds.DatabaseInstanceEngine.mysql({
13 version: rds.MysqlEngineVersion.VER_8_0,
14 }),
15 instanceType: ec2.InstanceType.of(
16 ec2.InstanceClass.T3,
17 ec2.InstanceSize.MICRO
18 ),
19 vpc,
20 subnetGroup: dbSubnetGroup,
21 securityGroups: [rdsSecurityGroup],
22
23 // Database configuration
24 databaseName: 'wordpress',
25 credentials: rds.Credentials.fromGeneratedSecret('wordpressadmin', {
26 excludeCharacters: '"@/\\\'',
27 }),
28
29 // Storage configuration
30 allocatedStorage: 20,
31 maxAllocatedStorage: 100,
32 storageEncrypted: true,
33 storageType: rds.StorageType.GP2,
34
35 // Backup and maintenance
36 backupRetention: cdk.Duration.days(7),
37 deleteAutomatedBackups: true,
38 deletionProtection: false,
39
40 // Multi-AZ for production
41 multiAz: true,
42
43 // Monitoring
44 monitoringInterval: cdk.Duration.seconds(60),
45 enablePerformanceInsights: true,
46
47 removalPolicy: cdk.RemovalPolicy.DESTROY,
48});
ECS Fargate Configuration
Cluster and Task Definition
Setting up the ECS cluster and WordPress container:
1// ECS Cluster
2const cluster = new ecs.Cluster(this, 'WordPressCluster', {
3 vpc,
4 containerInsights: true,
5});
6
7// Task Definition
8const taskDefinition = new ecs.FargateTaskDefinition(this, 'WordPressTaskDef', {
9 memoryLimitMiB: 2048,
10 cpu: 1024,
11});
12
13// WordPress container
14const wordpressContainer = taskDefinition.addContainer('wordpress', {
15 image: ecs.ContainerImage.fromRegistry('wordpress:latest'),
16 memoryReservationMiB: 1024,
17
18 // Environment variables
19 environment: {
20 WORDPRESS_DB_HOST: database.instanceEndpoint.hostname,
21 WORDPRESS_DB_NAME: 'wordpress',
22 WORDPRESS_TABLE_PREFIX: 'wp_',
23
24 // WordPress configuration
25 WORDPRESS_CONFIG_EXTRA: `
26 define('WP_REDIS_HOST', 'localhost');
27 define('FORCE_SSL_ADMIN', true);
28 define('WP_DEBUG', false);
29 define('WP_DEBUG_LOG', false);
30 define('WP_DEBUG_DISPLAY', false);
31 `,
32 },
33
34 // Secrets from AWS Secrets Manager
35 secrets: {
36 WORDPRESS_DB_USER: ecs.Secret.fromSecretsManager(
37 database.secret!,
38 'username'
39 ),
40 WORDPRESS_DB_PASSWORD: ecs.Secret.fromSecretsManager(
41 database.secret!,
42 'password'
43 ),
44 },
45
46 // Logging
47 logging: ecs.LogDrivers.awsLogs({
48 streamPrefix: 'wordpress',
49 logRetention: logs.RetentionDays.ONE_WEEK,
50 }),
51});
52
53// Configure container ports
54wordpressContainer.addPortMappings({
55 containerPort: 80,
56 protocol: ecs.Protocol.TCP,
57});
58
59// EFS volume mount
60taskDefinition.addVolume({
61 name: 'wordpress-data',
62 efsVolumeConfiguration: {
63 fileSystemId: fileSystem.fileSystemId,
64 transitEncryption: 'ENABLED',
65 accessPoint: accessPoint.accessPointId,
66 },
67});
68
69// Mount EFS volume in container
70wordpressContainer.addMountPoints({
71 sourceVolume: 'wordpress-data',
72 containerPath: '/var/www/html/wp-content',
73 readOnly: false,
74});
ECS Service with Auto Scaling
Configuring the service with automatic scaling:
1// Application Load Balancer
2const alb = new elbv2.ApplicationLoadBalancer(this, 'WordPressALB', {
3 vpc,
4 internetFacing: true,
5 securityGroup: albSecurityGroup,
6});
7
8// Target Group
9const targetGroup = new elbv2.ApplicationTargetGroup(this, 'WordPressTargets', {
10 vpc,
11 port: 80,
12 protocol: elbv2.ApplicationProtocol.HTTP,
13 targetType: elbv2.TargetType.IP,
14
15 // Health check configuration
16 healthCheck: {
17 enabled: true,
18 healthyHttpCodes: '200,302',
19 interval: cdk.Duration.seconds(30),
20 path: '/wp-admin/install.php',
21 protocol: elbv2.Protocol.HTTP,
22 timeout: cdk.Duration.seconds(5),
23 unhealthyThresholdCount: 2,
24 healthyThresholdCount: 5,
25 },
26});
27
28// ALB Listener
29const listener = alb.addListener('WordPressListener', {
30 port: 80,
31 protocol: elbv2.ApplicationProtocol.HTTP,
32 defaultTargetGroups: [targetGroup],
33});
34
35// ECS Service
36const service = new ecs.FargateService(this, 'WordPressService', {
37 cluster,
38 taskDefinition,
39 serviceName: 'wordpress-service',
40
41 // Desired count and capacity
42 desiredCount: 2,
43 minHealthyPercent: 50,
44 maxHealthyPercent: 200,
45
46 // Network configuration
47 assignPublicIp: false,
48 securityGroups: [ecsSecurityGroup],
49 vpcSubnets: {
50 subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
51 },
52
53 // Load balancer configuration
54 loadBalancers: [{
55 targetGroup,
56 containerName: 'wordpress',
57 containerPort: 80,
58 }],
59
60 // Enable service discovery
61 cloudMapOptions: {
62 name: 'wordpress',
63 cloudMapNamespace: cluster.defaultCloudMapNamespace,
64 dnsRecordType: servicediscovery.DnsRecordType.A,
65 },
66});
67
68// Auto Scaling Configuration
69const scaling = service.autoScaleTaskCount({
70 minCapacity: 1,
71 maxCapacity: 10,
72});
73
74// CPU-based scaling
75scaling.scaleOnCpuUtilization('CpuScaling', {
76 targetUtilizationPercent: 70,
77 scaleInCooldown: cdk.Duration.minutes(5),
78 scaleOutCooldown: cdk.Duration.minutes(2),
79});
80
81// Memory-based scaling
82scaling.scaleOnMemoryUtilization('MemoryScaling', {
83 targetUtilizationPercent: 80,
84 scaleInCooldown: cdk.Duration.minutes(5),
85 scaleOutCooldown: cdk.Duration.minutes(2),
86});
Custom WordPress Container
Creating an optimized WordPress container:
1# Dockerfile for optimized WordPress
2FROM wordpress:6.4-php8.2-apache
3
4# Install additional PHP extensions
5RUN apt-get update && apt-get install -y \
6 libmemcached-dev \
7 libzip-dev \
8 libfreetype6-dev \
9 libjpeg62-turbo-dev \
10 libpng-dev \
11 libwebp-dev \
12 && rm -rf /var/lib/apt/lists/*
13
14# Install PHP extensions for better performance
15RUN docker-php-ext-configure gd \
16 --with-freetype \
17 --with-jpeg \
18 --with-webp \
19 && docker-php-ext-install -j$(nproc) gd zip opcache
20
21# Install Redis extension for object caching
22RUN pecl install redis-5.3.7 \
23 && docker-php-ext-enable redis
24
25# Install APCu for additional caching
26RUN pecl install apcu \
27 && docker-php-ext-enable apcu
28
29# Copy custom PHP configuration
30COPY php.ini /usr/local/etc/php/conf.d/wordpress.ini
31
32# Copy custom WordPress configuration
33COPY wp-config-docker.php /usr/src/wordpress/
34
35# Copy custom .htaccess for better performance
36COPY .htaccess /var/www/html/
37
38# Set proper permissions
39RUN chown -R www-data:www-data /var/www/html \
40 && chmod -R 755 /var/www/html
41
42# Health check
43HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
44 CMD curl -f http://localhost/ || exit 1
PHP Optimization Configuration
1; php.ini - Production optimizations
2[PHP]
3memory_limit = 256M
4upload_max_filesize = 32M
5post_max_size = 32M
6max_execution_time = 300
7max_input_vars = 3000
8
9; OPcache configuration
10opcache.enable=1
11opcache.memory_consumption=128
12opcache.interned_strings_buffer=8
13opcache.max_accelerated_files=4000
14opcache.revalidate_freq=2
15opcache.fast_shutdown=1
16opcache.save_comments=1
17
18; APCu configuration
19apc.enabled=1
20apc.shm_size=32M
21apc.ttl=7200
22apc.enable_cli=1
23
24; Session configuration
25session.cookie_httponly=1
26session.cookie_secure=1
27session.use_strict_mode=1
WordPress Configuration
1<?php
2// wp-config-docker.php - Optimized WordPress configuration
3define('DB_NAME', getenv('WORDPRESS_DB_NAME'));
4define('DB_USER', getenv('WORDPRESS_DB_USER'));
5define('DB_PASSWORD', getenv('WORDPRESS_DB_PASSWORD'));
6define('DB_HOST', getenv('WORDPRESS_DB_HOST'));
7define('DB_CHARSET', 'utf8mb4');
8define('DB_COLLATE', '');
9
10// Security keys (should be generated for production)
11define('AUTH_KEY', 'your-auth-key');
12define('SECURE_AUTH_KEY', 'your-secure-auth-key');
13define('LOGGED_IN_KEY', 'your-logged-in-key');
14define('NONCE_KEY', 'your-nonce-key');
15define('AUTH_SALT', 'your-auth-salt');
16define('SECURE_AUTH_SALT', 'your-secure-auth-salt');
17define('LOGGED_IN_SALT', 'your-logged-in-salt');
18define('NONCE_SALT', 'your-nonce-salt');
19
20// WordPress Database Table prefix
21$table_prefix = getenv('WORDPRESS_TABLE_PREFIX') ?: 'wp_';
22
23// Redis Object Cache
24define('WP_REDIS_HOST', 'localhost');
25define('WP_REDIS_PORT', 6379);
26define('WP_REDIS_TIMEOUT', 1);
27define('WP_REDIS_READ_TIMEOUT', 1);
28
29// WordPress debugging
30define('WP_DEBUG', false);
31define('WP_DEBUG_LOG', false);
32define('WP_DEBUG_DISPLAY', false);
33
34// Security enhancements
35define('DISALLOW_FILE_EDIT', true);
36define('DISALLOW_FILE_MODS', true);
37define('FORCE_SSL_ADMIN', true);
38define('WP_POST_REVISIONS', 3);
39define('AUTOSAVE_INTERVAL', 300);
40
41// Performance optimizations
42define('WP_MEMORY_LIMIT', '256M');
43define('WP_MAX_MEMORY_LIMIT', '512M');
44define('COMPRESS_CSS', true);
45define('COMPRESS_SCRIPTS', true);
46define('CONCATENATE_SCRIPTS', true);
47
48// Multisite configuration (if needed)
49// define('WP_ALLOW_MULTISITE', true);
50
51// Custom content directory (if using EFS)
52define('WP_CONTENT_DIR', '/var/www/html/wp-content');
53define('WP_CONTENT_URL', 'http://' . $_SERVER['HTTP_HOST'] . '/wp-content');
54
55// Database repair
56define('WP_ALLOW_REPAIR', false);
57
58if (!defined('ABSPATH')) {
59 define('ABSPATH', __DIR__ . '/');
60}
61
62require_once ABSPATH . 'wp-settings.php';
CDN and Performance Optimization
CloudFront Distribution
Setting up global content delivery:
1// CloudFront Origin Access Control for S3
2const originAccessControl = new cloudfront.OriginAccessControl(this, 'OAC', {
3 description: 'WordPress static content OAC',
4});
5
6// S3 bucket for static assets
7const assetsBucket = new s3.Bucket(this, 'WordPressAssets', {
8 bucketName: `wordpress-assets-${cdk.Aws.ACCOUNT_ID}`,
9 versioned: true,
10 encryption: s3.BucketEncryption.S3_MANAGED,
11 blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
12 removalPolicy: cdk.RemovalPolicy.DESTROY,
13});
14
15// CloudFront distribution
16const distribution = new cloudfront.Distribution(this, 'WordPressDistribution', {
17 defaultRootObject: 'index.php',
18
19 // Origins
20 additionalBehaviors: {
21 // Static assets from S3
22 '/wp-content/uploads/*': {
23 origin: new origins.S3Origin(assetsBucket, {
24 originAccessControl,
25 }),
26 viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
27 cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
28 compress: true,
29 },
30
31 // WordPress admin (no caching)
32 '/wp-admin/*': {
33 origin: new origins.LoadBalancerV2Origin(alb, {
34 protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
35 }),
36 viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
37 cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
38 allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
39 },
40
41 // API endpoints (no caching)
42 '/wp-json/*': {
43 origin: new origins.LoadBalancerV2Origin(alb, {
44 protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
45 }),
46 viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
47 cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
48 allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
49 },
50 },
51
52 // Default behavior for dynamic content
53 defaultBehavior: {
54 origin: new origins.LoadBalancerV2Origin(alb, {
55 protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
56 }),
57 viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
58 cachePolicy: new cloudfront.CachePolicy(this, 'WordPressCachePolicy', {
59 cachePolicyName: 'WordPress-Dynamic-Content',
60 comment: 'Cache policy for WordPress dynamic content',
61 defaultTtl: cdk.Duration.seconds(0),
62 maxTtl: cdk.Duration.days(1),
63 minTtl: cdk.Duration.seconds(0),
64 cookieBehavior: cloudfront.CacheCookieBehavior.allowList(
65 'wordpress_*', 'wp-*', 'comment_*'
66 ),
67 headerBehavior: cloudfront.CacheHeaderBehavior.allowList(
68 'Accept-Encoding', 'CloudFront-Viewer-Country', 'Host'
69 ),
70 queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
71 enableAcceptEncodingGzip: true,
72 enableAcceptEncodingBrotli: true,
73 }),
74 allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
75 compress: true,
76 },
77
78 // Geographic restrictions
79 geoRestriction: cloudfront.GeoRestriction.allowlist('US', 'CA', 'GB', 'DE', 'FR'),
80
81 // Security headers
82 responseHeadersPolicy: new cloudfront.ResponseHeadersPolicy(this, 'SecurityHeaders', {
83 comment: 'Security headers for WordPress',
84 securityHeadersBehavior: {
85 contentTypeOptions: { override: true },
86 frameOptions: { frameOption: cloudfront.HeadersFrameOption.DENY, override: true },
87 referrerPolicy: { referrerPolicy: cloudfront.HeadersReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, override: true },
88 strictTransportSecurity: {
89 accessControlMaxAge: cdk.Duration.seconds(31536000),
90 includeSubdomains: true,
91 preload: true,
92 override: true,
93 },
94 },
95 }),
96});
Architecture Tradeoffs Analysis
ECS Fargate vs EC2 Container Service
Advantages of Fargate:
- Serverless containers: No EC2 instance management
- Auto-scaling: Scales based on demand automatically
- Cost efficiency: Pay only for running containers
- Security: AWS manages underlying infrastructure
Disadvantages:
- Cost at scale: More expensive than EC2 for consistent workloads
- Limited customization: Less control over underlying OS
- Cold starts: Slight delay when scaling up
- Storage limitations: EFS required for persistent storage
1// Cost comparison for different deployment options
2const costAnalysis = {
3 // Fargate (2 vCPU, 4GB RAM, running 24/7)
4 fargate: {
5 monthlyHours: 24 * 30,
6 vCpuCostPerHour: 0.04048,
7 memoryCostPerHour: 0.004445,
8 monthlyCost: (24 * 30) * (2 * 0.04048 + 4 * 0.004445), // ~$90/month
9 },
10
11 // EC2 with ECS (t3.medium)
12 ec2: {
13 instanceCost: 41.61, // t3.medium monthly
14 storageGost: 10, // EBS storage
15 monthlyCost: 51.61, // ~$52/month
16 },
17
18 // Managed services (WordPress.com, WP Engine)
19 managed: {
20 basicPlan: 25, // Basic plan
21 businessPlan: 96, // Business plan with CDN
22 enterprisePlan: 450, // Enterprise plan
23 }
24};
EFS vs S3 for File Storage
Why EFS for WordPress:
1// EFS advantages for WordPress file storage
2const efsAdvantages = {
3 fileSystem: 'POSIX-compliant file system interface',
4 concurrency: 'Multiple containers can read/write simultaneously',
5 performance: 'Low-latency access for frequent file operations',
6 plugins: 'WordPress plugins expect traditional file system',
7 uploads: 'Direct file uploads without additional processing'
8};
9
10// S3 alternative considerations
11const s3Alternative = {
12 cost: 'Lower storage costs for large files',
13 durability: '99.999999999% (11 9s) durability',
14 cdn: 'Native CloudFront integration',
15 limitations: 'Requires plugin for WordPress integration'
16};
RDS vs Self-managed MySQL
RDS MySQL Benefits:
- Automated backups and point-in-time recovery
- Multi-AZ deployment for high availability
- Automated patching and maintenance
- Performance Insights for monitoring
- Read replicas for read scaling
Cost vs Feature Comparison:
1-- RDS pricing (us-east-1, t3.micro)
2-- Instance: $0.017/hour = ~$12/month
3-- Storage: $0.115/GB/month
4-- Backup: Free for backup retention ≤ DB size
5
6-- Self-managed alternative
7-- EC2 t3.micro: $8.5/month
8-- EBS storage: $10/month
9-- Manual maintenance: Engineering time
10-- Total: ~$18.5/month + operational overhead
Performance Optimization Strategies
Database Optimization
1-- WordPress-specific MySQL optimizations
2-- my.cnf configuration for RDS parameter group
3
4[mysqld]
5# InnoDB settings for WordPress
6innodb_buffer_pool_size = 512M
7innodb_buffer_pool_instances = 2
8innodb_flush_log_at_trx_commit = 2
9innodb_flush_method = O_DIRECT
10innodb_log_file_size = 128M
11
12# Query cache (MySQL 5.7 and earlier)
13query_cache_type = 1
14query_cache_size = 64M
15query_cache_limit = 2M
16
17# Connection settings
18max_connections = 100
19max_connect_errors = 10000
20
21# Slow query logging
22slow_query_log = 1
23long_query_time = 2
24
25# WordPress-specific optimizations
26tmp_table_size = 64M
27max_heap_table_size = 64M
28join_buffer_size = 1M
29sort_buffer_size = 2M
Container Performance Tuning
1// Task definition with performance optimizations
2const optimizedTaskDefinition = new ecs.FargateTaskDefinition(this, 'OptimizedWordPressTask', {
3 memoryLimitMiB: 4096, // Increased memory
4 cpu: 2048, // Increased CPU
5
6 // Platform version for better performance
7 platformVersion: ecs.FargatePlatformVersion.LATEST,
8
9 // Task role for AWS service access
10 taskRole: taskRole,
11 executionRole: executionRole,
12});
13
14// WordPress container with resource limits
15const optimizedContainer = optimizedTaskDefinition.addContainer('wordpress', {
16 image: ecs.ContainerImage.fromRegistry('your-account.dkr.ecr.region.amazonaws.com/wordpress:optimized'),
17 memoryReservationMiB: 3072,
18 memoryLimitMiB: 4096,
19
20 // Resource allocation
21 cpu: 1536,
22
23 // Environment variables for performance
24 environment: {
25 // PHP-FPM optimizations
26 PHP_FPM_PM: 'dynamic',
27 PHP_FPM_PM_MAX_CHILDREN: '50',
28 PHP_FPM_PM_START_SERVERS: '10',
29 PHP_FPM_PM_MIN_SPARE_SERVERS: '5',
30 PHP_FPM_PM_MAX_SPARE_SERVERS: '20',
31 PHP_FPM_PM_MAX_REQUESTS: '1000',
32
33 // Apache optimizations
34 APACHE_MAX_REQUEST_WORKERS: '400',
35 APACHE_SERVER_LIMIT: '16',
36 APACHE_THREAD_LIMIT: '25',
37 APACHE_THREADS_PER_CHILD: '25',
38 },
39
40 // Health check
41 healthCheck: {
42 command: ['CMD-SHELL', 'curl -f http://localhost/wp-admin/admin-ajax.php?action=health || exit 1'],
43 interval: cdk.Duration.seconds(30),
44 timeout: cdk.Duration.seconds(5),
45 retries: 3,
46 startPeriod: cdk.Duration.seconds(60),
47 },
48});
Security Implementation
Network Security
1// Security group configurations with least privilege
2const albSecurityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
3 vpc,
4 description: 'Security group for Application Load Balancer',
5 allowAllOutbound: false,
6});
7
8// Allow HTTP and HTTPS traffic
9albSecurityGroup.addIngressRule(
10 ec2.Peer.anyIpv4(),
11 ec2.Port.tcp(80),
12 'Allow HTTP traffic'
13);
14albSecurityGroup.addIngressRule(
15 ec2.Peer.anyIpv4(),
16 ec2.Port.tcp(443),
17 'Allow HTTPS traffic'
18);
19
20// Allow outbound to ECS tasks
21albSecurityGroup.addEgressRule(
22 ecsSecurityGroup,
23 ec2.Port.tcp(80),
24 'Allow outbound to ECS tasks'
25);
26
27// ECS security group - only allow traffic from ALB
28ecsSecurityGroup.addIngressRule(
29 albSecurityGroup,
30 ec2.Port.tcp(80),
31 'Allow traffic from ALB'
32);
33
34// Allow outbound for database and EFS access
35ecsSecurityGroup.addEgressRule(
36 rdsSecurityGroup,
37 ec2.Port.tcp(3306),
38 'Allow database access'
39);
40ecsSecurityGroup.addEgressRule(
41 efsSecurityGroup,
42 ec2.Port.tcp(2049),
43 'Allow EFS access'
44);
Application Security
1// wp-config.php security enhancements
2// Disable file editing
3define('DISALLOW_FILE_EDIT', true);
4define('DISALLOW_FILE_MODS', true);
5
6// Security keys (use AWS Secrets Manager in production)
7define('WP_DEBUG', false);
8define('WP_DEBUG_LOG', false);
9define('WP_DEBUG_DISPLAY', false);
10
11// Database security
12define('DB_CHARSET', 'utf8mb4');
13define('DB_COLLATE', 'utf8mb4_unicode_ci');
14
15// Force SSL
16define('FORCE_SSL_ADMIN', true);
17if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
18 $_SERVER['HTTPS'] = 'on';
19}
20
21// Limit post revisions
22define('WP_POST_REVISIONS', 3);
23
24// Increase memory limit securely
25define('WP_MEMORY_LIMIT', '256M');
26
27// Hide WordPress version
28function remove_wp_version() {
29 return '';
30}
31add_filter('the_generator', 'remove_wp_version');
32
33// Disable XML-RPC
34add_filter('xmlrpc_enabled', '__return_false');
35
36// Security headers
37function add_security_headers() {
38 header('X-Content-Type-Options: nosniff');
39 header('X-Frame-Options: DENY');
40 header('X-XSS-Protection: 1; mode=block');
41 header('Referrer-Policy: strict-origin-when-cross-origin');
42 header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
43}
44add_action('send_headers', 'add_security_headers');
Monitoring and Observability
CloudWatch Metrics and Alarms
1// Custom metrics for WordPress performance
2const wordpressMetrics = {
3 // Application Load Balancer metrics
4 responseTime: new cloudwatch.Metric({
5 namespace: 'AWS/ApplicationELB',
6 metricName: 'TargetResponseTime',
7 dimensionsMap: {
8 LoadBalancer: alb.loadBalancerFullName,
9 },
10 statistic: 'Average',
11 }),
12
13 // ECS Service metrics
14 cpuUtilization: new cloudwatch.Metric({
15 namespace: 'AWS/ECS',
16 metricName: 'CPUUtilization',
17 dimensionsMap: {
18 ServiceName: service.serviceName,
19 ClusterName: cluster.clusterName,
20 },
21 statistic: 'Average',
22 }),
23
24 // RDS metrics
25 databaseConnections: new cloudwatch.Metric({
26 namespace: 'AWS/RDS',
27 metricName: 'DatabaseConnections',
28 dimensionsMap: {
29 DBInstanceIdentifier: database.instanceIdentifier,
30 },
31 statistic: 'Average',
32 }),
33};
34
35// CloudWatch Alarms
36const highResponseTimeAlarm = new cloudwatch.Alarm(this, 'HighResponseTime', {
37 metric: wordpressMetrics.responseTime,
38 threshold: 2.0,
39 evaluationPeriods: 2,
40 comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
41 alarmDescription: 'WordPress response time is too high',
42});
43
44const highCpuAlarm = new cloudwatch.Alarm(this, 'HighCPUUtilization', {
45 metric: wordpressMetrics.cpuUtilization,
46 threshold: 80,
47 evaluationPeriods: 2,
48 comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
49 alarmDescription: 'ECS tasks CPU utilization is high',
50});
51
52const highDbConnectionsAlarm = new cloudwatch.Alarm(this, 'HighDatabaseConnections', {
53 metric: wordpressMetrics.databaseConnections,
54 threshold: 80,
55 evaluationPeriods: 2,
56 comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
57 alarmDescription: 'Database connection count is high',
58});
Application Performance Monitoring
1// X-Ray tracing for request tracking
2const xrayRole = new iam.Role(this, 'XRayTaskRole', {
3 assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
4 managedPolicies: [
5 iam.ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess'),
6 ],
7});
8
9// Add X-Ray daemon sidecar container
10const xrayContainer = taskDefinition.addContainer('xray-daemon', {
11 image: ecs.ContainerImage.fromRegistry('amazon/aws-xray-daemon:latest'),
12 memoryReservationMiB: 32,
13 cpu: 32,
14 essential: false,
15
16 environment: {
17 AWS_REGION: cdk.Aws.REGION,
18 },
19
20 logging: ecs.LogDrivers.awsLogs({
21 streamPrefix: 'xray',
22 logRetention: logs.RetentionDays.ONE_WEEK,
23 }),
24});
25
26xrayContainer.addPortMappings({
27 containerPort: 2000,
28 protocol: ecs.Protocol.UDP,
29});
Backup and Disaster Recovery
Automated Backup Strategy
1// RDS automated backups
2const backupVault = new backup.BackupVault(this, 'WordPressBackupVault', {
3 backupVaultName: 'wordpress-backup-vault',
4 encryptionKey: kms.Alias.fromAliasName(this, 'BackupKey', 'alias/aws/backup'),
5});
6
7// Backup plan for RDS
8const backupPlan = new backup.BackupPlan(this, 'WordPressBackupPlan', {
9 backupPlanName: 'wordpress-backup-plan',
10 backupPlanRules: [
11 new backup.BackupPlanRule({
12 ruleName: 'daily-backup',
13 targets: [
14 new backup.BackupTarget({
15 backupResource: backup.BackupResource.fromRdsDatabase(database),
16 }),
17 ],
18 scheduleExpression: events.Schedule.cron({ hour: '2', minute: '0' }),
19 deleteAfter: cdk.Duration.days(30),
20 moveToColdStorageAfter: cdk.Duration.days(7),
21 }),
22 new backup.BackupPlanRule({
23 ruleName: 'weekly-backup',
24 targets: [
25 new backup.BackupTarget({
26 backupResource: backup.BackupResource.fromRdsDatabase(database),
27 }),
28 ],
29 scheduleExpression: events.Schedule.cron({ weekDay: '1', hour: '1', minute: '0' }),
30 deleteAfter: cdk.Duration.days(90),
31 moveToColdStorageAfter: cdk.Duration.days(30),
32 }),
33 ],
34});
35
36// EFS backup
37const efsBackup = new backup.BackupPlanRule({
38 ruleName: 'efs-daily-backup',
39 targets: [
40 new backup.BackupTarget({
41 backupResource: backup.BackupResource.fromEfsFileSystem(fileSystem),
42 }),
43 ],
44 scheduleExpression: events.Schedule.cron({ hour: '3', minute: '0' }),
45 deleteAfter: cdk.Duration.days(14),
46});
Cost Optimization Strategies
Resource Right-sizing
1// Cost optimization based on traffic patterns
2const costOptimizedConfig = {
3 // Development environment
4 development: {
5 rds: {
6 instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
7 multiAz: false,
8 backupRetention: cdk.Duration.days(1),
9 },
10 ecs: {
11 cpu: 512,
12 memory: 1024,
13 desiredCount: 1,
14 minCapacity: 1,
15 maxCapacity: 2,
16 },
17 },
18
19 // Production environment
20 production: {
21 rds: {
22 instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL),
23 multiAz: true,
24 backupRetention: cdk.Duration.days(7),
25 },
26 ecs: {
27 cpu: 1024,
28 memory: 2048,
29 desiredCount: 2,
30 minCapacity: 2,
31 maxCapacity: 10,
32 },
33 },
34};
35
36// Scheduled scaling for predictable traffic patterns
37const scheduledScaling = service.autoScaleTaskCount({
38 minCapacity: 1,
39 maxCapacity: 10,
40});
41
42// Scale up during business hours
43scheduledScaling.scaleOnSchedule('ScaleUpMorning', {
44 schedule: autoscaling.Schedule.cron({ hour: '8', minute: '0' }),
45 minCapacity: 3,
46 maxCapacity: 10,
47});
48
49// Scale down during off hours
50scheduledScaling.scaleOnSchedule('ScaleDownEvening', {
51 schedule: autoscaling.Schedule.cron({ hour: '20', minute: '0' }),
52 minCapacity: 1,
53 maxCapacity: 5,
54});
Cost Monitoring
1// Budget alerts for cost control
2const budget = new budgets.CfnBudget(this, 'WordPressBudget', {
3 budget: {
4 budgetName: 'wordpress-monthly-budget',
5 budgetLimit: {
6 amount: 100,
7 unit: 'USD',
8 },
9 timeUnit: 'MONTHLY',
10 budgetType: 'COST',
11 costFilters: {
12 service: ['Amazon Elastic Container Service', 'Amazon Relational Database Service'],
13 },
14 },
15 notificationsWithSubscribers: [
16 {
17 notification: {
18 notificationType: 'ACTUAL',
19 comparisonOperator: 'GREATER_THAN',
20 threshold: 80,
21 },
22 subscribers: [
23 {
24 subscriptionType: 'EMAIL',
25 address: 'devops@company.com',
26 },
27 ],
28 },
29 ],
30});
Deployment and CI/CD Pipeline
Multi-environment Deployment
1// Environment-specific configurations
2interface EnvironmentConfig {
3 stackName: string;
4 rdsInstanceType: ec2.InstanceType;
5 ecsTaskCount: number;
6 enableMultiAz: boolean;
7 backupRetentionDays: number;
8}
9
10const environments: { [key: string]: EnvironmentConfig } = {
11 dev: {
12 stackName: 'wordpress-dev',
13 rdsInstanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
14 ecsTaskCount: 1,
15 enableMultiAz: false,
16 backupRetentionDays: 1,
17 },
18 staging: {
19 stackName: 'wordpress-staging',
20 rdsInstanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.SMALL),
21 ecsTaskCount: 2,
22 enableMultiAz: true,
23 backupRetentionDays: 3,
24 },
25 prod: {
26 stackName: 'wordpress-prod',
27 rdsInstanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
28 ecsTaskCount: 3,
29 enableMultiAz: true,
30 backupRetentionDays: 7,
31 },
32};
33
34// Deploy stack for each environment
35Object.entries(environments).forEach(([env, config]) => {
36 new EcsWordpressStack(app, config.stackName, {
37 env: { region: 'us-east-1' },
38 config,
39 });
40});
CI/CD Pipeline with CodePipeline
1// CI/CD Pipeline
2const pipeline = new codepipeline.Pipeline(this, 'WordPressPipeline', {
3 pipelineName: 'wordpress-ecs-pipeline',
4 stages: [
5 {
6 stageName: 'Source',
7 actions: [
8 new codepipeline_actions.GitHubSourceAction({
9 actionName: 'Source',
10 owner: 'yennanliu',
11 repo: 'cdk-playground',
12 branch: 'main',
13 oauthToken: cdk.SecretValue.secretsManager('github-token'),
14 output: sourceArtifact,
15 }),
16 ],
17 },
18 {
19 stageName: 'Build',
20 actions: [
21 new codepipeline_actions.CodeBuildAction({
22 actionName: 'Build',
23 project: new codebuild.Project(this, 'BuildProject', {
24 buildSpec: codebuild.BuildSpec.fromObject({
25 version: '0.2',
26 phases: {
27 pre_build: {
28 commands: [
29 'echo Logging in to Amazon ECR...',
30 'aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com',
31 ],
32 },
33 build: {
34 commands: [
35 'echo Build started on `date`',
36 'echo Building the Docker image...',
37 'cd ecs-wordpress-2',
38 'docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .',
39 'docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG',
40 ],
41 },
42 post_build: {
43 commands: [
44 'echo Build completed on `date`',
45 'echo Pushing the Docker image...',
46 'docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG',
47 ],
48 },
49 },
50 }),
51 environment: {
52 buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
53 privileged: true,
54 },
55 }),
56 input: sourceArtifact,
57 }),
58 ],
59 },
60 {
61 stageName: 'Deploy',
62 actions: [
63 new codepipeline_actions.EcsDeployAction({
64 actionName: 'Deploy',
65 service: service,
66 input: buildArtifact,
67 }),
68 ],
69 },
70 ],
71});
Lessons Learned and Best Practices
1. Container Design
1# Multi-stage build for optimized images
2FROM wordpress:6.4-php8.2-apache AS base
3
4# Development stage
5FROM base AS development
6RUN apt-get update && apt-get install -y \
7 xdebug \
8 && docker-php-ext-enable xdebug
9
10# Production stage
11FROM base AS production
12# Remove unnecessary packages and files
13RUN apt-get autoremove -y \
14 && apt-get clean \
15 && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
16
17# Copy production-only configurations
18COPY --from=development /usr/local/etc/php/conf.d/production.ini /usr/local/etc/php/conf.d/
2. Database Connection Management
1// WordPress database connection optimization
2// wp-config.php additions
3define('WP_ALLOW_REPAIR', false);
4
5// Connection pooling for high traffic
6define('DB_CONNECTION_TIMEOUT', 5);
7define('DB_RETRY_INTERVAL', 1);
8
9// Custom database error handling
10function custom_db_error_handler($wp_error) {
11 error_log('WordPress database error: ' . $wp_error->get_error_message());
12
13 // Implement exponential backoff for connection retries
14 $retry_count = get_transient('db_retry_count') ?: 0;
15 $backoff_time = min(pow(2, $retry_count), 60); // Max 60 seconds
16
17 if ($retry_count < 5) {
18 set_transient('db_retry_count', $retry_count + 1, $backoff_time);
19 sleep($backoff_time);
20 return true; // Retry connection
21 }
22
23 return false; // Give up
24}
3. Monitoring and Alerting
1// Comprehensive monitoring dashboard
2const dashboard = new cloudwatch.Dashboard(this, 'WordPressDashboard', {
3 dashboardName: 'WordPress-ECS-Performance',
4 widgets: [
5 [
6 new cloudwatch.GraphWidget({
7 title: 'ECS Service Metrics',
8 left: [wordpressMetrics.cpuUtilization, wordpressMetrics.memoryUtilization],
9 right: [service.metricTaskCount()],
10 }),
11 ],
12 [
13 new cloudwatch.GraphWidget({
14 title: 'Application Load Balancer',
15 left: [wordpressMetrics.responseTime, alb.metricRequestCount()],
16 right: [alb.metricHttpCodeTarget(elbv2.HttpCodeTarget.TARGET_2XX_COUNT)],
17 }),
18 ],
19 [
20 new cloudwatch.GraphWidget({
21 title: 'RDS Performance',
22 left: [wordpressMetrics.databaseConnections, database.metricCPUUtilization()],
23 right: [database.metricReadLatency(), database.metricWriteLatency()],
24 }),
25 ],
26 ],
27});
Conclusion
Building a scalable WordPress platform on AWS ECS Fargate provides significant advantages for content-heavy applications:
- Automatic scaling handles traffic fluctuations without manual intervention
- Container isolation improves security and resource utilization
- Managed services reduce operational overhead
- High availability through multi-AZ deployments
- Cost efficiency with pay-per-use pricing models
Key takeaways from this implementation:
- Container Optimization: Design lightweight, secure containers with proper resource allocation
- Storage Strategy: Use EFS for shared WordPress files and RDS for reliable database operations
- Security First: Implement network isolation, encryption, and least-privilege access
- Monitor Everything: Comprehensive observability is crucial for production WordPress deployments
- Cost Management: Right-size resources and implement scheduled scaling for predictable traffic patterns
The complete implementation is available in the CDK playground repository, demonstrating how modern containerization approaches can transform traditional WordPress hosting into a scalable, enterprise-grade platform.
Whether you’re migrating from traditional hosting or building a new WordPress platform, the patterns demonstrated here provide a solid foundation for scalable, secure, and cost-effective content management systems on AWS.