Amazon EC2 Power Management

ec2-power - scheduled shutdown/startup of ec2/euca instances

Manage instances power state with tags based on cron time formats. Tags used are:

note all times should be UTC as thats what aws uses.

the script will ignore any host that does not have:

any host with an auto:ignore tag key (no value required) will not be stopped but will still be started if required

credit for this should go to schen1628 all I have done is changed output and added some sanity checking and the ignore tag to it really.

Cloud-init example setup

#!/bin/bash
LOCATION=http://wherever.you.put/ec2-power.py
OPERATOR=/home/ec2-user/ec2-power.py
yum install -y python-pip gcc
pip install croniter
wget -O $OPERATOR $LOCATION
chown ec2-user:ec2-user $OPERATOR
chmod 644 $OPERATOR
#echo '*/5 * * * * ec2-user python $OPERATOR >>/home/ec2-user/ec2-power.log 2>&1' >> /etc/crontab

Python ec2-power script

import boto.ec2
import croniter
import datetime

debug = 1

region = 'ap-southeast-2'
a_term = 'disableApiTermination'
a_stop = 'instanceInitiatedShutdownBehavior'

def time_to_action(sched, now, seconds):
   try:
      cron = croniter.croniter(sched, now)
      d1 = now + datetime.timedelta(0, seconds)
      if (seconds > 0):
         d2 = cron.get_next(datetime.datetime)
         ret = (now < d2 and d2 < d1)
      else:
         d2 = cron.get_prev(datetime.datetime)
         ret = (d1 < d2 and d2 < now)
      print "now: %s / d1: %s / d2: %s" % (now, d1, d2)
   except:
      ret = False
   print "time_to_action: %s" % ret
   return ret

now = datetime.datetime.now()
print "===================================="
print "RUNTIME = %s" % now

conn=boto.ec2.connect_to_region(region)
reservations = conn.get_all_instances()

start_list = []
stop_list = []

for res in reservations:
   for inst in res.instances:
      state = inst.state
      name = inst.tags['Name'] if 'Name' in inst.tags else 'Unknown'
      start_sched = inst.tags['auto:start'] if 'auto:start' in inst.tags else None
      stop_sched = inst.tags['auto:stop'] if 'auto:stop' in inst.tags else None
      term = 1 if conn.get_instance_attribute(inst.id, a_term, dry_run=False)[a_term] else 0
      stop = 1 if conn.get_instance_attribute(inst.id, a_stop, dry_run=False)[a_stop] == 'stop' else 0
      ignore = 1 if 'auto:ignore' in inst.tags else 0
      print "(%s) %-30s [%s] (%s/%s/%s) [%s] [%s]" % (inst.id, name, state, term, stop,
                                                      ignore, start_sched, stop_sched)

      if start_sched != None and state == "stopped" and time_to_action(start_sched, now, 16 * 60):
          start_list.append(inst.id)
      if stop_sched != None and state == "running" and time_to_action(stop_sched, now, 31 * -60):
        if term and stop and ignore == 0:
          stop_list.append(inst.id)
        else:
          print "ignoring stop of %s - term[%s], stop[%s], ignore[%s]" % (inst.id, term, stop, ignore)

print
if len(start_list) > 0:
   if debug:
      print "would start: %s" % start_list
   else:
      ret = conn.start_instances(instance_ids=start_list, dry_run=False)
      print "start_instances %s" % ret
if len(stop_list) > 0:
   if debug:
      print "would stop: %s" % stop_list
   else:
      ret = conn.stop_instances(instance_ids=stop_list, dry_run=False)
      print "stop_instances %s" % ret

An appropriate IAM role needs to be setup for this to work. This one has more rights than ec2-power uses (it's for a box that does several other things):

{
  "AWSTemplateFormatVersion" : "2010-09-09",

  "Description" : "IAM role/policy/profile for nat gateway.",

  "Parameters" : {
  },


  "Resources" : {

    "IAMRole" : {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version" : "2012-10-17",
          "Statement": [ {
            "Effect"   : "Allow",
            "Principal": {
              "Service": [ "ec2.amazonaws.com" ]
            },
              "Action"   : [ "sts:AssumeRole" ]
          } ]
        },
        "Path" : "/"
      }
    },

    "IAMPolicy" : {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyName": "root",
        "PolicyDocument": {
          "Version" : "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "ec2:DescribeInstances",
                "ec2:StartInstances",
                "ec2:StopInstances",
                "ec2:DescribeInstanceAttribute",
                "ec2:DescribeTags",
                "ec2:DescribeSpotPriceHistory",
                "elasticloadbalancing:DescribeInstanceHealth",
                "cloudwatch:GetMetricStatistics",
                "cloudwatch:ListMetrics",
                "cloudwatch:PutMetricData",
                "cloudwatch:DescribeAlarms"
              ],
              "Resource": "*"
            },
            {
              "Effect":"Allow",
              "Action":[
                "s3:ListBucket",
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
              ],
              "Resource":"arn:aws:s3:::<s3 bucket>/*"
            }
          ]
        },
        "Roles": [ {
          "Ref": "IAMRole"
        } ]
      }
    },

    "IAMProfile" : {
      "Type" : "AWS::IAM::InstanceProfile",
      "Properties" : {
        "Path" : "/",
        "Roles" : [ { "Ref" : "IAMRole" } ]
      }
    } 

  },

  "Outputs" : {

    "IAMRole" : {
      "Value" : { "Ref" : "IAMRole" },
      "Description" : "IAM Role ID"
    },

    "IAMPolicy" : {
      "Value" : { "Ref" : "IAMPolicy" },
      "Description" : "IAM Policy ID"
    },

    "IAMProfile" : {
      "Value" : { "Ref" : "IAMProfile" },
      "Description" : "IAM Profile ID"
    }

  }

}

The minimum set required is ec2:DescribeInstances, ec2:StartInstances, ec2:StopInstances, ec2:DescribeInstanceAttribute, ec2:DescribeTags

If you use this do note that the cronjob is commented out in the cloud-init example, the python script has a debug flag set to it will only report on actions that would be taken and <web-source> and <s3-bucket> have been obfuscated so need to be set for your environment.



Date: 2014-08-29 Fri

Emacs 24.5.1 (Org mode 8.2.10)