← Back to DevBytes

Scaling EC2: From Prototype to Production

Scaling EC2: From Prototype to Production

Amazon EC2 (Elastic Compute Cloud) is one of the most foundational services in AWS, providing resizable virtual machines — called instances — that power everything from small web apps to massive distributed systems. While launching a single EC2 instance for a prototype is straightforward, scaling that instance to handle production traffic reliably, securely, and cost-effectively requires a deeper understanding of AWS infrastructure. This tutorial walks you through the journey of taking an EC2-based application from a rough prototype to a production-ready, auto-scaling deployment.

What Is EC2 Scaling?

Scaling EC2 means adjusting the number and size of your compute resources in response to demand. There are two primary dimensions to scaling:

In production, horizontal scaling is almost always preferred because it offers better fault tolerance, eliminates single points of failure, and allows granular capacity adjustments. AWS provides several managed services to automate horizontal scaling, including Auto Scaling Groups, Elastic Load Balancing, and Application Load Balancers.

Why Scaling Matters

A prototype running on a single EC2 instance is fragile. If that instance fails, your application goes offline. If traffic spikes unexpectedly, the instance may become overwhelmed and unresponsive. Scaling matters because it directly addresses three production concerns:

Without a scaling strategy, you are forced to over-provision for worst-case scenarios, which wastes money, or under-provision and risk outages. Auto Scaling lets you find the right balance dynamically.

From Prototype to Production: The Architecture

Consider a typical prototype: you launch a single t2.micro instance, SSH into it, install your application, and point your domain's DNS record at its public IP. This works for testing but has several production problems: the IP changes on reboot, there is no redundancy, deployments require downtime, and there is no health checking.

The production architecture replaces this with the following components:

Step 1: Creating a Launch Template

A Launch Template captures the blueprint for your EC2 instances. It specifies the AMI, instance type, key pair, security groups, user data script, and IAM instance profile. By using a template, every instance launched by your Auto Scaling Group will be identical.

Here is an example of creating a launch template using the AWS CLI:

aws ec2 create-launch-template \
  --launch-template-name web-app-template \
  --version-description "Initial version" \
  --launch-template-data '{
    "ImageId": "ami-0c02fb55956c7d316",
    "InstanceType": "t3.small",
    "KeyName": "my-key-pair",
    "SecurityGroupIds": ["sg-0123456789abcdef0"],
    "IamInstanceProfile": {
      "Name": "EC2WebAppRole"
    },
    "UserData": "IyEvYmluL2Jhc2gKeXVtIHVwZGF0ZSAteQp5dW0gaW5zdGFsbCAteSBodHRwZApzeXN0ZW1jdGwgc3RhcnQgaHR0cGQKc3lzdGVtY3RsIGVuYWJsZSBodHRwZA==",
    "TagSpecifications": [
      {
        "ResourceType": "instance",
        "Tags": [
          {"Key": "Name", "Value": "web-app"},
          {"Key": "Environment", "Value": "production"}
        ]
      }
    ]
  }'

The UserData field is base64-encoded. In this example, it decodes to a bash script that updates the system, installs Apache, and starts the HTTP service. In a real deployment, you would use this script to pull your application code from a repository, install dependencies, and start your application server.

Step 2: Configuring the Application Load Balancer

Before creating the Auto Scaling Group, you need a load balancer and a target group. The ALB receives traffic from users and forwards it to healthy instances in the target group. Here is how to create both:

# Create the target group
aws elbv2 create-target-group \
  --name web-app-tg \
  --protocol HTTP \
  --port 80 \
  --vpc-id vpc-0123456789abcdef0 \
  --health-check-path /health \
  --health-check-interval-seconds 30 \
  --health-check-timeout-seconds 5 \
  --healthy-threshold-count 2 \
  --unhealthy-threshold-count 3

# Create the load balancer
aws elbv2 create-load-balancer \
  --name web-app-alb \
  --subnets subnet-0aaa subnet-0bbb subnet-0ccc \
  --security-groups sg-alb-0123456789abcdef0

# Create a listener that forwards to the target group
aws elbv2 create-listener \
  --load-balancer-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/web-app-alb/abc123 \
  --protocol HTTP \
  --port 80 \
  --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/web-app-tg/def456

Notice the /health health check path. Your application must expose an endpoint that returns a 200 status code when it is ready to serve traffic. The load balancer will periodically hit this endpoint and only route traffic to instances that pass the check.

Step 3: Creating the Auto Scaling Group

With the launch template and target group in place, you can now create the Auto Scaling Group. The ASG will maintain a desired number of instances, spread them across multiple subnets (and therefore multiple Availability Zones), and register them with the target group automatically.

aws autoscaling create-auto-scaling-group \
  --auto-scaling-group-name web-app-asg \
  --launch-template LaunchTemplateName=web-app-template,Version=1 \
  --min-size 2 \
  --max-size 10 \
  --desired-capacity 2 \
  --target-group-arns arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/web-app-tg/def456 \
  --vpc-zone-identifier "subnet-0aaa,subnet-0bbb,subnet-0ccc" \
  --health-check-type ELB \
  --health-check-grace-period 120

The key parameters here are min-size, max-size, and desired-capacity. The ASG will always try to maintain at least 2 instances (for high availability) and will never exceed 10 instances (to control costs). The health-check-type set to ELB means the ASG uses the load balancer's health checks to determine if an instance is healthy. If an instance fails, the ASG terminates it and launches a replacement.

