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-cliafter 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_dbdatabase and grant privileges to thewp_adminuser 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_urlin 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:
- 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"
- Initialize Terraform: Navigate to your Terraform project directory and run:
terraform init
- 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"
- 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.