Infrastructure as Code: Provisioning Secure WordPress 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 and optionally a default region. It’s crucial to manage your API token securely, ideally using environment variables or a secrets management system rather than hardcoding it directly into your Terraform configuration.
Create a file named providers.tf and add the following configuration:
terraform {
required_providers {
linode = {
source = "linode/linode"
version = "~> 1.20" # Pin to a specific version for stability
}
}
}
provider "linode" {
token = var.linode_api_token
# region = "us-east" # Optional: specify a default region
}
variable "linode_api_token" {
description = "Linode API Token"
type = string
sensitive = true # Mark as sensitive to prevent accidental exposure
}
# Example of how to set the variable (e.g., in a terraform.tfvars file or via environment variable)
# export TF_VAR_linode_api_token="your_linode_api_token_here"
In this setup, we declare the Linode provider and specify its version. A variable linode_api_token is defined to hold your API token, marked as sensitive. This token can be provided via a terraform.tfvars file (which should NOT be committed to version control) or, more securely, through an environment variable named TF_VAR_linode_api_token.
Provisioning a Linode Instance for WordPress
Next, we define the Linode instance that will host our WordPress site. This includes selecting an image (e.g., Ubuntu 22.04 LTS), a plan (e.g., Nanode 1GB), and specifying the root user’s SSH public key for secure access. We’ll also attach a public IP address.
Create a file named main.tf and add the following:
resource "linode_instance" "wordpress_server" {
image = "linode/ubuntu22.04"
plan = "nanode_1gb" # Or choose a suitable plan like "g6-nanode-1" for newer generations
region = "us-east" # Ensure this matches your desired region or is set in the provider
label = "wordpress-cluster-node-1"
root_pass = random_password.root_password.result # Use a random password for initial setup
authorized_keys = [var.ssh_public_key]
tags = ["wordpress", "webserver"]
# Ensure the instance is created before attempting to assign an IP if using a separate IP resource
# depends_on = [linode_instance.wordpress_server] # Not strictly necessary for a single instance
}
resource "random_password" "root_password" {
length = 16
special = true
override_special = "_%@"
}
variable "ssh_public_key" {
description = "SSH Public Key for instance access"
type = string
sensitive = true
}
output "wordpress_server_ip" {
description = "The public IP address of the WordPress server"
value = linode_instance.wordpress_server.ip_address
}
output "wordpress_server_id" {
description = "The ID of the WordPress server instance"
value = linode_instance.wordpress_server.id
}
Here, linode_instance.wordpress_server defines our virtual machine. We specify the OS image, instance plan, region, and a descriptive label. root_pass is generated randomly for security, and authorized_keys allows passwordless SSH login using your provided public key. The random_password resource ensures a unique, strong password is generated for the root user. The outputs wordpress_server_ip and wordpress_server_id will be displayed after Terraform applies the configuration, providing easy access to the server’s IP and its unique identifier.
Securing the WordPress Instance with a Firewall
A robust security posture is paramount. We’ll leverage Linode’s Cloud Firewall to restrict incoming traffic to only necessary ports (SSH, HTTP, HTTPS). This significantly reduces the attack surface.
Add the following to your main.tf file:
resource "linode_firewall" "wordpress_firewall" {
label = "wordpress-firewall"
description = "Firewall for WordPress cluster"
inbound_policy = "DROP" # Default to dropping all inbound traffic
outbound_policy = "ACCEPT" # Allow all outbound traffic
inbound_rules {
label = "Allow SSH"
ports = [22]
protocol = "TCP"
action = "ACCEPT"
}
inbound_rules {
label = "Allow HTTP"
ports = [80]
protocol = "TCP"
action = "ACCEPT"
}
inbound_rules {
label = "Allow HTTPS"
ports = [443]
protocol = "TCP"
action = "ACCEPT"
}
# Associate the firewall with the WordPress instance
instance_ids = [linode_instance.wordpress_server.id]
}
This linode_firewall resource defines a firewall named wordpress-firewall. The inbound_policy is set to DROP, meaning any traffic not explicitly allowed by the inbound_rules will be discarded. We then define rules to ACCEPT TCP traffic on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). Crucially, instance_ids = [linode_instance.wordpress_server.id] associates this firewall directly with the WordPress server instance we provisioned earlier. This ensures that as soon as the instance is created, it’s protected by these rules.
Automating WordPress Installation and Configuration
While Terraform provisions the infrastructure, we need a way to install and configure WordPress on the instance. A common and effective approach is to use user_data scripts or a configuration management tool. For simplicity and direct integration with Terraform, we’ll use a user_data script executed via cloud-init.
This script will:
- Update the system packages.
- Install Nginx, PHP, and MySQL (or MariaDB).
- Configure a basic Nginx virtual host for WordPress.
- Download and set up WordPress.
- Secure the MySQL/MariaDB installation.
Add the following user_data block to your linode_instance resource in main.tf:
resource "linode_instance" "wordpress_server" {
# ... (previous configuration) ...
user_data = templatefile("${path.module}/scripts/wordpress_setup.sh.tpl", {
db_name = "wordpress_db"
db_user = "wp_user"
db_password = random_password.db_password.result
wp_url = "http://${linode_instance.wordpress_server.ip_address}" # Or your domain
wp_title = "My Terraform WordPress"
wp_admin_user = "admin"
wp_admin_password = random_password.admin_password.result
wp_admin_email = "[email protected]"
})
# ... (rest of the instance configuration) ...
}
resource "random_password" "db_password" {
length = 16
special = true
override_special = "_%@"
}
resource "random_password" "admin_password" {
length = 16
special = true
override_special = "_%@"
}
output "wordpress_admin_password" {
description = "The generated WordPress admin password"
value = random_password.admin_password.result
sensitive = true
}
output "wordpress_db_password" {
description = "The generated WordPress database password"
value = random_password.db_password.result
sensitive = true
}
And create the template file scripts/wordpress_setup.sh.tpl:
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status.
# --- Variables passed from Terraform ---
DB_NAME="${db_name}"
DB_USER="${db_user}"
DB_PASSWORD="${db_password}"
WP_URL="${wp_url}"
WP_TITLE="${wp_title}"
WP_ADMIN_USER="${wp_admin_user}"
WP_ADMIN_PASSWORD="${wp_admin_password}"
WP_ADMIN_EMAIL="${wp_admin_email}"
# --- System Update and Install Packages ---
echo "Updating system packages..."
apt-get update -y
apt-get upgrade -y
echo "Installing Nginx, PHP, MySQL, and other dependencies..."
apt-get install -y nginx php-fpm php-mysql mariadb-server mariadb-client curl wget
# --- Configure MySQL/MariaDB ---
echo "Securing MariaDB installation..."
# Use a temporary file for the root password to avoid it being logged
MYSQL_ROOT_PASSWORD=$(openssl rand -base64 16)
mysqladmin -u root password "$MYSQL_ROOT_PASSWORD"
# Create database and user
mysql -u root -p"$MYSQL_ROOT_PASSWORD" <
In the main.tf, we use the templatefile function to render the wordpress_setup.sh.tpl script, injecting variables like database credentials and WordPress site details. Two new random_password resources are introduced for the database and WordPress admin passwords, with their values exposed as sensitive outputs. The user_data block is added to the linode_instance resource, ensuring this script runs upon instance creation.
The wordpress_setup.sh.tpl script performs a series of essential tasks. It updates the system, installs Nginx, PHP-FPM, and MariaDB. It then secures MariaDB, creates a dedicated database and user for WordPress, and configures Nginx with a basic virtual host. WordPress is downloaded, and the wp-config.php file is populated with the generated database credentials. Finally, it sets appropriate file permissions and restarts PHP-FPM. For full automation, integrating WP-CLI to perform the core installation is recommended.
Applying the Terraform Configuration
With the Terraform configuration files in place, the provisioning process is straightforward:
- Initialize Terraform: Run
terraform init in your project directory. This downloads the Linode provider. - Review the plan: Execute
terraform plan to see a detailed breakdown of the resources Terraform will create, modify, or destroy. - Apply the configuration: Run
terraform apply. Terraform will prompt you to confirm the actions. Type yes to proceed.
Ensure you have set the LINODE_API_TOKEN environment variable and provided your SSH_PUBLIC_KEY (e.g., via a terraform.tfvars file or another environment variable TF_VAR_ssh_public_key).
# Set your Linode API token (replace with your actual token or use a secure method)
export TF_VAR_linode_api_token="your_linode_api_token_here"
# Set your SSH public key (replace with your actual public key)
export TF_VAR_ssh_public_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC..."
# Initialize Terraform
terraform init
# Review the execution plan
terraform plan
# Apply the configuration to create resources
terraform apply
After the apply command completes successfully, Terraform will output the public IP address of your WordPress server and the generated admin password. You can then access your WordPress installation via a web browser at http://<your_server_ip> and log in using the provided admin credentials.
Next Steps and Enhancements
This setup provides a foundational, secure WordPress instance. For production environments, consider these enhancements:
- Database Clustering: For high availability and scalability, provision a managed Linode MySQL database or set up a separate database cluster using Terraform.
- Load Balancing: Introduce a Linode NodeBalancer to distribute traffic across multiple WordPress instances.
- SSL/TLS: Automate Let's Encrypt certificate provisioning and Nginx configuration for HTTPS.
- Caching: Implement server-level caching (e.g., Nginx FastCGI cache) or use a CDN.
- Monitoring and Logging: Integrate with Linode's monitoring tools or external services for performance and security.
- CI/CD Integration: Incorporate this Terraform configuration into a CI/CD pipeline for automated deployments and updates.
- More Sophisticated User Data: For complex setups, consider using Ansible or Chef via Terraform's provisioner or a dedicated orchestration tool.
By using Infrastructure as Code with Terraform, you can reliably and repeatedly provision secure, production-ready WordPress environments on Linode, significantly reducing manual effort and the potential for human error.