• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Infrastructure as Code: Provisioning Secure Ruby Clusters on AWS Using Terraform

Infrastructure as Code: Provisioning Secure Ruby Clusters on AWS Using Terraform

Terraform Project Structure and Provider Configuration

We’ll begin by establishing a robust Terraform project structure. This promotes maintainability and scalability. Our primary focus will be on provisioning a secure Ruby application cluster on AWS. This involves defining VPCs, subnets, security groups, EC2 instances, and load balancers. The core of our infrastructure definition will reside in `.tf` files within a dedicated directory.

First, let’s set up the AWS provider configuration. This block tells Terraform which cloud provider to interact with and specifies the region. For production environments, it’s crucial to manage AWS credentials securely, typically via environment variables or IAM roles, rather than hardcoding them.

Provider Configuration (`main.tf`)

# main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.0"
}

provider "aws" {
  region = "us-east-1" # Example region, adjust as needed
  # profile = "my-aws-profile" # Uncomment and configure if using AWS CLI profiles
  # access_key = var.aws_access_key_id # Example using variables for credentials
  # secret_key = var.aws_secret_access_key # Example using variables for credentials
}

# Define variables for sensitive information if not using environment variables or IAM roles
# variable "aws_access_key_id" {
#   description = "AWS Access Key ID"
#   type        = string
#   sensitive   = true
# }
#
# variable "aws_secret_access_key" {
#   description = "AWS Secret Access Key"
#   type        = string
#   sensitive   = true
# }

Networking: VPC, Subnets, and Security Groups

A secure network foundation is paramount. We’ll define a Virtual Private Cloud (VPC) with public and private subnets across multiple Availability Zones for high availability. Security groups will act as virtual firewalls, controlling inbound and outbound traffic to our instances.

VPC Definition (`vpc.tf`)

# vpc.tf

resource "aws_vpc" "app_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "ruby-app-vpc"
  }
}

resource "aws_internet_gateway" "app_igw" {
  vpc_id = aws_vpc.app_vpc.id
  tags = {
    Name = "ruby-app-igw"
  }
}

# Public Subnets (for NAT Gateways and Load Balancers)
resource "aws_subnet" "public_subnet_a" {
  vpc_id                  = aws_vpc.app_vpc.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "us-east-1a" # Adjust AZs as needed
  map_public_ip_on_launch = true
  tags = {
    Name = "ruby-app-public-a"
  }
}

resource "aws_subnet" "public_subnet_b" {
  vpc_id                  = aws_vpc.app_vpc.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "us-east-1b" # Adjust AZs as needed
  map_public_ip_on_launch = true
  tags = {
    Name = "ruby-app-public-b"
  }
}

# Private Subnets (for application instances)
resource "aws_subnet" "private_subnet_a" {
  vpc_id            = aws_vpc.app_vpc.id
  cidr_block        = "10.0.101.0/24"
  availability_zone = "us-east-1a" # Adjust AZs as needed
  tags = {
    Name = "ruby-app-private-a"
  }
}

resource "aws_subnet" "private_subnet_b" {
  vpc_id            = aws_vpc.app_vpc.id
  cidr_block        = "10.0.102.0/24"
  availability_zone = "us-east-1b" # Adjust AZs as needed
  tags = {
    Name = "ruby-app-private-b"
  }
}

# Route Table for Public Subnets
resource "aws_route_table" "public_rt" {
  vpc_id = aws_vpc.app_vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.app_igw.id
  }
  tags = {
    Name = "ruby-app-public-rt"
  }
}

resource "aws_route_table_association" "public_a_assoc" {
  subnet_id      = aws_subnet.public_subnet_a.id
  route_table_id = aws_route_table.public_rt.id
}

resource "aws_route_table_association" "public_b_assoc" {
  subnet_id      = aws_subnet.public_subnet_b.id
  route_table_id = aws_route_table.public_rt.id
}

# NAT Gateway and EIP for Private Subnet Outbound Access
resource "aws_eip" "nat_eip_a" {
  domain = "vpc"
  tags = {
    Name = "ruby-app-nat-eip-a"
  }
}

