AWS Disaster Recovery Automation w/ Powershell

Amazon has provided several powerful SDKs for developers to interface with Amazon Web Services (AWS). Anything that can be completed using the AWS console can be accomplished via SDK. In this post, we will examine how .Net developers can leverage the AWS SDK to provide a simple disaster recovery plan for a 3-tier SharePoint environment (domain controller, database server, SharePoint server). This approach could be applied to nearly any environment; for more information, you might want to review the AWS Disaster Recovery Demo.

Setup Amazon Simple Email Service (SES)

The first thing you will want to do is apply for access to SES and setup a verified sender address; this may take several hours.

Get the AWS SDK

Download the AWS SDK for .NET.

Get the scripts

After the SDK has been installed, pick a place to store the scripts (ex. C:\AWS). Next, download the four files below (AWSConfig.ps1, AWSUtilities.ps1, DailySnapshots.ps1, and WeeklySnapshots.ps1) into your AWS directory. Alternatively, ff you hover over the code, the Script Highlighter Plugin will provide the ability to copy/paste, view source, etc. Additionally, create a directory called “Logs” inside of your AWS directory.

AWSConfig.ps1

[Download]

############## C O N F I G ##############

#AWS SDK Path 
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\AWSSDK.dll"

#Access Keys
$accessKeyID="XXXXXXXXXXXXXXXXXXXX"
$secretAccessKey="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
$accountID = "############"

#Regions
#$serviceURL="https://ec2.us-west-1.amazonaws.com"
#$serviceURL="https://ec2.us-west-2.amazonaws.com"
#$serviceURL="https://ec2.us-east-1.amazonaws.com"

#Log
$LOG_PATH="C:\AWS\Logs\"

#Email
$FROM_ADDRESS = "you@example.com"
$ADMIN_ADDRESSES = "you@example.com","you2@example.com.com"

#Expiration
$EXPIRATION_DAYS = 5
$EXPIRATION_WEEKS = 4
$MAX_FUNCTION_RUNTIME = 60 # minutes

#Test
$TEST_URL = "http://example.com"

############## A W S  C L I E N T S ##############
#Global Amazon EC2 Client
$config=New-Object Amazon.EC2.AmazonEC2Config
$config.ServiceURL = $serviceURL
$EC2_CLIENT=[Amazon.AWSClientFactory]::CreateAmazonEC2Client($accessKeyID, $secretAccessKey, $config)

#Global Amazon SES Client
$SES_CLIENT=[Amazon.AWSClientFactory]::CreateAmazonSimpleEmailServiceClient($accessKeyID, $secretAccessKey)

 

AWSUtilities.ps1

[Download]

############## R E A D M E ##############
#--Variables in ALL CAPS live in AWSConfig.ps1

#Run next line only once; is is required to create source for Windows Event Log
#New-EventLog -Source "AWS PowerShell Utilities" -LogName "Application"

############## G L O B A L ##############
#global variable to hold email message
$global:email = ""

############## U T I L I T Y   F U N C T I O N S ##############

