When working with AWS EC2 Auto Scaling Groups (ASGs), many teams face a dilemma: how to maintain reliable baseline capacity while still benefiting from the cost savings of Spot Instances. The current ASG implementation doesn't natively support mixing On-Demand and Spot instances in a single group with different scaling behaviors.
Here are three practical ways to achieve this hybrid scaling pattern:
1. Multiple Auto Scaling Groups Strategy
Create two separate ASGs attached to the same load balancer:
Resources:
BaselineASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
MinSize: 2
MaxSize: 2
LaunchTemplate:
LaunchTemplateId: !Ref OnDemandLaunchTemplate
Version: !GetAtt OnDemandLaunchTemplate.LatestVersionNumber
SpotASG:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
MinSize: 0
MaxSize: 8
LaunchTemplate:
LaunchTemplateId: !Ref SpotLaunchTemplate
Version: !GetAtt SpotLaunchTemplate.LatestVersionNumber
2. Instance Weighting in Mixed Instances Policy
Use ASG's mixed instances policy with instance weighting:
MixedInstancesPolicy:
InstancesDistribution:
OnDemandBaseCapacity: 2
OnDemandPercentageAboveBaseCapacity: 0
LaunchTemplate:
LaunchTemplateSpecification:
LaunchTemplateId: !Ref MixedLaunchTemplate
Overrides:
- InstanceType: c5.large
WeightedCapacity: 1
- InstanceType: c5d.large
WeightedCapacity: 1
3. Custom Scaling with Lambda
Implement a Lambda function to manage the hybrid scaling logic:
import boto3
def lambda_handler(event, context):
asg = boto3.client('autoscaling')
ec2 = boto3.client('ec2')
# Get current metrics
response = asg.describe_auto_scaling_groups(AutoScalingGroupNames=['hybrid-asg'])
# Check if we need to maintain baseline
if len(response['AutoScalingGroups'][0]['Instances']) < 2:
# Launch on-demand instances
ec2.run_instances(
LaunchTemplate={'LaunchTemplateName':'on-demand-template'},
MinCount=1,
MaxCount=1
)
elif should_scale_out(): # Your custom scaling logic
# Request spot instances
ec2.request_spot_instances(
SpotPrice='0.05',
LaunchSpecification={
'LaunchTemplate': {'LaunchTemplateName':'spot-template'}
}
)
When using Spot instances, implement these safeguards:
- Set up CloudWatch alarms for Spot interruption warnings
- Configure ASG lifecycle hooks to handle Spot terminations gracefully
- Use EC2 Fleet with On-Demand fallback capacity
Many AWS users face a dilemma when configuring auto-scaling groups (ASGs): how to maintain reliable baseline capacity while leveraging cost-effective Spot Instances. The default ASG configuration forces an either-or choice between instance types, which doesn't match real-world operational needs.
The native ASG implementation has several constraints regarding mixed instance policies:
- Cannot guarantee specific counts of On-Demand vs Spot instances
- Spot instance interruptions can violate minimum capacity requirements
- No built-in way to prioritize On-Demand for baseline capacity
Option 1: Multiple ASGs with Load Balancer
Create separate ASGs for each instance type and manage them through a common ALB:
# On-Demand ASG
resource "aws_autoscaling_group" "od_asg" {
min_size = 2 # Your minimum baseline
max_size = 4
launch_template {
id = aws_launch_template.on_demand.id
version = "$Latest"
}
}
# Spot ASG
resource "aws_autoscaling_group" "spot_asg" {
min_size = 0
max_size = 10
mixed_instances_policy {
instances_distribution {
on_demand_base_capacity = 0
on_demand_percentage_above_base_capacity = 0
spot_allocation_strategy = "capacity-optimized"
}
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.spot.id
version = "$Latest"
}
}
}
}
Option 2: Instance Weighting with Mixed Policy
AWS does allow some mixing through the MixedInstancesPolicy, though with limitations:
mixed_instances_policy {
instances_distribution {
on_demand_base_capacity = 2 # Minimum On-Demand
on_demand_percentage_above_base_capacity = 0 # Everything else Spot
}
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.base.id
version = "$Latest"
}
override {
instance_type = "m5.large"
}
override {
instance_type = "m5.xlarge"
weighted_capacity = 2
}
}
}
For precise control, implement custom scaling logic through Lambda:
def lambda_handler(event, context):
# Get current metrics
ec2 = boto3.client('ec2')
cloudwatch = boto3.client('cloudwatch')
# Calculate needed capacity
current_load = get_cloudwatch_metric(...)
# Determine instance mix
if current_load > baseline_threshold:
launch_spot_instances(...)
else:
ensure_on_demand_baseline(...)
When implementing hybrid scaling:
- Set proper instance protection for On-Demand nodes
- Configure distinct termination policies for each type
- Monitor Spot interruption notices proactively
- Use capacity-optimized allocation strategy for Spot
Essential CloudWatch metrics to track:
- SpotInstanceInterruptionRate
- OnDemandInstanceCount
- HybridCostSavings
- CapacityGap (desired vs actual)