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
[sourcecode language="powershell"] ############## 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) [/sourcecode]
AWSUtilities.ps1
[sourcecode language="powershell"] ############## 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 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 } }[/sourcecode]
DailySnapshot.ps1
[sourcecode language="powershell"] ############## 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" } [/sourcecode]
WeeklySnapshot.ps1
[sourcecode language="powershell"] ############## 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" } [/sourcecode]
Configuration
Configure AWSConfig.ps1
Open AWSConfig.ps1:
- AWS Access Path
- 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
- Uncomment the the region that your instances are running in
- Choose a location to store logs
- Provide a from address (must be verified in Amazon Simple Email Services (SES))
Configure AWSUtilities.ps1
Open AWSUtilities.ps1 and execute the line: [sourcecode language=”powershell”]New-EventLog -Source “AWS PowerShell Utilities” -LogName “Application[/sourcecode]. 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:
- Verify the paths to AWSConfig.ps1 and AWSUtilities.ps1
- 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.
- Set the ENVIRONMENT_TYPE to the name of the environment (ex. production, development, etc.)
- BACKUP_TYPE should be set to the name of the backup (i.e. daily or weekly); however, you can customize if you like.
- 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.
Updated to fix issue with Instances that have custom tags.
It should probably be noted that some of this code pertains to the AWS SDK for .NET Version 1. Some of the functions have been deprecated. I’ve spent quite some time finding this out the hard way…
James – Thanks for the note. Would you be able to list the functions that were deprecated? That would be very helpful. Thanks in advance!
I too spent quite a bit of time digging through the code to get things working with AWS SDK for .NET v2.0 — and here are the fruits of that labor. Enjoy!
https://github.com/noahlh/aws-automated-backup-powershell
(Note to the author Chris — I didn’t see your contact info here so if I need to change any credits/attribution, please feel free to let me know)
Looks great. I have not had a chance to update this and appreciate the effort. Hopefully I was able to save you a little time on getting it working.
Thanks for the script. Quick question. The script creates the description for the snapshot. Is there an easy way to make that string the “Name” of the instance instead of the description?
If I remember correctly; the issue is that AWS generates the name and owns it; probably to prevent name collision. Therefore, we use Description to simulate a name.
Thank you Chris first and foremost for posting this! I’m having an issue while running the daily
at System.Management.Automation.ParameterBinderBase.CoerceTypeAsNeeded(CommandParameterInternal argument, String parameterName, Type toType, ParameterCollectionTypeInformation collectionTypeInfo, Object currentValue)
8/23/2014 12:07:38 PM AWS-Ireland-Frankfurt_Setup Production Daily Backup Starting
8/23/2014 12:07:38 PM function: System.Management.Automation.RuntimeException: You cannot call a method on a null-valued expression.
at System.Management.Automation.ParserOps.CallMethod(Token token, Object target, String methodName, Object[] paramArray, Boolean callStatic, Object valueToSet)
at System.Management.Automation.MethodCallNode.InvokeMethod(Object target, Object[] arguments, Object value)
at System.Management.Automation.MethodCallNode.Execute(Array input, Pipe outputPipe, ExecutionContext context)
at System.Management.Automation.AssignmentStatementNode.Execute(Array input, Pipe outputPipe, ExecutionContext context)
at System.Management.Automation.StatementListNode.ExecuteStatement(ParseTreeNode statement, Array input, Pipe outputPipe, ArrayList& resultList, ExecutionContext context)
8/23/2014 12:07:38 PM function: System.NotSupportedException: Specified method is not supported.
at System.Management.Automation.ParameterBinderBase.CoerceTypeAsNeeded(CommandParameterInternal argument, String parameterName, Type toType, ParameterCollectionTypeInformation collectionTypeInfo, Object currentValue)
8/23/2014 12:07:38 PM function: System.NotSupportedException: Specified method is not supported.
at System.Management.Automation.ParameterBinderBase.CoerceTypeAsNeeded(CommandParameterInternal argument, String parameterName, Type toType, ParameterCollectionTypeInformation collectionTypeInfo, Object currentValue)
8/23/2014 12:07:38 PM function: System.NotSupportedException: Specified method is not supported.
Which is causing.
The script failed due to call depth overflow. The call depth reached 1001 and the maximum is 1000.
+ CategoryInfo : InvalidOperation: (1001:Int32) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : CallDepthOverflow
Thank you in advance for the assistance.
Hello; are you running the original scripts? Amazon made some changes that broke the script. Someone had reworked it in a post above. Here is the link: https://github.com/noahlh/aws-automated-backup-powershell.
Thanks for the reply, I really appreciate it.
I actually got it working just now.I had to launch an instance in AWS in order for this to work. I’m thinking there is an issue with the AWS tools I installed or something.
Mike, Sorry about that this is the correct error.
PS C:\Windows\system32> C:\AWS\DailySnapshot.ps1
1
Exception calling “CreateAmazonEC2Client” with “3” argument(s): “No RegionEndpoint or ServiceURL configured”
At C:\AWS\AWSConfig.ps1:43 char:61
+ $EC2_CLIENT=[Amazon.AWSClientFactory]::CreateAmazonEC2Client <<<< ($accessKeyID, $secretAccessKey, $config)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
Well I’ve fixed the error above as well but one last issue I hope.
PS C:\Windows\system32> C:\AWS\DailySnapshot.ps1
1
AWS-Ireland-Frankfurt_Setup Production Daily Backup Starting
function: System.Management.Automation.SetValueInvocationException: Exception setting “InstanceIds”: “Cannot convert
the “i-b3xxxx” value of type
“System.String” to type “System.Collections.Generic.List`1[System.String]”.” —>
System.Management.Automation.PSInvalidCastException: Cannot convert the “i-b3d3xxx” value of type “System.String” to type
“System.Collections.Generic.List`1[System.String]”.
This is actually failing for me after doing all of the setup when I try to run dailysnapshot with the error
The script failed due to call depth overflow.
At C:\Utils\AWS\AWSUtilities.ps1:124 char:9
+ WriteToLog “$function : $exception” -isException $true
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (0:Int32) [], RuntimeException
+ FullyQualifiedErrorId : CallDepthOverflow
Can you help?
I was getting the same error.
Few things to check:
I ran the script manually several times and after the first attempt. Subsequent executions failed to write to the log file. I deleted the log file in c:\aws\log and that resolved the issue.
Second, email notifications weren’t working for me. Make sure you verify both the senders email, AND the recipient address in SES.
http://aws.amazon.com/about-aws/whats-new/2015/07/version-3-of-the-aws-sdk-for-net-is-available-for-production-use-in-net-3-5-and-4-5/
http://docs.aws.amazon.com/AWSSdkDocsNET/latest/V3/DeveloperGuide/net-dg-migration-guide-v3.html
This will require updates to the scripts.
I have EC2 volumes with custom tags. I run a backup script that creates weekly snapshots of a bunch of volumes. How do I put the same volume tags on my snapshots when the backup script runs.
I want yo leverage powershell to apply the tags. Any ideas?
All SES emails end up in quarantine no matter what verified address is used