Shutdown EC2 periodically to save cost

Shutdown EC2 periodically to save cost

Many times, you might forget to shut down EC2 after late-night learning and forget to turn off your EC2 instances. This article focuses on to Shutdown EC2 periodically to save costs

Task

Create a Lambda function that shuts down EC2 instances periodically using AWS CloudWatch Events.

This task is achieved in two ways

  1. Manually (Recommended for first-time practice)
  2. Automated using CloudFormation (Recommended for saving time)

2. Automated using CloudFormation IaaC Template

I like automating things. So here is how to achieve this

  1. Create an IAM Role for CloudFormation to deploy stacks and create resources. You can just skip if you already have a role.
  2. Go to the AWS Console. Go to ClouFormation and Create a Stack.
  3. Pass Params to CloudFormation
  4. Select IAM Role for CloudFormation to create stacks.
  5. Checkbox CAPABILITY IAM as Ticked
  6. Deploy Stack. Wait for Stack to deploy.
  7. Check for EC2 instances if they are running or stopped.

import boto3
import json
ec2 = boto3.client('ec2')
def get_instance_ids():
  all_instances = ec2.describe_instances()
  instance_ids = []
  for reservation in all_instances['Reservations']:
    for instance in reservation['Instances']:
      print("InstanceID",instance['InstanceId'])
      if instance["State"]["Name"]=="running":
        instance_ids.append(instance['InstanceId'])
  return instance_ids
                  
def handler(event, context):
    instance_ids = get_instance_ids()
    if len(instance_ids) != 0:
      ec2.stop_instances(InstanceIds=instance_ids)
    response = "Successfully stopped instances: " + str(instance_ids)
    return {'statusCode': 200,'body': json.dumps(response)}

lambda_file.py

AWSTemplateFormatVersion: "2010-09-09"
Description: Cloudformation IaaC code to ShutDown all ec2 at Time t. It solves the problem that you forget to shutdown ec2 and get an unwanted bill. Please be aware of what you are doing and do not use this in your company account even by mistake.
Parameters:
  CronExpression:
    Description: "Enter a cron expression to schedule your event. For Example cron(30 1 * * ? *) means that your lambda will run everyday at 1:10 am. The Time Zone here refers to the time zone in which your AWS region lies."
    Type: String
    Default: "cron(30 1 * * ? *)"
  LogsParam:
    Description: Do you want to store logs. Storing logs is chargeble.
    Type: String
    Default: "No"
    AllowedValues: ["Yes", "No"]
Conditions: 
  UseAWSLambdaBasicExecutionRole: !Equals [!Ref LogsParam, "Yes"] # If LogsParam equals Yes then set UseAWSLambdaBasicExecutionRole as True
  
Resources:
  # Create IAM Role for Lambda. Lamba needs some permissions to shutdown VMs
  LambdaStopEC2Rule:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      # If UseAWSLambdaBasicExecutionRole is True then attach the Lambda Basic Execution Role else attach nothing
      ManagedPolicyArns: !If [UseAWSLambdaBasicExecutionRole, arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole, !Ref "AWS::NoValue"]
      Path: /
      Policies:
        - PolicyName: LambdaStopEC2Policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeInstances
                  - ec2:StopInstances
                  - ec2:DescribeInstanceStatus
                Resource: '*' 
  # Create Lambda that will shutdowm VMs              
  StopAllEC2Lambda:
    Type: AWS::Lambda::Function
    Properties:
      Description: Start starts build for a code build project
      Handler: index.handler
      Runtime: python3.9
      Role: !GetAtt "LambdaStopEC2Rule.Arn"
      # Timeout: 20
      MemorySize: 128
      Code:
        ZipFile: !Sub |
          import boto3
          import json
          ec2 = boto3.client('ec2')
          def get_instance_ids():
            all_instances = ec2.describe_instances()
            instance_ids = []
            for reservation in all_instances['Reservations']:
              for instance in reservation['Instances']:
                print("InstanceID",instance['InstanceId'])
                if instance["State"]["Name"]=="running":
                  instance_ids.append(instance['InstanceId'])
            return instance_ids

          def handler(event, context):
              instance_ids = get_instance_ids()
              if len(instance_ids) != 0:
                ec2.stop_instances(InstanceIds=instance_ids)
              response = "Successfully stopped instances: " + str(instance_ids)
              return {'statusCode': 200,'body': json.dumps(response)}
 # Create CloudWatch Events Rule. This rule will execute Lambda periodically
  ScheduledRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "ScheduledRule"
      ScheduleExpression: !Ref  CronExpression
      State: "ENABLED"      
      Targets:
        - Arn: !GetAtt StopAllEC2Lambda.Arn
          Id: "StopAllEC2Lambda"
  # CloudWatch Events Rule needs permission to invoke Lambda. So attaching lambda invoke permission.
  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref StopAllEC2Lambda
      Action: "lambda:InvokeFunction"
      Principal: "events.amazonaws.com"
      SourceArn:
        Fn::GetAtt:
          - "ScheduledRule"
          - "Arn"
Outputs:
  MyStacksRegion:
    Value: !Ref "AWS::Region"
    Description: Your AWS Region

template.yaml

Shivam Anand