• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Infrastructure as Code: Provisioning Secure WordPress Clusters on DigitalOcean Using Terraform

Infrastructure as Code: Provisioning Secure WordPress Clusters on DigitalOcean Using Terraform

Terraform Provider Configuration for DigitalOcean

To begin provisioning infrastructure on DigitalOcean with Terraform, we first need to configure the DigitalOcean provider. This involves specifying your API token and the region where your resources will be deployed. It’s crucial to manage your API token securely, ideally using environment variables rather than hardcoding it directly into your Terraform configuration files.

Create a file named versions.tf (or integrate this into your main main.tf) to define the required Terraform version and the DigitalOcean provider. The version constraint ensures compatibility with a specific range of the provider, preventing unexpected behavior due to future updates.

versions.tf

# versions.tf
terraform {
  required_version = ">= 1.0.0"

  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
  }
}

provider "digitalocean" {
  token = var.do_token
  # You can also specify a region here if you don't want to use the variable
  # region = "nyc3"
}

variable "do_token" {
  description = "DigitalOcean API token"
  type        = string
  sensitive   = true
}

variable "do_region" {
  description = "The DigitalOcean region to deploy resources in"
  type        = string
  default     = "nyc3"
}

To set the do_token variable, you can use an environment variable:

export TF_VAR_do_token="YOUR_DIGITALOCEAN_API_TOKEN"

Alternatively, you can create a terraform.tfvars file (ensure this file is not committed to version control if it contains sensitive information):

# terraform.tfvars
do_token = "YOUR_DIGITALOCEAN_API_TOKEN"
do_region = "nyc3"

Provisioning a Highly Available WordPress Cluster

A robust WordPress setup requires more than just a single Droplet. For high availability and scalability, we’ll provision multiple Droplets for web servers, a managed database, and potentially a load balancer. Terraform excels at defining these interconnected resources.

Web Server Droplets

We’ll define a set of identical Droplets that will serve our WordPress application. These will be configured with a user data script to install and configure Nginx and PHP-FPM. A common approach is to use a load balancer to distribute traffic across these web servers.

main.tf – Web Servers

# main.tf

# Define variables for Droplet configuration
variable "web_droplet_count" {
  description = "Number of web server Droplets"
  type        = number
  default     = 2
}

variable "web_droplet_size" {
  description = "Size of the web server Droplets (e.g., 's-2vcpu-4gb')"
  type        = string
  default     = "s-2vcpu-4gb"
}

variable "ssh_key_fingerprint" {
  description = "Fingerprint of the SSH key to be added to Droplets"
  type        = string
  # You can get this from 'doctl compute ssh-key list' or the DigitalOcean control panel
  # Example: "a1:b2:c3:d4:e5:f6:78:90:12:34:56:78:90:ab:cd:ef"
}

# Resource for multiple web server Droplets
resource "digitalocean_droplet" "web" {
  count              = var.web_droplet_count
  name               = "wp-web-${count.index + 1}"
  region             = var.do_region
  size               = var.web_droplet_size
  image              = "ubuntu-22-04-x64" # Or your preferred OS image
  ssh_keys           = [var.ssh_key_fingerprint]
  monitoring         = true
  user_data          = file("scripts/web_setup.sh") # Path to your user data script

  tags = ["wordpress", "web"]

  lifecycle {
    create_before_destroy = true
  }
}

# Output the public IPs of the web servers
output "web_droplet_public_ips" {
  description = "Public IP addresses of the web server Droplets"
  value       = digitalocean_droplet.web[*].ipv4_address
}

scripts/web_setup.sh – Nginx and PHP-FPM Installation

#!/bin/bash
# scripts/web_setup.sh

# Update package lists and install Nginx, PHP, and necessary modules
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

# Configure Nginx to serve PHP files
# This is a basic configuration. For production, you'd want more robust settings.
cat < /etc/nginx/sites-available/wordpress
server {
    listen 80;
    server_name _; # Listen on all hostnames

    root /var/www/html; # This will be configured later when we mount storage
    index index.php index.html index.htm;

    location / {
        try_files \$uri \$uri/ /index.php?\$args;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        # Make sure the socket path matches your PHP-FPM configuration
        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;
    }
}
EOF

# Remove default Nginx site and enable our new configuration
rm /etc/nginx/sites-enabled/default
ln -s /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/wordpress

# Restart Nginx and PHP-FPM to apply changes
systemctl restart nginx
systemctl restart php8.1-fpm # Adjust PHP version if needed

# Ensure PHP-FPM starts on boot
systemctl enable php8.1-fpm # Adjust PHP version if needed