Step 4: Defining Scaling Policies

Static capacity is not enough for production. You need dynamic scaling policies that respond to changing demand. The most common approach is target tracking scaling, where you specify a target metric value and AWS automatically adjusts capacity to maintain that target.

aws autoscaling put-scaling-policy \
  --auto-scaling-group-name web-app-asg \
  --policy-name cpu-target-tracking \
  --policy-type TargetTrackingScaling \
  --target-tracking-configuration '{
    "TargetValue": 50.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ASGAverageCPUUtilization"
    },
    "ScaleOutCooldown": 60,
    "ScaleInCooldown": 300
  }'

This policy tells the ASG to maintain an average CPU utilization of 50% across all instances. When CPU exceeds 50%, the ASG adds instances. When it drops below 50%, the ASG removes instances. The ScaleOutCooldown of 60 seconds allows quick scale-out during traffic spikes, while the ScaleInCooldown of 300 seconds prevents premature scale-in that could cause instability.

For more sophisticated scaling, you can use step scaling policies triggered by CloudWatch alarms:

# Create a CloudWatch alarm for high traffic
aws cloudwatch put-metric-alarm \
  --alarm-name high-request-count \
  --metric-name RequestCount \
  --namespace AWS/ApplicationELB \
  --statistic Sum \
  --period 60 \
  --threshold 1000 \
  --comparison-operator GreaterThanThreshold \
  --dimensions Name=LoadBalancer,Value=app/web-app-alb/abc123 \
  --evaluation-periods 2 \
  --alarm-actions arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:abc:autoScalingGroupName/web-app-asg:policyName/high-traffic-scale-out

Step 5: Using Infrastructure as Code with Terraform

Managing all of these resources through the CLI is error-prone and difficult to reproduce. In production, you should use Infrastructure as Code. Here is a Terraform example that provisions the entire stack:

resource "aws_launch_template" "web_app" {
  name_prefix   = "web-app-"
  image_id      = "ami-0c02fb55956c7d316"
  instance_type = "t3.small"
  key_name      = "my-key-pair"

  vpc_security_group_ids = [aws_security_group.web_app.id]

  user_data = base64encode(<<-EOF
    #!/bin/bash
    yum update -y
    yum install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<h1>Hello from $(hostname)</h1>" > /var/www/html/index.html
  EOF
  )

  tag_specifications {
    resource_type = "instance"
    tags = {
      Name        = "web-app"
      Environment = "production"
    }
  }
}

resource "aws_lb_target_group" "web_app" {
  name     = "web-app-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.main.id

  health_check {
    path                = "/"
    interval            = 30
    timeout             = 5
    healthy_threshold   = 2
    unhealthy_threshold = 3
  }
}

resource "aws_autoscaling_group" "web_app" {
  name                = "web-app-asg"
  vpc_zone_identifier = [aws_subnet.private_a.id, aws_subnet.private_b.id, aws_subnet.private_c.id]
  min_size            = 2
  max_size            = 10
  desired_capacity    = 2

  launch_template {
    id      = aws_launch_template.web_app.id
    version = "$Latest"
  }

  target_group_arns = [aws_lb_target_group.web_app.arn]

  health_check_type         = "ELB"
  health_check_grace_period = 120
}

resource "aws_autoscaling_policy" "cpu_tracking" {
  name                   = "cpu-target-tracking"
  autoscaling_group_name = aws_autoscaling_group.web_app.name
  policy_type            = "TargetTrackingScaling"

  target_tracking_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ASGAverageCPUUtilization"
    }
    target_value = 50.0
  }
}

This Terraform configuration is declarative and version-controlled. You can apply it to create the infrastructure, modify it to make changes, and destroy it cleanly when no longer needed. This reproducibility is essential for production environments.

Best Practices for Production EC2 Scaling

Beyond the basic setup, several best practices will make your EC2 scaling robust and maintainable:

Handling Deployments Without Downtime

Scaling infrastructure is only half the battle — you also need a strategy for deploying new application versions without downtime. The recommended approach is to use the ASG's native instance refresh feature, which performs a rolling update:

aws autoscaling start-instance-refresh \
  --auto-scaling-group-name web-app-asg \
  --strategy Rolling \
  --preferences '{
    "MinHealthyPercentage": 50,
    "InstanceWarmup": 120,
    "CheckpointPercentages": [20, 40, 60, 80],
    "CheckpointDelay": 300
  }'

This command tells the ASG to replace all instances with new ones from the updated launch template, while always keeping at least 50% of instances healthy. The checkpoint percentages create pauses at each stage, giving you time to verify the deployment is working before proceeding. If something goes wrong, you can roll back by canceling the instance refresh.

Conclusion

Scaling EC2 from a prototype to a production system involves moving from a single fragile instance to a resilient, automated, and self-healing architecture. By combining Launch Templates, Auto Scaling Groups, Application Load Balancers, and dynamic scaling policies, you create an infrastructure that adapts to demand, survives failures, and optimizes costs. Pairing this with Infrastructure as Code tools like Terraform and following best practices around immutability, multi-AZ deployment, and proper monitoring ensures that your production environment is not only scalable but also maintainable and secure. The investment in setting up these patterns pays off the first time traffic spikes unexpectedly and your system scales automatically instead of crashing — which is ultimately what production readiness is all about.

— Ad —

Google AdSense will appear here after approval

← Back to all articles