Infrastructure as Code: Provisioning Secure WooCommerce Clusters on Linode Using Terraform
Terraform Provider Configuration for Linode
To begin provisioning 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 secrets management system rather than hardcoding it directly into your Terraform configuration files.
Create a file named providers.tf in your Terraform project directory. This file will house the provider configuration.
Securing the Linode API Token
The most secure method for handling your Linode API token is via an environment variable. Before running any Terraform commands, set the LINODE_TOKEN environment variable in your shell:
export LINODE_TOKEN="your_linode_api_token_here"
Replace your_linode_api_token_here with your actual Linode API token. You can generate this token from your Linode Cloud Manager under your account settings.
Defining the Linode Provider Block
Now, within your providers.tf file, define the Linode provider block. Terraform will automatically pick up the LINODE_TOKEN environment variable.
terraform {
required_providers {
linode = {
source = "linode/linode"
version = "~> 1.0" # Specify a version constraint
}
}
}
provider "linode" {
# The LINODE_TOKEN environment variable will be used automatically.
# Alternatively, you could explicitly set it here, but this is less secure:
# token = var.linode_api_token
}
# Optional: Define a variable for the token if you prefer to pass it via a .tfvars file
# variable "linode_api_token" {
# description = "Linode API Token"
# type = string
# sensitive = true
# }
The required_providers block ensures that Terraform knows which providers to download and use, along with version constraints for reproducibility. The provider "linode" block configures the provider itself. By not explicitly setting the token argument, Terraform defaults to using the LINODE_TOKEN environment variable.
Provisioning a Secure WooCommerce Cluster: Core Components
A robust WooCommerce cluster typically involves several key components: a load balancer, multiple web servers running PHP-FPM and Nginx, and a managed database. We’ll use Terraform to provision these resources on Linode.
Defining Network Resources: VPC and Subnets
For enhanced security and network isolation, we’ll deploy our cluster within a Virtual Private Cloud (VPC). This allows us to control network traffic precisely.
Create a file named network.tf.
resource "linode_vpc" "woocommerce_vpc" {
description = "VPC for WooCommerce cluster"
region = "us-east" # Choose your preferred region
label = "woocommerce-vpc"
}
resource "linode_vpc_subnet" "app_subnet" {
vpc_id = linode_vpc.woocommerce_vpc.id
label = "app-subnet"
ipv4_τητα = "10.0.1.0/24" # Private subnet for application servers
region = linode_vpc.woocommerce_vpc.region
}
resource "linode_vpc_subnet" "db_subnet" {
vpc_id = linode_vpc.woocommerce_vpc.id
label = "db-subnet"
ipv4_τητα = "10.0.2.0/24" # Private subnet for database servers
region = linode_vpc.woocommerce_vpc.region
}
Here, we define a VPC and two private subnets: one for application servers and another for the database. Using private subnets ensures that these resources are not directly accessible from the public internet, enhancing security. The IP ranges 10.0.1.0/24 and 10.0.2.0/24 are examples; adjust them as needed for your network design.
Provisioning the Managed Database (MySQL)
A managed database service is essential for reliability and ease of maintenance. Linode offers managed MySQL databases.
Add the following to your database.tf file:
resource "linode_database" "woocommerce_db" {
type = "mysql"
engine = "mysql-8.0" # Specify desired MySQL version
region = linode_vpc.woocommerce_vpc.region
label = "woocommerce-db"
description = "Managed MySQL for WooCommerce"
plan = "db-s-1vcpu-2gb" # Choose an appropriate plan
replicas = 1 # For high availability, consider increasing replicas
vpc_id = linode_vpc.woocommerce_vpc.id
subnet_id = linode_vpc_subnet.db_subnet.id
password = var.db_password # Use a secure password variable
username = "wp_user"
allow_list = [
linode_vpc_subnet.app_subnet.ipv4_τητα # Allow access from app subnet
]
}
variable "db_password" {
description = "Password for the database user"
type = string
sensitive = true
}
This resource block defines a managed MySQL instance. We specify the type, engine, region, plan, and importantly, associate it with our VPC and the dedicated database subnet. The allow_list is critical for security; it restricts incoming connections to only those originating from the application subnet, preventing unauthorized access.
You’ll need to define the db_password variable. For production, this should be managed securely, perhaps via a .tfvars file that is not committed to version control, or through a secrets manager.
Provisioning Web Servers (Nginx + PHP-FPM)
We’ll deploy multiple Linode compute instances to act as our web servers. These will run Nginx as the web server and PHP-FPM for processing PHP requests. Using multiple instances behind a load balancer provides scalability and redundancy.
Create a file named webservers.tf.
resource "linode_instance" "web_server" {
count = 2 # Number of web server instances
label = "webserver-${count.index}"
region = linode_vpc.woocommerce_vpc.region
type = "g6-nanode-1" # Choose an appropriate instance type
image = "linode/ubuntu22.04"
root_pass = var.root_password # Secure root password
authorized_keys = [var.ssh_public_key] # For SSH access
vpc_id = linode_vpc.woocommerce_vpc.id
subnet_id = linode_vpc_subnet.app_subnet.id
connection {
type = "ssh"
user = "root"
private_key = file(var.ssh_private_key_path)
host = self.ip_address
timeout = "5m"
}
provisioner "remote-exec" {
inline = [
"apt update -y",
"apt install -y nginx php-fpm php-mysql php-gd php-xml php-mbstring php-curl php-zip",
"systemctl enable nginx",
"systemctl enable php8.1-fpm", # Adjust PHP version if necessary
"ufw allow 'Nginx Full'",
"ufw allow 'Nginx HTTP'",
"ufw allow 'Nginx HTTPS'",
"ufw allow ssh",
"ufw --force enable",
# Configure Nginx and PHP-FPM (details below)
]
}
tags = ["webserver", "woocommerce"]
}
variable "root_password" {
description = "Root password for Linode instances"
type = string
sensitive = true
}
variable "ssh_public_key" {
description = "Public SSH key for accessing instances"
type = string
}
variable "ssh_private_key_path" {
description = "Path to the private SSH key"
type = string
}
This block defines a collection of web server instances using the count meta-argument. Each instance is placed in the application subnet. The connection block configures how Terraform connects to the instances via SSH. The remote-exec provisioner is used to install Nginx, PHP-FPM, and necessary PHP extensions. It also configures the firewall (UFW) to allow necessary traffic.
Configuring Nginx and PHP-FPM
The remote-exec provisioner is a good starting point, but for production, it’s often better to use configuration management tools (like Ansible, Chef, or Puppet) or to bake custom images. For this example, we’ll use Terraform’s template_file data source to generate Nginx and PHP-FPM configuration files and then use remote-exec to upload and apply them.
Create a file named nginx.conf.tpl in a templates subdirectory:
# /templates/nginx.conf.tpl
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.php index.html index.htm;
server_name _; # Catch-all server name
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Make sure this matches your PHP-FPM pool configuration
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 sensitive files
location ~ /\.ht {
deny all;
}
# Caching for static assets (optional but recommended)
location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {
expires 30d;
log_not_found off;
}
}
Now, update your webservers.tf to include the template rendering and file upload:
# ... (previous linode_instance resource) ...
resource "linode_instance" "web_server" {
count = 2
label = "webserver-${count.index}"
region = linode_vpc.woocommerce_vpc.region
type = "g6-nanode-1"
image = "linode/ubuntu22.04"
root_pass = var.root_password
authorized_keys = [var.ssh_public_key]
vpc_id = linode_vpc.woocommerce_vpc.id
subnet_id = linode_vpc_subnet.app_subnet.id
connection {
type = "ssh"
user = "root"
private_key = file(var.ssh_private_key_path)
host = self.ip_address
timeout = "5m"
}
provisioner "remote-exec" {
inline = [
"apt update -y",
"apt install -y nginx php-fpm php-mysql php-gd php-xml php-mbstring php-curl php-zip",
"systemctl enable nginx",
"systemctl enable php8.1-fpm",
"ufw allow 'Nginx Full'",
"ufw allow 'Nginx HTTP'",
"ufw allow 'Nginx HTTPS'",
"ufw allow ssh",
"ufw --force enable",
# Create web root directory
"mkdir -p /var/www/html",
"chown www-data:www-data /var/www/html",
]
}
# Upload Nginx configuration
provisioner "file" {
content = templatefile("${path.module}/templates/nginx.conf.tpl", {
# Pass variables to the template if needed
})
destination = "/etc/nginx/sites-available/default"
}
# Upload PHP-FPM configuration (optional, but good for tuning)
# You might want to create a php-fpm.conf.tpl and upload it to /etc/php/8.1/fpm/php-fpm.conf
# For simplicity, we'll assume default PHP-FPM pool settings are sufficient for now.
provisioner "remote-exec" {
inline = [
"systemctl restart nginx",
"systemctl restart php8.1-fpm", # Adjust PHP version if needed
]
}
tags = ["webserver", "woocommerce"]
}
# ... (variable definitions) ...
The templatefile function renders the nginx.conf.tpl, substituting any variables defined within it. The provisioner "file" then uploads this rendered configuration to the default Nginx site configuration path on the remote server. Finally, Nginx and PHP-FPM are restarted to apply the changes.
Provisioning the Load Balancer
A load balancer distributes incoming traffic across the web servers, ensuring high availability and scalability. Linode’s NodeBalancers are a good choice.
Create a file named loadbalancer.tf.
resource "linode_nodebalancer" "woocommerce_lb" {
label = "woocommerce-lb"
region = linode_vpc.woocommerce_vpc.region
client_conn_throttle = 100 # Adjust as needed
ipv4 = linode_vpc.woocommerce_vpc.ipv4_τητα # Assign public IPv4 from VPC
# ipv6 = linode_vpc.woocommerce_vpc.ipv6_τητα # Optionally assign public IPv6
# Health check configuration
health_check {
protocol = "tcp"
port = 80
check = "/index.php" # A simple check to ensure PHP is running
interval = 5
timeout = 3
unhealthy_threshold = 3
healthy_threshold = 3
}
# Frontend configuration (HTTP)
frontend {
protocol = "http"
port = 80
ssl_certificate_id = null # Configure SSL later
ssl_key = null
}
# Frontend configuration (HTTPS) - requires SSL certificate
# frontend {
# protocol = "https"
# port = 443
# ssl_certificate_id = var.linode_ssl_certificate_id # Reference your Linode SSL cert ID
# ssl_key = null
# }
# Backend configuration (referencing web servers)
# We will dynamically add web servers to the NodeBalancer
}
# Resource to associate web servers with the NodeBalancer
resource "linode_nodebalancer_node" "web_nodes" {
count = length(linode_instance.web_server)
nodebalancer_id = linode_nodebalancer.woocommerce_lb.id
address = linode_instance.web_server[count.index].ip_address
port = 80
label = "webserver-${count.index}"
weight = 100
mode = "active"
# Use the private IP address for internal communication
}
# Optional: Add a public IP to the VPC if not already assigned
resource "linode_vpc_ipv4_address" "lb_public_ip" {
vpc_id = linode_vpc.woocommerce_vpc.id
subnet_id = linode_vpc_subnet.app_subnet.id # Assign to app subnet for simplicity
public = true
}
# Update NodeBalancer to use the newly assigned public IP
resource "linode_nodebalancer" "woocommerce_lb" {
# ... (previous attributes) ...
ipv4 = linode_vpc_ipv4_address.lb_public_ip.address
# ... (rest of the attributes) ...
}
This configuration defines a NodeBalancer and then dynamically adds each web server instance as a backend node. The health check ensures that traffic is only sent to healthy web servers. We also assign a public IPv4 address from the VPC to the NodeBalancer.
Securing the Cluster: Firewall and SSL
Security is paramount for any e-commerce platform. We’ve already configured UFW on the web servers to allow only necessary ports. For the NodeBalancer, we’ll focus on SSL termination.
SSL Termination on the Load Balancer
For production, you should obtain an SSL certificate (e.g., from Let’s Encrypt or a commercial CA) and upload it to Linode. You can then reference its ID in the NodeBalancer configuration.
First, upload your certificate and key to Linode via the Cloud Manager or API. Let’s assume you have a certificate ID, which you would pass as a variable:
variable "linode_ssl_certificate_id" {
description = "ID of the Linode SSL Certificate to use for HTTPS"
type = number
default = null # Set to your certificate ID in production
}
# Update the NodeBalancer resource in loadbalancer.tf:
resource "linode_nodebalancer" "woocommerce_lb" {
# ... (previous attributes) ...
frontend {
protocol = "http"
port = 80
ssl_certificate_id = null
ssl_key = null
}
frontend {
protocol = "https"
port = 443
ssl_certificate_id = var.linode_ssl_certificate_id
ssl_key = null # Linode manages the key when using a certificate ID
}
}
With this, the NodeBalancer will handle SSL termination, decrypting HTTPS traffic before forwarding it to the web servers over HTTP (within the private VPC network). This simplifies certificate management on the web servers.
Deploying WooCommerce
Once the infrastructure is provisioned, you’ll need to deploy WooCommerce. This typically involves:
- Downloading WordPress and WooCommerce.
- Configuring WordPress
wp-config.phpwith database credentials. - Setting up Nginx virtual hosts for your domain.
- Securing the installation (e.g., changing default keys, setting strong passwords).
This deployment step is often best handled by a separate configuration management tool or a CI/CD pipeline. However, you could use Terraform’s remote-exec provisioner for a basic setup, though it’s not recommended for complex deployments.
Terraform Workflow
To apply this infrastructure:
- Initialize Terraform:
terraform init - Review the plan:
terraform plan -var="db_password=your_secure_db_password" -var="root_password=your_secure_root_password" -var="ssh_public_key=ssh-rsa AAAAB3NzaC1yc2E..." -var="ssh_private_key_path=~/.ssh/id_rsa" - Apply the configuration:
terraform apply -var="db_password=your_secure_db_password" -var="root_password=your_secure_root_password" -var="ssh_public_key=ssh-rsa AAAAB3NzaC1yc2E..." -var="ssh_private_key_path=~/.ssh/id_rsa"
Remember to replace placeholder values with your actual sensitive data and SSH keys. For production, use a .tfvars file for variables and ensure it’s not committed to version control.
Destroying the Infrastructure
When you no longer need the cluster, you can destroy all provisioned resources:
terraform destroy -var="db_password=your_secure_db_password" -var="root_password=your_secure_root_password" -var="ssh_public_key=ssh-rsa AAAAB3NzaC1yc2E..." -var="ssh_private_key_path=~/.ssh/id_rsa"
This comprehensive Terraform setup provides a robust, scalable, and secure foundation for your WooCommerce cluster on Linode.