resource "aws_nat_gateway" "nat_gw_a" {
  allocation_id = aws_eip.nat_eip_a.id
  subnet_id     = aws_subnet.public_subnet_a.id
  tags = {
    Name = "ruby-app-nat-gw-a"
  }
  depends_on = [aws_internet_gateway.app_igw]
}

resource "aws_eip" "nat_eip_b" {
  domain = "vpc"
  tags = {
    Name = "ruby-app-nat-eip-b"
  }
}

resource "aws_nat_gateway" "nat_gw_b" {
  allocation_id = aws_eip.nat_eip_b.id
  subnet_id     = aws_subnet.public_subnet_b.id
  tags = {
    Name = "ruby-app-nat-gw-b"
  }
  depends_on = [aws_internet_gateway.app_igw]
}

# Route Table for Private Subnets
resource "aws_route_table" "private_rt_a" {
  vpc_id = aws_vpc.app_vpc.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gw_a.id
  }
  tags = {
    Name = "ruby-app-private-rt-a"
  }
}

resource "aws_route_table_association" "private_a_assoc" {
  subnet_id      = aws_subnet.private_subnet_a.id
  route_table_id = aws_route_table.private_rt_a.id
}

resource "aws_route_table" "private_rt_b" {
  vpc_id = aws_vpc.app_vpc.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.nat_gw_b.id
  }
  tags = {
    Name = "ruby-app-private-rt-b"
  }
}

resource "aws_route_table_association" "private_b_assoc" {
  subnet_id      = aws_subnet.private_subnet_b.id
  route_table_id = aws_route_table.private_rt_b.id
}

Security Group Definitions (`security_groups.tf`)

# security_groups.tf

resource "aws_security_group" "load_balancer_sg" {
  name        = "ruby-app-lb-sg"
  description = "Allow HTTP and HTTPS traffic to the load balancer"
  vpc_id      = aws_vpc.app_vpc.id

  ingress {
    description = "HTTP from anywhere"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTPS from anywhere"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "ruby-app-lb-sg"
  }
}

resource "aws_security_group" "app_instance_sg" {
  name        = "ruby-app-instance-sg"
  description = "Allow traffic from load balancer and SSH from bastion/admin IPs"
  vpc_id      = aws_vpc.app_vpc.id

  # Allow traffic from the load balancer SG on the application port (e.g., 3000 for Rails)
  ingress {
    description     = "App traffic from LB"
    from_port       = 3000 # Adjust to your application's port
    to_port         = 3000 # Adjust to your application's port
    protocol        = "tcp"
    security_groups = [aws_security_group.load_balancer_sg.id]
  }

  # Allow SSH access from a specific IP range (e.g., your office or bastion host)
  # IMPORTANT: Restrict this to the minimum necessary IPs for security.
  ingress {
    description = "SSH access"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["YOUR_ADMIN_IP_ADDRESS/32"] # Replace with your actual IP or CIDR
  }

  # Allow all outbound traffic (necessary for package installs, external API calls, etc.)
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "ruby-app-instance-sg"
  }
}

# Optional: Security Group for Bastion Host (if you implement one)
# resource "aws_security_group" "bastion_sg" {
#   name        = "ruby-app-bastion-sg"
#   description = "Allow SSH access to bastion host"
#   vpc_id      = aws_vpc.app_vpc.id
#
#   ingress {
#     description = "SSH from anywhere (restrict this in production!)"
#     from_port   = 22
#     to_port     = 22
#     protocol    = "tcp"
#     cidr_blocks = ["0.0.0.0/0"] # VERY INSECURE - restrict to specific IPs
#   }
#
#   egress {
#     from_port   = 0
#     to_port     = 0
#     protocol    = "-1"
#     cidr_blocks = ["0.0.0.0/0"]
#   }
#
#   tags = {
#     Name = "ruby-app-bastion-sg"
#   }
# }

Application Instances and Load Balancer

Now, we’ll define the EC2 instances that will host our Ruby application and an Application Load Balancer (ALB) to distribute traffic. We’ll use an Auto Scaling Group to ensure the desired number of instances are running and to handle scaling based on demand. For simplicity, we’ll use a basic AMI and a user data script to install Ruby and deploy the application. In a production scenario, you’d likely use a more robust AMI (e.g., Packer-built) and a more sophisticated deployment strategy (e.g., CodeDeploy, ECS, EKS).