#Description: Returns true if function has been running longer than permitted 
#Returns: bool
function IsTimedOut([datetime] $start, [string] $functionName)
{    

    $current = new-timespan $start (get-date)

    If($current.Minutes -ge $MAX_FUNCTION_RUNTIME)
    {
        WriteToLogAndEmail "$FunctionName has taken longer than $MAX_FUNCTION_RUNTIME min. Aborting!"
        throw new-object System.Exception "$FunctionName has taken longer than $MAX_FUNCTION_RUNTIME min. Aborting!"
        return $true
    } 
    return $false
}
#Description: Adds a tag to an Amazon Web Services Resource
#Returns: n/a
function AddTagToResource([string] $resourceID, [string] $key, [string] $value)
{   
    try
    {
        $tag = new-object amazon.EC2.Model.Tag
        $tag.Key=$key
        $tag.Value=$value

        $createTagsRequest = new-object amazon.EC2.Model.CreateTagsRequest
        $createTagsResponse = $EC2_CLIENT.CreateTags($createTagsRequest.WithResourceId($resourceID).WithTag($tag))
        $createTagsResult = $createTagsResponse.CreateTagsResult; 
    }
    catch [Exception]
    {
        $function = "AddTagToResource"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }
}
#Description: Add carriage return characters for formatting purposes (ex. email)
#Returns: string[]
function FixNewLines([string[]] $text)
{    
    $returnText=""
    try
    {
        for($i=0;$i -le $text.Length;$i++)
        {
            $returnText+=$text[$i]+"`r`n"
        }
    }
    catch [Exception]
    {
        $function = "FixNewLines"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }

    return $returnText

}
#Description: Returns the current log name by determining the timestamp for the first day of the current week
#Returns: string
function GetLogDate
{
    $dayOfWeek = (get-date).DayOfWeek
    switch($dayOfWeek)
    {
        "Sunday" {$dayOfWeekNumber=0}
        "Monday" {$dayOfWeekNumber=1}
        "Tuesday" {$dayOfWeekNumber=2}
        "Wednesday" {$dayOfWeekNumber=3}
        "Thursday" {$dayOfWeekNumber=4}
        "Friday" {$dayOfWeekNumber=5}
        "Saturday" {$dayOfWeekNumber=6}
    }
    if($dayOfWeekNumber -eq 0)
    {
        $logDate = get-date -f yyyyMMdd
    }
    else
    {
        $logDate = get-date ((get-date).AddDays($dayOfWeekNumber * -1)) -f yyyyMMdd
    } 
    $logName = $logDate + ".txt"
    return  $logName  
}
#Description: Writes a message to a log file, console
#Returns: n/a
function WriteToLog([string[]] $text, [bool] $isException = $false)
{    
    try
    {
        if((Test-Path $LOG_PATH) -eq $false)
        {
            [IO.Directory]::CreateDirectory($LOG_PATH) 
        }
        $date = GetLogDate
        $logFilePath = $LOG_PATH + $date + ".txt"
        $currentDatetime = get-date -format G 
        add-content -Path $logFilePath -Value "$currentDatetime $text"
        write-host "$datetime $text"
        if($isException)
        {
            write-eventlog -Logname "Application" -EntryType "Information" -EventID "0" -Source "AWS PowerShell Utilities" -Message $text
        }
    }
    catch [Exception]
    {
        $function = "WriteToLog"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }    
}
#Description: Writes a email variable for later usage
#Returns: n/a
function WriteToEmail([string[]] $text, [bool] $excludeTimeStamp = $false)
{    
    try
    {
        if($excludeTimeStamp)
        {
            $global:email += "$text`r`n"
        }
        else
        {
            $datetime = get-date -format G 
            $global:email += "$datetime $text`r`n"
        }
    }
    catch [Exception]
    {
        $function = "WriteToEmail"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }
}

#Description: Write to log and email
#Returns: n/a
function WriteToLogAndEmail([string[]] $text, [bool] $isException = $false)
{
    WriteToLog $text $isException
    WriteToEmail $text
}
############## E M A I L   F U N C T I O N S ##############

