Infrastructure as Code: Provisioning Secure WooCommerce Clusters on AWS Using Terraform
Terraform Project Structure and Provider Configuration
We’ll begin by establishing a robust Terraform project structure. This organization is crucial for managing complexity, especially as our infrastructure grows. Our root module will house the main configuration files, including the provider block and backend configuration.
The AWS provider configuration specifies the region and any necessary authentication details. For production environments, it’s highly recommended to use IAM roles or environment variables for credentials rather than hardcoding them. We’ll also configure the Terraform backend to store our state file remotely, typically in an S3 bucket, which is essential for collaboration and state locking.
`main.tf`
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "your-terraform-state-bucket-name" # Replace with your S3 bucket name
key = "woocommerce/prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "your-terraform-lock-table" # Replace with your DynamoDB table name for state locking
encrypt = true
}
}
provider "aws" {
region = "us-east-1" # Or your preferred AWS region
# For production, consider using IAM roles or environment variables for credentials
# profile = "your-aws-profile"
}
# Variables will be defined in variables.tf
# Resources will be defined in respective .tf files (e.g., network.tf, compute.tf, rds.tf)
Networking Foundation: VPC, Subnets, and Security Groups
A secure and well-architected network is paramount for any production application. We’ll define a Virtual Private Cloud (VPC) with public and private subnets across multiple Availability Zones for high availability. Essential security groups will be created to control inbound and outbound traffic, ensuring only necessary ports are open.
`network.tf`
# network.tf
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "woocommerce-vpc"
}
}
# Internet Gateway
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "woocommerce-igw"
}
}
# Public Subnets (for NAT Gateways and potentially load balancers)
resource "aws_subnet" "public" {
count = 2 # Number of Availability Zones
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.${count.index * 64}/26"
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true # For simplicity, though often managed by NAT Gateway
tags = {
Name = "woocommerce-public-subnet-${count.index}"
}
}
# Private Subnets (for application servers and RDS)
resource "aws_subnet" "private" {
count = 2 # Number of Availability Zones
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.${count.index * 64}/26"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "woocommerce-private-subnet-${count.index}"
}
}
# NAT Gateways (for outbound internet access from private subnets)
resource "aws_eip" "nat" {
count = length(aws_subnet.public)
domain = "vpc"
}
resource "aws_nat_gateway" "nat" {
count = length(aws_subnet.public)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
depends_on = [aws_internet_gateway.gw]
tags = {
Name = "woocommerce-nat-gw-${count.index}"
}
}
# Public Route Table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
tags = {
Name = "woocommerce-public-rt"
}
}
resource "aws_route_table_association" "public" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
# Private Route Table
resource "aws_route_table" "private" {
count = length(aws_subnet.private)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat[count.index].id
}
tags = {
Name = "woocommerce-private-rt-${count.index}"
}
}
resource "aws_route_table_association" "private" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
# Data source for Availability Zones
data "aws_availability_zones" "available" {
state = "available"
}
# Security Group for Load Balancer
resource "aws_security_group" "lb" {
name = "woocommerce-lb-sg"
description = "Allow HTTP and HTTPS inbound traffic"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from anywhere"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "woocommerce-lb-sg"
}
}
# Security Group for Application Servers (EC2 instances)
resource "aws_security_group" "app" {
name = "woocommerce-app-sg"
description = "Allow traffic from Load Balancer and SSH from trusted IPs"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from LB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.lb.id]
}
ingress {
description = "HTTPS from LB"
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.lb.id]
}
# SSH access from a specific trusted IP range (e.g., your office or bastion host)
ingress {
description = "SSH from trusted IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["YOUR_TRUSTED_IP_ADDRESS/32"] # IMPORTANT: Replace with your actual trusted IP
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "woocommerce-app-sg"
}
}
# Security Group for RDS Database
resource "aws_security_group" "rds" {
name = "woocommerce-rds-sg"
description = "Allow MySQL traffic from Application Servers"
vpc_id = aws_vpc.main.id
ingress {
description = "MySQL from App Servers"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "woocommerce-rds-sg"
}
}
Database Tier: RDS for MySQL
A managed relational database service like AWS RDS is crucial for reliability and scalability. We’ll provision an RDS instance for MySQL, placing it in private subnets for enhanced security. Parameter groups and option groups can be customized for performance tuning and specific MySQL features.
`rds.tf`
# rds.tf
# RDS Subnet Group
resource "aws_db_subnet_group" "woocommerce" {
name = "woocommerce-db-subnet-group"
subnet_ids = [for subnet in aws_subnet.private : subnet.id] # Use private subnets
tags = {
Name = "woocommerce-db-subnet-group"
}
}
# RDS Instance
resource "aws_db_instance" "woocommerce" {
identifier = "woocommerce-db"
engine = "mysql"
engine_version = "8.0" # Specify your desired MySQL version
instance_class = "db.t3.medium" # Choose an appropriate instance class
allocated_storage = 50 # GB
storage_type = "gp2" # General Purpose SSD
db_name = "woocommerce_db"
username = var.db_username # Defined in variables.tf
password = var.db_password # Defined in variables.tf
parameter_group_name = "default.mysql8.0" # Or a custom parameter group
# option_group_name = "default:mysql-8-0" # Or a custom option group
db_subnet_group_name = aws_db_subnet_group.woocommerce.name
vpc_security_group_ids = [aws_security_group.rds.id]
skip_final_snapshot = true # Set to false for production backups
publicly_accessible = false # Crucial for security
multi_az = true # For high availability
storage_encrypted = true # Enable encryption at rest
backup_retention_period = 7 # Days
# kms_key_id = "arn:aws:kms:..." # Specify if using a custom KMS key
tags = {
Name = "woocommerce-db-instance"
}
}
Application Tier: EC2 Instances and Auto Scaling
We’ll deploy EC2 instances to host our WooCommerce application. An Auto Scaling Group (ASG) will manage these instances, ensuring that the application scales automatically based on demand and maintains a desired capacity. A Launch Template defines the configuration for new instances, including the AMI, instance type, user data for bootstrapping, and IAM instance profile.
`compute.tf`
# compute.tf
# IAM Role for EC2 Instances
resource "aws_iam_role" "ec2_role" {
name = "woocommerce-ec2-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
# IAM Instance Profile
resource "aws_iam_instance_profile" "ec2_profile" {
name = "woocommerce-ec2-profile"
role = aws_iam_role.ec2_role.name
}
# User Data for EC2 Instances (Bootstrapping)
# This script will install Apache, PHP, MySQL client, and clone your WooCommerce application.
# It also configures Apache to serve your site and sets up basic security.
locals {
user_data = <<-EOT
#!/bin/bash
exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
echo "Starting user data script..."
# Update packages and install necessary software
sudo apt-get update -y
sudo apt-get upgrade -y
sudo apt-get install -y apache2 php libapache2-mod-php php-mysql php-curl php-gd php-mbstring php-xml php-xmlrpc php-soap php-intl php-zip unzip wget
# Configure Apache
sudo a2enmod rewrite
sudo systemctl restart apache2
# Download and extract WooCommerce (replace with your actual deployment method)
# For production, consider using a CI/CD pipeline to deploy code.
# This is a placeholder for demonstration.
cd /var/www/html
sudo wget https://wordpress.org/latest.zip -O wordpress.zip
sudo unzip wordpress.zip
sudo mv wordpress/* .
sudo rm wordpress.zip
sudo chown -R www-data:www-data /var/www/html
# Configure WordPress/WooCommerce (replace with your actual configuration)
# This would typically involve wp-cli or manual configuration files.
# For simplicity, we'll assume a basic setup.
# You'll need to create wp-config.php with your database credentials.
# Example:
# sudo cp wp-config-sample.php wp-config.php
# sudo sed -i "s/database_name_here/woocommerce_db/" wp-config.php
# sudo sed -i "s/username_here/var.db_username/" wp-config.php
# sudo sed -i "s/password_here/var.db_password/" wp-config.php
# sudo sed -i "s/localhost/var.db_endpoint/" wp-config.php
echo "User data script finished."
EOT
}
# Launch Template
resource "aws_launch_template" "woocommerce" {
name_prefix = "woocommerce-lt-"
image_id = "ami-0abcdef1234567890" # Replace with a suitable Amazon Linux 2 or Ubuntu AMI ID
instance_type = "t3.medium" # Choose an appropriate instance type
iam_instance_profile {
arn = aws_iam_instance_profile.ec2_profile.arn
}
network_interfaces {
associate_public_ip_address = false # Instances will be in private subnets
security_groups = [aws_security_group.app.id]
}
user_data = base64encode(local.user_data)
# Add SSH key for manual access if needed (consider bastion host for production)
# key_name = "your-ssh-key-pair-name"
tags {
Name = "woocommerce-app-instance"
}
lifecycle {
create_before_destroy = true
}
}
# Auto Scaling Group
resource "aws_autoscaling_group" "woocommerce" {
name = "woocommerce-asg"
desired_capacity = 2
min_size = 1
max_size = 5
vpc_zone_identifier = [for subnet in aws_subnet.private : subnet.id] # Deploy in private subnets
launch_template {
id = aws_launch_template.woocommerce.id
version = "$Latest"
}
# Health check configuration
health_check_type = "ELB" # Or "EC2" if not using ELB
health_check_grace_period = 300
# Attach to target group (defined in load_balancer.tf)
target_group_arns = [aws_lb_target_group.woocommerce.arn]
# Scaling policies (optional, but recommended)
# resource "aws_autoscaling_policy" "scale_up" {
# name = "woocommerce-scale-up"
# scaling_adjustment = 1
# autoscaling_group_name = aws_autoscaling_group.woocommerce.name
# adjustment_type = "ChangeInCapacity"
# cooldown = 300
# }
# resource "aws_autoscaling_policy" "scale_down" {
# name = "woocommerce-scale-down"
# scaling_adjustment = -1
# autoscaling_group_name = aws_autoscaling_group.woocommerce.name
# adjustment_type = "ChangeInCapacity"
# cooldown = 300
# }
# Example CloudWatch alarm for scaling up based on CPU utilization
# resource "aws_cloudwatch_metric_alarm" "cpu_high" {
# alarm_name = "woocommerce-cpu-high"
# comparison_operator = "GreaterThanOrEqualToThreshold"
# evaluation_periods = "2"
# metric_name = "CPUUtilization"
# namespace = "AWS/EC2"
# period = "120"
# statistic = "Average"
# threshold = "70"
# alarm_description = "This metric monitors cpu high for woocommerce ASG"
# alarm_actions = [aws_autoscaling_policy.scale_up.id]
# dimensions = {
# AutoScalingGroupName = aws_autoscaling_group.woocommerce.name
# }
# }
tags = [
{
key = "Name"
value = "woocommerce-app-instance"
propagate_at_launch = true
}
]
}
Load Balancing and SSL Termination
An Application Load Balancer (ALB) distributes incoming traffic across the EC2 instances in the Auto Scaling Group. We’ll configure listeners for HTTP and HTTPS, with SSL termination handled at the ALB. This offloads SSL processing from the application servers and simplifies certificate management.
`load_balancer.tf`
# load_balancer.tf
# Application Load Balancer
resource "aws_lb" "woocommerce" {
name = "woocommerce-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.lb.id]
subnets = [for subnet in aws_subnet.public : subnet.id] # Public subnets for ALB
enable_deletion_protection = false # Set to true for production
tags = {
Name = "woocommerce-alb"
}
}
# ALB Target Group
resource "aws_lb_target_group" "woocommerce" {
name = "woocommerce-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "instance"
health_check {
enabled = true
interval = 30
path = "/" # Or a specific health check endpoint
port = "traffic-port"
protocol = "HTTP"
matcher = "200" # Expect HTTP 200 OK
timeout = 5
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = {
Name = "woocommerce-tg"
}
}
# Listener for HTTP (redirect to HTTPS)
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.woocommerce.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
# Listener for HTTPS
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.woocommerce.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08" # Choose an appropriate SSL policy
certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/your-certificate-id" # Replace with your ACM certificate ARN
default_action {
target_group_arn = aws_lb_target_group.woocommerce.arn
}
}
Variables and Outputs
Centralizing configuration values in variables makes the Terraform code reusable and easier to manage. Outputs provide useful information about the deployed infrastructure, such as the ALB DNS name.
`variables.tf`
# variables.tf
variable "db_username" {
description = "Username for the RDS database"
type = string
sensitive = true # Mark as sensitive to prevent exposure in logs
}
variable "db_password" {
description = "Password for the RDS database"
type = string
sensitive = true
}
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
variable "vpc_cidr" {
description = "CIDR block for the VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidrs" {
description = "CIDR blocks for public subnets"
type = list(string)
default = ["10.0.1.0/26", "10.0.1.64/26"]
}
variable "private_subnet_cidrs" {
description = "CIDR blocks for private subnets"
type = list(string)
default = ["10.0.2.0/26", "10.0.2.64/26"]
}
`outputs.tf`
# outputs.tf
output "alb_dns_name" {
description = "The DNS name of the Application Load Balancer"
value = aws_lb.woocommerce.dns_name
}
output "alb_zone_id" {
description = "The Zone ID of the Application Load Balancer"
value = aws_lb.woocommerce.zone_id
}
output "rds_endpoint" {
description = "The endpoint of the RDS database instance"
value = aws_db_instance.woocommerce.endpoint
}
output "rds_port" {
description = "The port of the RDS database instance"
value = aws_db_instance.woocommerce.port
}
Deployment and Management Workflow
With the Terraform configuration in place, the deployment process is straightforward:
- Initialize Terraform: Run
terraform initto download provider plugins and configure the backend. - Plan the Infrastructure: Execute
terraform planto review the changes Terraform will make to your AWS environment. This is a critical step to catch any unintended modifications. - Apply the Configuration: Run
terraform applyto provision the resources. You will be prompted to enter sensitive variables like the database username and password. - Destroy Resources: When no longer needed, use
terraform destroyto tear down all provisioned resources, preventing unnecessary costs.
For managing sensitive variables like database credentials, consider using environment variables (e.g., TF_VAR_db_username, TF_VAR_db_password) or a secrets management system like AWS Secrets Manager integrated with Terraform.