# Create a placeholder index.php for testing
echo "" > /var/www/html/info.php

The web_setup.sh script installs Nginx and PHP-FPM, configures Nginx to pass PHP requests to PHP-FPM, and sets up a basic virtual host. It also creates a placeholder info.php file for initial testing. In a production environment, you would replace /var/www/html with a mounted volume for persistent storage and add more security hardening to Nginx.

Managed Database (PostgreSQL or MySQL)

For a production WordPress site, using a managed database service is highly recommended. DigitalOcean offers Managed Databases for PostgreSQL and MySQL. This offloads the operational burden of managing, backing up, and scaling your database.

main.tf – Managed Database

# main.tf (continued)

# Example for Managed MySQL Database
resource "digitalocean_database_cluster" "wordpress_db" {
  name      = "wordpress-db-cluster"
  engine    = "mysql"
  version   = "8.0" # Specify your desired MySQL version
  region    = var.do_region
  size      = "db-s-1vcpu-2gb" # Adjust size based on expected load
  node_count = 1 # For a single primary node. For HA, increase this and use read replicas.
  # For High Availability, consider using a higher node_count and enabling read_replicas.
  # For MySQL, HA is typically achieved with replication.
  # For PostgreSQL, HA is built-in with multiple nodes.

  # Enable automatic backups
  backup_restore {
    automatic = true
    day       = "sunday"
    time      = "03:00" # UTC time
  }

  tags = ["wordpress", "database"]
}

# Output database connection details
output "database_host" {
  description = "Hostname of the managed database"
  value       = digitalocean_database_cluster.wordpress_db.host
  sensitive   = true
}

output "database_port" {
  description = "Port of the managed database"
  value       = digitalocean_database_cluster.wordpress_db.port
}

output "database_name" {
  description = "Name of the managed database"
  value       = digitalocean_database_cluster.wordpress_db.database
  sensitive   = true
}

output "database_user" {
  description = "Username for the managed database"
  value       = digitalocean_database_cluster.wordpress_db.username
  sensitive   = true
}

output "database_password" {
  description = "Password for the managed database"
  value       = digitalocean_database_cluster.wordpress_db.password
  sensitive   = true
}

This configuration provisions a managed MySQL database. For PostgreSQL, you would change the engine to "pg" and the version accordingly. The size and node_count should be selected based on your anticipated database load. Enabling automatic backups is critical for data recovery.

Load Balancer

To distribute traffic across the web server Droplets and provide a single entry point, a DigitalOcean Load Balancer is essential. This also allows for seamless scaling of web servers without downtime.

main.tf – Load Balancer

# main.tf (continued)

resource "digitalocean_loadbalancer" "wordpress_lb" {
  name     = "wordpress-lb"
  region   = var.do_region
  # Use a droplet tag to target the web servers
  # This assumes your web Droplets have the tag "wordpress" and "web"
  # If you used a different tag, update it here.
  droplet_tag = "web"

  forwarding_rule {
    entry_protocol    = "http"
    entry_port        = 80
    to_protocol       = "http"
    to_port           = 80
    # Optional: If you plan to use HTTPS, you'll need to configure SSL certificates
    # certificate_id = digitalocean_certificate.your_cert.id
  }

  # Health check configuration
  healthcheck {
    port     = 80
    protocol = "http"
    path     = "/healthz" # Create a /healthz endpoint on your web servers
  }

  # Sticky sessions can be useful for some applications, but generally not for stateless WordPress
  # sticky_sessions {
  #   type = "cookies"
  # }

  tags = ["wordpress", "loadbalancer"]
}

# Output the Load Balancer's IP address
output "loadbalancer_ip" {
  description = "Public IP address of the Load Balancer"
  value       = digitalocean_loadbalancer.wordpress_lb.ip
}

This configuration creates a load balancer that directs HTTP traffic on port 80 to any Droplets tagged with “web”. A health check is configured to ensure traffic is only sent to healthy instances. You’ll need to create a /healthz endpoint on your web servers for this to function correctly.

Securing the WordPress Cluster

Security is paramount. We’ll focus on network security (firewalls) and secure access to the database.

Firewall Configuration (Security Groups)

DigitalOcean’s firewall rules (applied via Droplet tags) are essential for controlling network access. We’ll restrict access to the web servers and database.

main.tf – Firewall Rules

# main.tf (continued)

