Skip to content

Instantly share code, notes, and snippets.

@ValterAndrei
Last active September 2, 2024 13:54
Show Gist options
  • Save ValterAndrei/9f3ce4a0ccce2840b7c451302f26127e to your computer and use it in GitHub Desktop.
Save ValterAndrei/9f3ce4a0ccce2840b7c451302f26127e to your computer and use it in GitHub Desktop.
Automating Instance Start/Stop - AWS, Lambda, EC2, RDS and Ruby

Automating Instance Start/Stop with AWS Lambda

  • Ruby 3.3
  • EC2 Instances
  • RDS DB Instance

Create Policy

IAM > Policies > Create policy > JSON > [copy + paste] > Next > Policy name: start-stop-instance-policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:StartInstances",
        "ec2:StopInstances",
        "rds:DescribeDBInstances",
        "rds:StartDBInstance",
        "rds:StopDBInstance"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

Create Role

IAM > Roles > Create role > Trusted entity type: AWS service > Use case: Lambda > Next > Role name: start-stop-instance-role

default (not change)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "sts:AssumeRole"
      ],
      "Principal": {
        "Service": [
          "lambda.amazonaws.com"
        ]
      }
    }
  ]
}

Attach Policy to Role

IAM > Roles > start-stop-instance-role > Add permissions > Attach policies > start-stop-instance-policy


Create Lambda

Lambda > Create function > Function name: start_instances

Environment variables
BASTION_ID = "i-0a1b2c3d4e5f6g7h8",
DEV_APP_ID = "i-9j8h7g6f5e4d3c2b1a0",
RDS_INSTANCE_ID = "dev-instance",
REGION = "us-east-1",
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/..."
# ruby version: 3.3

require 'aws-sdk-ec2'
require 'aws-sdk-rds'


def lambda_handler(*)
  # Environment variables
  bastion_id        = ENV.fetch('BASTION_ID')
  dev_app_id        = ENV.fetch('DEV_APP_ID')
  rds_instance_id   = ENV.fetch('RDS_INSTANCE_ID')
  region            = ENV.fetch('REGION')
  slack_webhook_url = ENV.fetch('SLACK_WEBHOOK_URL')
  # stop_function_url = ENV.fetch('STOP_FUNCTION_URL')

  # Create AWS Clients
  ec2_client = Aws::EC2::Client.new(region:)
  rds_client = Aws::RDS::Client.new(region:)

  # Check if EC2 instances are in a state available to start
  ec2_response = ec2_client.describe_instances(instance_ids: [bastion_id, dev_app_id])
  ec2_instances = ec2_response.reservations.flat_map(&:instances)

  ec2_instances.each do |instance|
    ec2_instance_status = instance.state.name

    if ec2_instance_status != 'stopped'
      return {
        status: 422,
        message: "The EC2 instance #{instance.instance_id} is not in a state available to be started.",
        state: ec2_instance_status
      }
    end
  end

  # Check if RDS instance is in a state available to start
  rds_response = rds_client.describe_db_instances(db_instance_identifier: rds_instance_id)
  rds_instance = rds_response.db_instances[0]
  rds_instance_status = rds_instance.db_instance_status

  if rds_instance_status != 'stopped'
    return {
      status: 422,
      message: "The RDS instance #{rds_instance_id} is not in a state available to be started.",
      state: rds_instance_status
    }
  end

  # Start instances
  ec2_client.start_instances(instance_ids: [bastion_id, dev_app_id])
  rds_client.start_db_instance(db_instance_identifier: rds_instance_id)

  # Create Slack message
  message = {
    blocks: [
      {
        type: 'header',
        text: {
          type: 'plain_text',
          text: ':coffee: Instances started'
        }
      },
      # {
      #   type: 'section',
      #   text: {
      #     type: 'mrkdwn',
      #     text: "_Click *<#{stop_function_url}|here>* to stop the instances._"
      #   }
      # },
      {
        type: 'divider'
      },
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: 'System available in 20 minutes.'
        },
        accessory: {
          type: 'button',
          text: {
            type: 'plain_text',
            text: ':computer: Access system'
          },
          url: 'http://dev-app.com'
        }
      }
    ]
  }

  # Send Slack message
  uri = URI(slack_webhook_url)
  Net::HTTP.post(uri, message.to_json, 'Content-Type' => 'application/json')

  # Return success message from Lambda
  {
    status: 200,
    message: 'Instances started successfully.'
  }