#Description: Sends an email via Amazone Simple Email Service
#Returns: n/a
function SendSesEmail([string] $from, [string[]]$toList, [string]$subject, [string]$body)
{   
    try
    {
        # Make an Email Request
        $request = new-object -TypeName Amazon.SimpleEmail.Model.SendEmailRequest

        # "$request | gm" provides a lot of neat things you can do
        $request.Source = $from
        $list = new-object 'System.Collections.Generic.List[string]'
        foreach($address in $toList)
        {
            $list.Add($address)
        }
        $request.Destination = $list
        $subjectObject = new-object -TypeName Amazon.SimpleEmail.Model.Content
        $subjectObject.data = $subject
        $bodyContent = new-object -TypeName Amazon.SimpleEmail.Model.Content
        $bodyContent.data = $body
        $bodyObject = new-object -TypeName Amazon.SimpleEmail.Model.Body
        $bodyObject.text = $bodyContent
        $message = new-object -TypeName Amazon.SimpleEmail.Model.Message
        $message.Subject = $subjectObject
        $message.Body = $bodyObject
        $request.Message = $message

        # Send the message
        $response = $SES_CLIENT.SendEmail($request)
    }
    catch [Exception]
    {
        $function = "SendSesEmail"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }

}
#Description: Sends an status email to administrators
#Returns: n/a
function SendStatusEmail([string[]] $toAddress, [string] $successString = "", [string] $subject = "")
{ 

    try
    {   
        if($subject -eq "")
        {
            $subject = $successString + ": $ENVIRONMENT_NAME $ENVIRONMENT_TYPE $BACKUP_TYPE Backup"
        }

        $body = $global:email

        if($toAddress -eq $null -or $toAddress -eq "") { $toAddress = $ADMIN_ADDRESSES }

        SendSesEmail $FROM_ADDRESS $toAddress $subject $body
    }
    catch [Exception]
    {
        $function = "SendStatusEmail"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }
} 
############## I N S T A N C E   F U N C T I O N S ##############

