Infrastructure as Code: Provisioning Secure PHP 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 rather than hardcoding it directly into your Terraform configuration files. This prevents accidental exposure of sensitive credentials in version control.
Create a file named main.tf and add the following provider configuration. Replace YOUR_LINODE_API_TOKEN with your actual Linode API token, or preferably, set it as an environment variable named LINODE_TOKEN.
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
default = "" # If not using environment variable, uncomment and set here, but NOT recommended for production
}
# Example of how to set the variable from an environment variable
# terraform {
# ...
# backend "remote" {
# ...
# }
# }
#
# variable "linode_api_token" {
# description = "Linode API Token"
# type = string
# sensitive = true
# }
#
# provider "linode" {
# token = lookup(var.linode_api_token, "LINODE_TOKEN", "") # This syntax is illustrative; actual env var usage is via `TF_VAR_linode_api_token` or `terraform.tfvars`
# }
To use an environment variable, set it in your shell before running Terraform commands:
export LINODE_TOKEN="YOUR_LINODE_API_TOKEN"
Then, in your main.tf, you can reference it like this:
provider "linode" {
token = var.linode_api_token
}
variable "linode_api_token" {
description = "Linode API Token"
type = string
sensitive = true
default = env("LINODE_TOKEN") # This is the correct way to reference env vars for variables
}
Provisioning a Secure PHP Cluster
A robust PHP cluster typically involves multiple components: a load balancer, web servers running PHP-FPM, and potentially a database. For this example, we’ll focus on provisioning the web servers and a basic Nginx setup for load balancing. We’ll use a custom user data script to configure Nginx and PHP-FPM upon instance boot.
First, let’s define the Linode instances that will serve our PHP application. We’ll create a count-based resource to spin up multiple web servers. Each server will have a public IP address and will be configured with a standard Ubuntu LTS image.
resource "linode_instance" "php_web_server" {
count = 3 # Number of web servers
label = "php-web-${count.index + 1}"
region = "us-east" # Choose your preferred region
type = "g6-nanode-1" # Choose an appropriate instance type
image = "linode/ubuntu22.04"
root_pass = random_password.root_password[count.index].result
authorized_keys = [
# Add your SSH public keys here for secure access
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD..."
]
user_data = templatefile("${path.module}/scripts/bootstrap.sh", {
server_id = count.index + 1
# Add any other variables needed by your bootstrap script
})
tags = ["php-cluster", "webserver"]
lifecycle {
create_before_destroy = true
}
}
resource "random_password" "root_password" {
count = 3
length = 16
special = true
}
output "web_server_ips" {
description = "Public IP addresses of the PHP web servers"
value = linode_instance.php_web_server[*].ip_address
}
Bootstrap Script for Nginx and PHP-FPM
The user_data script is critical for automating the setup of each web server. This script will install Nginx, PHP-FPM, and configure them to serve your application. It will also set up a basic firewall for security.
Create a directory named scripts in the same directory as your main.tf file. Inside scripts, create a file named bootstrap.sh with the following content:
#!/bin/bash
# --- Security Updates and Package Installation ---
apt-get update -y
apt-get upgrade -y
apt-get install -y nginx php-fpm php-mysql php-mbstring php-xml php-gd php-curl unzip
# --- Firewall Configuration ---
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw --force enable
# --- Nginx Configuration ---
# Remove default Nginx configuration
rm /etc/nginx/sites-available/default
rm /etc/nginx/sites-enabled/default
# Create a new Nginx site configuration
cat <<EOF > /etc/nginx/sites-available/php-app
server {
listen 80;
server_name _; # Listen on all hostnames
root /var/www/html;
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 if necessary
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
location ~ /\.ht {
deny all;
}
}
EOF
# Enable the new Nginx site
ln -s /etc/nginx/sites-available/php-app /etc/nginx/sites-enabled/
# Test Nginx configuration and reload
nginx -t
systemctl reload nginx
# --- PHP-FPM Configuration (Optional: Adjust if needed) ---
# Ensure PHP-FPM is running and enabled
systemctl enable php8.1-fpm # Adjust PHP version if necessary
systemctl start php8.1-fpm
# --- Application Deployment Placeholder ---
# In a real-world scenario, you would deploy your application code here.
# This could involve git clone, rsync, or a deployment tool.
# For demonstration, we'll create a simple index.php
mkdir -p /var/www/html
chown www-data:www-data /var/www/html
cat <<EOF > /var/www/html/index.php
<?php
echo "Hello from PHP server ID: ${server_id}!";
phpinfo();
?>
EOF
chown www-data:www-data /var/www/html/index.php
echo "Bootstrap script finished."
Load Balancer Configuration
For a production-ready cluster, a dedicated load balancer is essential. Linode offers managed Load Balancers, which are the simplest and most robust option. We’ll provision a Linode Load Balancer and point it to our web servers.
resource "linode_loadbalancer" "php_lb" {
label = "php-app-lb"
region = "us-east" # Must match web server region
vpc_id = linode_instance.php_web_server[0].vpc_id # Associate with the VPC of one of the web servers
# Configure HTTP listener
listener {
protocol = "http"
port = 80
target {
protocol = "http"
port = 80
# Use the IP addresses of the web servers as targets
# The 'count' index here is crucial to map to the correct web server IPs
target_nodes = [
for i in range(length(linode_instance.php_web_server)) : linode_instance.php_web_server[i].id
]
}
ssl_certificate_id = null # For HTTP, no certificate needed
}
# Optional: Configure HTTPS listener (requires a certificate)
# listener {
# protocol = "https"
# port = 443
# target {
# protocol = "http" # Or "https" if your web servers handle SSL termination
# port = 80
# target_nodes = [
# for i in range(length(linode_instance.php_web_server)) : linode_instance.php_web_server[i].id
# ]
# }
# ssl_certificate_id = linode_certificate.my_cert.id # Assuming you have a Linode Certificate resource defined
# }
tags = ["php-cluster", "loadbalancer"]
}
output "load_balancer_ip" {
description = "The public IP address of the Linode Load Balancer"
value = linode_loadbalancer.php_lb.ip
}
In this configuration, the load balancer will distribute incoming HTTP traffic across the three web servers. The target_nodes attribute dynamically references the IDs of the provisioned web server instances. If you intend to use HTTPS, you would need to provision a Linode Certificate resource and reference its ID in the HTTPS listener configuration.
Database Provisioning (Optional but Recommended)
For a production PHP application, a separate database is almost always required. Linode offers managed MySQL and PostgreSQL databases, which are highly recommended for ease of management and reliability. Here’s how you can provision a managed MySQL database:
resource "linode_database" "php_db" {
label = "php-app-db"
region = "us-east" # Match your application region
engine = "mysql"
version = "8.0"
plan = "db-s1-mysql-1vcpu-1gb" # Choose an appropriate plan
replication = false # Set to true for high availability
# Security: Use a strong, randomly generated password
password = random_password.db_password.result
username = "appuser"
tags = ["php-cluster", "database"]
}
resource "random_password" "db_password" {
length = 20
special = true
}
output "db_host" {
description = "The hostname of the managed database"
value = linode_database.php_db.host
}
output "db_port" {
description = "The port of the managed database"
value = linode_database.php_db.port
}
output "db_username" {
description = "The username for the managed database"
value = linode_database.php_db.username
}
output "db_password" {
description = "The password for the managed database"
value = linode_database.php_db.password
sensitive = true
}
You would then update your PHP application’s database connection configuration to use these output values. For security, it’s best to inject these credentials into your application via environment variables or a secrets management system rather than hardcoding them.
Deployment Workflow
With your Terraform configuration in place, the deployment workflow is straightforward:
- Initialize Terraform: Run
terraform initin your project directory. This downloads the Linode provider and any other necessary plugins. - Review the Plan: Execute
terraform plan. This command shows you exactly what infrastructure Terraform will create, modify, or destroy. Carefully review this output to ensure it matches your expectations. - Apply the Configuration: Run
terraform apply. Terraform will prompt you to confirm the changes. Typeyesto proceed with provisioning the Linode resources. - Access Your Application: Once the apply is complete, Terraform will output the IP address of your load balancer. You can access your PHP application by navigating to this IP address in your web browser. You should see the “Hello from PHP server ID: X!” message.
- Destroy Resources: When you no longer need the infrastructure, run
terraform destroyto tear down all provisioned resources and avoid ongoing costs.
This setup provides a scalable, secure, and automated way to deploy PHP applications on Linode, leveraging the power of Infrastructure as Code.