EC2 Instance Configuration (`app_instances.tf`)

# app_instances.tf

# Data source to find the latest Amazon Linux 2 AMI
data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# User data script to install Ruby and deploy a sample application
locals {
  user_data_script = <<-EOF
    #!/bin/bash
    exec > >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1
    echo "Starting user data script..."

    # Update packages
    sudo yum update -y

    # Install Ruby (using RVM for flexibility)
    sudo gpg2 --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CFC142126F2083921902A96A52370573F
    curl -sSL https://get.rvm.io | bash -s stable
    source /usr/local/rvm/scripts/rvm
    rvm requirements
    rvm install 3.1.2 # Specify your desired Ruby version
    rvm use 3.1.2 --default

    # Install Bundler
    gem install bundler

    # Install Node.js and Yarn (often needed for Rails asset compilation)
    curl -sL https://rpm.nodesource.com/setup_18.x | sudo bash -
    sudo yum install -y nodejs
    sudo npm install -g yarn

    # Create application directory
    sudo mkdir -p /srv/app
    cd /srv/app

    # Clone your application repository (replace with your actual repo URL)
    # For demonstration, we'll create a dummy app.
    # git clone YOUR_APP_REPO_URL .
    # For demo purposes:
    echo "

Hello from Ruby App!

" | sudo tee index.html sudo apt-get install -y nginx # Install Nginx as a reverse proxy sudo systemctl start nginx sudo systemctl enable nginx # If you have a Rails app, you'd typically do: # bundle install # RAILS_ENV=production bundle exec rails assets:precompile # Configure a process manager like systemd or Puma/Unicorn echo "User data script finished." EOF user_data_script = replace(local.user_data_script, "YOUR_APP_REPO_URL", "https://github.com/your-username/your-ruby-app.git") # Replace with actual repo } resource "aws_launch_template" "app_launch_template" { name_prefix = "ruby-app-lt-" image_id = data.aws_ami.amazon_linux_2.id instance_type = "t3.medium" # Adjust instance type as needed key_name = "your-ssh-key-pair-name" # Replace with your EC2 key pair name vpc_security_group_ids = [aws_security_group.app_instance_sg.id] user_data = base64encode(local.user_data_script) tags = { Name = "ruby-app-instance" } lifecycle { create_before_destroy = true } } resource "aws_autoscaling_group" "app_asg" { name_prefix = "ruby-app-asg-" desired_capacity = 2 min_size = 1 max_size = 5 vpc_zone_identifier = [aws_subnet.private_subnet_a.id, aws_subnet.private_subnet_b.id] launch_template { id = aws_launch_template.app_launch_template.id version = "$Latest" } # Health check configuration health_check_type = "ELB" health_check_grace_period = 300 # Seconds to allow instances to become healthy # Attach to the ALB Target Group (defined below) target_group_arns = [aws_lb_target_group.app_tg.arn] tags = [ { key = "Name" value = "ruby-app-instance" propagate_at_launch = true } ] }

Application Load Balancer (`load_balancer.tf`)

# load_balancer.tf

resource "aws_lb" "app_alb" {
  name               = "ruby-app-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.load_balancer_sg.id]
  subnets            = [aws_subnet.public_subnet_a.id, aws_subnet.public_subnet_b.id]

  enable_deletion_protection = false # Set to true for production

  tags = {
    Name = "ruby-app-alb"
  }
}

resource "aws_lb_listener" "http_listener" {
  load_balancer_arn = aws_lb.app_alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app_tg.arn
  }
}

# Optional: HTTPS Listener with ACM Certificate
# resource "aws_lb_listener" "https_listener" {
#   load_balancer_arn = aws_lb.app_alb.arn
#   port              = "443"
#   protocol          = "HTTPS"
#   ssl_policy        = "ELBSecurityPolicy-2016-08" # Or a more recent policy
#   certificate_arn   = "arn:aws:acm:us-east-1:123456789012:certificate/YOUR_ACM_CERT_ID" # Replace with your ACM certificate ARN
#
#   default_action {
#     type             = "forward"
#     target_group_arn = aws_lb_target_group.app_tg.arn
#   }
# }