#Description: Returns an Amazon Web Service Instance object for a given instance Id
#Returns: Instance
function GetInstance([string] $instanceID)
{
    try
    {
        $instancesRequest = new-object amazon.EC2.Model.DescribeInstancesRequest
        $instancesResponse = $EC2_CLIENT.DescribeInstances($instancesRequest.WithInstanceId($instanceID))
        $instancesResult = $instancesResponse.DescribeInstancesResult.Reservation
        return $instancesResult[0].RunningInstance[0]
    }
    catch [Exception]
    {
        $function = "GetInstance"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }
}
#Description: Returns all Amazon Web Service Instance objects
#Returns: ArrayList<Instance>
function GetAllInstances()
{
    try
    {
        $instancesRequest = new-object amazon.EC2.Model.DescribeInstancesRequest
        $instancesResponse = $EC2_CLIENT.DescribeInstances($instancesRequest)
        $instancesResult = $instancesResponse.DescribeInstancesResult.Reservation

         $allInstances = new-object System.Collections.ArrayList

        foreach($reservation in $instancesResult)
        {
            foreach($instance in $reservation.RunningInstance)
            {
                $allInstances.Add($instance) | out-null
            }
        }

        return $allInstances
    }
    catch [Exception]
    {
        $function = "GetAllInstances"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }
}
#Description: Returns an ArrayList of all running Amazon Web Service Instance objects that are
#Returns: Instance
function GetRunningInstances()
{
    try
    {

        $allInstances = GetAllInstances
        $runningInstances = new-object System.Collections.ArrayList

        foreach($instance in $allInstances)
        {
            if($instance.InstanceState.Name -eq "running")
            {
                $runningInstances.Add($instance) | out-null
            }
        }

        return $runningInstances
    }
    catch [Exception]
    {
        $function = "GetRunningInstances"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }
}
#Description: Gets the status of an Amazon Web Service Instance object for a given instance Id
#Returns: string
function GetInstanceStatus([string] $instanceID)
{
    try
    {
        $instance = GetInstance $instanceID
        return $instance.InstanceState.Name
    }
    catch [Exception]
    {
        $function = "GetInstanceStatus"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }
}
#Description: Gets the name of an Amazon Web Service Instance object for a given instance Id
#Returns: string
function GetInstanceName([string] $instanceID)
{
    try
    {
        $name = ""
        $instance = GetInstance $instanceID
        foreach($tag in $instance.Tag)
        {
            if($tag.Key -eq "Name")
            {
                $name = $tag.Value
            }
        }
        return $name
    }
    catch [Exception]
    {
        $function = "GetInstanceName"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }
}
#Description: Starts an Amazon Web Service Instance object for a given instance Id
#Returns: n/a
function StartInstance([string] $instanceID)
{    
    try
    {
        $instanceStatus = GetInstanceStatus $instanceID
        $name = GetInstanceName $instanceID
        if($instanceStatus -eq "running")
        {   
            WriteToLog "Instance $name ($instanceID) Already started"
            WriteToEmail "$name already started"
        }
        else
        {
            #Start instance    
            $startReq = new-object amazon.EC2.Model.StartInstancesRequest
            $startReq.InstanceId.Add($instanceID);    

            WriteToLog "Instance $name ($instanceID) Starting"    
            $startResponse = $EC2_CLIENT.StartInstances($startReq)
            $startResult = $startResponse.StartInstancesResult;

            #Wait for instance to finish starting. Unlike Stop instance,start one at a time (ex. DC, SQL, SP)
            $instancesRequest = new-object amazon.EC2.Model.DescribeInstancesRequest     

            $start = get-date

            do{
                #abort if infinite loop or otherwise
                if(IsTimedOut $start) { break } 

                start-sleep -s 5
                $instancesResponse = $EC2_CLIENT.DescribeInstances($instancesRequest.WithInstanceId($instanceID))
                $instancesResult = $instancesResponse.DescribeInstancesResult.Reservation
            }
            while($instancesResult[0].RunningInstance[0].InstanceState.Name -ne "running") 

            WriteToLog "Instance $name ($instanceID) Started"  
            WriteToEmail "$name started"               
        }
    }
    catch [Exception]
    {
        $function = "StartInstance"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }    
}
#Description: Starts one or more Amazon Web Service Instance object for a collection of instance Ids
#Returns: n/a
function StartInstances ([string[]] $instanceIDs)
{   
    try
    {
        $start = get-date

        foreach($instanceID in $instanceIDs)
        {
            StartInstance $instanceID            
        }

        $end = get-date
        $finish = new-timespan $start $end
        $finishMinutes = $finish.Minutes
        $finishSeconds = $finish.Seconds 
        WriteToLog "Start Instances completed in $finishMinutes min $finishSeconds sec"

    }
    catch [Exception]
    {
        $function = "Start Instances"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }

}
#Description: Starts all Amazon Web Service Instances
#Returns: n/a
function StartAllInstances()
{
    try
    {
        $instances = GetRunningInstances
        foreach($instance in $instances)
        {
            if($STARTALL_EXCEPTIONS -notcontains $instance.InstanceID)
            {
                StopInstance($instance)
            }
        }
    }
    catch [Exception]
    {
        $function = "StopRunningInstances"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }
}
#Description: Stops an Amazon Web Service Instance object for a given instance Id
#Returns: bool - is instance already stopped?
function StopInstance([string] $instanceID)
{    
    try
    {
        $instanceStatus = GetInstanceStatus $instanceID
        $name = GetInstanceName $instanceID
        if($instanceStatus -eq "stopped")
        {   
            WriteToLog "$name ($instanceID) Already Stopped"
            WriteToLog "$name already stopped"
            return $true
        }
        else
        {
            #Stop instance    
            $stopReq = new-object amazon.EC2.Model.StopInstancesRequest
            $stopReq.InstanceId.Add($instanceID);

            WriteToLog "Instance $name ($instanceID) Stopping"
            $stopResponse = $EC2_CLIENT.StopInstances($stopReq)
            $stopResult = $stopResponse.StopInstancesResult;  
            return $false      
        }
    }
    catch [Exception]
    {
        $function = "StopInstance"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }
}
#Description: Stops one or more Amazon Web Service Instance object for a collection of instance Ids
#Returns: n/a
function StopInstances([string[]] $instanceIDs)
{    
    try
    {    
        $statusInstanceIDs = new-object System.Collections.ArrayList($null)
        $statusInstanceIDs.AddRange($instanceIDs)

        #Stop all instances
        foreach($instanceID in $instanceIDs)
        {        
            if(StopInstance $instanceID)
            {
                $statusInstanceIDs.Remove($instanceID)
            }
        }
        #Wait for all instances to finish stopping
        $instancesRequest = new-object amazon.EC2.Model.DescribeInstancesRequest   

        $start = get-date        
        do
        {
            #abort if infinite loop or otherwise
            if(IsTimedOut $start) { break } 

            start-sleep -s 5
            foreach($instanceID in $statusInstanceIDs)
            {
                $status = GetInstanceStatus $instanceID
                if($status -eq "stopped")
                {
                    $name = GetInstanceName $instanceID
                    WriteToLog "Instance $name ($instanceID) Stopped"
                    WriteToEmail "$name stopped"
                    $statusInstanceIDs.Remove($instanceID)
                    break
                }
            }      
        }
        while($statusInstanceIDs.Count -ne 0)        

        $end = get-date
        $finish = new-timespan $start $end
        $finishMinutes = $finish.Minutes
        $finishSeconds = $finish.Seconds         
        WriteToLog "Stop Instances completed in $finishMinutes min $finishSeconds sec"
    }
    catch [Exception]
    {
        $function = "StopInstances"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }
}
#Description: Stops all Amazon Web Service Instances
#Returns: n/a
function StopAllInstances()
{
    try
    {
        [System.Collections.ArrayList]$instances = GetAllInstances
        foreach($instance in $instances)
        {
            if($STOPALL_EXCEPTIONS -notcontains $instance.InstanceID)
            {
                StopInstance($instance.InstanceID)
            }
        }
    }
    catch [Exception]
    {
        $function = "StopAllInstances"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }
}

