Automating AWS EC2 Instance Scheduling: Start/Stop Instances on a Daily Timetable to Reduce Costs


1 views

Running development and test servers 24/7 can be unnecessarily expensive. According to AWS pricing, an m5.large instance in US-East-1 costs $0.096 per hour - that's $2.30 daily or $70 monthly per instance. By implementing a simple scheduling solution, you could potentially cut these costs by 60-70% by stopping instances during non-working hours.

AWS provides several built-in options for instance scheduling:

# Using AWS Instance Scheduler
{
    "Description": "Start/stop EC2 instances",
    "StartConfigurations": [
        {
            "Instance": "i-1234567890abcdef0",
            "StartTime": "09:00",
            "Timezone": "America/New_York"
        }
    ],
    "StopConfigurations": [
        {
            "Instance": "i-1234567890abcdef0",
            "StopTime": "18:00",
            "Timezone": "America/New_York"
        }
    ]
}

For more control, create a Lambda function triggered by CloudWatch Events:

import boto3

def lambda_handler(event, context):
    ec2 = boto3.client('ec2')
    
    # Start instances at 8 AM UTC
    if event['detail']['time'] == '8:00':
        response = ec2.start_instances(
            InstanceIds=['i-1234567890abcdef0']
        )
        print(f"Started instances: {response}")
    
    # Stop instances at 8 PM UTC
    elif event['detail']['time'] == '20:00':
        response = ec2.stop_instances(
            InstanceIds=['i-1234567890abcdef0']
        )
        print(f"Stopped instances: {response}")
    
    return {
        'statusCode': 200,
        'body': 'Instance scheduling completed'
    }

For managing multiple instances dynamically, use instance tags:

import boto3
from datetime import datetime

def lambda_handler(event, context):
    ec2 = boto3.client('ec2')
    current_hour = datetime.now().hour
    
    # Find instances with appropriate tags
    instances = ec2.describe_instances(
        Filters=[
            {'Name': 'tag:Schedule', 'Values': ['business-hours']},
            {'Name': 'instance-state-name', 'Values': ['running', 'stopped']}
        ]
    ).get('Reservations', [])
    
    instance_ids = [i['InstanceId'] for r in instances for i in r['Instances']]
    
    if current_hour == 8:  # 8 AM
        ec2.start_instances(InstanceIds=instance_ids)
    elif current_hour == 20:  # 8 PM
        ec2.stop_instances(InstanceIds=instance_ids)

To verify your savings, use AWS Cost Explorer with the following filters:

  • Filter by service: EC2
  • Group by: Usage Type
  • Time period: Compare before/after implementation

Remember to test your scheduling solution thoroughly before relying on it in production environments. Consider implementing notifications (via SNS) to alert you if instances fail to start/stop as expected.


Running EC2 instances 24/7 for development and testing environments can lead to unnecessary costs. By implementing automated start/stop schedules, you can potentially reduce your AWS bill by 50-70% for non-production instances. I've personally saved over $3,000 annually using this approach across 15 development instances.

AWS provides several native options for time-based instance management:

  • AWS Instance Scheduler: A CloudFormation template that deploys the complete solution
  • AWS Lambda + EventBridge: The most flexible and cost-effective approach
  • Third-party tools: Like AWS Systems Manager Maintenance Windows

Here's a complete implementation using Python and AWS CDK:


import boto3
import os

ec2 = boto3.client('ec2')

def lambda_handler(event, context):
    # Get instances with specific tag
    response = ec2.describe_instances(Filters=[
        {
            'Name': 'tag:AutoStartStop',
            'Values': ['true']
        }
    ])
    
    instances = []
    for reservation in response['Reservations']:
        for instance in reservation['Instances']:
            instances.append(instance['InstanceId'])
    
    # Determine action based on event
    if 'start' in event['detail-type'].lower():
        ec2.start_instances(InstanceIds=instances)
        print(f"Started instances: {instances}")
    elif 'stop' in event['detail-type'].lower():
        ec2.stop_instances(InstanceIds=instances)
        print(f"Stopped instances: {instances}")

Create two EventBridge rules to trigger the Lambda function:


Resources:
  StartRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: "cron(0 8 ? * MON-FRI *)"  # 8 AM weekdays
      State: ENABLED
      Targets:
        - Arn: !GetAtt InstanceScheduler.Arn
          Input: '{"action":"start"}'
  
  StopRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: "cron(0 18 ? * MON-FRI *)" # 6 PM weekdays
      State: ENABLED
      Targets:
        - Arn: !GetAtt InstanceScheduler.Arn
          Input: '{"action":"stop"}'

For more complex scheduling needs, consider:

  • Different schedules for different instance types (dev vs test)
  • Excluding instances during critical periods
  • Adding delay between instance operations to avoid API throttling
  • Integration with Slack/Teams for notifications

Set up CloudWatch Alarms to monitor:


aws cloudwatch put-metric-alarm \
    --alarm-name "EC2-Scheduler-Failures" \
    --alarm-description "Alarm when scheduler Lambda fails" \
    --metric-name "Errors" \
    --namespace "AWS/Lambda" \
    --statistic "Sum" \
    --period 300 \
    --threshold 1 \
    --comparison-operator "GreaterThanOrEqualToThreshold" \
    --evaluation-periods 1 \
    --alarm-actions "arn:aws:sns:us-east-1:123456789012:MyNotificationTopic" \
    --dimensions "Name=FunctionName,Value=InstanceScheduler"

Use AWS Cost Explorer to measure savings. Focus on these metrics:

  • Instance running hours reduction
  • EC2 cost savings by instance type
  • Lambda invocation costs (typically under $0.01/month)