Infrastructure as Code: Provisioning Secure Ruby Clusters on OVH Using Terraform
OVHcloud Provider Configuration for Terraform
To provision resources on OVHcloud, we’ll leverage the official Terraform OVHcloud provider. This involves setting up credentials and specifying the region. For security, it’s highly recommended to use environment variables or a dedicated Terraform variables file rather than hardcoding sensitive information directly in your configuration.
First, ensure you have the OVHcloud provider block defined in your Terraform configuration. This typically resides in a providers.tf file.
OVHcloud Provider Block
# providers.tf
terraform {
required_providers {
ovh = {
source = "ovh/ovh"
version = "~> 1.0" # Specify a version constraint
}
}
}
provider "ovh" {
endpoint = "ovh-eu" # Or "ovh-us", "ovh-ca"
# Credentials can be provided via environment variables:
# OVH_APPLICATION_KEY
# OVH_APPLICATION_SECRET
# OVH_CONSUMER_KEY
# OVH_ENDPOINT (optional, if not set in endpoint argument)
}
You’ll need to obtain your OVHcloud API credentials. This involves creating an application within the OVHcloud control panel. The key components are:
- Application Key: Your primary API key.
- Application Secret: A secret associated with your key.
- Consumer Key: A key generated after authorizing your application.
These credentials should be set as environment variables before running Terraform commands:
Setting Environment Variables
export OVH_APPLICATION_KEY="your_application_key" export OVH_APPLICATION_SECRET="your_application_secret" export OVH_CONSUMER_KEY="your_consumer_key" # export OVH_ENDPOINT="ovh-eu" # If not specified in provider block
With the provider configured and credentials set, we can proceed to define the infrastructure for our Ruby cluster.
Defining the Ruby Cluster Infrastructure
Our Ruby cluster will consist of several components: a public cloud project, a private network, and multiple instances to host the Ruby application. We’ll also set up security groups to control network access.
Terraform Configuration for the Cluster
# main.tf
# --- Project and Network ---
resource "ovh_cloud_project" "ruby_project" {
service_name = "ruby-cluster-project"
region = "GRA1" # Example region, choose your preferred one
description = "Project for hosting Ruby cluster"
}
resource "ovh_cloud_network_private" "ruby_network" {
service_name = ovh_cloud_project.ruby_project.service_name
name = "ruby-cluster-vpc"
region = ovh_cloud_project.ruby_project.region
cidr = "10.0.0.0/16"
description = "Private network for Ruby cluster"
}
# --- Security Groups ---
resource "ovh_cloud_network_private_subnet" "ruby_subnet" {
service_name = ovh_cloud_project.ruby_network.service_name
network_id = ovh_cloud_network_private.ruby_network.network_id
region = ovh_cloud_project.ruby_network.region
cidr = "10.0.1.0/24"
name = "ruby-cluster-subnet"
description = "Subnet for Ruby cluster instances"
}
resource "ovh_cloud_securitygroup" "ruby_sg" {
service_name = ovh_cloud_project.ruby_project.service_name
region = ovh_cloud_project.ruby_project.region
name = "ruby-cluster-sg"
description = "Security group for Ruby cluster"
}
# Allow SSH access from a specific IP range (e.g., your office IP)
resource "ovh_cloud_securitygroup_rule" "ssh_ingress" {
service_name = ovh_cloud_securitygroup.ruby_sg.service_name
security_group_id = ovh_cloud_securitygroup.ruby_sg.id
region = ovh_cloud_securitygroup.ruby_sg.region
protocol = "tcp"
port_in_range = 22
cidr = "YOUR_OFFICE_IP/32" # IMPORTANT: Replace with your actual IP
direction = "in"
description = "Allow SSH access"
}
# Allow HTTP access from anywhere
resource "ovh_cloud_securitygroup_rule" "http_ingress" {
service_name = ovh_cloud_securitygroup.ruby_sg.service_name
security_group_id = ovh_cloud_securitygroup.ruby_sg.id
region = ovh_cloud_securitygroup.ruby_sg.region
protocol = "tcp"
port_in_range = 80
cidr = "0.0.0.0/0"
direction = "in"
description = "Allow HTTP access"
}
# Allow HTTPS access from anywhere
resource "ovh_cloud_securitygroup_rule" "https_ingress" {
service_name = ovh_cloud_securitygroup.ruby_sg.service_name
security_group_id = ovh_cloud_securitygroup.ruby_sg.id
region = ovh_cloud_securitygroup.ruby_sg.region
protocol = "tcp"
port_in_range = 443
cidr = "0.0.0.0/0"
direction = "in"
description = "Allow HTTPS access"
}
# Allow all outbound traffic (adjust if stricter egress control is needed)
resource "ovh_cloud_securitygroup_rule" "all_egress" {
service_name = ovh_cloud_securitygroup.ruby_sg.service_name
security_group_id = ovh_cloud_securitygroup.ruby_sg.id
region = ovh_cloud_securitygroup.ruby_sg.region
protocol = "any"
cidr = "0.0.0.0/0"
direction = "out"
description = "Allow all outbound traffic"
}
# --- Instances ---
# Define instance variables for easier management
variable "instance_count" {
description = "Number of Ruby application instances"
type = number
default = 3
}
variable "instance_flavor" {
description = "Flavor for the Ruby instances (e.g., 'b2-7', 'c2-15')"
type = string
default = "b2-7" # Example: 2 vCPU, 7 GB RAM
}
variable "instance_image" {
description = "Image ID for the Ruby instances (e.g., Ubuntu 22.04 LTS)"
type = string
default = "ubuntu-2204-jammy-amd64" # Find latest image IDs in OVHcloud console or via API
}
resource "ovh_cloud_instance" "ruby_app_instance" {
count = var.instance_count
service_name = ovh_cloud_project.ruby_project.service_name
region = ovh_cloud_project.ruby_project.region
name = "ruby-app-${count.index + 1}"
flavor_name = var.instance_flavor
image_id = var.instance_image
ssh_key_name = "my-terraform-ssh-key" # IMPORTANT: Ensure this SSH key is uploaded to your OVHcloud account
public_cloud_network_uuid = ovh_cloud_network_private.ruby_network.network_id # Connect to the private network
security_group_ids = [ovh_cloud_securitygroup.ruby_sg.id]
user_data = templatefile("${path.module}/scripts/bootstrap.sh.tpl", {
app_name = "my-ruby-app"
# Add any other variables needed for bootstrapping
})
lifecycle {
create_before_destroy = true
}
}
# --- Outputs ---
output "ruby_instance_ips" {
description = "Public IPs of the Ruby application instances"
value = [for instance in ovh_cloud_instance.ruby_app_instance : instance.public_ip]
}
output "ruby_private_ips" {
description = "Private IPs of the Ruby application instances"
value = [for instance in ovh_cloud_instance.ruby_app_instance : instance.private_ip]
}
In the above configuration:
- We define a
ovh_cloud_projectto logically group our resources. - A private network (
ovh_cloud_network_private) and subnet (ovh_cloud_network_private_subnet) are created for internal communication. - A security group (
ovh_cloud_securitygroup) is established with specific ingress rules for SSH (from a trusted IP), HTTP, and HTTPS. Egress is set to allow all outbound traffic, which is common but can be restricted further for enhanced security. - We use variables (
instance_count,instance_flavor,instance_image) to make the instance configuration flexible. - Multiple instances (
ovh_cloud_instance) are provisioned, connected to the private network, and assigned the security group. - Crucially,
ssh_key_namerefers to an SSH key pair that must be pre-uploaded to your OVHcloud account. Terraform will inject the public key for access. - The
user_dataparameter uses a template file (scripts/bootstrap.sh.tpl) to execute a shell script on instance boot. This is essential for installing Ruby, dependencies, and deploying your application.
Instance Bootstrapping with User Data
The user_data script is a critical part of automating the setup of your Ruby application on each instance. This script runs as root on first boot.
# scripts/bootstrap.sh.tpl
#!/bin/bash -xe
# Update package lists and install essential packages
apt-get update -y
apt-get upgrade -y
apt-get install -y \
git \
curl \
wget \
build-essential \
libssl-dev \
zlib1g-dev \
libreadline-dev \
libyaml-dev \
libsqlite3-dev \
sqlite3 \
libxml2-dev \
libxslt1-dev \
libcurl4-openssl-dev \
software-properties-common \
libffi-dev \
nginx \
ufw
# Install Ruby using rbenv for version management
# This is a robust way to manage Ruby versions and gems
export PATH="$HOME/.rbenv/bin:$PATH"
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-installer | bash
# Configure rbenv to load automatically
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
# Install a specific Ruby version (e.g., 3.2.2)
rbenv install 3.2.2
rbenv global 3.2.2
gem install bundler --no-document
# Configure Nginx as a reverse proxy
# Assuming your Ruby app runs on port 3000
NGINX_CONF="/etc/nginx/sites-available/ruby_app"
cat <<EOF > $NGINX_CONF
server {
listen 80 default_server;
server_name _; # Catch all hostnames
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
}
EOF
# Enable the Nginx site and restart Nginx
ln -sf $NGINX_CONF /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default # Remove default Nginx site
nginx -t # Test Nginx configuration
systemctl restart nginx
# Configure Firewall (UFW)
ufw allow ssh
ufw allow http
ufw allow https
ufw allow 3000 # Allow traffic to the Ruby app port if direct access is needed for debugging
ufw --force enable
# --- Application Deployment Placeholder ---
# This section is a placeholder. You would typically:
# 1. Clone your application repository.
# 2. Install gems using 'bundle install'.
# 3. Configure your application (e.g., database connection strings).
# 4. Start your Ruby application using a process manager like Puma or Unicorn.
# Example (conceptual):
# cd /var/www/
# git clone YOUR_APP_REPO app_name
# cd app_name
# bundle install --without development test
# # Configure database.yml, secrets.yml etc.
# # Start Puma/Unicorn (e.g., using systemd service)
# systemctl start my-ruby-app.service
# -----------------------------------------
echo "Ruby cluster instance bootstrapping complete."
This script:
- Updates the system and installs necessary build tools, Git, Nginx, and UFW.
- Installs
rbenvand a specific Ruby version (e.g., 3.2.2), along with Bundler. This is crucial for reproducible Ruby environments. - Configures Nginx as a reverse proxy to forward requests from port 80 to your Ruby application running on port 3000.
- Sets up UFW (Uncomplicated Firewall) to allow SSH, HTTP, and HTTPS traffic.
- Includes a placeholder for your actual application deployment steps (cloning the repo, installing gems, starting the application server).
Important Considerations for User Data:
- Idempotency: Ensure your scripts are idempotent, meaning they can be run multiple times without unintended side effects.
- Secrets Management: Do NOT hardcode sensitive information (database passwords, API keys) directly in the user data script. Use a secrets management solution (like HashiCorp Vault, AWS Secrets Manager, or OVHcloud’s own secrets service if available) and fetch them during runtime or inject them securely.
- Process Management: For production, use a process manager like
systemd,supervisord, Puma, or Unicorn to ensure your Ruby application runs reliably, restarts on failure, and is managed effectively. - Image Selection: Always use up-to-date and secure base images. Verify the image ID for your chosen region and OS.
Terraform Workflow
Once your Terraform files (providers.tf, main.tf, and the scripts/bootstrap.sh.tpl file) are in place, and your OVHcloud credentials are set as environment variables, you can provision your infrastructure:
Initialization
terraform init
This command downloads the OVHcloud provider plugin.
Plan and Review
terraform plan
This command shows you what Terraform will create, modify, or destroy. Carefully review the plan to ensure it matches your expectations.
Apply
terraform apply
Terraform will prompt you to confirm the changes. Type yes to proceed with provisioning the resources on OVHcloud. After the apply is complete, the output will display the public and private IP addresses of your Ruby instances.
Destroy
terraform destroy
When you no longer need the infrastructure, this command will tear down all provisioned resources. Use with caution.
Security Best Practices and Next Steps
This setup provides a foundational secure Ruby cluster. For production environments, consider the following:
- SSH Key Management: Use a dedicated SSH key for Terraform and manage instance access keys securely. Rotate keys regularly.
- Secrets Management: Implement a robust secrets management solution for database credentials, API keys, and application secrets.
- Database Provisioning: Provision a managed database service (e.g., OVHcloud Managed Databases for PostgreSQL or MySQL) instead of running databases on application instances.
- Load Balancing: For high availability and scalability, introduce a load balancer (e.g., OVHcloud Load Balancer) in front of your Ruby instances.
- Monitoring and Logging: Integrate monitoring tools (Prometheus, Grafana) and centralized logging solutions (ELK stack, Loki) for visibility into your cluster’s health and performance.
- CI/CD Integration: Integrate Terraform into your CI/CD pipeline for automated infrastructure deployments and updates.
- Immutable Infrastructure: For true scalability and reliability, consider building custom machine images with your application pre-installed, rather than relying solely on user data for bootstrapping.
- Network Segmentation: Further segment your network if you have different tiers of applications or services.
By leveraging Infrastructure as Code with Terraform and adhering to security best practices, you can efficiently and reliably provision and manage your Ruby clusters on OVHcloud, enabling scalable and resilient deployments.