############## S N A P S H O T   F U N C T I O N S ##############

#Description: Returns a Amazon Web Service Snapshot with a given snapshot Id
#Returns: Snapshot
function GetSnapshot([string] $snapshotID)
{
    try
    {
        $snapshotsRequest = new-object amazon.EC2.Model.DescribeSnapshotsRequest
        $snapshotsResponse = $EC2_CLIENT.DescribeSnapshots($snapshotsRequest.WithSnapshotId($snapshotID))
        $snapshotsResult = $snapshotsResponse.DescribeSnapshotsResult
        return $snapshotsResult.Snapshot[0]
    }
    catch [Exception]
    {
        $function = "GetSnapshot"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }
}
#Description: Returns all Amazon Web Service Snapshots
#Returns: Snapshot[]
function GetAllSnapshots
{
    try
    {
        $snapshotsRequest = new-object amazon.EC2.Model.DescribeSnapshotsRequest
        $snapshotsResponse = $EC2_CLIENT.DescribeSnapshots($snapshotsRequest.WithOwner($accountID))
        $snapshotsResult = $snapshotsResponse.DescribeSnapshotsResult
        return $snapshotsResult.Snapshot
    }
    catch [Exception]
    {
        $function = "GetAllSnapshots"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }
}
#Description: Returns the Description for Amazon Web Service Snapshot with a given snapshot Id
#Returns: string - description of snapshot
function GetSnapshotDescription([string] $snapshotID)
{
    try
    {
        $snapshot = GetSnapshot $snapshotID
        return $snapshot.Description
    }
    catch [Exception]
    {
        $function = "GetSnapshotDescription"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return $null
    }    
}
#Description: Deletes an Amazon Web Service Snapshot with a given snapshot Id
#Returns: n/a
function DeleteSnapshot([string] $snapshotID)
{    
    try
    {
        $name = GetSnapshotDescription $snapshotID                 
        WriteToLog "Snapshot $name ($snapshotID) Deleting"

        $deleteSnapshotRequest = new-object amazon.EC2.Model.DeleteSnapshotRequest
        $deleteSnapshotResponse = $EC2_CLIENT.DeleteSnapshot($deleteSnapshotRequest.WithSnapshotId($snapshotID))
        $deleteSnapshotResult = $deleteSnapshotResponse.DeleteSnapshotResult; 

        WriteToLog "Snapshot $name ($snapshotID) Deleted" 
        WriteToEmail "Snapshot Deleted: $name"    

    }
    catch [Exception]
    {
        $function = "DeleteSnapshot"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
    }

}

#Description: Creates an Amazon Web Service Snapshot for a given instance Id
#Returns: string - newly created snapshotID
function CreateSnapshotForInstance([string] $volumeID, [string] $instanceID)
{    
    try
    {
        #Generate meaningful description for snapshot
        $date = get-date -format yyyyMMddhhmmss
        $name = GetInstanceName $instanceID
        $description = "{0} {1} {2}" -f $name, $BACKUP_TYPE, $date

        WriteToLog "Instance $name ($instanceID) Creating Snapshot"

        $createSnapshotRequest = new-object amazon.EC2.Model.CreateSnapshotRequest
        $createSnapshotResponse = $EC2_CLIENT.CreateSnapshot($createSnapshotRequest.WithVolumeId($volumeID).WithDescription($description))
        $createSnapshotResult = $createSnapshotResponse.CreateSnapshotResult; 

        WriteToLog "Snapshot $description Created for $name ($instanceID)"
        WriteToEmail "$name snapshot successful"
        return $createSnapshotResult.Snapshot.SnapshotId
    }
    catch [Exception]
    {
        $function = "CreateSnapshotForInstance"
        $exception = $_.Exception.ToString()
        WriteToEmail "$name snapshot failed, Exception:"
        WriteToLogAndEmail "function: $exception" -isException $true
        return $null
    }
}
#Description: Creates Amazon Web Service Snapshots for a collection of instance Ids
#Parameters: $instanceIDs string[]
#Returns: n/a
function CreateSnapshotsForInstances([string[]] $instanceIDs)
{
    try
    {
        if($InstanceIDs -ne $null)
        {
            $volumesRequest = new-object amazon.EC2.Model.DescribeVolumesRequest
            $volumesResponse = $EC2_CLIENT.DescribeVolumes($volumesRequest)
            $volumesResult = $volumesResponse.DescribeVolumesResult
            foreach($volume in $volumesResult.Volume)
            {

                if($InstanceIDs -contains $volume.Attachment[0].InstanceId)
                {            
                    #Create the snapshot
                    $snapshotId = CreateSnapshotForInstance $volume.VolumeId $volume.Attachment[0].InstanceId           

                    #Wait for snapshot creation to complete
                    $snapshotsRequest = new-object amazon.EC2.Model.DescribeSnapshotsRequest   

                    $start = get-date             
                    do
                    {
                        #abort if infinite loop or otherwise
                        if(IsTimedOut $Start) { break } 

                        start-sleep -s 5
                        $snapshotsResponse = $EC2_CLIENT.DescribeSnapshots($snapshotsRequest.WithSnapshotId($snapshotId))
                        $snapshotsResult = $snapshotsResponse.DescribeSnapshotsResult
                    }
                    while($snapshotsResult.Snapshot[0].Status -ne "completed")            

                }            
            }  
        }
        else
        {
            WriteToLogAndEmail "Backup failed; no InstanceIDs to process"
        }
    }
    catch [Exception]
    {
        $function = "CreateSnapshotForInstances"
        $exception = $_.Exception.ToString()
        WriteToLogAndEmail "function: $exception" -isException $true
    }
}
#Description: Returns true if passed date is before the current date minus $EXPIRATION_DAYS value
#Returns: bool
function IsDailySnapshotExpired([datetime] $backupDate)
{
    try
    {
        $expireDate = (get-date).AddDays($EXPIRATION_DAYS*-1)
        return ($backupDate) -lt ($expireDate)
    }
    catch [Exception]
    {
        $function = "IsDailySnapshotExpired"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return false
    }    
}

