• 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 OVH Using Terraform

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

OVHcloud Provider Configuration for Terraform

To provision resources on OVHcloud using Terraform, we first need to configure the OVHcloud provider. This involves specifying your OVHcloud API credentials. It’s crucial to manage these credentials securely, avoiding hardcoding them directly in your Terraform configuration files. Environment variables are a common and recommended approach.

The OVHcloud provider requires the following environment variables to be set:

  • OVH_ENDPOINT: The API endpoint for your OVHcloud region (e.g., ovh-eu, ovh-us, ca.api.ovh.com).
  • OVH_APPLICATION_KEY: Your OVHcloud application key.
  • OVH_APPLICATION_SECRET: Your OVHcloud application secret.
  • OVH_CONSUMER_KEY: Your OVHcloud consumer key.

Here’s how you would define the provider in your Terraform configuration (e.g., in a providers.tf file):

providers.tf

# providers.tf

terraform {
  required_providers {
    ovh = {
      source  = "ovh/ovh"
      version = "~> 1.0" # Specify a version constraint
    }
  }
}

provider "ovh" {
  # Credentials will be read from environment variables:
  # OVH_ENDPOINT, OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY
}

To obtain your API credentials, you’ll need to create an application within the OVHcloud control panel under “API Credentials”. Ensure the application has the necessary read/write permissions for the resources you intend to manage.

Designing the WordPress Cluster Architecture

A robust WordPress cluster requires several components: a load balancer, multiple web servers, a managed database, and potentially a caching layer. For this setup, we’ll leverage OVHcloud’s Public Cloud services:

  • Load Balancer: OVHcloud Load Balancer (LBaaS) to distribute traffic across web servers.
  • Web Servers: Instances running a web server (e.g., Nginx) and PHP-FPM, serving WordPress. We’ll use Auto-Scaling Groups for dynamic scaling.
  • Database: OVHcloud Managed Database for PostgreSQL or MySQL. For simplicity in this example, we’ll assume a single instance, but a highly available setup would involve replication.
  • Storage: Object Storage (S3 compatible) for media uploads, decoupling from individual web server instances.
  • Networking: A Virtual Private Cloud (VPC) to isolate resources, with public IPs for the load balancer and private IPs for internal communication.

Terraform Configuration for Core Infrastructure

Let’s start by defining the network and the database. This forms the foundation of our cluster.

network.tf

# network.tf

resource "ovh_cloud_project_network_private" "wp_vpc" {
  service_name = var.ovh_service_name
  name         = "wp-cluster-vpc"
  region       = var.ovh_region
  subnet {
    name     = "wp-subnet-public"
    cidr     = "10.0.1.0/24"
    dhcp     = true
    gateway  = "10.0.1.254"
    no_gateway = false
  }
  subnet {
    name     = "wp-subnet-private"
    cidr     = "10.0.2.0/24"
    dhcp     = true
    gateway  = "10.0.2.254"
    no_gateway = false
  }
}

resource "ovh_cloud_project_network_public_ip" "lb_ip" {
  service_name = var.ovh_service_name
  region       = var.ovh_region
  subnet_id    = ovh_cloud_project_network_private.wp_vpc.subnet[0].id # Public subnet
}

database.tf

# database.tf

resource "ovh_db_service" "wp_db" {
  service_name = var.ovh_service_name
  plan_name    = "default" # Or choose a more appropriate plan
  version      = "14"      # Specify desired PostgreSQL version
  region       = var.ovh_region
  description  = "Managed PostgreSQL for WordPress cluster"
}

resource "ovh_db_user" "wp_user" {
  service_name = ovh_db_service.wp_db.id
  username     = "wp_admin"
  password     = random_password.db_password.result
}

resource "random_password" "db_password" {
  length           = 16
  special          = true
  override_special = "_%@"
}

# Note: For production, consider setting up read replicas and proper access control.
# This example uses a single instance for simplicity.

variables.tf

# variables.tf

variable "ovh_service_name" {
  description = "The OVHcloud Public Cloud service name."
  type        = string
}

variable "ovh_region" {
  description = "The OVHcloud region for deployment."
  type        = string
  default     = "GRA" # Example: Gravelines, France
}

