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:
- auto:start : 0 10 * * * - will start instance at 10am daily
- auto:stop : 0 20 * * * - will halt instance at 8pm daily
- auto:ignore : (null) - host will be ignored from stop schedule until tag is removed
note all times should be UTC as thats what aws uses.
the script will ignore any host that does not have:
- disableApiTermination = True
- instanceInitiatedShutdownBehaviour = stop
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.