Created
November 11, 2014 21:36
-
-
Save ezeeetm/57549c43ef9a550354f1 to your computer and use it in GitHub Desktop.
ec2 instance scheduler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Initialize AWS session w/ specific IAM role for this script. | |
# Redact access/secret keys in commits, or do not include this file. | |
log INFO "initializing AWS session" | |
$accessKey = "AK for user with IAM role below" | |
$secretKey = "SK for user with IAM role below" | |
Initialize-AWSDefaults -AccessKey $accessKey -SecretKey $secretKey -Region us-east-1 | |
log INFO "initializing AWS complete" | |
<# | |
IAM Policy ec2IA#######17 | |
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Action": ["ec2:StartInstances", "ec2:StopInstances", "ec2:DescribeInstanceStatus", "ec2:DescribeInstances"], | |
"Effect": "Allow", | |
"Resource": "*" | |
}, | |
{ | |
"Action": ["sns:publish"], | |
"Effect": "Allow", | |
"Resource": "*" | |
} | |
] | |
} | |
#> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
######################################################################################################################### | |
# Script name: instanceScheduler.ps1 | |
# Version: v0.1 | |
# Date: 08Nov2014 | |
# Author: ezeeetm | |
# Purpose: start/stop ec2 instances on a schedule, with retries and SNS notifications on WARNING/FATAL events | |
# Usage: ./instanceScheduler.ps1 | |
# Dependencies: aws.ps1, logging.ps1, schedule.txt | |
# Notes: | |
# In order to give the retries sufficient time to process, | |
# do not schedule starts/stops closer together than $maxRetries * $retryIntervals seconds | |
# http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-ItemType-InstanceStateType.html | |
# Valid values: 0 (pending) | 16 (running) | 32 (shutting-down) | 48 (terminated) | 64 (stopping) | 80 (stopped) | |
######################################################################################################################### | |
# Initialize logging and AWS session, see dependencies above | |
# aws.ps1 uses specific IAM role for this script, redact access/secret keys or omit this file in commits | |
. .\logging.ps1 | |
. .\aws.ps1 | |
# config | |
$scheduleFile = '.\schedule.txt' | |
$debug = $false #toggle $true/$false to turn console debugging on/off | |
$skipDays = @("Saturday","Sunday") | |
$chkInterval = 15 #secs | |
$retryInterval = 60 #secs | |
$maxRetries = 5 | |
$pendingChanges = @() | |
# get instances/schedules from $scheduleFile | |
function getSchedule | |
{ | |
$schedule = Get-Content $scheduleFile | |
$instances = @() | |
foreach ($line in $schedule) | |
{ | |
if ($line.StartsWith("#") -or ($line.length -eq 0)){continue} # skip comments and blank lines | |
try | |
{ | |
$line = $line.split(",") | |
$instance = New-Object PSObject | |
Add-Member -InputObject $instance -MemberType NoteProperty -Name id -Value $line[0].Trim().ToLower() | |
Add-Member -InputObject $instance -MemberType NoteProperty -Name startTime -Value $line[1].Trim() | |
Add-Member -InputObject $instance -MemberType NoteProperty -Name stopTime -Value $line[2].Trim() | |
Add-Member -InputObject $instance -MemberType NoteProperty -Name currState -Value $null | |
Add-Member -InputObject $instance -MemberType NoteProperty -Name desState -Value $null | |
Add-Member -InputObject $instance -MemberType NoteProperty -Name retryCredits -Value $null | |
$instances += $instance | |
} | |
catch | |
{ | |
log FATAL "check the schedule file for correct syntax, script terminating" | |
exit | |
} | |
} | |
return $instances | |
} | |
# main loop | |
while ($true) | |
{ | |
# load new schedule whenever script starts or schedule file is changed | |
if(($((gci $scheduleFile).LastWriteTime) -gt $schedLastWrite) -or (!$schedLastWrite)) | |
{ | |
$instanceList = getSchedule | |
$schedLastWrite = (gci $scheduleFile).LastWriteTime | |
log INFO "script started or schedule modified, reading new schedule" | |
if ($debug) | |
{ | |
write-host "DEBUG: schedLastWrite- $schedLastWrite`n" -foregroundcolor yellow | |
write-host "DEBUG: instances from getSchedule function:" -foregroundcolor yellow | |
foreach ($instance in $instanceList){write-host $instance -foregroundcolor yellow} | |
} | |
} | |
# if today is not a skip day, rock on | |
$dow = (Get-Date).DayOfWeek.ToString() | |
if (!$skipDays.contains($dow)) | |
{ | |
# add instance objects with a start or stop scheduled for this minute to $pendingChanges array | |
$now = Get-Date -format "HHmm" | |
foreach($instance in $instanceList) | |
{ | |
if($instance.startTime -eq $now) | |
{ | |
log INFO "added to pending: $($instance.id) / desired state *running* @ $($instance.startTime) / $maxRetries retries" | |
$instance.desState = "running" | |
$instance.retryCredits = $maxRetries | |
$pendingChanges += $instance | |
} | |
if($instance.stopTime -eq $now) | |
{ | |
log INFO "added to pending: $($instance.id) / desired state *stopped* @ $($instance.stopTime) / $maxRetries retries" | |
$instance.desState = "stopped" | |
$instance.retryCredits = $maxRetries | |
$pendingChanges += $instance | |
} | |
} | |
} | |
# refresh instance states and remove any whose current state matches desired state | |
if ($pendingChanges) | |
{ | |
foreach ($instance in $pendingChanges) | |
{ | |
$currInstance = Get-EC2Instance $instance.id | |
$instance.currState = $currInstance.Instances.state.name.Value | |
if ($instance.currState -eq $instance.desState) | |
{ | |
log INFO "removed from pending: $($instance.id), desired state *$($instance.desState)* achieved" | |
$pendingChanges = $pendingChanges -ne $instance | |
} | |
} | |
} | |
# if $pendingChanges still isn't empty, do stuff | |
if ($pendingChanges) | |
{ | |
foreach($instance in $pendingChanges) | |
{ | |
if(($instance.desState -eq "stopped") -and ($instance.currState -eq "running")) #do nothing for pending, shutting-down, terminated, or stopping | |
{ | |
log INFO "stopping $($instance.id)" | |
try {$r = stop-ec2instance $instance.id} catch{log WARNING "problem with API call to stop $($instance.id)"} | |
log INFO "ec2 response: instance = $($r.InstanceId), currentState = $($r.CurrentState.Name.Value), previousState = $($r.PreviousState.Name.Value)" | |
} | |
if(($instance.desState -eq "running") -and ($instance.currState -eq "stopped")) #do nothing for pending, shutting-down, terminated, or stopping | |
{ | |
log INFO "starting $($instance.id)" | |
try {$r = start-ec2instance $instance.id} catch {log WARNING "problem with API call to start $($instance.id)"} | |
log INFO "ec2 response: instance = $($r.InstanceId), currentState = $($r.CurrentState.Name.Value), previousState = $($r.PreviousState.Name.Value)" | |
} | |
# decrement $retryCredits. | |
# If instances hit $maxRetries, drop and send SNS notification (in logging) | |
$instance.retryCredits -= 1 | |
if ($instance.retryCredits -eq 0) | |
{ | |
log WARNING "removed from pending: ($instance.id), $maxRetries failed retries" | |
$pendingChanges = $pendingChanges -ne $instance | |
} | |
} | |
start-sleep -s $retryInterval | |
} | |
start-sleep -s $chkInterval | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#logging.ps1 | |
clear | |
$targetARN = 'arn:aws:sns:us-east-1:#####your:ARN:path:here######' | |
# create .\<ScriptName>Logs\ dir if it does not exit | |
$path = Split-Path $MyInvocation.MyCommand.Path | |
Set-Location -Path $path | |
$ScriptName = "instanceScheduler" | |
$logPath = ".\"+$ScriptName+"Logs" | |
if((Test-Path $logPath) -eq $false){md -Name $logPath | out-null} #suppress stdout | |
# start new log file w/ timestamp filename and date header | |
$logStartDateTime = Get-Date -f yyyy-MM-dd | |
$logHeader = "Log File Started: "+(Get-Date).ToString()+"`r`n-------------------------------------" | |
$logHeader | Out-File ".\$logPath\$logStartDateTime.log" -Append | |
Function log ($logLevel, $logData) | |
{ | |
$logEntry = (Get-Date).ToString()+"`t"+$logLevel+"`t"+$logData | |
Write-Host $logEntry | |
$logEntry | Out-File ".\$logPath\$logStartDateTime.log" -Append | |
if (($logLevel -eq "WARNING") -or ($logLevel -eq "FATAL")) | |
{ | |
Publish-SNSMessage -Message $logEntry -Subject $logLevel -TargetArn $targetARN | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#accepted format: instanceId,startTime,stopTime | |
#example (uncomment actual entries): | |
#i-a1s2d3f4,0700,1900 | |
i-a1s2d3f4,0700,1900 | |
i-a1s2d3f4,0700,1900 | |
i-a1s2d3f4,0700,1900 | |
i-a1s2d3f4,0700,1900 | |
i-a1s2d3f4,0700,1900 | |
i-a1s2d3f4,0700,1900 | |
i-a1s2d3f4,0700,1900 | |
i-a1s2d3f4,0700,1900 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment