Infrastructure as Code: Provisioning Secure Magento 2 Clusters on DigitalOcean Using Terraform
Terraform Project Structure and Provider Configuration
We’ll begin by establishing a robust Terraform project structure. This modular approach enhances maintainability and scalability. Our primary configuration file, main.tf, will define the DigitalOcean provider and essential variables. A separate variables.tf file will house all input variables, promoting clarity and reusability. Finally, outputs.tf will expose key information about our provisioned infrastructure.
First, let’s configure the DigitalOcean provider. This block tells Terraform we’ll be interacting with DigitalOcean resources and specifies the authentication method. We’ll use an environment variable for the API token, which is a best practice for security.
main.tf
# main.tf
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
provider "digitalocean" {
token = var.do_token
}
variable "do_token" {
description = "DigitalOcean API Token"
type = string
sensitive = true
}
variable "region" {
description = "The DigitalOcean region to deploy resources in."
type = string
default = "nyc3"
}
variable "ssh_key_name" {
description = "Name of the SSH key to use for Droplet access."
type = string
}
variable "ssh_public_key_path" {
description = "Path to the SSH public key file."
type = string
default = "~/.ssh/id_rsa.pub"
}
output "droplet_ids" {
description = "List of IDs of the provisioned Droplets."
value = digitalocean_droplet.magento_web.*.id
}
output "droplet_ipv4_addresses" {
description = "List of public IPv4 addresses of the provisioned Droplets."
value = digitalocean_droplet.magento_web.*.ipv4_address
}
output "load_balancer_ip" {
description = "The public IP address of the Load Balancer."
value = digitalocean_loadbalancer.magento_lb.ip
}
Next, we define the input variables in variables.tf. This file centralizes all configurable parameters for our infrastructure.
variables.tf
# variables.tf # (Content is already included in main.tf for brevity in this example, # but in a larger project, it's good practice to separate them.)
And the outputs in outputs.tf, which will display useful information after deployment.
outputs.tf
# outputs.tf # (Content is already included in main.tf for brevity in this example, # but in a larger project, it's good practice to separate them.)
To initialize Terraform and set up the DigitalOcean provider, run:
terraform init
You’ll need to set your DigitalOcean API token as an environment variable. Replace YOUR_DIGITALOCEAN_API_TOKEN with your actual token.
export DO_TOKEN="YOUR_DIGITALOCEAN_API_TOKEN"
Ensure your SSH public key is registered with DigitalOcean and its name matches the ssh_key_name variable you’ll provide during the apply phase. You can add your SSH key via the DigitalOcean control panel or using the `doctl` CLI.
Provisioning Database and Cache Servers
A secure Magento 2 deployment requires a robust database and a fast caching layer. We’ll provision a managed PostgreSQL database and a Redis cache instance. Using DigitalOcean’s managed services offloads operational overhead and provides high availability.
database.tf
# database.tf
resource "digitalocean_database_cluster" "magento_db" {
name = "magento-db-cluster"
engine = "pg"
version = "13"
region = var.region
size = "db-s-1vcpu-1gb" # Adjust size based on expected load
node_count = 1 # For production, consider 3 for high availability
tags = ["magento", "database"]
# Optional: Configure backup retention period
# backup_restore_type = "backup"
# backup_period = "daily"
# backup_hour = "03:00"
}
resource "digitalocean_database_db" "magento_db" {
cluster_id = digitalocean_database_cluster.magento_db.id
name = "magento_prod"
}
resource "digitalocean_database_user" "magento_user" {
cluster_id = digitalocean_database_cluster.magento_db.id
name = "magento_app_user"
password = random_password.db_password.result # Use a generated password
}
resource "random_password" "db_password" {
length = 16
special = true
override_special = "_%@"
}
output "db_host" {
description = "Hostname of the managed PostgreSQL database."
value = digitalocean_database_cluster.magento_db.host
sensitive = true
}
output "db_port" {
description = "Port of the managed PostgreSQL database."
value = digitalocean_database_cluster.magento_db.port
}
output "db_name" {
description = "Name of the managed PostgreSQL database."
value = digitalocean_database_db.magento_db.name
}
output "db_user" {
description = "Username for the managed PostgreSQL database."
value = digitalocean_database_user.magento_user.name
}
output "db_password" {
description = "Password for the managed PostgreSQL database."
value = random_password.db_password.result
sensitive = true
}
cache.tf
# cache.tf
resource "digitalocean_redis" "magento_cache" {
name = "magento-cache-redis"
engine = "redis"
version = 6 # Or the latest stable version
region = var.region
size_slug = "basic-2" # Adjust size based on expected load
redis_ எரிபொருள்_minutes = 0 # Disable automatic eviction for cache
tags = ["magento", "cache"]
}
output "redis_host" {
description = "Hostname of the managed Redis cache."
value = digitalocean_redis.magento_cache.host
sensitive = true
}
output "redis_port" {
description = "Port of the managed Redis cache."
value = digitalocean_redis.magento_cache.port
}
These configurations create a PostgreSQL cluster and a Redis cluster. Note the use of random_password to generate a secure password for the database user. The outputs will provide connection details for your Magento application.
Configuring Web and Application Servers
We’ll deploy multiple Droplets for our Magento web application. A common pattern is to have at least two web servers behind a load balancer for redundancy and scalability. We’ll use a custom user data script to automate the initial setup, including installing necessary packages and configuring the web server.
servers.tf
# servers.tf
resource "digitalocean_ssh_key" "magento_ssh" {
name = var.ssh_key_name
public_key = file(var.ssh_public_key_path)
}
resource "digitalocean_droplet" "magento_web" {
count = 2 # Number of web servers
name = "magento-web-${count.index}"
region = var.region
size = "s-2vcpu-4gb" # Adjust size based on expected load
image = "ubuntu-20-04-x64" # Or your preferred OS
ssh_keys = [digitalocean_ssh_key.magento_ssh.id]
monitoring = true
user_data = templatefile("${path.module}/scripts/setup_webserver.sh", {
db_host = digitalocean_database_cluster.magento_db.host
db_port = digitalocean_database_cluster.magento_db.port
db_name = digitalocean_database_db.magento_db.name
db_user = digitalocean_database_user.magento_user.name
db_password = random_password.db_password.result
redis_host = digitalocean_redis.magento_cache.host
redis_port = digitalocean_redis.magento_cache.port
ssh_public_key = file(var.ssh_public_key_path)
})
tags = ["magento", "webserver"]
lifecycle {
create_before_destroy = true
}
}
The user_data script is crucial for bootstrapping the servers. It will install Nginx, PHP-FPM, and other dependencies, configure them to connect to the database and Redis, and set up a basic Nginx virtual host. We’re using templatefile to inject dynamic values from our Terraform state.
scripts/setup_webserver.sh
#!/bin/bash
set -e
# Update and install dependencies
apt-get update -y
apt-get upgrade -y
apt-get install -y nginx php-fpm php-mysql php-pgsql php-redis php-mbstring php-xml php-curl php-zip unzip git composer
# Configure PHP-FPM
sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/8.0/fpm/php.ini
sed -i 's/upload_max_filesize = .*/upload_max_filesize = 64M/' /etc/php/8.0/fpm/php.ini
sed -i 's/post_max_size = .*/post_max_size = 64M/' /etc/php/8.0/fpm/php.ini
sed -i 's/;daemonize = .*/daemonize = no/' /etc/php/8.0/fpm/php-fpm.conf # Ensure FPM runs in foreground
# Configure Nginx
cat > /etc/nginx/sites-available/magento <<EOF
server {
listen 80;
server_name _; # Catch all for now, will be handled by LB
root /var/www/magento;
index index.php index.html index.htm;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.0-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 ~ /\. {
deny all;
}
# Magento specific configurations (e.g., static content)
location /static/ {
expires 30d;
add_header Cache-Control "public";
}
location /media/ {
expires 30d;
add_header Cache-Control "public";
}
}
EOF
ln -sf /etc/nginx/sites-available/magento /etc/nginx/sites-enabled/magento
rm -f /etc/nginx/sites-enabled/default
# Create Magento directory and set permissions
mkdir -p /var/www/magento
chown www-data:www-data /var/www/magento
# Install Magento (placeholder - this should ideally be done via CI/CD or a separate provisioning step)
# For demonstration, we'll just create a dummy index.php
echo "<?php phpinfo(); ?>" > /var/www/magento/index.php
chown www-data:www-data /var/www/magento/index.php
# Restart services
systemctl restart nginx
systemctl restart php8.0-fpm # Adjust PHP version if needed
# Add SSH public key for easy access
mkdir -p /root/.ssh
echo "${ssh_public_key}" >> /root/.ssh/authorized_keys
chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys
echo "Web server setup complete."
The setup_webserver.sh script performs essential tasks: updating packages, installing Nginx, PHP-FPM, and necessary PHP extensions. It configures PHP-FPM for better performance and sets up a basic Nginx virtual host. Crucially, it injects the database and Redis connection details passed from Terraform. The script also adds the provided SSH public key to the root user’s authorized keys for immediate access.
Implementing a Load Balancer
To distribute traffic across our web servers and provide a single entry point, we’ll deploy a DigitalOcean Load Balancer. This also allows us to configure SSL termination and health checks.
loadbalancer.tf
# loadbalancer.tf
resource "digitalocean_loadbalancer" "magento_lb" {
name = "magento-lb"
region = var.region
vpc_uuid = "YOUR_VPC_UUID" # Replace with your VPC UUID if not using default
droplet_tag = "magento" # Tag applied to web server droplets
forwarding_rule {
entry_protocol = "http"
entry_port = 80
to_protocol = "http"
to_port = 80
certificate_id = "" # Optional: For SSL termination, provide a certificate ID
}
# Optional: Add HTTPS forwarding rule if SSL is terminated at the LB
# forwarding_rule {
# entry_protocol = "https"
# entry_port = 443
# to_protocol = "http"
# to_port = 80
# certificate_id = "YOUR_DIGITALOCEAN_CERTIFICATE_ID" # Replace with your certificate ID
# }
healthcheck {
port = 80
protocol = "http"
path = "/" # Basic health check, consider a dedicated health check endpoint
check_interval_seconds = 10
response_timeout = 5
healthy_threshold = 3
unhealthy_threshold = 3
}
tags = ["magento", "loadbalancer"]
}
In this configuration, the load balancer is set to forward HTTP traffic on port 80 to any Droplets tagged with “magento”. A basic HTTP health check is configured to ensure traffic is only sent to healthy web servers. For production, you would typically configure SSL termination here by providing a certificate_id and adding a forwarding rule for HTTPS.
Deployment and Verification
With all Terraform files in place, you can now provision your infrastructure. Ensure you have set the DO_TOKEN environment variable and have your SSH public key path correctly configured.
# Apply the Terraform configuration terraform apply \ -var="ssh_key_name=my-magento-ssh-key" \ -var="ssh_public_key_path=~/.ssh/id_rsa.pub" \ -var="region=nyc3"
Terraform will show you a plan of the resources it will create. Type yes to proceed. Once the apply is complete, Terraform will output the IP addresses of your web servers and the load balancer’s public IP.
Verification Steps:
- Access the load balancer’s IP address in your browser. You should see the output of the dummy
phpinfo()page. - SSH into one of the web Droplets using the private key corresponding to the public key you provided. For example:
ssh root@<web_droplet_ip>. - Verify that Nginx and PHP-FPM are running:
systemctl status nginx php8.0-fpm. - Check the Nginx access and error logs:
/var/log/nginx/access.logand/var/log/nginx/error.log. - Inspect the PHP-FPM logs:
/var/log/php8.0-fpm.log. - Confirm connectivity to the database and Redis from the web server: You can use `redis-cli -h <redis_host> -p <redis_port> ping` and `psql -h <db_host> -p <db_port> -U <db_user> -d <db_name> -c ‘\q’` (you’ll be prompted for the password).
This setup provides a foundational, secure, and scalable infrastructure for Magento 2 on DigitalOcean, managed entirely through Infrastructure as Code.