Infrastructure as Code: Provisioning Secure Magento 2 Clusters on Linode Using Terraform
Terraform Provider Configuration for Linode
To provision infrastructure on Linode using Terraform, we first need to configure the Linode provider. This involves specifying your Linode API token. It’s crucial to manage this token securely, ideally using environment variables or a dedicated secrets management system rather than hardcoding it directly into your Terraform configuration files.
Create a file named providers.tf and add the following configuration:
terraform {
required_providers {
linode = {
source = "linode/linode"
version = "~> 1.0"
}
}
}
provider "linode" {
token = var.linode_api_token
}
variable "linode_api_token" {
description = "Linode API Token"
type = string
sensitive = true
}
variable "region" {
description = "The Linode region to deploy resources in."
type = string
default = "us-east"
}
variable "ssh_public_key" {
description = "The public SSH key to deploy for root access."
type = string
}
variable "magento_domain" {
description = "The domain name for the Magento installation."
type = string
default = "example.com"
}
variable "magento_db_password" {
description = "Password for the Magento database user."
type = string
sensitive = true
default = "supersecretpassword"
}
You can set the linode_api_token and ssh_public_key environment variables before running Terraform commands, or create a terraform.tfvars file (ensure this file is not committed to version control if it contains sensitive data):
# terraform.tfvars linode_api_token = "your_linode_api_token_here" ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD..."
Provisioning Database Servers (MySQL)
For a secure and performant Magento 2 cluster, a dedicated database server is essential. We’ll provision a Linode Instance for MySQL. For production environments, consider using Linode’s managed database services for enhanced reliability and scalability, but for this example, we’ll set up a self-managed MySQL instance.
Create a file named database.tf:
resource "linode_instance" "magento_db" {
label = "magento-db-server"
image = "ubuntu/22.04"
region = var.region
type = "g6-nanode-1" # Adjust instance type based on expected load
root_pass = random_password.db_root_password.result
authorized_keys = [
file("~/.ssh/id_rsa.pub") # Ensure your SSH key is present
]
tags = ["magento", "database"]
connection {
type = "ssh"
user = "root"
private_key = file("~/.ssh/id_rsa") # Path to your private SSH key
host = self.ip_address
timeout = "5m"
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update -y",
"sudo DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-server",
"sudo systemctl start mysql",
"sudo systemctl enable mysql",
"sudo mysql -e \"ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${random_password.db_root_password.result}';\"",
"sudo mysql -e \"CREATE DATABASE IF NOT EXISTS magento_db;\"",
"sudo mysql -e \"CREATE USER IF NOT EXISTS magento_user@'%' IDENTIFIED BY '${var.magento_db_password}';\"",
"sudo mysql -e \"GRANT ALL PRIVILEGES ON magento_db.* TO 'magento_user'@'%';\"",
"sudo mysql -e \"FLUSH PRIVILEGES;\"",
"sudo sed -i 's/^bind-address.*/bind-address = 0.0.0.0/' /etc/mysql/mysql.conf.d/mysqld.cnf", # WARNING: For security, restrict this to specific IPs in production
"sudo systemctl restart mysql"
]
}
}
resource "random_password" "db_root_password" {
length = 16
special = true
override_special = "_%@"
}
output "magento_db_ip" {
description = "The public IP address of the Magento database server."
value = linode_instance.magento_db.ip_address
}
output "magento_db_root_password" {
description = "The root password for the Magento database server."
value = random_password.db_root_password.result
sensitive = true
}
Security Note: Binding MySQL to 0.0.0.0 is insecure for production. In a real-world scenario, you would configure the bind-address to the private IP of the database server and restrict access from the web server IPs only. This is typically achieved by configuring Linode’s firewall or using security groups.
Provisioning Web Servers (Nginx + PHP-FPM)
We’ll provision two identical web servers for redundancy and load balancing. Each server will run Nginx as the web server and PHP-FPM for executing PHP code.
Create a file named webservers.tf:
resource "linode_instance" "magento_web" {
count = 2 # Provision two web servers
label = "magento-web-${count.index + 1}"
image = "ubuntu/22.04"
region = var.region
type = "g6-nanode-2" # Adjust instance type based on expected load
root_pass = random_password.web_root_password[count.index].result
authorized_keys = [
file("~/.ssh/id_rsa.pub")
]
tags = ["magento", "web"]
connection {
type = "ssh"
user = "root"
private_key = file("~/.ssh/id_rsa")
host = self.ip_address
timeout = "5m"
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update -y",
"sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nginx php-fpm php-mysql php-curl php-gd php-mbstring php-xml php-zip php-intl",
"sudo systemctl start nginx",
"sudo systemctl enable nginx",
"sudo systemctl start php8.1-fpm", # Adjust PHP version as needed
"sudo systemctl enable php8.1-fpm",
# Configure Nginx for Magento (basic setup)
"sudo rm /etc/nginx/sites-available/default",
"sudo tee /etc/nginx/sites-available/magento.conf <<EOF
server {
listen 80;
server_name ${var.magento_domain};
root /var/www/html/magento; # Magento will be installed here later
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.1-fpm.sock; # Adjust PHP version
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~* /(composer\.json|composer\.lock|\.env|\.htaccess|app/etc/env\.php|var/cache|var/session) {
deny all;
return 404;
}
}
EOF",
"sudo ln -s /etc/nginx/sites-available/magento.conf /etc/nginx/sites-enabled/",
"sudo nginx -t",
"sudo systemctl reload nginx"
]
}
}
resource "random_password" "web_root_password" {
count = 2
length = 16
special = true
override_special = "_%@"
}
output "magento_web_ips" {
description = "The public IP addresses of the Magento web servers."
value = linode_instance.magento_web[*].ip_address
}
Load Balancer Configuration
A load balancer is crucial for distributing traffic across the web servers and providing a single point of access. We’ll use a Linode NodeBalancer.
Create a file named loadbalancer.tf:
resource "linode_nodebalancer" "magento_lb" {
label = "magento-lb"
region = var.region
client_conn_throttle = 100 # Adjust as needed
# Basic health check configuration
health_check {
protocol = "tcp"
port = 80
check = "/status" # A simple HTTP check endpoint
interval = 10
timeout = 5
unhealthy_threshold = 3
healthy_threshold = 3
}
}
resource "linode_nodebalancer_node" "magento_web_nodes" {
count = length(linode_instance.magento_web)
label = "web-node-${count.index + 1}"
nodebalancer_id = linode_nodebalancer.magento_lb.id
address = linode_instance.magento_web[count.index].ip_address
port = 80
weight = 100 # Equal weighting
}
resource "linode_nodebalancer_config" "magento_lb_config" {
nodebalancer_id = linode_nodebalancer.magento_lb.id
port = 80
protocol = "http"
algorithm = "roundrobin"
stickiness = "table" # Session persistence for Magento
}
output "magento_lb_ip" {
description = "The public IP address of the Magento NodeBalancer."
value = linode_nodebalancer.magento_lb.ipv4
}
Note on Health Checks: The health check check = "/status" assumes you will create a simple PHP file (e.g., /var/www/html/status.php) on your web servers that outputs a 200 OK response. This is a basic check; for production, consider more robust checks.
Magento 2 Installation and Configuration
The final step is to install Magento 2 on the web servers and configure it to use the database. This can be done using a combination of Terraform’s remote-exec provisioner and shell scripts. For more complex deployments, consider using Ansible or a similar configuration management tool orchestrated by Terraform.
We’ll add a provisioner to the webservers.tf file. For simplicity, this example installs Magento on the first web server. In a production setup, you’d want to ensure consistency across all web servers, potentially using shared storage (like NFS) or a deployment pipeline.
# Add this to the linode_instance "magento_web" resource in webservers.tf
provisioner "remote-exec" {
inline = [
# ... (previous commands for Nginx and PHP-FPM) ...
# Magento Installation (on the first web server)
"if [ \"${count.index}\" = \"0\" ]; then",
" sudo apt-get install -y composer git unzip wget",
" sudo mkdir -p /var/www/html/magento",
" sudo chown www-data:www-data /var/www/html/magento",
" cd /var/www/html/magento",
" sudo -u www-data composer create-project --repository-url=https://repo.magento.com/ magento/project-community-edition .",
" sudo -u www-data php bin/magento setup:install --base-url=http://${var.magento_domain}/ \\",
" --db-host=${linode_instance.magento_db.ip_address} \\",
" --db-name=magento_db \\",
" --db-user=magento_user \\",
" --db-password='${var.magento_db_password}' \\",
" --admin-user=admin \\",
" --admin-password=AdminPassword123 \\", # CHANGE THIS IN PRODUCTION
" --admin-email=admin@${var.magento_domain} \\",
" --language=en_US \\",
" --currency=USD \\",
" --timezone=America/New_York \\",
" --use-rewrites-module=1",
" sudo -u www-data php bin/magento setup:upgrade",
" sudo -u www-data php bin/magento cache:enable",
" sudo -u www-data php bin/magento indexer:reindex",
" sudo chmod -R 775 var pub/static pub/media app/etc", # Adjust permissions as needed
" sudo chown -R www-data:www-data var pub/static pub/media app/etc",
" sudo systemctl restart php8.1-fpm", # Adjust PHP version
" sudo systemctl reload nginx",
"fi"
]
}
Important Considerations for Production:
- Database Security: Restrict the MySQL user’s access to only the web server’s private IP address instead of ‘%’.
- Admin Credentials: Use strong, unique passwords for the Magento admin user and ensure they are managed securely.
- Composer Authentication: For private Magento repositories or extensions, you’ll need to configure Composer authentication.
- Shared Storage: For a truly scalable Magento setup, consider using a shared filesystem (e.g., NFS, Linode Object Storage with S3-compatible access) for
pub/mediaandvar/view_preprocessedto ensure consistency across web nodes. - HTTPS: This example uses HTTP. For production, you must configure SSL/TLS, likely using Let’s Encrypt, and update Nginx and the NodeBalancer to use HTTPS.
- Caching: Integrate Redis or Memcached for improved performance.
- Deployment Strategy: For frequent updates, implement a CI/CD pipeline that deploys code to all web servers consistently.
- Firewall: Configure Linode’s firewall to restrict access to necessary ports only.
Running Terraform
Once you have your Terraform files organized (e.g., in a directory named magento-iac), navigate to that directory in your terminal and run the following commands:
cd magento-iac terraform init terraform plan terraform apply
terraform init initializes the Terraform workspace and downloads the necessary provider plugins.
terraform plan shows you a preview of the infrastructure changes that will be made.
terraform apply provisions the resources on Linode. You will be prompted to confirm the changes.
After the apply is complete, you can access your Magento 2 installation via the NodeBalancer’s IP address (outputted as magento_lb_ip).