end

Create EventBridge

Lambda > Functions > start_instances > Add trigger > EventBridge > Create a new rule > Rule name: start_instances_event

# Start DEV instance at 05:30h, from Monday to Friday (GMT-3).

cron(30 8 ? * MON-FRI *)

Create Lambda

Lambda > Create function > Function name: stop_instances

Environment variables
BASTION_ID = "i-0a1b2c3d4e5f6g7h8",
DEV_APP_ID = "i-9j8h7g6f5e4d3c2b1a0",
RDS_INSTANCE_ID = "dev-instance",
REGION = "us-east-1",
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/..."
# ruby version: 3.3

require 'aws-sdk-ec2'
require 'aws-sdk-rds'


def lambda_handler(*)
  # Environment variables
  bastion_id         = ENV.fetch('BASTION_ID')
  dev_app_id         = ENV.fetch('DEV_APP_ID')
  rds_instance_id    = ENV.fetch('RDS_INSTANCE_ID')
  region             = ENV.fetch('REGION')
  slack_webhook_url  = ENV.fetch('SLACK_WEBHOOK_URL')
  # start_function_url = ENV.fetch('START_FUNCTION_URL')

  # Create AWS Clients
  ec2_client = Aws::EC2::Client.new(region:)
  rds_client = Aws::RDS::Client.new(region:)

  # Check if EC2 instances are in a state available to stop
  ec2_response = ec2_client.describe_instances(instance_ids: [bastion_id, dev_app_id])
  ec2_instances = ec2_response.reservations.flat_map(&:instances)

  ec2_instances.each do |instance|
    ec2_instance_status = instance.state.name

    if ec2_instance_status != 'running'
      return {
        status: 422,
        message: "The EC2 instance #{instance.instance_id} is not in a state available to be stopped.",
        state: ec2_instance_status
      }
    end
  end

  # Check if RDS instance is in a state available to stop
  rds_response = rds_client.describe_db_instances(db_instance_identifier: rds_instance_id)
  rds_instance = rds_response.db_instances[0]
  rds_instance_status = rds_instance.db_instance_status

  if rds_instance_status != 'available'
    return {
      status: 422,
      message: "The RDS instance #{rds_instance_id} is not in a state available to be stopped.",
      state: rds_instance_status
    }
  end

  # Stop instances
  ec2_client.stop_instances(instance_ids: [bastion_id, dev_app_id])
  rds_client.stop_db_instance(db_instance_identifier: rds_instance_id)

  # Create Slack message
  message = {
    blocks: [
      {
        type: 'header',
        text: {
          type: 'plain_text',
          text: ':crescent_moon: Instances stopped'
        }
      },
      # {
      #   type: 'section',
      #   text: {
      #     type: 'mrkdwn',
      #     text: "_Click *<#{start_function_url}|here>* to start the instances._"
      #   }
      # }
    ]
  }

  # Send Slack message
  uri = URI(slack_webhook_url)
  Net::HTTP.post(uri, message.to_json, 'Content-Type' => 'application/json')

  # Return success message from Lambda
  {
    status: 200,
    message: 'Instances successfully disconnected.'
  }
end

Create EventBridge

Lambda > Functions > stop_instances > Add trigger > EventBridge > Create a new rule > Rule name: stop_instances_event

# Stop DEV instance at 20:00h, from Monday to Friday (GMT-3).

cron(00 23 ? * MON-FRI *)

References:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment