Infrastructure as Code: Provisioning Secure Shopify 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 obtaining API credentials and specifying them in your Terraform configuration. It’s crucial to manage these credentials securely, ideally using environment variables or a dedicated secrets management system rather than hardcoding them directly into your Terraform files.
The OVHcloud provider requires the following environment variables to be set:
OVH_ENDPOINT: The API endpoint for your region (e.g.,ovh-eu,ovh-us,ovh-ca).OVH_APPLICATION_KEY: Your OVHcloud API application key.OVH_APPLICATION_SECRET: Your OVHcloud API application secret.OVH_CONSUMER_KEY: Your OVHcloud API consumer key.
Here’s how you would typically define the provider in your main.tf file. Note that we are not explicitly setting the credentials here, relying on environment variables for security.
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
# For example: export OVH_ENDPOINT="ovh-eu"
}
Designing the Shopify Cluster Architecture
A secure Shopify cluster on OVH typically involves several components: a load balancer, multiple web servers (running Nginx and PHP-FPM), a robust database (e.g., Percona XtraDB Cluster or MariaDB Galera Cluster), and potentially a caching layer (like Redis or Memcached). For this example, we’ll focus on provisioning the core web tier and a managed database service.
We’ll use OVHcloud’s Public Cloud Instances for the web servers and their managed PostgreSQL service for the database. Security best practices dictate using private networks (VPC) for inter-instance communication and restricting public access to only the load balancer.
Terraform Module for Web Servers
Let’s define a reusable Terraform module for our web servers. This module will create an instance, configure networking, and set up basic security groups.
Create a directory named modules/webserver and add the following files:
modules/webserver/main.tf:
resource "ovh_compute_instance" "shopify_web" {
name = "${var.environment}-shopify-web-${count.index}"
image = "ubuntu_20_04" # Or your preferred OS image
flavor = var.instance_flavor
region = var.region
network_id = var.private_network_id
ssh_key_names = [var.ssh_key_name]
user_data = file("${path.module}/scripts/setup_webserver.sh")
count = var.instance_count
lifecycle {
create_before_destroy = true
}
tags = {
Environment = var.environment
Role = "shopify-web"
}
}
resource "ovh_compute_instance_public_ip" "shopify_web_public_ip" {
count = var.assign_public_ip ? var.instance_count : 0
instance_id = ovh_compute_instance.shopify_web[count.index].id
}
output "webserver_ips" {
description = "Public IPs of the web servers"
value = [for i, ip in ovh_compute_instance_public_ip.shopify_web_public_ip : ip.address]
}
output "webserver_private_ips" {
description = "Private IPs of the web servers"
value = [for instance in ovh_compute_instance.shopify_web : instance.private_ip]
}
modules/webserver/variables.tf:
variable "environment" {
description = "Deployment environment (e.g., prod, staging)"
type = string
}
variable "region" {
description = "OVHcloud region for deployment"
type = string
}
variable "instance_flavor" {
description = "Flavor of the instances (e.g., 's1-2', 'c2-7')"
type = string
}
variable "instance_count" {
description = "Number of web server instances"
type = number
default = 2
}
variable "private_network_id" {
description = "ID of the private network (VPC) to attach instances to"
type = string
}
variable "ssh_key_name" {
description = "Name of the SSH key to use for instance access"
type = string
}
variable "assign_public_ip" {
description = "Whether to assign a public IP to each instance"
type = bool
default = false # Typically false, managed by LB
}
modules/webserver/scripts/setup_webserver.sh (This is a simplified example; a production setup would be more robust):
#!/bin/bash set -euxo pipefail # Update packages and install Nginx, PHP-FPM, and necessary extensions apt-get update -y apt-get install -y nginx php-fpm php-mysql php-mbstring php-xml php-curl php-zip unzip # Configure Nginx cat </etc/nginx/sites-available/shopify server { listen 80 default_server; listen [::]:80 default_server; root /var/www/html; # Your Shopify root directory index index.php index.html index.htm; server_name _; # Catch all location / { try_files \$uri \$uri/ /index.php?\$query_string; } 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 hidden files location ~ /\.ht { deny all; } } EOF # Remove default Nginx site rm /etc/nginx/sites-enabled/default # Enable the new Shopify site ln -s /etc/nginx/sites-available/shopify /etc/nginx/sites-enabled/shopify # Restart Nginx and PHP-FPM systemctl restart nginx systemctl restart php8.1-fpm # Adjust PHP version if needed # Basic security hardening (e.g., firewall rules - requires ufw to be installed) # apt-get install -y ufw # ufw allow ssh # ufw allow http # ufw allow https # ufw enable
Provisioning the Database
For a production Shopify cluster, a managed database service is highly recommended. OVHcloud offers managed PostgreSQL. We’ll provision a PostgreSQL instance within our private network.
resource "ovh_db_instance" "shopify_db" {
engine = "postgresql"
version = "13" # Specify desired PostgreSQL version
plan_name = "ESSENTIAL" # Or "PROFESSIONAL" for higher performance/availability
region = var.region
network_type = "private" # Crucial for security
name = "${var.environment}-shopify-db"
description = "PostgreSQL for Shopify cluster"
storage_size = 50 # GB
storage_type = "SSD"
public = false # Ensure it's not publicly accessible
# For ESSENTIAL plan, replicas are not available. For PROFESSIONAL, consider replicas.
# replicas = 1 # Only for PROFESSIONAL plan
tags = {
Environment = var.environment
Role = "shopify-db"
}
}
output "db_connection_info" {
description = "Database connection details"
value = {
host = ovh_db_instance.shopify_db.hostname
port = ovh_db_instance.shopify_db.port
username = ovh_db_instance.shopify_db.username
password = ovh_db_instance.shopify_db.password # Handle password securely!
}
sensitive = true # Mark as sensitive to prevent logging
}
Load Balancer Configuration
A load balancer is essential for distributing traffic and providing a single entry point. OVHcloud’s Public Cloud Load Balancer can be provisioned via Terraform. We’ll configure it to forward HTTP/HTTPS traffic to our web servers.
resource "ovh_cloud_load_balancer" "shopify_lb" {
name = "${var.environment}-shopify-lb"
description = "Load balancer for Shopify cluster"
region = var.region
public_network_enabled = true # Make it accessible from the internet
# Define frontend for HTTP
frontend {
name = "http"
port = 80
default_backend_id = ovh_cloud_load_balancer_backend.shopify_web_backend.id
protocol = "http"
}
# Define frontend for HTTPS (if SSL is terminated at LB)
# frontend {
# name = "https"
# port = 443
# default_backend_id = ovh_cloud_load_balancer_backend.shopify_web_backend.id
# ssl_certificate = file("path/to/your/certificate.pem") # Load your SSL cert
# ssl_private_key = file("path/to/your/private_key.pem") # Load your private key
# protocol = "https"
# }
}
resource "ovh_cloud_load_balancer_backend" "shopify_web_backend" {
name = "shopify-web-backend"
port = 80 # Port Nginx listens on
protocol = "http"
# Health check configuration
health_check {
path = "/"
port = 80
interval = 5000 # milliseconds
timeout = 1000 # milliseconds
method = "GET"
status = 200
}
# Add web servers to the backend pool
dynamic "load_balancer_servers" {
for_each = ovh_compute_instance.shopify_web # Reference instances from the webserver module
content {
address = load_balancer_servers.value.private_ip
status = "UP" # Initial status, health checks will manage this
port = 80
}
}
}
output "load_balancer_ip" {
description = "Public IP address of the load balancer"
value = ovh_cloud_load_balancer.shopify_lb.public_ip
}
Networking and Security Groups
Proper network segmentation and firewall rules are paramount for security. We’ll create a private network (VPC) and ensure instances only communicate within this network, with the load balancer being the only public-facing component.
resource "ovh_compute_network_private" "shopify_vpc" {
name = "${var.environment}-shopify-vpc"
region = var.region
detach = false # Keep network even if no instances are attached
vlan_id = 2000 # Choose an available VLAN ID
description = "Private network for Shopify cluster"
}
# Security group for web servers: Allow traffic from LB and within VPC
resource "ovh_compute_security_group" "shopify_web_sg" {
name = "${var.environment}-shopify-web-sg"
region = var.region
description = "Security group for Shopify web servers"
network_id = ovh_compute_network_private.shopify_vpc.id
}
resource "ovh_compute_security_group_rule" "web_allow_http_from_lb" {
security_group_id = ovh_compute_security_group.shopify_web_sg.id
direction = "in"
protocol = "tcp"
port_min = 80
port_max = 80
remote_ip_prefix = ovh_cloud_load_balancer.shopify_lb.public_ip # Restrict to LB IP
description = "Allow HTTP from Load Balancer"
}
resource "ovh_compute_security_group_rule" "web_allow_ssh_from_trusted" {
security_group_id = ovh_compute_security_group.shopify_web_sg.id
direction = "in"
protocol = "tcp"
port_min = 22
port_max = 22
remote_ip_prefix = "YOUR_TRUSTED_IP_RANGE/32" # e.g., "192.168.1.0/24" or your office IP
description = "Allow SSH from trusted network"
}
resource "ovh_compute_security_group_rule" "web_allow_internal_traffic" {
security_group_id = ovh_compute_security_group.shopify_web_sg.id
direction = "in"
protocol = "all" # Allow all internal traffic within the VPC
port_min = 0
port_max = 65535
remote_ip_prefix = ovh_compute_network_private.shopify_vpc.cidr # Use VPC CIDR
description = "Allow all internal traffic within VPC"
}
# Security group for database: Allow traffic only from web servers
resource "ovh_compute_security_group" "shopify_db_sg" {
name = "${var.environment}-shopify-db-sg"
region = var.region
description = "Security group for Shopify database"
network_id = ovh_compute_network_private.shopify_vpc.id
}
resource "ovh_compute_security_group_rule" "db_allow_postgres_from_web" {
security_group_id = ovh_compute_security_group.shopify_db_sg.id
direction = "in"
protocol = "tcp"
port_min = 5432 # Default PostgreSQL port
port_max = 5432
remote_ip_prefix = ovh_compute_network_private.shopify_vpc.cidr # Allow from VPC
description = "Allow PostgreSQL from within VPC"
}
# Associate security groups with instances
# This requires modifying the ovh_compute_instance resource in the webserver module
# to include: security_group_ids = [ovh_compute_security_group.shopify_web_sg.id]
# And for the database, if it were an instance:
# security_group_ids = [ovh_compute_security_group.shopify_db_sg.id]
# Note: Managed DBs might not directly support SG association in the same way.
# Network ACLs or specific DB security settings would apply.
# For OVH Managed DBs, network type 'private' and ensuring it's not public is key.
Important Security Note: The remote_ip_prefix for SSH access should be as restrictive as possible. Avoid using 0.0.0.0/0. For the database, restricting access to only the VPC CIDR is crucial. If using managed services, consult OVHcloud’s documentation for specific network access control mechanisms.
Putting It All Together: Root Module
Now, let’s define the root module (e.g., in main.tf in your project root) that orchestrates these resources.
# main.tf
# --- Variables ---
variable "region" {
description = "OVHcloud region"
type = string
default = "GRA" # Example: Gravelines, France
}
variable "environment" {
description = "Deployment environment"
type = string
default = "staging"
}
variable "web_instance_flavor" {
description = "Flavor for web server instances"
type = string
default = "s1-2" # Example: 2 vCPU, 4 GB RAM
}
variable "web_instance_count" {
description = "Number of web server instances"
type = number
default = 3
}
variable "ssh_key_name" {
description = "Name of the SSH key registered in OVHcloud"
type = string
# Example: "my-prod-key" - Ensure this key exists in your OVHcloud account
}
# --- Networking ---
resource "ovh_compute_network_private" "shopify_vpc" {
name = "${var.environment}-shopify-vpc"
region = var.region
detach = false
vlan_id = 2000
description = "Private network for Shopify cluster"
}
# --- Modules ---
module "webservers" {
source = "./modules/webserver"
environment = var.environment
region = var.region
instance_flavor = var.web_instance_flavor
instance_count = var.web_instance_count
private_network_id = ovh_compute_network_private.shopify_vpc.id
ssh_key_name = var.ssh_key_name
assign_public_ip = false # Let LB handle public access
}
# --- Database ---
resource "ovh_db_instance" "shopify_db" {
engine = "postgresql"
version = "13"
plan_name = "ESSENTIAL" # Or "PROFESSIONAL"
region = var.region
network_type = "private"
name = "${var.environment}-shopify-db"
description = "PostgreSQL for Shopify cluster"
storage_size = 50
storage_type = "SSD"
public = false
tags = {
Environment = var.environment
Role = "shopify-db"
}
}
# --- Load Balancer ---
resource "ovh_cloud_load_balancer" "shopify_lb" {
name = "${var.environment}-shopify-lb"
description = "Load balancer for Shopify cluster"
region = var.region
public_network_enabled = true
frontend {
name = "http"
port = 80
default_backend_id = ovh_cloud_load_balancer_backend.shopify_web_backend.id
protocol = "http"
}
# Add HTTPS frontend if needed
}
resource "ovh_cloud_load_balancer_backend" "shopify_web_backend" {
name = "shopify-web-backend"
port = 80
protocol = "http"
health_check {
path = "/"
port = 80
interval = 5000
timeout = 1000
method = "GET"
status = 200
}
# Dynamically add servers from the webserver module
load_balancer_servers {
address = module.webservers.webserver_private_ips[0] # Example for first server
port = 80
}
load_balancer_servers {
address = module.webservers.webserver_private_ips[1] # Example for second server
port = 80
}
# Add more servers based on module.webservers.webserver_private_ips if instance_count > 2
# A more robust approach would use a for_each loop if the LB resource supported it directly for servers.
# As of current OVH provider, dynamic blocks for servers are not directly supported in the same way.
# You might need to manually list them or use a null_resource with a provisioner for complex dynamic additions.
# For simplicity here, we'll list them explicitly for a fixed count.
# If instance_count is dynamic, consider a data source or a provisioner.
}
# --- Outputs ---
output "load_balancer_ip" {
description = "Public IP address of the load balancer"
value = ovh_cloud_load_balancer.shopify_lb.public_ip
}
output "db_host" {
description = "Database hostname"
value = ovh_db_instance.shopify_db.hostname
}
output "db_port" {
description = "Database port"
value = ovh_db_instance.shopify_db.port
}
output "db_username" {
description = "Database username"
value = ovh_db_instance.shopify_db.username
}
output "db_password" {
description = "Database password"
value = ovh_db_instance.shopify_db.password
sensitive = true
}
To apply this configuration:
- Ensure your OVHcloud API credentials are set as environment variables (
OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY). - Ensure your SSH key is registered in your OVHcloud account and its name is correctly set in the
ssh_key_namevariable. - Run
terraform initto download the OVH provider. - Run
terraform planto review the planned changes. - Run
terraform applyto provision the infrastructure.
Post-Provisioning Steps
After Terraform has successfully provisioned the infrastructure, you’ll need to:
- Configure your Shopify application to use the database credentials output by Terraform.
- Upload your Shopify application code to the web servers (e.g., via SCP, rsync, or a CI/CD pipeline). Ensure the web root (e.g.,
/var/www/html) is correctly set up. - Configure Nginx and PHP-FPM on the web servers as needed for your specific Shopify setup. The provided
setup_webserver.shis a basic template. - If using HTTPS, upload your SSL certificate and key to the load balancer configuration (or manage SSL termination on the web servers).
- Set up monitoring and alerting for your instances, load balancer, and database.
This Terraform configuration provides a solid, secure foundation for a Shopify cluster on OVHcloud, leveraging Infrastructure as Code principles for repeatability and manageability.