Infrastructure as Code: Provisioning Secure Ruby 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 secrets management system rather than hardcoding it directly into your Terraform configuration.
Create a file named providers.tf in your Terraform project directory. This file will house the provider configuration.
providers.tf
# providers.tf
terraform {
required_providers {
linode = {
source = "linode/linode"
version = "~> 1.20" # Specify a version constraint
}
}
}
# Configure the Linode provider
# It's recommended to set LINODE_TOKEN environment variable
# export LINODE_TOKEN="your_linode_api_token"
provider "linode" {
# token = var.linode_api_token # Alternatively, use a variable
}
# Optional: Define a variable for the token if not using environment variables
# variable "linode_api_token" {
# description = "Linode API Token"
# type = string
# sensitive = true
# }
After creating this file, you’ll need to initialize Terraform. Navigate to your project directory in your terminal and run:
terraform init
Defining the Ruby Cluster Resources
Now, let’s define the core infrastructure for our Ruby cluster. This will include a VPC network for private communication, a firewall for security, and multiple Linode instances that will host our Ruby applications. We’ll aim for a highly available setup with at least two application servers and a separate database server.
main.tf – VPC, Firewall, and Instances
# main.tf
# Define the Linode VPC network
resource "linode_vpc" "ruby_cluster_vpc" {
region = "us-east" # Choose your preferred region
label = "ruby-cluster-vpc"
description = "VPC for the Ruby cluster"
}
# Define a firewall for the VPC
resource "linode_firewall" "ruby_cluster_firewall" {
label = "ruby-cluster-firewall"
description = "Firewall for Ruby cluster instances"
ipv4 {
inbound {
label = "Allow SSH"
protocol = "TCP"
ports = ["22"]
any_in = true
}
inbound {
label = "Allow HTTP"
protocol = "TCP"
ports = ["80"]
any_in = true
}
inbound {
label = "Allow HTTPS"
protocol = "TCP"
ports = ["443"]
any_in = true
}
# Add rules for inter-instance communication if needed, e.g., for database connections
inbound {
label = "Allow App to DB"
protocol = "TCP"
ports = ["5432"] # PostgreSQL default port
from_addresses = [linode_instance.app_server.*.private_ip] # Allow from app servers
}
outbound {
label = "Allow All Outbound"
protocol = "ALL"
any_in = true
any_out = true
}
}
# Associate firewall with the VPC
# Note: Linode provider v1.20+ allows direct association with VPC
# For older versions, you might need to associate with individual instances.
# This example assumes a recent provider version.
vpc_id = linode_vpc.ruby_cluster_vpc.id
}
# Define the database server instance
resource "linode_instance" "db_server" {
region = linode_vpc.ruby_cluster_vpc.region
type = "g6-nanode-1" # Adjust instance type as needed
label = "ruby-cluster-db"
image = "debian 12" # Or your preferred OS image
root_pass = "your_strong_db_password" # **CHANGE THIS** or use secrets management
private_ip = true # Enable private IP for VPC communication
vpc_id = linode_vpc.ruby_cluster_vpc.id
tags = ["database", "ruby-cluster"]
# Associate firewall with the instance
firewall_id = linode_firewall.ruby_cluster_firewall.id
# User data for initial setup (e.g., installing PostgreSQL)
user_data = file("scripts/db_setup.sh")
}
# Define the first application server instance
resource "linode_instance" "app_server" {
count = 2 # Create two application servers for redundancy
region = linode_vpc.ruby_cluster_vpc.region
type = "g6-nanode-1" # Adjust instance type as needed
label = "ruby-cluster-app-${count.index}"
image = "debian 12" # Or your preferred OS image
root_pass = "your_strong_app_password" # **CHANGE THIS** or use secrets management
private_ip = true # Enable private IP for VPC communication
vpc_id = linode_vpc.ruby_cluster_vpc.id
tags = ["application", "ruby-cluster"]
# Associate firewall with the instance
firewall_id = linode_firewall.ruby_cluster_firewall.id
# User data for initial setup (e.g., installing Ruby, Nginx, Passenger)
user_data = file("scripts/app_setup.sh")
# Ensure DB is provisioned before app servers start their setup
depends_on = [linode_instance.db_server]
}
# Output private IPs for reference
output "db_server_private_ip" {
description = "Private IP address of the database server"
value = linode_instance.db_server.private_ip
}
output "app_server_private_ips" {
description = "Private IP addresses of the application servers"
value = linode_instance.app_server.*.private_ip
}
Instance Setup Scripts
The user_data argument in the linode_instance resource allows us to execute scripts upon instance creation. These scripts are crucial for bootstrapping our servers with the necessary software and configurations.
scripts/db_setup.sh
#!/bin/bash # scripts/db_setup.sh # Update package list and install PostgreSQL apt-get update -y apt-get install -y postgresql postgresql-contrib # Configure PostgreSQL for remote access (adjust as per your security needs) # This is a basic example; for production, use more restrictive settings. echo "listen_addresses = '*'" >> /etc/postgresql/15/main/postgresql.conf # Adjust version if needed echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/15/main/pg_hba.conf # Restart PostgreSQL service systemctl restart postgresql # Create a database user and database for the Ruby application # **IMPORTANT**: Replace 'your_app_user' and 'your_app_password' with strong credentials. # Consider using a secrets manager for passwords. sudo -u postgres psql -c "CREATE USER your_app_user WITH PASSWORD 'your_app_password';" sudo -u postgres psql -c "CREATE DATABASE your_app_db OWNER your_app_user;" echo "Database server setup complete."
scripts/app_setup.sh
#!/bin/bash
# scripts/app_setup.sh
# Update package list and install essential packages
apt-get update -y
apt-get install -y curl git nginx build-essential libssl-dev libreadline-dev zlib1g-dev
# Install Ruby using rbenv (or another version manager)
# This is a robust way to manage Ruby versions.
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-installer | bash
# Add rbenv to PATH for the current session and for future logins
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
# Install Ruby (e.g., latest stable version)
rbenv install 3.2.2 # Replace with your desired Ruby version
rbenv global 3.2.2
gem install bundler --no-document
# Install Passenger for Nginx
gem install passenger --no-document
passenger-install-nginx-module --auto --prefix=/opt/nginx --nginx-dir=/opt/nginx --extra-configure-flags="--with-compat"
# Configure Nginx to use Passenger
# This is a simplified configuration. You'll likely need to customize it
# for your specific Ruby application (e.g., setting root, app_env, etc.).
NGINX_CONF="/etc/nginx/nginx.conf"
PASSENGER_ROOT=$(passenger --root)
PASSENGER_MODULE=$(find /opt/nginx/modules -name "ngx_http_passenger_module.so" | head -n 1)
sed -i "/http {/ i \ load_module modules/ngx_http_passenger_module.so;\n passenger_root ${PASSENGER_ROOT};\n passenger_ruby $(rbenv prefix)/bin/ruby;" ${NGINX_CONF}
# Create a basic Nginx site configuration for the Ruby app
APP_DOMAIN="your_app_domain.com" # **CHANGE THIS**
APP_ROOT="/var/www/your_ruby_app" # **CHANGE THIS**
mkdir -p ${APP_ROOT}
cat > /etc/nginx/sites-available/ruby_app <<EOF
server {
listen 80;
server_name ${APP_DOMAIN};
root ${APP_ROOT}/public; # Assuming your Rails app has a public directory
passenger_enabled on;
passenger_app_env production; # Or development, staging
passenger_base_uri /; # Adjust if your app is mounted under a sub-path
passenger_ruby $(rbenv prefix)/bin/ruby;
}
EOF
# Enable the site and remove default
ln -s /etc/nginx/sites-available/ruby_app /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
# Test Nginx configuration and restart
nginx -t
systemctl restart nginx
echo "Application server setup complete."
echo "Remember to deploy your Ruby application to ${APP_ROOT} and configure your database connection."
Deployment and Management
With the Terraform configuration in place, you can provision your infrastructure. Ensure you have the Linode CLI installed and authenticated, or that the LINODE_TOKEN environment variable is set.
Terraform Workflow
# Initialize Terraform (if not already done) terraform init # Review the execution plan terraform plan # Apply the configuration to create resources terraform apply # To destroy the infrastructure when no longer needed # terraform destroy
After running terraform apply, Terraform will create the VPC, firewall, database server, and application servers on Linode. The user_data scripts will execute, setting up PostgreSQL on the database server and Ruby, Nginx, and Passenger on the application servers. The output variables will provide the private IP addresses, which you can use to configure your application’s database connection strings.
Security Considerations and Next Steps
This setup provides a foundational secure environment. However, several critical security aspects need further attention for production deployments:
- Database Passwords: Never hardcode database passwords. Use Linode Secrets, HashiCorp Vault, or environment variables injected securely.
- SSH Key Management: Instead of
root_pass, use SSH keys for secure access. You can manage SSH keys via Terraform’slinode_ssh_keyresource or by pre-loading them onto your Linode account. - Firewall Rules: The provided firewall rules are basic. Restrict inbound access to only necessary ports and sources. For the database, only allow connections from your application server’s private IP range.
- Application Deployment: The
app_setup.shscript installs the necessary software but doesn’t deploy your application. Implement a CI/CD pipeline (e.g., using GitHub Actions, GitLab CI, Jenkins) to automate application deployments to the specifiedAPP_ROOT. - HTTPS: Configure Nginx for HTTPS using Let’s Encrypt or another certificate authority. This can be automated with tools like Certbot.
- Monitoring and Logging: Integrate robust monitoring and logging solutions for your cluster.
- Database Backups: Implement a reliable database backup strategy.
- Instance Hardening: Further harden your Linode instances by disabling root login via SSH, configuring sudo access, and regularly applying security patches.
By leveraging Terraform, you can consistently and reliably provision secure, scalable Ruby clusters on Linode, enabling faster development cycles and more robust deployments.