Auto stop/start your EC2 instances with this Cloudformation template

Published: Sat 12 March 2022
Updated: Sun 03 April 2022 by Ludo In Soft

My ready-to-use Cloudformation template for automatically stopping and restarting your EC2 AWS instances. You'd like to save some money on AWS, by automatically shutting down your EC2 development instances at night? Install this Cloudformation template and spend less money!

One of the recommendations of AWS is to automatically shut down your EC2 development instances at night and on weekends. By doing this, you reduce the cost of your AWS bill significantly. With this Cloudformation template, you can easily create and maintain your own daily schedule which will automatically stop or start your instances at the time you specified.

So first install this template in your Cloudformation console, and adjust the start and stop times, via the 2 parameters that start with 'cron(...)'. These are Cron Unix type expressions, modified to the Amazon sauce. All instances tagged with 'hibernate = True' will be then stopped or started according to your schedule, by two simple Python Lambda functions.

The general format of the AWS cron expression is:

  • cron( minute hour day month weekday year ).

Beware, the times are expressed in the GMT timezone! The full documentation with examples of cron expression are here. For example cron ( 5 12 ? * MON-FRI * ) means, every day at 12:05 GMT from Monday to Friday.

In the template below, you may notice that I target the new Arm64/Graviton 2 processors because they are faster and cheaper than Lambda running on x86_64.

Last tip, don't forget to enable hibernation on your EC2 instances for faster shutdown and wake up! 😉

If you have some questions or other tips, feel free to comment below the code.

# 
AWSTemplateFormatVersion: 2010-09-09
Description: Stop start tagged EC2 instances with specified schedule

Parameters:
  StartTime:
    Type: String
    Default: "cron(5 12 ? * MON-FRI *)" 
    Description: "Start Instances cron expression GMT time! Bonus: If you leave this empty instances won't be started"
  StopTime:
    Type: String
    Default: "cron(5 23 ? * MON-FRI *)" 
    Description: "Stop Instances cron expression GMT time!"

Conditions:
  DoNotStart: !Equals [ !Ref StartTime, "" ]
  DoStart: !Not [ Condition: DoNotStart ]

Resources:
# Role for the lambda function
  Role:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
          Sid: ""
      Path: "/"
      Policies:
      - PolicyName: "AllowXRay"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - xray:PutTraceSegments
            - xray:PutTelemetryRecords
            Resource:
            - "*"
      - PolicyName: "CreateLogs"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*"
      - PolicyName: "CreateLogsGroup"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action: 
            - logs:CreateLogGroup
            Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
      - PolicyName: "EC2"
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
          - Effect: "Allow"
            Action:
            - ec2:DescribeInstances
            - ec2:DescribeTags
            - ec2:StopInstances
            - ec2:StartInstances
            Resource:
            - "*"

# LAMDBA Python Code for auto start/stop instances
  LambdaStopInstances:
    Type: AWS::Lambda::Function
    Properties:
      Handler: "index.lambda_handler"
      Role: !GetAtt Role.Arn
      Runtime: python3.9
      MemorySize: 128
      Architectures:
      - arm64
      Timeout: 60
      Description: Stop or hibernate tagged instances 
      TracingConfig:
        Mode: Active
      Code:
        ZipFile: |
          import boto3
          import logging

          # setup simple logging for INFO
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          # define the connection
          ec2 = boto3.resource('ec2')
          def lambda_handler(event, context):
              # Use the filter() method of the instances collection to retrieve
              # all running EC2 instances.
              filters = [{
                      'Name': 'tag:hibernate',
                      'Values': ['True', 'true']
                  },
                  {
                      'Name': 'instance-state-name', 
                      'Values': ['running']
                  }
              ]

              # filter the instances
              instances = ec2.instances.filter(Filters=filters)
              RunningInstances = [instance.id for instance in instances]              
              # make sure there are actually instances to shut down. 
              if len(RunningInstances) > 0:
                  action = ec2.instances.filter(InstanceIds=RunningInstances).stop()
                  print(action)

  LambdaStartInstances:
    Type: AWS::Lambda::Function
    Properties:
      Handler: "index.lambda_handler"
      Role: !GetAtt Role.Arn
      Runtime: python3.9
      MemorySize: 128
      Architectures:
      - arm64
      Timeout: 60
      Description: Start tagged instances 
      TracingConfig:
        Mode: Active
      Code:
        ZipFile: |
          import boto3
          import logging

          # setup simple logging for INFO
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          # define the connection
          ec2 = boto3.resource('ec2')
          def lambda_handler(event, context):
              # Use the filter() method of the instances collection to retrieve
              # all running EC2 instances.
              filters = [{
                      'Name': 'tag:hibernate',
                      'Values': ['True', 'true']
                  },
                  {
                      'Name': 'instance-state-name', 
                      'Values': ['stopped']
                  }
              ]

              # filter the instances
              instances = ec2.instances.filter(Filters=filters)
              StoppedInstances = [instance.id for instance in instances]              
              # make sure there are actually instances to start. 
              if len(StoppedInstances) > 0:
                  action = ec2.instances.filter(InstanceIds=StoppedInstances).start()
                  print(action)

# Lambdas scheduling
  EventLambdaStopRule:
    Type: AWS::Events::Rule
    Properties:
      Description: EventLambdaStopRule
      ScheduleExpression: !Sub "${StopTime}" 
      State: ENABLED
      Targets:
      - Arn: !GetAtt LambdaStopInstances.Arn 
        Id: "LambdaStop"

  EventLambdaStartRule:
    Type: AWS::Events::Rule
    Properties:
      Description: EventLambdaStartRule
      ScheduleExpression: !Sub "${StartTime}" 
      State: ENABLED
      Targets:
      - Arn: !GetAtt LambdaStartInstances.Arn 
        Id: "LambdaStart"
    Condition: DoStart

  PermissionForEventsToInvokeLambdaStop:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref LambdaStopInstances
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventLambdaStopRule.Arn

  PermissionForEventsToInvokeLambdaStart:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref LambdaStartInstances
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventLambdaStartRule.Arn
    Condition: DoStart

Have fun!

LD. --

Speak your mind: