Infrastructure as Code: Provisioning Secure WooCommerce Clusters on DigitalOcean 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 primary configuration files.
First, let’s define the DigitalOcean provider. This block tells Terraform that we intend to manage resources on DigitalOcean and specifies the authentication method. For security, we’ll use an API token, ideally sourced from an environment variable to avoid hardcoding credentials.
main.tf – Provider Setup
# main.tf
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
provider "digitalocean" {
token = var.do_token
}
variable "do_token" {
description = "DigitalOcean API Token"
type = string
sensitive = true
}
variable "region" {
description = "The DigitalOcean region to deploy resources in."
type = string
default = "nyc3"
}
variable "project_name" {
description = "A prefix for all resources created."
type = string
default = "secure-woo"
}
To use this, you’ll need to set the DO_TOKEN environment variable:
export DO_TOKEN="your_digitalocean_api_token_here"
Provisioning Core Infrastructure: VPC, Databases, and Load Balancers
A secure WooCommerce cluster necessitates a well-defined network. We’ll start by creating a Virtual Private Cloud (VPC) to isolate our resources. Within this VPC, we’ll provision a managed PostgreSQL database for WooCommerce’s data and a highly available Load Balancer to distribute traffic.
network.tf – VPC and Firewall
# network.tf
resource "digitalocean_vpc" "main" {
name = "${var.project_name}-vpc"
region = var.region
ip_range = "10.10.0.0/16"
}
resource "digitalocean_firewall" "woocommerce_fw" {
name = "${var.project_name}-firewall"
# Associate firewall with the VPC
vpc_ids = [digitalocean_vpc.main.id]
# Allow SSH for management
inbound_rule {
protocol = "tcp"
ports = ["22"]
sources {
addresses = ["0.0.0.0/0"] # Restrict this in production
}
}
# Allow HTTP/HTTPS for WooCommerce
inbound_rule {
protocol = "tcp"
ports = ["80", "443"]
sources {
addresses = ["0.0.0.0/0"] # Load balancer will handle external access
}
}
# Allow internal communication within the VPC
inbound_rule {
protocol = "tcp"
ports = ["0-65535"]
sources {
droplet_ids = [] # Will be populated by droplet resources later
load_balancer_ids = [digitalocean_loadbalancer.main.id]
vpc_ids = [digitalocean_vpc.main.id]
}
}
inbound_rule {
protocol = "udp"
ports = ["0-65535"]
sources {
droplet_ids = [] # Will be populated by droplet resources later
load_balancer_ids = [digitalocean_loadbalancer.main.id]
vpc_ids = [digitalocean_vpc.main.id]
}
}
inbound_rule {
protocol = "icmp"
sources {
droplet_ids = [] # Will be populated by droplet resources later
load_balancer_ids = [digitalocean_loadbalancer.main.id]
vpc_ids = [digitalocean_vpc.main.id]
}
}
# Allow outbound traffic
outbound_rule {
protocol = "tcp"
ports = ["0-65535"]
destinations {
addresses = ["0.0.0.0/0"]
}
}
outbound_rule {
protocol = "udp"
ports = ["0-65535"]
destinations {
addresses = ["0.0.0.0/0"]
}
}
outbound_rule {
protocol = "icmp"
destinations {
addresses = ["0.0.0.0/0"]
}
}
}
Note the `vpc_ids` association for the firewall. In a production environment, you would significantly restrict the `0.0.0.0/0` source for SSH to specific IP ranges or bastion hosts.
database.tf – Managed PostgreSQL
# database.tf
resource "digitalocean_database_cluster" "woocommerce_db" {
name = "${var.project_name}-db"
engine = "pg"
version = "14" # Specify your desired PostgreSQL version
region = var.region
size = "db-s-2vcpu-4gb" # Adjust size based on expected load
node_count = 1 # For production, consider 2+ for high availability
# Associate with the VPC for private networking
vpc_uuid = digitalocean_vpc.main.id
# Enable automatic backups
backup_restore_type = "automated"
# Configure database name and user
database {
name = "woocommerce_db"
# Password will be automatically generated and accessible via output
}
# Configure maintenance window
maintenance_window {
day = "sunday"
hour = "03:00"
}
}
output "woocommerce_db_host" {
description = "The hostname of the WooCommerce database."
value = digitalocean_database_cluster.woocommerce_db.host
sensitive = true
}
output "woocommerce_db_port" {
description = "The port of the WooCommerce database."
value = digitalocean_database_cluster.woocommerce_db.port
}
output "woocommerce_db_name" {
description = "The name of the WooCommerce database."
value = digitalocean_database_cluster.woocommerce_db.database[0].name
}
output "woocommerce_db_user" {
description = "The username for the WooCommerce database."
value = digitalocean_database_cluster.woocommerce_db.username
sensitive = true
}
output "woocommerce_db_password" {
description = "The password for the WooCommerce database."
value = digitalocean_database_cluster.woocommerce_db.password
sensitive = true
}
We’re using sensitive = true for outputs that contain credentials. These will be masked in Terraform output and can be securely accessed via the Terraform state or by referencing them in other resources.
loadbalancer.tf – High Availability Load Balancer
# loadbalancer.tf
resource "digitalocean_loadbalancer" "main" {
name = "${var.project_name}-lb"
region = var.region
vpc_uuid = digitalocean_vpc.main.id
# Droplets will be added dynamically via a count or for_each
# For now, we'll define the health check and forwarding rules
healthcheck {
port = 80
protocol = "http"
}
forwarding_rule {
entry_protocol = "http"
entry_port = 80
target_protocol = "http"
target_port = 80
target_backend = digitalocean_loadbalancer.main.droplet_ids # Placeholder, will be populated
}
forwarding_rule {
entry_protocol = "https"
entry_port = 443
target_protocol = "https"
target_port = 443
target_backend = digitalocean_loadbalancer.main.droplet_ids # Placeholder, will be populated
# SSL certificate details would go here for HTTPS
# ssl_certificate_id = digitalocean_certificate.my_cert.id
}
# Enable sticky sessions if needed for certain WooCommerce plugins
# sticky_sessions {
# type = "cookies"
# cookie_name = "woocommerce_session"
# cookie_ttl = 3600
# }
}
# Placeholder for SSL certificate resource if using HTTPS directly on LB
# resource "digitalocean_certificate" "my_cert" {
# name = "${var.project_name}-ssl-cert"
# private_key = file("path/to/your/private.key")
# certificate = file("path/to/your/certificate.crt")
# }
output "loadbalancer_ip" {
description = "The public IP address of the load balancer."
value = digitalocean_loadbalancer.main.ip
}
The droplet_ids in the forwarding rules are placeholders. These will be dynamically populated once we define our Droplet resources and associate them with the load balancer.
Deploying WooCommerce Application Servers
We’ll deploy multiple Droplets to host our WooCommerce application. These Droplets will be configured with Nginx as a reverse proxy and PHP-FPM to serve the WordPress/WooCommerce application. Using a count meta-argument allows us to easily scale the number of application servers.
app.tf – Application Droplets
# app.tf
resource "digitalocean_droplet" "app_server" {
count = 2 # Start with 2 app servers, adjust as needed
name = "${var.project_name}-app-${count.index}"
region = var.region
size = "s-2vcpu-4gb" # Adjust size based on expected load
image = "ubuntu-22-04-x64" # Or your preferred OS
vpc_uuid = digitalocean_vpc.main.id
# Use SSH keys for secure access
ssh_keys = ["your-ssh-key-fingerprint-or-id"] # Replace with your actual SSH key
# User data for initial provisioning (cloud-init)
user_data = templatefile("${path.module}/scripts/bootstrap.sh", {
db_host = digitalocean_database_cluster.woocommerce_db.host
db_port = digitalocean_database_cluster.woocommerce_db.port
db_name = digitalocean_database_cluster.woocommerce_db.database[0].name
db_user = digitalocean_database_cluster.woocommerce_db.username
db_password = digitalocean_database_cluster.woocommerce_db.password
domain_name = "your-domain.com" # Replace with your actual domain
})
# Add Droplets to the firewall's internal rules
provisioner "remote-exec" {
inline = [
"sudo ufw allow from ${digitalocean_vpc.main.ip_range} to any port 80,443",
"sudo ufw allow from ${digitalocean_vpc.main.ip_range} to any port 22",
"sudo ufw allow from ${digitalocean_loadbalancer.main.ip} to any port 80,443", # Allow LB access
"sudo ufw enable"
]
connection {
type = "ssh"
user = "root" # Or your default user
private_key = file("~/.ssh/id_rsa") # Path to your private SSH key
host = self.ipv4_address
timeout = "5m"
}
}
# Associate Droplets with the Load Balancer
lifecycle {
create_before_destroy = true
}
}
# Associate Droplets with the Load Balancer
resource "digitalocean_loadbalancer_droplet" "app_lb_association" {
for_each = toset([for droplet in digitalocean_droplet.app_server : droplet.id])
loadbalancer_id = digitalocean_loadbalancer.main.id
droplet_id = each.value
}
# Update firewall to include Droplet IDs
resource "digitalocean_firewall" "woocommerce_fw" {
# ... (previous firewall configuration) ...
# Dynamically add Droplet IDs to inbound rules for internal communication
inbound_rule {
protocol = "tcp"
ports = ["0-65535"]
sources {
droplet_ids = [for droplet in digitalocean_droplet.app_server : droplet.id]
load_balancer_ids = [digitalocean_loadbalancer.main.id]
vpc_ids = [digitalocean_vpc.main.id]
}
}
inbound_rule {
protocol = "udp"
ports = ["0-65535"]
sources {
droplet_ids = [for droplet in digitalocean_droplet.app_server : droplet.id]
load_balancer_ids = [digitalocean_loadbalancer.main.id]
vpc_ids = [digitalocean_vpc.main.id]
}
}
inbound_rule {
protocol = "icmp"
sources {
droplet_ids = [for droplet in digitalocean_droplet.app_server : droplet.id]
load_balancer_ids = [digitalocean_loadbalancer.main.id]
vpc_ids = [digitalocean_vpc.main.id]
}
}
}
output "app_server_ips" {
description = "Public IP addresses of the application servers."
value = [for droplet in digitalocean_droplet.app_server : droplet.ipv4_address]
}
The user_data script (scripts/bootstrap.sh) is critical. It will run on Droplet boot, installing Nginx, PHP, and necessary WordPress/WooCommerce dependencies, and configuring them to connect to the managed database. It also sets up the domain name for Nginx virtual hosts.
scripts/bootstrap.sh – Droplet Bootstrapping Script
#!/bin/bash
set -euo pipefail
# Variables passed from Terraform
DB_HOST="${db_host}"
DB_PORT="${db_port}"
DB_NAME="${db_name}"
DB_USER="${db_user}"
DB_PASSWORD="${db_password}"
DOMAIN_NAME="${domain_name}"
# Update package lists and install essential packages
apt-get update -y
apt-get install -y nginx php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-xmlrpc php-soap php-intl php-zip unzip curl wget
# Configure Nginx
cat <<EOF > /etc/nginx/sites-available/woocommerce
server {
listen 80;
server_name ${DOMAIN_NAME} www.${DOMAIN_NAME};
root /var/www/html/wordpress; # Assuming WordPress is installed here
index index.php index.html index.htm;
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version if needed
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
# Caching for static assets (optional but recommended)
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires max;
log_not_found off;
}
}
EOF
# Enable the Nginx site and remove default
ln -sf /etc/nginx/sites-available/woocommerce /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
# Restart Nginx
systemctl restart nginx
# Configure PHP-FPM (adjust settings for performance/security)
# Example: Increase memory limit, max execution time
sed -i 's/memory_limit = .*/memory_limit = 256M/' /etc/php/8.1/fpm/php.ini # Adjust PHP version
sed -i 's/max_execution_time = .*/max_execution_time = 300/' /etc/php/8.1/fpm/php.ini # Adjust PHP version
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php/8.1/fpm/php.ini # Adjust PHP version
sed -i 's/post_max_size = .*/post_max_size = 64M/' /etc/php/8.1/fpm/php.ini # Adjust PHP version
# Restart PHP-FPM
systemctl restart php8.1-fpm # Adjust PHP version
# Install WordPress and WooCommerce (example: using WP-CLI)
# Ensure WP-CLI is installed or download it
# curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
# chmod +x wp-cli.phar
# mv wp-cli.phar /usr/local/bin/wp
# Create WordPress directory and set permissions
mkdir -p /var/www/html/wordpress
chown www-data:www-data /var/www/html/wordpress
# Download WordPress (if not already present)
if [ ! -f /var/www/html/wordpress/wp-load.php ]; then
cd /var/www/html/wordpress
wget https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz
mv wordpress/* .
rm -rf wordpress latest.tar.gz
chown -R www-data:www-data .
fi
# Configure WordPress database connection
cd /var/www/html/wordpress
wp config create --dbname=${DB_NAME} --dbuser=${DB_USER} --dbpass=${DB_PASSWORD} --dbhost=${DB_HOST}:${DB_PORT} --allow-root
# Install WooCommerce plugin (example)
wp plugin install woocommerce --activate --allow-root
# Set WordPress permalinks (optional, but good practice)
wp rewrite structure '/%postname%/' --allow-root
echo "Bootstrap script finished."
Remember to replace your-ssh-key-fingerprint-or-id with your actual DigitalOcean SSH key identifier and your-domain.com with your domain. The user_data script uses wp-cli for automating WordPress and WooCommerce setup. Ensure wp-cli is installed on the base image or add its installation to the script.
Security Hardening and Final Touches
Security is paramount. Beyond network segmentation with VPCs and firewalls, we’ll ensure secure access and consider further hardening steps.
SSH Key Management
We’ve used SSH keys for Droplet access. In a production scenario, avoid using password authentication entirely. Ensure your private keys are protected and consider using a secrets management system for automated deployments.
HTTPS Configuration
For HTTPS, you have a few options:
- Let’s Encrypt via Certbot on Droplets: The
bootstrap.shscript can be extended to install and configure Certbot, automating certificate issuance and renewal. Nginx would then be configured to use these certificates. - DigitalOcean Load Balancer SSL Termination: Upload your SSL certificate directly to the DigitalOcean Load Balancer. This offloads SSL processing from your application servers. This requires uncommenting and configuring the
digitalocean_certificateresource and referencing its ID in the Load Balancer’s forwarding rules.
For simplicity in this example, we’ve commented out the direct SSL configuration on the Load Balancer. If you choose this route, you’ll need to manage your certificate files and potentially use a separate Terraform resource for certificate management.
Database Security
The managed PostgreSQL database is already secured by being within the VPC and accessible only via its private IP. The generated credentials should be treated as secrets and rotated periodically.
Deployment Workflow
With the Terraform configuration in place, the deployment process is straightforward:
- Initialize Terraform: Navigate to your Terraform project directory and run
terraform init. This downloads the DigitalOcean provider plugin. - Plan Infrastructure: Run
terraform planto see a preview of the resources Terraform will create, modify, or destroy. Review this output carefully. - Apply Infrastructure: Execute
terraform apply. Terraform will prompt for confirmation before provisioning the resources on DigitalOcean. - Access Your Site: Once applied, retrieve the Load Balancer’s IP address from the Terraform output and point your domain’s DNS records to it. You should then be able to access your secure WooCommerce site.
- Destroy Infrastructure (when needed): To tear down all provisioned resources, run
terraform destroy.
This IaC approach provides a repeatable, version-controlled, and secure method for deploying and managing your WooCommerce infrastructure on DigitalOcean, enabling rapid scaling and disaster recovery.