# Firewall for the Load Balancer (allows HTTP/HTTPS from anywhere)
resource "digitalocean_firewall" "lb_firewall" {
  name = "wordpress-lb-firewall"

  tags = ["wordpress", "loadbalancer"]

  # Allow HTTP and HTTPS traffic to the load balancer
  inbound_rule {
    protocol  = "tcp"
    port_range = "80"
    sources {
      addresses = ["0.0.0.0/0"]
    }
  }
  inbound_rule {
    protocol  = "tcp"
    port_range = "443"
    sources {
      addresses = ["0.0.0.0/0"]
    }
  }

  # Outbound rules are usually permissive by default, but you can restrict if needed.
  # outbound_rule {
  #   protocol = "tcp"
  #   port_range = "all"
  #   destinations {
  #     addresses = ["0.0.0.0/0"]
  #   }
  # }
}

# Firewall for the Web Servers (allows traffic from Load Balancer and SSH)
resource "digitalocean_firewall" "web_firewall" {
  name = "wordpress-web-firewall"

  tags = ["wordpress", "web"]

  # Allow HTTP and HTTPS traffic from the Load Balancer's IP
  # Note: DigitalOcean Load Balancers have a fixed IP range.
  # It's more robust to allow traffic from the LB's internal network if possible,
  # or rely on the LB tag. For simplicity here, we'll allow from the LB's public IP.
  # A more advanced setup might use private networking.
  inbound_rule {
    protocol  = "tcp"
    port_range = "80"
    sources {
      # This is the IP of the loadbalancer resource
      addresses = [digitalocean_loadbalancer.wordpress_lb.ip]
    }
  }
  inbound_rule {
    protocol  = "tcp"
    port_range = "443"
    sources {
      addresses = [digitalocean_loadbalancer.wordpress_lb.ip]
    }
  }

  # Allow SSH access from a specific IP or range (e.g., your office IP)
  # Replace with your actual IP or a CIDR block
  inbound_rule {
    protocol  = "tcp"
    port_range = "22"
    sources {
      addresses = ["YOUR_ADMIN_IP/32"] # e.g., "203.0.113.5/32"
    }
  }

  # Allow access to PHP-FPM from the Load Balancer (if not using Unix sockets directly)
  # This is often not needed if Nginx and PHP-FPM are on the same Droplet and using sockets.
  # If they are on separate Droplets, you'd need to open the PHP-FPM port.

  # Outbound rules: Allow access to the database and external resources
  outbound_rule {
    protocol = "tcp"
    port_range = digitalocean_database_cluster.wordpress_db.port
    destinations {
      # Allow outbound to the database host
      addresses = [digitalocean_database_cluster.wordpress_db.host]
    }
  }
  outbound_rule {
    protocol = "tcp"
    port_range = "80" # For updates, external APIs, etc.
    destinations {
      addresses = ["0.0.0.0/0"]
    }
  }
  outbound_rule {
    protocol = "tcp"
    port_range = "443" # For HTTPS connections
    destinations {
      addresses = ["0.0.0.0/0"]
    }
  }
}

# Firewall for the Database (allows access only from web servers)
resource "digitalocean_firewall" "db_firewall" {
  name = "wordpress-db-firewall"

  tags = ["wordpress", "database"]

  # Allow inbound traffic from the web servers' IP addresses
  # This is more secure than allowing from the LB IP if web servers are on a private network.
  # For simplicity, we'll allow from the web Droplets' public IPs.
  # A better approach would be to use private networking.
  inbound_rule {
    protocol  = "tcp"
    port_range = tostring(digitalocean_database_cluster.wordpress_db.port)
    sources {
      # Target Droplets with the "wordpress" and "web" tags
      droplet_ids = [for droplet in digitalocean_droplet.web : droplet.id]
      # Alternatively, use tags if your web servers have a specific tag for DB access
      # tag = "wordpress-web-access"
    }
  }

  # Outbound rules: Allow necessary outbound traffic (e.g., for updates, if needed)
  outbound_rule {
    protocol = "tcp"
    port_range = "80"
    destinations {
      addresses = ["0.0.0.0/0"]
    }
  }
  outbound_rule {
    protocol = "tcp"
    port_range = "443"
    destinations {
      addresses = ["0.0.0.0/0"]
    }
  }
}

The firewall rules are crucial. The load balancer firewall allows public access on HTTP/HTTPS. The web server firewall restricts incoming traffic to only come from the load balancer’s IP and allows SSH from a trusted source. The database firewall is the most restrictive, only allowing connections from the web server Droplets. Remember to replace YOUR_ADMIN_IP/32 with your actual public IP address for SSH access.

Secure Database Credentials Management

