This project deploys a production-ready WordPress application on AWS using Terraform. It provisions a WordPress EC2 instance in a private subnet, accessible publicly via an Application Load Balancer (ALB), with an RDS MySQL database and an S3 bucket for media offloading. A bastion host (toggleable via a boolean variable) secures SSH access to the EC2 instance and RDS database administration. Terraform state is managed remotely in an S3 bucket with DynamoDB locking for team collaboration.
- Terraform: Infrastructure as code
- AWS: EC2, RDS, S3, ALB, IAM, VPC
- Docker: Runs WordPress on the EC2 instance
- Ubuntu: EC2 operating system
- MySQL: Database via RDS
- Terraform CLI installed (terraform command available).
- AWS CLI installed and configured with valid credentials (~/.aws/credentials).
- An AWS account with permissions to create S3 buckets, DynamoDB tables, EC2 instances, ALB, RDS instances and IAM roles/policies.
- SSH key pair uploaded to AWS
-
backend-setup/: Terraform configuration to create the S3 bucket and DynamoDB table for remote state storage and locking.
- providers.tf: AWS provider configuration.
- main.tf: Defines the S3 bucket (with versioning and encryption), DynamoDB table.
-
main-project/: Terraform configuration for the WordPress application.
- providers.tf: AWS provider configuration.
- versions.tf: Terraform and provider version configuration.
- backend.tf: Configures the remote S3 backend (added after backend setup).
- main.tf: Orchestrates VPC, EC2, RDS, S3, and ALB modules.
- variables.tf: (Optional) Defines variables (e.g., enable_bastion, ips, db_username).
- terraform.tfvars: (Optional) Sets variable values.
- modules/vpc/: Custom VPC with public and private subnets.
- modules/ec2/: EC2 instance for WordPress (private subnet) and optional bastion host (public subnet).
- modules/rds/: MySQL RDS instance in private subnet.
- modules/s3/: S3 bucket for media offloading with IAM roles.
- modules/alb/: Application Load Balancer for public WordPress access.
The backend infrastructure (S3 bucket and DynamoDB table) must be created first and managed with a local state.
Navigate to the backend-setup/ directory:
cd backend-setup/Initialize Terraform:
terraform initReview the plan:
terraform planApply the configuration to create the S3 bucket and DynamoDB table:
terraform applyConfirm by typing yes. This creates the S3 bucket (sd-tfstate-bucket) and DynamoDB table (sd-terraform-state-lock). The state is stored locally in terraform.tfstate.
The main project will be initialized locally before configuring the remote backend.
Navigate to the main-project/ directory:
cd main-project/Ensure there is no backend.tf file yet to avoid remote backend errors.
Initialize Terraform:
terraform init(Optional) Review the plan (do not apply yet):
terraform planAfter creating the backend infrastructure, configure the main project to use the remote S3 backend with DynamoDB locking.
Add the backend.tf file to main-project/ with the following content:
terraform {
backend "s3" {
bucket = "sd-tfstate-bucket"
key = "main-project/terraform.tfstate"
region = "eu-central-1" # Replace with your AWS region
dynamodb_table = "sd-terraform-state-lock"
encrypt = true
}
}Re-initialize Terraform to migrate the local state to the S3 bucket:
terraform init -migrate-stateConfirm the migration by typing yes. This moves the local state to the S3 bucket and enables DynamoDB locking.
With the remote backend configured, deploy the WordPress infrastructure.
Review the plan:
terraform planApply the configuration to create the EC2 instance, RDS MySQL database, S3 media bucket, and IAM roles:
terraform applyConfirm by typing yes. The state is now stored in the S3 bucket, with locking managed by DynamoDB.
Access WordPress via the ALB URL (http://<lb_dns_name>) from the output lb_url.
- SSH to WordPress EC2:
- If enable_bastion = true (default):
ssh -A -i <private-key>.pem ubuntu@<bastion_public_ip> -J ubuntu@<wordpress_private_ip>- If enable_bastion = false (set in terraform.tfvars): Requires NAT gateway or public IP for WordPress EC2 (not recommended for production).
ssh -i <private-key>.pem ubuntu@<wordpress_private_ip>Find <bastion_public_ip> in module.ec2.bastion_public_ip and <wordpress_private_ip> via AWS console.
- RDS Admin Access:
Create SSH tunnel via bastion:
ssh -i dorka_key.pem -L 3306:<rds_endpoint>:3306 ubuntu@<bastion_public_ip> -NConnect to RDS:
mysql -h 127.0.0.1 -u <db_username> -p<db_password> <db_name><rds_endpoint> is from module.rds.rds_endpoint.
Access the WordPress site using ALB URL.
Complete the WordPress setup wizard in the browser, using the RDS database credentials (db_username, db_password, db_name) and the RDS endpoint (available in the AWS console or Terraform outputs).
Configure an S3-compatible WordPress plugin (e.g., WP Offload Media) to use the sd-wordpress-media-bucket for media storage. Use the EC2 instance profile for authentication.
To destroy the infrastructure:
In the main-project/ directory, destroy the WordPress resources:
terraform destroyConfirm by typing yes. This uses the remote state and DynamoDB locking.
In the backend-setup/ directory, destroy the backend infrastructure:
cd ../backend-setup/
terraform destroyConfirm by typing yes.
Note: If you encounter a BucketNotEmpty error, manually empty the bucket using the AWS CLI:
aws s3api delete-objects \
--bucket sd-tfstate-bucket \
--delete "$(aws s3api list-object-versions --bucket sd-tfstate-bucket --query='{Objects: Versions[].{Key:Key,VersionId:VersionId}}')"
aws s3api delete-objects \
--bucket sd-tfstate-bucket \
--delete "$(aws s3api list-object-versions --bucket sd-tfstate-bucket --query='{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}')"Then retry terraform destroy.
- Security: The provided configurations are for demonstration. For production:
- Avoid hardcoded credentials in user_data or Terraform variables; use AWS Secrets Manager.
- Restrict the EC2 security group ingress (e.g., limit SSH to your IP).
- Enable HTTPS with a certificate (e.g., AWS Certificate Manager or Let's Encrypt).
- Use a custom VPC and private subnets for RDS instead of publicly_accessible = false.
- RDS: The RDS instance is configured with skip_final_snapshot = true for simplicity. In production, enable snapshots.
- S3: The sd-wordpress-media-bucket is used for media offloading. Ensure the WordPress plugin is configured with the correct bucket name and IAM role.
- Region: Replace eu-central-1 with your desired AWS region in all configurations.
For issues, check the AWS credentials, region consistency, or Terraform error messages. Refer to the Terraform and AWS documentation for advanced configurations.