resource "aws_lb_target_group" "app_tg" {
  name     = "ruby-app-tg"
  port     = 3000 # The port your Ruby application listens on
  protocol = "HTTP"
  vpc_id   = aws_vpc.app_vpc.id

  health_check {
    enabled             = true
    interval            = 30
    path                = "/" # Adjust to a health check endpoint in your app
    port                = "traffic port"
    protocol            = "HTTP"
    matcher             = "200" # Expect HTTP 200 OK
    timeout             = 5
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }

  tags = {
    Name = "ruby-app-tg"
  }
}

Deployment Workflow and Security Considerations

With the Terraform code defined, the deployment workflow is straightforward:

  • Initialize: Run terraform init in your project directory. This downloads the necessary AWS provider plugin.
  • Plan: Execute terraform plan. This command shows you exactly what resources Terraform will create, modify, or destroy. Review this output carefully.
  • Apply: Run terraform apply. Confirm with ‘yes’ when prompted. Terraform will then provision all the defined AWS resources.
  • Destroy: When you no longer need the infrastructure, run terraform destroy to clean up all created resources.

Security Best Practices and Enhancements

  • IAM Roles: Instead of using access keys for EC2 instances, assign IAM roles with least-privilege permissions. This is handled by the `aws_iam_instance_profile` and `aws_iam_role` resources in Terraform.
  • Secrets Management: Never hardcode secrets (API keys, database passwords) in your Terraform code or user data scripts. Use AWS Secrets Manager or HashiCorp Vault and retrieve them at runtime.
  • SSH Key Management: Ensure your SSH keys are managed securely. Avoid storing them in version control. Use a secure method for distributing them to instances (e.g., AWS Systems Manager Parameter Store with encryption).
  • Patching: Implement a regular patching strategy for your AMIs and installed software. Consider using AWS Systems Manager Patch Manager.
  • Logging and Monitoring: Integrate with AWS CloudWatch for logging and monitoring. Ensure your application logs are sent to CloudWatch Logs. Set up alarms for critical metrics.
  • HTTPS: Always use HTTPS in production. Configure an ACM certificate and an HTTPS listener on your ALB.
  • Network Segmentation: The VPC and subnet design already provides basic segmentation. Further restrict traffic between subnets if necessary.
  • Immutable Infrastructure: Aim for immutable infrastructure. Instead of updating instances in place, build new AMIs with updated code/dependencies and replace old instances.
  • Bastion Host: For enhanced security, consider deploying a bastion host in a public subnet. All SSH access should go through the bastion host, which has a tightly controlled security group.

Example: Adding an IAM Role for EC2 Instances

To grant EC2 instances permissions to interact with other AWS services (e.g., S3 for logs, Secrets Manager), you’d define an IAM role and attach it to the launch template.

# iam.tf

resource "aws_iam_role" "app_instance_role" {
  name = "ruby-app-instance-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

# Example policy: Allow reading from Secrets Manager
resource "aws_iam_policy" "secrets_manager_read_policy" {
  name        = "ruby-app-secrets-manager-read"
  description = "Allow reading secrets from Secrets Manager"
  policy      = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = [
          "secretsmanager:GetSecretValue",
          "secretsmanager:DescribeSecret"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:secretsmanager:us-east-1:*:secret:your-app-secrets-*" # Restrict resource ARN
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "secrets_manager_attach" {
  role       = aws_iam_role.app_instance_role.name
  policy_arn = aws_iam_policy.secrets_manager_read_policy.arn
}

# Attach the role to the launch template
resource "aws_launch_template" "app_launch_template" {
  # ... (previous configuration) ...

  iam_instance_profile {
    name = aws_iam_instance_profile.app_profile.name
  }
}

resource "aws_iam_instance_profile" "app_profile" {
  name = "ruby-app-instance-profile"
  role = aws_iam_role.app_instance_role.name
}

By following these steps and incorporating the security best practices, you can provision a robust, scalable, and secure Ruby application cluster on AWS using Infrastructure as Code with Terraform.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala