Production-ready, modular WordPress deployment on AWS using Terraform
This repository provides production-tested Terraform modules for deploying highly available WordPress infrastructure on AWS. The architecture follows AWS Well-Architected Framework principles with emphasis on security, scalability, and cost optimization.
- High Availability: Multi-AZ deployment with Auto Scaling Groups (2-10 instances)
- Load Balancing: Application Load Balancer with health checks and session stickiness
- Modern Stack: PHP 8.2, Apache 2.4, WordPress latest, MySQL 8.0 (RDS)
- Security: VPC isolation, security groups, encrypted RDS, IAM best practices
- Scalability: CloudWatch-based auto-scaling on CPU metrics
- Monitoring: CloudWatch alarms for CPU, health checks, and scaling events
- Portability: No hardcoded values, fully parameterized configuration
┌─────────────────────────────────────────────────────────────┐
│ AWS Cloud │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ VPC (10.x.0.0/16) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │Public Subnet │ │Public Subnet │ │Private Sub. │ │ │
│ │ │ AZ-1 │ │ AZ-2 │ │ AZ-1/2 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌─────────┐ │ │ │
│ │ │ │ ALB │◄─┼──┼──│ EC2 │ │ │ │ RDS │ │ │ │
│ │ │ │ │ │ │ │ ASG │──┼──┼─│ MySQL │ │ │ │
│ │ │ └────────┘ │ │ │ (2-10) │ │ │ │Multi-AZ │ │ │ │
│ │ │ │ │ └────────┘ │ │ └─────────┘ │ │ │
│ │ └──────────────┘ └──────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ Internet Gateway ──► NAT Gateway ──► Route Tables │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Module | Resources | Purpose |
|---|---|---|
| VPC | VPC, Subnets, IGW, Route Tables | Network isolation with public/private topology |
| Security | Security Groups, Key Pairs | Firewall rules for web, SSH, and database access |
| Database | RDS MySQL, Subnet Groups | Managed database with Multi-AZ, backups, encryption |
| Compute | ALB, ASG, Launch Template, CloudWatch | Auto-scaling WordPress instances with load balancing |
Before you begin, ensure you have:
- AWS Account with administrative or power user permissions
- Terraform >= 1.5.0 (Download)
- AWS CLI configured with credentials (Setup Guide)
- SSH Key Pair (optional, for EC2 access)
- S3 Bucket for Terraform state (recommended for production)
Your IAM user/role needs permissions for:
- EC2 (instances, security groups, launch templates)
- VPC (subnets, route tables, internet gateways)
- RDS (database instances, subnet groups)
- ELB (application load balancers, target groups)
- Auto Scaling (groups, policies)
- CloudWatch (alarms, metrics)
git clone <your-repository-url>
cd terraform-wordpress-modules/environments/prodCreate an S3 bucket for state storage:
aws s3 mb s3://your-terraform-state-bucket-2025
aws dynamodb create-table \
--table-name terraform-state-locks \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUESTUpdate main.tf backend configuration with your bucket name.
cp terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars:
# Basic Configuration
aws_region = "us-east-1"
project_name = "my-site"
environment = "production"
# Network Access
allowed_ssh_cidrs = ["YOUR_IP/32"] # Replace with your IP
# SSH Key (generate with: ssh-keygen -t ed25519)
create_key_pair = true
public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..."
# Database Configuration
db_name = "wordpress_prod"
db_username = "wp_admin"
db_password = "CHANGE_THIS_STRONG_PASSWORD_123!"
# WordPress Admin
wp_title = "My Production Site"
wp_admin = "admin"
wp_password = "CHANGE_THIS_STRONG_PASSWORD_456!"
wp_email = "admin@yourdomain.com"
# Cost Management
cost_center = "prod-web-001"
owner_email = "devops@yourdomain.com"# Initialize Terraform and download providers
terraform init
# Preview changes
terraform plan
# Deploy infrastructure
terraform apply
# Get WordPress URL
terraform output website_url- Open the URL from
terraform output website_url - You'll be redirected to
/wp-admin/install.php - Complete the 5-minute installation wizard:
- Site Title: (from wp_title)
- Username: (from wp_admin)
- Password: (from wp_password)
- Email: (from wp_email)
- Click "Install WordPress"
- Log in and start building your site
Control your infrastructure size and scaling behavior:
# Instance Configuration
instance_type = "t3.medium" # t3.small, t3.medium, t3.large
min_size = 2 # Minimum instances (HA requires 2+)
max_size = 10 # Maximum instances
desired_capacity = 3 # Initial instance count
# Database Configuration
db_instance_class = "db.t3.medium" # db.t3.small, db.r5.large, etc.
db_allocated_storage = 100 # GBThe infrastructure includes automatic scaling:
- Scale Up: When average CPU > 70% for 4 minutes
- Scale Down: When average CPU < 20% for 4 minutes
- Cooldown: 5 minutes between scaling actions
- Health Check: 5-minute grace period for new instances
- Use AWS Secrets Manager for passwords (not terraform.tfvars)
- Enable MFA on AWS account
- Restrict
allowed_ssh_cidrsto specific IPs or use bastion host - Enable VPC Flow Logs for network monitoring
- Use SSL/TLS certificates (add HTTPS listener to ALB)
- Enable RDS encryption at rest (configured by default)
- Regular security updates via Auto Scaling Group refresh
- Implement AWS WAF for application-level protection
- Use CloudTrail for API auditing
Store sensitive values securely:
# Store database password
aws secretsmanager create-secret \
--name prod/wordpress/db_password \
--secret-string "your-secure-password"
# Store WordPress admin password
aws secretsmanager create-secret \
--name prod/wordpress/wp_password \
--secret-string "your-secure-password"Update main.tf to reference secrets:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/wordpress/db_password"
}
module "wordpress" {
# ...
db_password = data.aws_secretsmanager_secret_version.db_password.secret_string
}- High CPU Usage: Triggers when CPU > 70% (scale up)
- Low CPU Usage: Triggers when CPU < 20% (scale down)
- Unhealthy Targets: Alerts when instances fail health checks
- Database Connections: (optional) Monitor RDS connection limits
# Instance console output
aws ec2 get-console-output --instance-id i-xxxxx --output text
# Application logs (requires SSH access)
ssh -i your-key.pem ec2-user@INSTANCE_IP
sudo tail -f /var/log/httpd/error_log
sudo tail -f /var/log/user-data.log# Trigger rolling update of all instances
terraform apply -replace="module.wordpress.module.compute.aws_launch_template.wordpress"
# Or use AWS CLI
aws autoscaling start-instance-refresh \
--auto-scaling-group-name my-site-prod-asg \
--preferences MinHealthyPercentage=50RDS automated backups are enabled by default (7-day retention):
# Create manual snapshot
aws rds create-db-snapshot \
--db-instance-identifier my-site-prod-database \
--db-snapshot-identifier my-site-manual-backup-$(date +%Y%m%d)
# Restore from snapshot (requires new DB instance)
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier my-site-restored \
--db-snapshot-identifier my-site-manual-backup-20251003To reset WordPress (for testing or reinstallation):
# Get database endpoint
DB_ENDPOINT=$(terraform output -raw database_endpoint)
# Connect and clear
mysql -h $DB_ENDPOINT -u wp_admin -p << EOF
DROP DATABASE IF EXISTS wordpress_prod;
CREATE DATABASE wordpress_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
EOF| Resource | Type | Quantity | Est. Cost/Month |
|---|---|---|---|
| EC2 Instances | t3.medium | 2-3 avg | $60-90 |
| Application Load Balancer | - | 1 | $25 |
| RDS MySQL | db.t3.medium | 1 Multi-AZ | $120 |
| EBS Storage | gp3 | 100 GB | $10 |
| Data Transfer | Outbound | ~100 GB | $9 |
| Total | ~$224-254/month |
Prices based on us-east-1 region, subject to change
# Development/Testing: Reduce instance sizes
instance_type = "t3.small" # ~$15/month per instance
db_instance_class = "db.t3.small" # ~$30/month (Single-AZ)
min_size = 1
max_size = 2
desired_capacity = 1
# Enable auto-shutdown for non-prod
# Use AWS Instance Scheduler or LambdaSet up AWS Budget alerts:
aws budgets create-budget \
--account-id YOUR_ACCOUNT_ID \
--budget file://budget-config.json \
--notifications-with-subscribers file://notifications.json# Backup database
aws rds create-db-snapshot \
--db-instance-identifier $(terraform output -raw database_endpoint | cut -d: -f1) \
--db-snapshot-identifier final-backup-$(date +%Y%m%d-%H%M)
# Export WordPress files (requires access to EC2 instance)
# Files are lost when instances terminate - consider EFS for shared storage# Review what will be deleted
terraform plan -destroy
# Destroy all resources
terraform destroy
# Confirm by typing: yes# If using local state
rm -rf .terraform
rm terraform.tfstate*
rm .terraform.lock.hcl
# If using S3 backend, optionally delete state bucket
aws s3 rb s3://your-terraform-state-bucket-2025 --force
aws dynamodb delete-table --table-name terraform-state-locksCheck for remaining resources:
# Check EC2 instances
aws ec2 describe-instances --filters "Name=tag:Project,Values=my-site" \
--query "Reservations[].Instances[].[InstanceId,State.Name]"
# Check RDS instances
aws rds describe-db-instances --query "DBInstances[].[DBInstanceIdentifier,DBInstanceStatus]"
# Check load balancers
aws elbv2 describe-load-balancers --query "LoadBalancers[].[LoadBalancerName,State.Code]"
# Check VPCs
aws ec2 describe-vpcs --filters "Name=tag:Project,Values=my-site" \
--query "Vpcs[].[VpcId,Tags[?Key=='Name'].Value|[0]]"# Check for any remaining charges
aws ce get-cost-and-usage \
--time-period Start=2025-10-01,End=2025-10-31 \
--granularity MONTHLY \
--metrics "UnblendedCost" \
--group-by Type=DIMENSION,Key=SERVICEProblem: terraform destroy fails with dependency errors
Solution:
# Manually terminate instances first
aws autoscaling set-desired-capacity --auto-scaling-group-name my-site-prod-asg --desired-capacity 0
sleep 60
# Then retry destroy
terraform destroyProblem: RDS instance in "deleting" state
Solution: RDS deletion can take 5-10 minutes. Final snapshot is created automatically (unless disabled).
Problem: VPC won't delete due to ENIs
Solution: Wait 5 minutes for ENIs to detach, or manually delete network interfaces in the AWS Console.
# Check target health
aws elbv2 describe-target-health --target-group-arn <TG_ARN>
# Check instance console output
INSTANCE_ID=$(aws ec2 describe-instances --filters "Name=tag:aws:autoscaling:groupName,Values=my-site-prod-asg" --query "Reservations[0].Instances[0].InstanceId" --output text)
aws ec2 get-console-output --instance-id $INSTANCE_ID --output text | tail -100Common causes:
- PHP Version: Ensure PHP 8.2 is installed (not PHP 5.4)
- Database Connection: Verify RDS endpoint and credentials
- File Permissions: Check apache:apache ownership
- wp-config.php: Validate syntax with
php -l wp-config.php
# Test database connectivity from instance
mysql -h DATABASE_ENDPOINT -u wp_admin -p -e "SELECT 1;"
# Check security group rules
aws ec2 describe-security-groups --group-ids <DB_SG_ID>- Request SSL certificate in AWS Certificate Manager
- Add HTTPS listener to ALB:
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.wordpress.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = "arn:aws:acm:region:account:certificate/xxx"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.wordpress.arn
}
}Distribute static assets globally:
resource "aws_cloudfront_distribution" "wordpress" {
origin {
domain_name = aws_lb.wordpress.dns_name
origin_id = "wordpress-alb"
}
# ... additional CloudFront configuration
}Use EFS for wp-content/uploads across instances:
resource "aws_efs_file_system" "wordpress" {
encrypted = true
tags = { Name = "wordpress-content" }
}Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/improvement) - Test changes in a dev environment
- Update documentation
- Submit a pull request
This project is licensed under the MIT License - see LICENSE file for details.
- Issues: GitHub Issues
- Documentation: AWS Well-Architected Framework
- Terraform: AWS Provider Docs
- WordPress: WordPress Codex
💰 Cost Awareness: This infrastructure incurs ongoing AWS charges. Always run terraform destroy when testing is complete to avoid unnecessary costs.
Author: Manuel Bauka