#Description: Returns true if passed date is before the current date minus $EXPIRATION_WEEKS value
#Parameters: $backupDate datetime
#Returns: bool
function IsWeeklySnapshotExpired([datetime] $backupDate)
{
    try
    {
        $expireDate = (get-date).AddDays(($EXPIRATION_WEEKS * 7) * -1)
        return ($backupDate) -lt ($expireDate)
    }
    catch [Exception]
    {
        $function = "IsWeeklySnapshotExpired"
        $exception = $_.Exception.ToString()
        WriteToLog "function: $exception" -isException $true
        return false
    }    
}
#Description: Deleted old daily snapshots
#Parameters: n/a
#Returns: n/a
function CleanupDailySnapshots
{
    try
    {
        WriteToLog "Cleaning up daily snapshots"
        $deleteCount = 0

        $snapshots = GetAllSnapshots
        foreach($snapshot in $snapshots)
        {
            $description = $snapshot.Description
            $snapshotID = $snapshot.SnapshotId
            if($snapshot.Description.Contains("Daily"))
            {
                $backupDateTime = get-date $snapshot.StartTime
                $expired = IsDailySnapshotExpired $backupDateTime
                if($expired)
                {
                    DeleteSnapshot $snapshot.SnapshotId
                    $deleteCount ++
                    WriteToLog "$description ($snapshotID) Expired"
                }

            }
        }
        WriteToLogAndEmail "$deleteCount daily snapshots deleted"
    }
    catch [Exception]
    {
        $function = "CleanupWeeklySnapshots"
        $exception = $_.Exception.ToString()
        WriteToLogAndEmail "function: $exception" -isException $true
        return false
    } 
}
#Description: Deleted old weekly snapshots
#Parameters: n/a
#Returns: n/a
function CleanupWeeklySnapshots
{
    try
    {
        WriteToLog "Cleaning up weekly snapshots"
        $deleteCount = 0

        $snapshots = GetAllSnapshots
        foreach($snapshot in $snapshots)
        {
            $description = $snapshot.Description
            $snapshotID = $snapshot.SnapshotId
            if($snapshot.Description.Contains("Weekly"))
            {
                $backupDateTime = get-date $snapshot.StartTime
                $expired = IsWeeklySnapshotExpired $backupDateTime
                if($expired)
                {
                    DeleteSnapshot $snapshot.SnapshotId
                    $deleteCount ++
                    WriteToLog "$description ($snapshotID) Expired"
                }

            }
        }
        WriteToLogAndEmail "$deleteCount weekly snapshots deleted"
    }
    catch [Exception]
    {
        $function = "CleanupDailySnapshots"
        $exception = $_.Exception.ToString()
        WriteToLogAndEmail "function: $exception" -isException $true
        return false
    } 
}

 

DailySnapshot.ps1

[Download]


############## C O N F I G ##############
."C:\AWS\AWSConfig.ps1"

#Environment
$ENVIRONMENT_NAME = "My Environment"
$ENVIRONMENT_TYPE = "Development"
$BACKUP_TYPE = "Daily"
$stagingInstanceIDs="i-xxxxxxxx","i-xxxxxxxx","i-xxxxxxxx"

############## F U N C T I O N S ##############
."C:\AWS\AWSUtilities.ps1"

############## M A I N ##############

