Infrastructure as Code: Provisioning Secure Ruby Clusters on Google Cloud Using Terraform
Terraform Project Structure and Provider Configuration
We’ll begin by establishing a robust Terraform project structure. This organization is crucial for managing complexity, especially as your infrastructure grows. Our core configuration will reside in main.tf, variables in variables.tf, and outputs in outputs.tf. For this example, we’ll focus on provisioning a secure Ruby cluster on Google Cloud Platform (GCP). This involves setting up a Virtual Private Cloud (VPC), firewall rules, and Compute Engine instances.
First, let’s define the GCP provider and its authentication. It’s best practice to avoid hardcoding credentials. Instead, leverage environment variables or GCP’s built-in credential discovery mechanisms.
main.tf – Provider Block
# main.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
}
}
provider "google" {
project = var.gcp_project_id
region = var.gcp_region
}
variables.tf – Input Variables
# variables.tf
variable "gcp_project_id" {
description = "The GCP project ID to deploy resources into."
type = string
}
variable "gcp_region" {
description = "The GCP region for resource deployment."
type = string
default = "us-central1"
}
variable "vpc_name" {
description = "Name for the VPC network."
type = string
default = "ruby-cluster-vpc"
}
variable "subnet_name" {
description = "Name for the subnet."
type = string
default = "ruby-cluster-subnet"
}
variable "subnet_cidr" {
description = "CIDR block for the subnet."
type = string
default = "10.0.1.0/24"
}
variable "ruby_instance_count" {
description = "Number of Ruby application instances."
type = number
default = 2
}
variable "ruby_instance_machine_type" {
description = "Machine type for Ruby instances."
type = string
default = "e2-medium"
}
variable "ruby_instance_image" {
description = "GCP image for Ruby instances."
type = string
default = "ubuntu-os-cloud/ubuntu-2004-lts"
}
variable "ssh_user" {
description = "Username for SSH access to instances."
type = string
default = "deployer"
}
variable "ssh_public_key_path" {
description = "Path to the public SSH key file."
type = string
}
Networking: VPC, Subnet, and Firewall Rules
A secure infrastructure starts with well-defined networking. We’ll create a custom VPC network and a subnet to isolate our Ruby cluster. Crucially, we’ll implement strict firewall rules to control ingress and egress traffic, allowing only necessary ports for application access and management.
main.tf – Network Resources
# main.tf (continued)
resource "google_compute_network" "vpc_network" {
name = var.vpc_name
auto_create_subnetworks = false
routing_mode = "REGIONAL"
}
resource "google_compute_subnetwork" "subnet" {
name = var.subnet_name
ip_cidr_range = var.subnet_cidr
region = var.gcp_region
network = google_compute_network.vpc_network.id
}
# Allow SSH access from a specific IP range (e.g., your office or bastion host)
resource "google_compute_firewall" "allow_ssh" {
name = "${var.vpc_name}-allow-ssh"
network = google_compute_network.vpc_network.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"] # WARNING: Restrict this in production!
target_tags = ["ruby-cluster"]
}
# Allow HTTP/HTTPS access to the application
resource "google_compute_firewall" "allow_http_https" {
name = "${var.vpc_name}-allow-http-https"
network = google_compute_network.vpc_network.name
allow {
protocol = "tcp"
ports = ["80", "443"]
}
source_ranges = ["0.0.0.0/0"] # Allow from anywhere for public access
target_tags = ["ruby-cluster"]
}
# Allow internal communication within the subnet
resource "google_compute_firewall" "allow_internal" {
name = "${var.vpc_name}-allow-internal"
network = google_compute_network.vpc_network.name
allow {
protocol = "tcp"
ports = ["0-65535"]
}
allow {
protocol = "udp"
ports = ["0-65535"]
}
allow {
protocol = "icmp"
}
source_ranges = [var.subnet_cidr]
target_tags = ["ruby-cluster"]
}
# Deny all other egress traffic by default (requires explicit rules for outbound)
# This is a more advanced security posture. For simplicity, we'll omit a deny-all egress rule here,
# but it's highly recommended for production environments.
Compute Engine Instances for Ruby Cluster
We’ll provision multiple Compute Engine instances to form our Ruby cluster. Each instance will be configured with a startup script to install Ruby, necessary dependencies, and deploy a basic application. We’ll use instance templates and managed instance groups for scalability and resilience, though for this initial setup, we’ll focus on individual instances for clarity.
main.tf – Compute Engine Resources
# main.tf (continued)
data "google_compute_image" "ruby_image" {
name = var.ruby_instance_image
}
resource "google_compute_instance" "ruby_app_instance" {
count = var.ruby_instance_count
name = "ruby-app-${count.index}"
machine_type = var.ruby_instance_machine_type
zone = "${var.gcp_region}-a" # Example zone, consider making this configurable or using autoscaling
tags = ["ruby-cluster", "ssh", "http", "https"]
boot_disk {
initialize_params {
image = data.google_compute_image.ruby_image.self_link
size = 20
type = "pd-ssd"
}
}
network_interface {
subnetwork = google_compute_subnetwork.subnet.id
access_config {
// Ephemeral public IP, consider using static IPs or a Load Balancer
}
}
metadata_startup_script = templatefile("${path.module}/scripts/startup.sh.tpl", {
ssh_user = var.ssh_user
})
// SSH Key injection for initial access
metadata = {
ssh-keys = "${var.ssh_user}:${file(var.ssh_public_key_path)}"
}
service_account {
scopes = ["cloud-platform"] // Adjust scopes as needed for your application
}
lifecycle {
create_before_destroy = true
}
}
scripts/startup.sh.tpl – Instance Startup Script
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status.
# Update package lists and install essential packages
sudo apt-get update -y
sudo apt-get install -y ruby-full ruby-dev build-essential git curl software-properties-common
# Install Bundler
sudo gem install bundler
# Create a deployment directory
sudo mkdir -p /opt/ruby_app
sudo chown ${ssh_user}:${ssh_user} /opt/ruby_app
cd /opt/ruby_app
# --- Basic Ruby Application Deployment ---
# In a real-world scenario, you'd clone from a Git repository,
# manage secrets securely, and configure a web server (e.g., Puma, Unicorn).
# Example: Create a simple Sinatra app
cat << EOF > app.rb
require 'sinatra'
get '/' do
"Hello from Ruby Cluster Instance: #{Socket.gethostname}!"
end
EOF
# Example: Create a Gemfile
cat << EOF > Gemfile
source 'https://rubygems.org'
gem 'sinatra'
EOF
# Install gems
bundle install --path vendor/bundle
# --- Systemd Service for Application ---
# This ensures the Ruby app runs as a service and restarts on failure.
cat << EOF | sudo tee /etc/systemd/system/ruby_app.service
[Unit]
Description=Ruby Application Service
After=network.target
[Service]
User=${ssh_user}
Group=${ssh_user}
WorkingDirectory=/opt/ruby_app
Environment="BUNDLE_GEMFILE=/opt/ruby_app/Gemfile"
ExecStart=/usr/bin/bundle exec ruby app.rb -o 0.0.0.0 -p 4567
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd, enable and start the service
sudo systemctl daemon-reload
sudo systemctl enable ruby_app.service
sudo systemctl start ruby_app.service
echo "Ruby application setup complete."
Outputs and Accessing the Cluster
Finally, we define outputs to easily retrieve important information about our deployed infrastructure, such as the public IP addresses of the instances. This makes it simple to connect to and manage the cluster.
outputs.tf – Output Values
# outputs.tf
output "instance_public_ips" {
description = "Public IP addresses of the Ruby application instances."
value = google_compute_instance.ruby_app_instance[*].network_interface[0].access_config[0].nat_ip
}
output "ssh_command_template" {
description = "Template command to SSH into an instance."
value = "ssh -i <path_to_your_private_key> ${var.ssh_user}@%s"
}
Deployment Workflow
To deploy this infrastructure, follow these steps:
- Initialize Terraform: Navigate to your Terraform project directory and run
terraform init. This downloads the necessary provider plugins. - Set GCP Credentials: Ensure your GCP credentials are set up. The easiest way is often by setting the
GOOGLE_APPLICATION_CREDENTIALSenvironment variable to the path of your service account key file, or by runninggcloud auth application-default login. - Create a
terraform.tfvarsfile: This file will hold your specific variable values.
terraform.tfvars – Example Values
# terraform.tfvars gcp_project_id = "your-gcp-project-id" ssh_public_key_path = "~/.ssh/id_rsa.pub" # Path to your public SSH key # gcp_region = "us-east1" # Optional: override default region # ruby_instance_count = 3 # Optional: override default instance count
- Plan the Deployment: Run
terraform plan -var-file="terraform.tfvars"to see a preview of the resources Terraform will create. - Apply the Deployment: Execute
terraform apply -var-file="terraform.tfvars". Terraform will prompt for confirmation before provisioning the resources. - Access Your Cluster: Once applied, Terraform will output the public IP addresses of your instances. You can then SSH into them using the provided command template and your private key. For example, if an IP is
34.123.45.67, the command would be:ssh -i ~/.ssh/id_rsa [email protected]. You can then access your Ruby application via a web browser athttp://<instance_public_ip>:4567. - Destroy Resources: When you no longer need the infrastructure, run
terraform destroy -var-file="terraform.tfvars"to clean up all provisioned resources.
Security Considerations and Next Steps
This setup provides a foundational secure Ruby cluster. However, for production environments, consider the following enhancements:
- Restrict SSH Source Ranges: Replace
0.0.0.0/0in theallow_sshfirewall rule with specific IP addresses or CIDR blocks of trusted networks (e.g., your office VPN, bastion host). - Managed Instance Groups (MIGs) and Load Balancing: For high availability and scalability, replace individual instances with a MIG and front it with a GCP Load Balancer. This also simplifies IP management and health checking.
- Secrets Management: Integrate with GCP Secret Manager or HashiCorp Vault for managing application secrets instead of embedding them in startup scripts or code.
- Immutable Infrastructure: Build Docker images for your Ruby application and deploy them using GKE or Compute Engine instance templates with Container-Optimized OS. This promotes consistency and simplifies rollbacks.
- CI/CD Integration: Automate Terraform runs within your CI/CD pipeline (e.g., GitLab CI, GitHub Actions, Cloud Build) for consistent and repeatable deployments.
- Monitoring and Logging: Set up Cloud Monitoring and Cloud Logging for your instances and applications.
- Private IP Addressing: For internal services, avoid public IPs and rely on private networking and internal load balancers.