The database credentials (host, port, name, user, password) are sensitive. Terraform marks them as sensitive, and they will be masked in the output. For actual WordPress installation, these credentials need to be passed to the application. A common pattern is to use environment variables on the web servers or a secrets management system.

WordPress Installation and Configuration

With the infrastructure provisioned, the next step is to install WordPress itself. This typically involves downloading WordPress, configuring its wp-config.php file with the database credentials, and setting up the web server’s document root.

This part is often handled by a separate automation tool like Ansible, Chef, or even a more advanced user data script. For simplicity in this Terraform example, we’ll outline the conceptual steps and how you might pass variables.

Passing Database Credentials to Web Servers

You can use Terraform’s remote-exec provisioner (though generally discouraged for production due to complexity and potential failure points) or, more robustly, use a configuration management tool triggered after Terraform applies. For this example, let’s assume you’ll use a separate script or Ansible playbook that consumes the Terraform outputs.

If you were to use remote-exec (use with caution):

# main.tf (example using remote-exec - NOT RECOMMENDED FOR PRODUCTION)
# resource "digitalocean_droplet" "web" {
#   ... existing config ...
#   provisioner "remote-exec" {
#     inline = [
#       "sudo apt-get update -y",
#       "sudo apt-get install -y wget",
#       "sudo wget https://wordpress.org/latest.tar.gz -O /tmp/wordpress.tar.gz",
#       "sudo tar -xzf /tmp/wordpress.tar.gz -C /var/www/html --strip-components=1",
#       "sudo chown -R www-data:www-data /var/www/html",
#       "sudo chmod -R 755 /var/www/html",
#       # Create wp-config.php with database details
#       "sudo cp /var/www/html/wp-config-sample.php /var/www/html/wp-config.php",
#       "sudo sed -i \"s/database_name_here/${digitalocean_database_cluster.wordpress_db.database}/\" /var/www/html/wp-config.php",
#       "sudo sed -i \"s/username_here/${digitalocean_database_cluster.wordpress_db.username}/\" /var/www/html/wp-config.php",
#       "sudo sed -i \"s/password_here/${digitalocean_database_cluster.wordpress_db.password}/\" /var/www/html/wp-config.php",
#       "sudo sed -i \"s/localhost/${digitalocean_database_cluster.wordpress_db.host}/\" /var/www/html/wp-config.php",
#       # Add security keys (generate these securely)
#       "sudo sed -i \"s/put your unique phrase here/$(openssl rand -base64 32)/\" /var/www/html/wp-config.php",
#       "sudo sed -i \"s/put your unique phrase here/$(openssl rand -base64 32)/\" /var/www/html/wp-config.php",
#       # ... repeat for other security keys ...
#       "sudo systemctl restart nginx",
#       "sudo systemctl restart php8.1-fpm"
#     ]
#
#     connection {
#       type        = "ssh"
#       user        = "root" # Or a user with sudo privileges
#       private_key = file("~/.ssh/id_rsa") # Path to your private SSH key
#       host        = self.ipv4_address
#     }
#   }
# }

A more robust approach involves using Ansible. You would output the database credentials and then run an Ansible playbook that connects to the provisioned Droplets and performs the WordPress installation. This separates infrastructure provisioning from application deployment.

Deployment Workflow

The typical workflow for deploying this infrastructure using Terraform is as follows:

  • Initialize Terraform: Run terraform init in your project directory. This downloads the DigitalOcean provider and any other necessary plugins.
  • Review Plan: Run terraform plan. This command shows you exactly what resources Terraform will create, modify, or destroy. Carefully review this output to ensure it matches your expectations.
  • Apply Configuration: Run terraform apply. Terraform will prompt you to confirm the changes. Type yes to proceed with provisioning the infrastructure on DigitalOcean.
  • Destroy Resources: When you no longer need the infrastructure, run terraform destroy. This will tear down all the resources created by Terraform, preventing unnecessary costs.

By using Infrastructure as Code with Terraform, you gain version control over your infrastructure, enable repeatable deployments, and significantly reduce the risk of manual configuration errors. This approach is fundamental for building scalable, secure, and resilient cloud environments.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing thread pools deadlock during concurrent ActiveRecord transaction processing on Linode Servers
  • Securing Your E-commerce APIs: Preventing SQL Injection (SQLi) in customized checkout queries in WooCommerce Implementations
  • Disaster Recovery 101: Architecting Auto-Failovers for MySQL and Ruby Deployments on Linode
  • High-Throughput Caching Strategies: Scaling MySQL for Perl Application APIs
  • Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Laravel Deployments on DigitalOcean

Copyright © 2026 · Vinay Vengala