variable "instance_flavor" {
  description = "The flavor for the web server instances."
  type        = string
  default     = "s1-2" # Example: General purpose, 2 vCPU, 4GB RAM
}

variable "instance_image" {
  description = "The image for the web server instances."
  type        = string
  default     = "ubuntu-2004" # Example: Ubuntu 20.04 LTS
}

variable "instance_ssh_key_name" {
  description = "The name of the SSH key to use for instances."
  type        = string
}

variable "load_balancer_name" {
  description = "Name for the OVHcloud Load Balancer."
  type        = string
  default     = "wp-cluster-lb"
}

variable "db_instance_name" {
  description = "Name for the Managed Database instance."
  type        = string
  default     = "wp-cluster-db"
}

Provisioning Web Servers with Auto-Scaling

We’ll use OVHcloud’s Instance Auto-Scaling Group (ASG) to manage our web servers. This allows us to automatically adjust the number of instances based on load, ensuring high availability and cost-efficiency. We’ll also define a user data script to bootstrap our instances with Nginx, PHP-FPM, and WordPress.

webservers.tf

# webservers.tf

resource "ovh_cloud_project_instance_user_data" "wp_bootstrap" {
  service_name = var.ovh_service_name
  content = templatefile("${path.module}/scripts/bootstrap.sh.tpl", {
    db_host     = ovh_db_service.wp_db.host
    db_port     = ovh_db_service.wp_db.port
    db_name     = "wordpress_db" # Define this in variables or outputs
    db_user     = ovh_db_user.wp_user.username
    db_password = random_password.db_password.result
    wp_site_url = "http://${ovh_cloud_project_network_public_ip.lb_ip.address}" # Placeholder, will be updated
    aws_access_key_id     = var.aws_access_key_id     # For S3 media
    aws_secret_access_key = var.aws_secret_access_key # For S3 media
    aws_s3_bucket_name    = var.aws_s3_bucket_name    # For S3 media
    aws_s3_region         = var.aws_s3_region         # For S3 media
  })
}

resource "ovh_cloud_project_instance_group" "wp_asg" {
  service_name = var.ovh_service_name
  name         = "wp-webservers"
  region       = var.ovh_region
  instance {
    flavor_id = var.instance_flavor
    image_id  = var.instance_image
    ssh_key_name = var.instance_ssh_key_name
    public_cloud_network_id = ovh_cloud_project_network_private.wp_vpc.id
    user_data = ovh_cloud_project_instance_user_data.wp_bootstrap.content
    boot_id = ovh_cloud_project_instance_user_data.wp_bootstrap.id
  }
  # Auto-scaling configuration
  autoscaling {
    min_instances = 2
    max_instances = 10
    scale_down_unhealthy_threshold = 3
    scale_down_period_in_seconds = 300
    scale_up_unhealthy_threshold = 3
    scale_up_period_in_seconds = 300
    scale_up_cooldown_in_seconds = 600
    scale_down_cooldown_in_seconds = 600
  }
  # We'll attach this to the load balancer later
  load_balancer_id = ovh_cloud_project_loadbalancer.wp_lb.id
  load_balancer_port = 80
  load_balancer_protocol = "http"
}

# Placeholder for S3 media storage configuration (requires separate setup)
variable "aws_access_key_id" {
  description = "AWS Access Key ID for S3 media storage."
  type        = string
  sensitive   = true
}

variable "aws_secret_access_key" {
  description = "AWS Secret Access Key for S3 media storage."
  type        = string
  sensitive   = true
}

variable "aws_s3_bucket_name" {
  description = "Name of the S3 bucket for WordPress media."
  type        = string
}

variable "aws_s3_region" {
  description = "AWS region for the S3 bucket."
  type        = string
  default     = "us-east-1"
}

scripts/bootstrap.sh.tpl

#!/bin/bash
set -euxo pipefail

# Update system and install necessary packages
apt-get update -y
apt-get upgrade -y
apt-get install -y nginx php-fpm php-mysql php-gd php-xml php-mbstring php-curl unzip wget curl