try
{
    $start = Get-Date
    WriteToLogAndEmail "$ENVIRONMENT_NAME $ENVIRONMENT_TYPE $BACKUP_TYPE Backup Starting" -excludeTimeStamp $true

    CreateSnapshotsForInstances $stagingInstanceIDs

    CleanupDailySnapshots

    WriteToLogAndEmail "$ENVIRONMENT_NAME $ENVIRONMENT_TYPE $BACK_UPTYPE Backup Complete" -excludeTimeStamp $true   

    $end = Get-Date
    $timespan = New-TimeSpan $start $end
    $hours=$timespan.Hours
    $minutes=$timespan.Minutes    
    WriteToEmail "Backup took $hours hr(s) and $minutes min(s)"

    WriteToEmail "Click here to test: $TEST_URL" -excludeTimeStamp $true
    SendStatusEmail -successString "SUCCESS"
}
catch
{
    SendStatusEmail -successString "FAILED"
}

 

WeeklySnapshot.ps1

[Download]

############## C O N F I G ##############
."C:\AWS\AWSConfig.ps1"

#Environment
$ENVIRONMENT_NAME = "My Environment"
$ENVIRONMENT_TYPE = "Development"
$BACKUP_TYPE = "Weekly"
$stagingInstanceIDs="i-xxxxxxxx","i-xxxxxxxx","i-xxxxxxxx"

############## F U N C T I O N S ##############
."C:\AWS\AWSUtilities.ps1"

############## M A I N ##############

try
{
    $start = Get-Date
    WriteToLogAndEmail "$ENVIRONMENT_NAME $ENVIRONMENT_TYPE $BACKUP_TYPE Backup Starting" -excludeTimeStamp $true

    StopInstances $stagingInstanceIDs
    CreateSnapshotsForInstances $stagingInstanceIDs
    StartInstances $stagingInstanceIDs

    CleanupWeeklySnapshots

    WriteToLogAndEmail "$ENVIRONMENT_NAME $ENVIRONMENT_TYPE $BACK_UPTYPE Backup Complete" -excludeTimeStamp $true

    $end = Get-Date
    $timespan = New-TimeSpan $start $end
    $hours=$timespan.Hours
    $minutes=$timespan.Minutes    
    WriteToEmail "Backup took $hours hours and $minutes to complete"

    WriteToEmail "Click here to test: $TEST_URL" -excludeTimeStamp $true

    SendStatusEmail -successString "SUCCESS"
}
catch
{
    SendStatusEmail -successString "FAILED"
}

 

Configuration

Configure AWSConfig.ps1

Open AWSConfig.ps1:

  1. AWS Access Path
  2. Provide the Access Key ID,  Secret Access Key, and Account ID for your account. Get Account ID, Access Key and Secret Access Key from AWS: My Account / Console > Security Credentials
  3. Uncomment the the region that your instances are running in
  4. Choose a location to store logs
  5. Provide a from address (must be verified in Amazon Simple Email Services (SES))

Configure AWSUtilities.ps1

Open AWSUtilities.ps1 and execute the line:

New-EventLog -Source "AWS PowerShell Utilities" -LogName "Application

. This will allow the scripts to add entries into the event log.

Configure DailySnapshots.ps1 & WeeklySnapshots.ps1

Open both DailySnapshots.ps1 and WeeklySnapshots.ps1 and:

  1. Verify the paths to AWSConfig.ps1 and AWSUtilities.ps1
  2. Set the ENVIRONMENT_NAME to whatever you want to call the environment (ex. the name of the customer). This variable is used in notification emails.
  3. Set the ENVIRONMENT_TYPE to the name of the environment (ex. production, development, etc.)
  4. BACKUP_TYPE should be set to the name of the backup (i.e. daily or weekly); however, you can customize if you like.
  5. Set stagingInstanceIDs to the instances that make up your environment

Test it out

When you execute the DailySnapshot.ps1 script, the script will create a snapshot of each volume for each instance that you provided without shutting them down. Once it is complete, you should receive an email stating the status of the backup. Similarly, when you execute WeeklySnapshot.ps1, the script will shut down the instances, create snapshots of each volume on each instance, start them back up in the order listed, and then send you an email notification. You can see this in action if you review the AWS Disaster Recovery Demo.