Skip to content

Instantly share code, notes, and snippets.

@ezeeetm
Created November 11, 2014 21:36
Show Gist options
  • Save ezeeetm/57549c43ef9a550354f1 to your computer and use it in GitHub Desktop.
Save ezeeetm/57549c43ef9a550354f1 to your computer and use it in GitHub Desktop.
ec2 instance scheduler
# 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": "*"
}
]
}
#>
#########################################################################################################################
# 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
}
#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
}
}
#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