# Configure Nginx
cat > /etc/nginx/sites-available/wordpress <<EOF
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/html;
    index index.php index.html index.htm;

    server_name _;

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

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock; # Adjust PHP version if needed
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

    # Enable S3 media offload (requires wp-cli and S3-offload plugin)
    location ~* ^/wp-content/uploads/(.*)\.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 30d;
        add_header Cache-Control "public, no-transform";
        # This is a placeholder. Actual S3 integration requires wp-cli and a plugin.
        # Example: rewrite ^/wp-content/uploads/(.*)$ /s3-offload/$1 break;
    }
}
EOF

# Enable Nginx site and restart
ln -sf /etc/nginx/sites-available/wordpress /etc/nginx/sites-enabled/
systemctl restart nginx
systemctl enable nginx

# Configure PHP-FPM (adjust as needed for performance)
sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' /etc/php/7.4/fpm/php.ini # Adjust PHP version
sed -i 's/memory_limit = .*/memory_limit = 256M/' /etc/php/7.4/fpm/php.ini
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php/7.4/fpm/php.ini
sed -i 's/post_max_size = .*/post_max_size = 64M/' /etc/php/7.4/fpm/php.ini
systemctl restart php7.4-fpm # Adjust PHP version

# Download and configure WordPress
WP_PATH="/var/www/html"
mkdir -p $WP_PATH
cd $WP_PATH

