Infrastructure as Code: Provisioning Secure Magento 2 Clusters on OVH Using Terraform
OVH Provider Configuration for Terraform
To provision resources on OVHcloud using Terraform, we first need to configure the OVH provider. This involves specifying your OVH API credentials and the region where you intend to deploy your Magento 2 cluster. 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 configuration.
The OVH provider requires your Application Key, Application Secret, Consumer Key, and the endpoint for the OVH API. For production environments, it’s highly recommended to use a dedicated service user with restricted permissions.
Terraform Configuration File (provider.tf)
# provider.tf
terraform {
required_providers {
ovh = {
source = "ovh/ovh"
version = "~> 2.0" # Pin to a specific major version for stability
}
}
}
provider "ovh" {
# It is strongly recommended to use environment variables for sensitive credentials.
# export OVH_APPLICATION_KEY="YOUR_APP_KEY"
# export OVH_APPLICATION_SECRET="YOUR_APP_SECRET"
# export OVH_CONSUMER_KEY="YOUR_CONSUMER_KEY"
# export OVH_ENDPOINT="ovh-eu" # or "ovh-us", "ovh-ca"
application_key = var.ovh_application_key
application_secret = var.ovh_application_secret
consumer_key = var.ovh_consumer_key
endpoint = var.ovh_endpoint
}
variable "ovh_application_key" {
description = "OVH Application Key for API access."
type = string
sensitive = true
}
variable "ovh_application_secret" {
description = "OVH Application Secret for API access."
type = string
sensitive = true
}
variable "ovh_consumer_key" {
description = "OVH Consumer Key for API access."
type = string
sensitive = true
}
variable "ovh_endpoint" {
description = "OVH API endpoint (e.g., ovh-eu, ovh-us, ovh-ca)."
type = string
default = "ovh-eu"
}
To initialize Terraform with this configuration, you would typically run:
terraform init
And then apply the configuration, ensuring your environment variables are set:
export OVH_APPLICATION_KEY="your_app_key_here" export OVH_APPLICATION_SECRET="your_app_secret_here" export OVH_CONSUMER_KEY="your_consumer_key_here" terraform apply
Networking and Security Groups
A secure Magento 2 deployment necessitates a well-defined network infrastructure. This includes setting up a Virtual Private Cloud (VPC), subnets, and crucially, security groups to control inbound and outbound traffic. For Magento, we need to allow HTTP (80), HTTPS (443), SSH (22), and potentially other ports for database access or management tools.
VPC and Subnet Configuration (network.tf)
# network.tf
resource "ovh_cloud_project_network_private" "magento_vpc" {
service_name = var.ovh_service_name
name = "magento-vpc"
region = var.ovh_region
description = "VPC for Magento 2 cluster"
}
resource "ovh_cloud_project_network_subnet" "magento_subnet" {
service_name = var.ovh_service_name
network_id = ovh_cloud_project_network_private.magento_vpc.id
name = "magento-subnet-public"
region = var.ovh_region
cidr = "10.0.1.0/24" # Example CIDR block
gateway = "10.0.1.254"
dhcp_enabled = true
description = "Public subnet for Magento web servers"
}
# You might want a private subnet for databases or other backend services
resource "ovh_cloud_project_network_subnet" "magento_subnet_private" {
service_name = var.ovh_service_name
network_id = ovh_cloud_project_network_private.magento_vpc.id
name = "magento-subnet-private"
region = var.ovh_region
cidr = "10.0.2.0/24" # Example CIDR block
gateway = "10.0.2.254"
dhcp_enabled = true
description = "Private subnet for Magento database servers"
}
variable "ovh_service_name" {
description = "The name of your OVH cloud project."
type = string
}
variable "ovh_region" {
description = "The OVH region to deploy resources in (e.g., 'GRA1', 'BHS1')."
type = string
}
Security Group Rules (security.tf)
# security.tf
resource "ovh_cloud_project_security_group" "magento_sg" {
service_name = var.ovh_service_name
name = "magento-security-group"
region = var.ovh_region
description = "Security group for Magento 2 cluster"
network_id = ovh_cloud_project_network_private.magento_vpc.id
}
# Allow SSH access from a specific IP range (e.g., your office or bastion host)
resource "ovh_cloud_project_security_group_rule" "allow_ssh" {
service_name = var.ovh_service_name
security_group_id = ovh_cloud_project_security_group.magento_sg.id
region = var.ovh_region
protocol = "tcp"
port_min = 22
port_max = 22
cidr = "YOUR_TRUSTED_IP_RANGE/32" # e.g., "203.0.113.0/24" or "192.168.1.0/24"
direction = "ingress"
description = "Allow SSH access"
}
# Allow HTTP access from anywhere
resource "ovh_cloud_project_security_group_rule" "allow_http" {
service_name = var.ovh_service_name
security_group_id = ovh_cloud_project_security_group.magento_sg.id
region = var.ovh_region
protocol = "tcp"
port_min = 80
port_max = 80
cidr = "0.0.0.0/0"
direction = "ingress"
description = "Allow HTTP access"
}
# Allow HTTPS access from anywhere
resource "ovh_cloud_project_security_group_rule" "allow_https" {
service_name = var.ovh_service_name
security_group_id = ovh_cloud_project_security_group.magento_sg.id
region = var.ovh_region
protocol = "tcp"
port_min = 443
port_max = 443
cidr = "0.0.0.0/0"
direction = "ingress"
description = "Allow HTTPS access"
}
# Allow all outbound traffic (common for web servers to fetch updates, etc.)
resource "ovh_cloud_project_security_group_rule" "allow_outbound" {
service_name = var.ovh_service_name
security_group_id = ovh_cloud_project_security_group.magento_sg.id
region = var.ovh_region
protocol = "any"
port_min = 0
port_max = 0
cidr = "0.0.0.0/0"
direction = "egress"
description = "Allow all outbound traffic"
}
# You might want to restrict database access to only your web servers' subnet
# resource "ovh_cloud_project_security_group_rule" "allow_db_from_web" {
# service_name = var.ovh_service_name
# security_group_id = ovh_cloud_project_security_group.magento_sg.id
# region = var.ovh_region
# protocol = "tcp"
# port_min = 3306 # Default MySQL port
# port_max = 3306
# cidr = "10.0.1.0/24" # CIDR of your web server subnet
# direction = "ingress"
# description = "Allow MySQL access from web subnet"
# }
Important: Replace YOUR_TRUSTED_IP_RANGE/32 with your actual IP address or network range. For production, avoid 0.0.0.0/0 for SSH. Consider using a bastion host or VPN for secure administrative access.
Compute Instances for Magento 2
A typical Magento 2 cluster involves multiple instances: web servers (running Nginx/Apache, PHP-FPM), a database server (MySQL/MariaDB), and potentially caching layers (Redis, Varnish). We’ll provision these using OVH’s Public Cloud Instances.
Web Server Instances (webservers.tf)
# webservers.tf
resource "ovh_cloud_project_instance" "magento_web" {
count = var.web_server_count # e.g., 2 for HA
service_name = var.ovh_service_name
name = "magento-web-${count.index}"
region = var.ovh_region
flavor_name = var.web_server_flavor # e.g., "vps-ssd-2" or a specific instance type
image_name = "Debian 11" # Or your preferred OS image
ssh_key_name = var.ssh_key_name # Ensure this SSH key is uploaded to your OVH project
# Attach to the public subnet
network_id = ovh_cloud_project_network_subnet.magento_subnet.id
# Associate with the security group
security_group_ids = [ovh_cloud_project_security_group.magento_sg.id]
# User data for initial setup (e.g., installing Nginx, PHP, Magento prerequisites)
user_data = base64encode(templatefile("${path.module}/cloud-init/webserver.yaml", {
db_host = ovh_cloud_project_database.magento_db.private_connection_uri # Assuming DB is in private network
db_user = var.db_user
db_password = var.db_password
magento_root_dir = "/var/www/html/magento"
# Add other variables needed for cloud-init
}))
tags = {
environment = var.environment
role = "magento-web"
}
lifecycle {
create_before_destroy = true
}
}
variable "web_server_count" {
description = "Number of web server instances."
type = number
default = 2
}
variable "web_server_flavor" {
description = "Flavor name for web server instances."
type = string
default = "vps-ssd-2" # Example flavor
}
variable "ssh_key_name" {
description = "Name of the SSH key uploaded to OVH for instance access."
type = string
}
variable "environment" {
description = "Deployment environment (e.g., 'production', 'staging')."
type = string
default = "production"
}
Database Instance (database.tf)
For a production Magento 2 setup, using a managed database service is highly recommended for reliability and ease of management. OVH offers managed database services (e.g., MySQL, PostgreSQL).
# database.tf
resource "ovh_cloud_project_database" "magento_db" {
service_name = var.ovh_service_name
engine = "mysql"
version = "8.0" # Specify desired MySQL version
plan_name = "professional-1" # Choose an appropriate plan based on expected load
region = var.ovh_region
name = "magento-db"
description = "Managed MySQL database for Magento 2"
# Configure network access. For enhanced security, use private network access.
# Ensure your web servers are in the same VPC or have network connectivity.
network_type = "private" # or "public" if absolutely necessary, but less secure
# Optional: Configure backup settings
# backup_day_of_month = 1
# backup_hour = 3
tags = {
environment = var.environment
role = "magento-db"
}
}
# Note: Database credentials (username, password) are typically managed
# by the managed service and accessed via its API or console.
# Terraform can retrieve these if the provider supports it, or you might
# need to set them manually or via a separate process.
# For simplicity, we'll assume we can retrieve connection details.
# Example of retrieving connection details (may vary by provider version/features)
# You might need to create a user and grant privileges separately.
# The 'private_connection_uri' is a placeholder and might not be directly available
# without further configuration or specific provider resource.
# A common approach is to use the OVH API or CLI to get connection strings.
# For demonstration, let's assume we can get the hostname and port.
# In a real scenario, you'd likely use a data source or a separate script
# to fetch the full connection details after the database is provisioned.
# Example: Fetching database host and port (hypothetical, check OVH provider docs)
# data "ovh_cloud_project_database_info" "magento_db_info" {
# service_name = var.ovh_service_name
# db_id = ovh_cloud_project_database.magento_db.id
# }
# For now, we'll use variables for DB credentials that will be passed to cloud-init
variable "db_user" {
description = "Database username for Magento."
type = string
default = "magento_user"
sensitive = true
}
variable "db_password" {
description = "Database password for Magento."
type = string
sensitive = true
}
Caching Layer (Optional – Redis)
For performance, Redis is often used for Magento’s caching. You can provision a managed Redis instance or deploy it on a separate compute instance.
# cache.tf (Example for managed Redis)
resource "ovh_cloud_project_database" "magento_redis" {
service_name = var.ovh_service_name
engine = "redis"
version = "6.2" # Specify desired Redis version
plan_name = "cache-basic" # Choose an appropriate plan
region = var.ovh_region
name = "magento-redis"
description = "Managed Redis cache for Magento 2"
network_type = "private"
tags = {
environment = var.environment
role = "magento-cache"
}
}
# Similar to the database, you'll need to retrieve Redis connection details.
# This might involve fetching the host and port from the OVH API.
# For cloud-init, you'd pass these details.
Cloud-Init for Instance Configuration
Cloud-init is essential for automating the initial setup of your instances. This includes installing software, configuring services, and deploying your Magento application. Below is a conceptual example of a webserver.yaml file.
cloud-init/webserver.yaml Example
# cloud-init/webserver.yaml
#cloud-config
package_update: true
packages:
- nginx
- php8.1
- php8.1-fpm
- php8.1-mysql
- php8.1-xml
- php8.1-mbstring
- php8.1-zip
- php8.1-gd
- php8.1-curl
- composer
- git
- unzip
runcmd:
# Configure Nginx and PHP-FPM
- sed -i 's/user = www-data/user = nginx/' /etc/php/8.1/fpm/pool.d/www.conf
- sed -i 's/group = www-data/group = nginx/' /etc/php/8.1/fpm/pool.d/www.conf
- sed -i 's/;listen.owner = www-data/;listen.owner = nginx/' /etc/php/8.1/fpm/pool.d/www.conf
- sed -i 's/;listen.group = www-data/;listen.group = nginx/' /etc/php/8.1/fpm/pool.d/www.conf
- sed -i 's/;listen.mode = 0660/;listen.mode = 0660/' /etc/php/8.1/fpm/pool.d/www.conf
- systemctl enable nginx
- systemctl enable php8.1-fpm
- systemctl start nginx
- systemctl start php8.1-fpm
# Deploy Magento (This is a simplified example. Use a proper deployment strategy)
- mkdir -p ${magento_root_dir}
- chown nginx:nginx ${magento_root_dir}
- cd ${magento_root_dir}
# - git clone your-magento-repo . # Or use composer create-project
- composer create-project --repository-url=https://repo.magento.com/ magento/project-community-edition .
- chown -R nginx:nginx ${magento_root_dir}
# Magento CLI commands (requires database and cache to be ready)
# These commands are best run AFTER initial provisioning and configuration.
# Consider running them via a separate script or a deployment pipeline.
# - php bin/magento setup:install --db-host=${db_host} --db-name=magento_db --db-user=${db_user} --db-password=${db_password} --admin-user=admin --admin-password=your_secure_admin_password [email protected] --base-url=http://your-domain.com
# - php bin/magento setup:upgrade
# - php bin/magento cache:enable
# - php bin/magento deploy:mode:set production
write_files:
- path: /etc/nginx/sites-available/magento
permissions: '0644'
content: |
server {
listen 80;
server_name your-domain.com www.your-domain.com; # Replace with your domain
root ${magento_root_dir};
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ ^/index\.php/ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Adjust if using TCP socket
fastcgi_read_timeout 300; # Increase timeout for Magento operations
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
log_not_found off;
access_log off;
}
location /var/ {
deny all;
}
location /app/etc/ {
deny all;
}
}
- path: /etc/nginx/sites-enabled/magento
permissions: '0644'
content: |
# This is a symlink, content is managed by /etc/nginx/sites-available/magento
# Ensure the symlink is created correctly.
# A more robust approach is to use a template file for the symlink.
# For simplicity, we'll assume the symlink is created manually or via another step.
# A better cloud-init would handle symlinking:
# - ln -s /etc/nginx/sites-available/magento /etc/nginx/sites-enabled/magento
# - rm /etc/nginx/sites-enabled/default # Remove default Nginx site
# Ensure the default Nginx site is removed and the new one is enabled
# This part is tricky with cloud-init's sequential execution.
# It's often better to manage Nginx config via Ansible/Chef/Puppet after initial boot.
# However, for a basic setup:
runcmd:
- rm -f /etc/nginx/sites-enabled/default
- ln -s /etc/nginx/sites-available/magento /etc/nginx/sites-enabled/magento
- systemctl reload nginx
# Configure firewall (ufw example, adjust if using iptables directly)
# This assumes ufw is installed.
# - apt-get update && apt-get install -y ufw
# - ufw allow ssh
# - ufw allow http
# - ufw allow https
# - ufw enable
Note: The cloud-init script above is a starting point. A production deployment would require more robust handling of Magento installation, configuration, SSL setup, and potentially integration with a CDN and load balancer. The database and cache connection details need to be correctly passed and used. The private_connection_uri for the database is a placeholder; you’ll need to obtain the actual connection string or host/port/credentials from OVH’s managed service interface or API.
Load Balancer and DNS
For high availability and scalability, a load balancer is essential. OVH offers Load Balancer services. DNS configuration will point your domain to the load balancer’s IP address.
OVH Load Balancer (Conceptual)
Terraform can provision an OVH Load Balancer. This typically involves defining the load balancer resource, backend servers (your web instances), and frontend listeners (ports 80, 443).
# loadbalancer.tf (Conceptual Example)
resource "ovh_cloud_project_loadbalancer" "magento_lb" {
service_name = var.ovh_service_name
name = "magento-lb"
region = var.ovh_region
type = "public" # or "private"
# Other LB configurations like SSL certificates, health checks, etc.
}
# Associate web instances with the load balancer
resource "ovh_cloud_project_loadbalancer_backend" "magento_lb_backend" {
count = ovh_cloud_project_instance.magento_web.count
service_name = var.ovh_service_name
lb_id = ovh_cloud_project_loadbalancer.magento_lb.id
instance_id = ovh_cloud_project_instance.magento_web[count.index].id
port = 80 # Traffic will be forwarded to web servers on port 80
weight = 100
ssl_healthcheck = false
tcp_healthcheck = true
advanced_healthcheck = false
}
# Frontend listener for HTTP
resource "ovh_cloud_project_loadbalancer_frontend" "magento_lb_frontend_http" {
service_name = var.ovh_service_name
lb_id = ovh_cloud_project_loadbalancer.magento_lb.id
port = 80
protocol = "http"
default_backend_id = ovh_cloud_project_loadbalancer_backend.magento_lb_backend[0].id # Point to one of the backends
}
# Frontend listener for HTTPS (requires SSL certificate configuration)
# resource "ovh_cloud_project_loadbalancer_frontend" "magento_lb_frontend_https" {
# service_name = var.ovh_service_name
# lb_id = ovh_cloud_project_loadbalancer.magento_lb.id
# port = 443
# protocol = "https"
# ssl_certificate_id = "your-ssl-certificate-id" # Obtain from OVH SSL services
# default_backend_id = ovh_cloud_project_loadbalancer_backend.magento_lb_backend[0].id
# }
Deployment Workflow
The typical workflow for deploying and managing your Magento 2 cluster with Terraform on OVH:
- Initialize: Run
terraform initto download the OVH provider and any other necessary modules. - Plan: Execute
terraform planto review the infrastructure changes Terraform will make. This is a critical step to catch potential errors before applying. - Apply: Run
terraform applyto provision all the resources defined in your configuration. This includes VPC, subnets, security groups, instances, databases, and load balancers. - Magento Setup: After instances are provisioned, the cloud-init script will attempt to install and configure software. However, critical Magento setup commands (like
setup:install,setup:upgrade, cache configuration) are often best executed post-provisioning via a CI/CD pipeline (e.g., Jenkins, GitLab CI, GitHub Actions) or a configuration management tool (Ansible, Chef, Puppet). This allows for more control over the Magento application deployment itself. - Update: Modify your Terraform files (e.g., change instance size, add more web servers) and run
terraform planandterraform applyagain to update the infrastructure. - Destroy: When no longer needed, run
terraform destroyto tear down all provisioned resources and avoid ongoing costs.
Security Best Practices and Considerations
- Least Privilege: Ensure your OVH API credentials used by Terraform have only the necessary permissions.
- SSH Key Management: Securely manage your SSH keys. Do not embed private keys in your Terraform code.
- Database Security: Use private network access for your managed database. Regularly rotate database credentials.
- Firewall Rules: Strictly define security group rules. Only open necessary ports and restrict source IP addresses where possible.
- SSL/TLS: Implement SSL/TLS for all HTTPS traffic. Use OVH’s managed SSL certificates or integrate with Let’s Encrypt.
- Regular Updates: Keep your OS, PHP, Nginx, and Magento versions up-to-date with security patches. Automate this process using configuration management tools.
- Monitoring and Logging: Set up robust monitoring and logging for your instances and services.
- Backups: Ensure regular, tested backups of your Magento database and file system.