# Download WordPress if not already present (e.g., on first boot)
if [ ! -f "$WP_PATH/wp-config-sample.php" ]; then
    wget https://wordpress.org/latest.tar.gz
    tar -xzf latest.tar.gz
    mv wordpress/* .
    rm -rf wordpress latest.tar.gz
fi

# Configure wp-config.php
if [ ! -f "$WP_PATH/wp-config.php" ]; then
    cp wp-config-sample.php wp-config.php
    sed -i "s/database_name_here/${DB_NAME}/" wp-config.php
    sed -i "s/username_here/${DB_USER}/" wp-config.php
    sed -i "s/password_here/${DB_PASSWORD}/" wp-config.php
    sed -i "s/localhost/${DB_HOST}:${DB_PORT}/" wp-config.php

    # Generate unique salts and keys
    curl -s https://api.wordpress.org/secret-key/1.1/salt/ >> wp-config.php
fi

# Set correct permissions
chown -R www-data:www-data $WP_PATH
chmod -R 755 $WP_PATH

# Install WP-CLI for potential future management and S3 integration
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

# Configure S3 media offload (requires manual plugin installation and configuration via wp-cli)
# Example: wp plugin install s3-upload-middleware --activate --path=$WP_PATH
# Example: wp s3-upload-middleware set-bucket $AWS_S3_BUCKET_NAME --region=$AWS_S3_REGION --access-key-id=$AWS_ACCESS_KEY_ID --secret-access-key=$AWS_SECRET_ACCESS_KEY --path=$WP_PATH
# Note: These commands are illustrative and might require adjustments based on the specific plugin.
# It's often better to manage plugin installation and configuration outside of bootstrap for complex setups.

# Ensure WordPress database is created (this is a simplified approach)
# A more robust solution would involve a separate script or manual creation.
# For now, we rely on the user to create the database manually or via a separate Terraform resource.
echo "WordPress bootstrap script finished. Please ensure the database '${DB_NAME}' exists and the user '${DB_USER}' has access."

Important Notes on the Bootstrap Script:

  • The script assumes a PostgreSQL database. If using MySQL, adjust PHP extensions and connection strings accordingly.
  • The PHP version (php7.4-fpm.sock) might need to be updated based on the chosen OS image.
  • The S3 media offload configuration is a placeholder. You’ll need to install a suitable WordPress plugin (e.g., S3 Uploads, WP Offload Media Lite) and configure it, likely using wp-cli after the instance is provisioned. This can be done via a separate Terraform resource or a post-provisioning script.
  • Database creation is not automated here. You’ll need to create the wordpress_db database and grant privileges to the wp_admin user on the managed database instance. This can be done manually or via a separate Terraform resource for the database user and schema.
  • The wp_site_url in the bootstrap script is a placeholder. It will be dynamically set by the load balancer’s IP.

Configuring the Load Balancer

The OVHcloud Load Balancer will direct incoming traffic to the web server instances. We need to define the load balancer itself, a frontend listener, and a backend pool that targets our auto-scaling group.

loadbalancer.tf

# loadbalancer.tf

resource "ovh_cloud_project_loadbalancer" "wp_lb" {
  service_name = var.ovh_service_name
  name         = var.load_balancer_name
  region       = var.ovh_region
  public_ip_id = ovh_cloud_project_network_public_ip.lb_ip.id
}

resource "ovh_cloud_project_loadbalancer_frontend" "wp_frontend" {
  service_name    = ovh_cloud_project_loadbalancer.wp_lb.id
  name            = "wp-http"
  port            = 80
  protocol        = "http"
  default_ssl_id  = "" # Add SSL configuration here for HTTPS
  allowed_sources = ["0.0.0.0/0"]
}

resource "ovh_cloud_project_loadbalancer_backend" "wp_backend" {
  service_name = ovh_cloud_project_loadbalancer.wp_lb.id
  name         = "wp-webservers-backend"
  port         = 80
  protocol     = "http"
  method       = "roundrobin" # Or 'leastconn'
  # Health check configuration
  health_check {
    port     = 80
    protocol = "http"
    path     = "/"
    interval = 5 # seconds
    timeout  = 3 # seconds
    unhealthy_threshold = 3
    healthy_threshold   = 2
  }
}

# Link frontend to backend
resource "ovh_cloud_project_loadbalancer_frontend_backend" "wp_frontend_to_backend" {
  service_name = ovh_cloud_project_loadbalancer.wp_lb.id
  frontend_id  = ovh_cloud_project_loadbalancer_frontend.wp_frontend.id
  backend_id   = ovh_cloud_project_loadbalancer_backend.wp_backend.id
}

# Note: The instance group needs to be associated with the load balancer.
# This is done within the ovh_cloud_project_instance_group resource using `load_balancer_id` and `load_balancer_port`.
# The instance group will automatically register its instances with the backend pool.

Putting It All Together: Main Configuration and Outputs

A root main.tf file will orchestrate these resources. We’ll also define outputs for easy access to critical information.

main.tf

# main.tf

provider "ovh" {
  # Credentials are read from environment variables
}

# --- Network Resources ---
module "network" {
  source = "./network" # Assuming network.tf is in a 'network' subdirectory
  ovh_service_name = var.ovh_service_name
  ovh_region       = var.ovh_region
}

# --- Database Resources ---
module "database" {
  source = "./database" # Assuming database.tf is in a 'database' subdirectory
  ovh_service_name = var.ovh_service_name
  ovh_region       = var.ovh_region
}

# --- Web Server Resources ---
module "webservers" {
  source = "./webservers" # Assuming webservers.tf is in a 'webservers' subdirectory
  ovh_service_name = var.ovh_service_name
  ovh_region       = var.ovh_region
  instance_flavor  = var.instance_flavor
  instance_image   = var.instance_image
  instance_ssh_key_name = var.instance_ssh_key_name
  db_host          = module.database.wp_db_host
  db_port          = module.database.wp_db_port
  db_name          = "wordpress_db" # Define this properly
  db_user          = module.database.wp_db_user
  db_password      = module.database.wp_db_password
  aws_access_key_id     = var.aws_access_key_id
  aws_secret_access_key = var.aws_secret_access_key
  aws_s3_bucket_name    = var.aws_s3_bucket_name
  aws_s3_region         = var.aws_s3_region
  load_balancer_id      = module.loadbalancer.wp_lb_id
}

# --- Load Balancer Resources ---
module "loadbalancer" {
  source = "./loadbalancer" # Assuming loadbalancer.tf is in a 'loadbalancer' subdirectory
  ovh_service_name = var.ovh_service_name
  ovh_region       = var.ovh_region
  load_balancer_name = var.load_balancer_name
  lb_public_ip_address = module.network.lb_public_ip_address
}

# --- Outputs ---
output "load_balancer_ip" {
  description = "The public IP address of the Load Balancer."
  value       = module.network.lb_public_ip_address
}

output "database_host" {
  description = "The hostname of the managed database."
  value       = module.database.wp_db_host
}

output "database_port" {
  description = "The port of the managed database."
  value       = module.database.wp_db_port
}

output "database_user" {
  description = "The username for the managed database."
  value       = module.database.wp_db_user
}

output "database_password" {
  description = "The password for the managed database."
  value       = module.database.wp_db_password
  sensitive   = true
}

You would then organize your Terraform files into modules (e.g., ./network, ./database, ./webservers, ./loadbalancer) for better maintainability. Each module would contain its respective .tf files and potentially a variables.tf and outputs.tf.

Deployment Workflow

To deploy this infrastructure:

  1. Set Environment Variables: Ensure your OVHcloud API credentials and any other required secrets (like AWS keys for S3) are set as environment variables.

Example:

export OVH_ENDPOINT="ovh-eu"
export OVH_APPLICATION_KEY="YOUR_APP_KEY"
export OVH_APPLICATION_SECRET="YOUR_APP_SECRET"
export OVH_CONSUMER_KEY="YOUR_CONSUMER_KEY"
export AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_KEY"
  1. Initialize Terraform: Navigate to your Terraform project directory and run:
terraform init
  1. Review the Plan: Generate an execution plan to see what Terraform will create, modify, or destroy. You’ll need to provide values for your custom variables (e.g., ovh_service_name, instance_ssh_key_name).
terraform plan \
  -var="ovh_service_name=your-ovh-service-name" \
  -var="instance_ssh_key_name=your-ssh-key-name" \
  -var="aws_s3_bucket_name=your-wordpress-media-bucket"
  1. Apply the Configuration: If the plan looks correct, apply it to provision the infrastructure.
terraform apply \
  -var="ovh_service_name=your-ovh-service-name" \
  -var="instance_ssh_key_name=your-ssh-key-name" \
  -var="aws_s3_bucket_name=your-wordpress-media-bucket"

After the apply completes, Terraform will output the Load Balancer’s IP address. You can then access your WordPress installation via this IP. Remember to configure your DNS records to point to this IP address.

Security Considerations and Next Steps

This Terraform configuration provides a foundation for a secure WordPress cluster. However, several security aspects require further attention:

  • HTTPS/SSL: The load balancer configuration lacks SSL termination. You should configure SSL certificates on the OVHcloud Load Balancer for secure HTTPS traffic.
  • Database Security: The managed database should be configured with stricter access controls. Limit inbound traffic to only the VPC subnet where your web servers reside. Consider using private IPs for database access if possible.
  • Firewall Rules: Implement security groups or firewall rules within your OVHcloud VPC to restrict traffic to only necessary ports (e.g., 80/443 for web servers, specific DB port for web servers to DB).
  • Instance Hardening: The bootstrap script installs necessary packages but doesn’t perform extensive system hardening. Consider using tools like CIS benchmarks or custom security scripts to harden your instances.
  • Secrets Management: While environment variables are used for Terraform credentials, consider more robust secrets management solutions like HashiCorp Vault for sensitive application secrets (e.g., WordPress salts, API keys).
  • S3 Media Offload Plugin: Ensure the chosen S3 media offload plugin is correctly configured and secured.
  • WordPress Security Plugins: Install and configure security plugins within WordPress itself (e.g., Wordfence, Sucuri) to protect against common web vulnerabilities.
  • Regular Updates: Automate or schedule regular updates for the OS, Nginx, PHP, and WordPress core/plugins/themes.

By leveraging Infrastructure as Code with Terraform, you can reliably and repeatedly provision secure, scalable WordPress clusters on OVHcloud, significantly reducing manual configuration errors and improving deployment velocity.

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 indexing lock conflicts and high CPU during bulk stock updates on DigitalOcean Servers
  • How to Debug and Fix memory leaks and socket exhaustion in daemon processes in Modern C++ Applications
  • Infrastructure as Code: Provisioning Secure PHP Clusters on DigitalOcean Using Terraform
  • Fixing Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Legacy Laravel Codebases Without Breaking API Contracts
  • An Auditor’s Checklist for Securing Laravel Backends on Google Cloud

Copyright © 2026 · Vinay Vengala