[Update 2017-03-29: The original version of the script below automates creation of AMIs in EC2 as long as the instance name tag is unique. An updated version of the script handles duplicate name tags and terminated instances.]
One of EC2’s most powerful features is the ability to create AMIs — Amazon Machine Images — of entire instances (servers). While this isn’t new to fans of virtualization who have long benefited from “virtual hard disk” images of their servers, AWS offers some enhancements around AMIs which make life easier.
Two killer features are that new AMIs are deltas of previous AMIs. This cuts down on storage space (for which you pay) and encourages regular backups. And, as with everything else in AWS, the process of creating an AMI is scriptable in the whole range of AWS-supported languages and in the AWS CLI. (But we all know the AWS PowerShell cmdlets are the coolest of them all, no?)
The challenge, from my perspective, is that AWS manages “instance ids” and “AMI ids” and “snapshot ids” as unique identifiers. But I think in terms of name tags which are part of the AWS tagging system. This may not be that obvious to you as you use the AWS Console because, under the covers, the console is using unique identifiers while you are probably focused on the logical tag names.
I wanted to create a PowerShell script to create nicely tagged AMIs. I assumed it would be a snap to pass a simple PowerShell script my instances’ name tags and have it create beautifully-tagged AMIs and snapshots.
I was wrong. It turned out to be a fair bit of work to do this — and in the process, I discovered the first lesson to keep in mind about the AWS API: it’s eventually consistent. Think about it: if every call to the AWS API was synchronous, it would be impossible to scale AWS to the massive scale it has reached today. Practically speaking, it means you need to either check repeatedly that the API call has succeeded or take a more simplistic approach and wait a few seconds for things to “settle”.
Using PowerShell for DevOps in AWS, you quickly learn two things. First, there’s not that many good examples of using the AWS PowerShell cmdlets on the internet and, two, your new best friend is the AWS PowerShell Reference. AWS keeps the PowerShell cmdlets updated in lock-step with the API on other platforms (thanks, guys!). This means PowerShell is a first-class client in AWS. And I think the AWS PowerShell cmdlets are a thing of beauty when combined with PowerShell’s pipeline.
So, what follows is probably not the best PowerShell code — and I doubt an AWS aficionado will like the way I did this. [Update 2017-03-29: More than two years later, this code has performed perfectly for me in production use.]
But it was a helluva lot of fun to develop and it does the trick for me. With this little script and the Windows Server Task Scheduler, I can create tagged AMIs with ease whenever I need them.
The entire script is available at the end of this post along with screenshots of the output. (And here is a post on how to delete AMIs along with all of its associated snapshots.)
Some explanations of what I did and what I discovered are immediately below. (Expand the lines of code below by using the controls in the toolbar at the top of the code window.)
All of my instances have a “Name” tag that uniquely names the system and purpose of the instance. I use that unique name to retrieve the ID of the running instance here:
$instanceID = Get-EC2Instance -Filter @{name='tag:Name'; values=$instanceName} | Select -ExpandProperty instances
Here we actually create the AMI and get its ID in return. But — and this is a BIG but — even though a subsequent EC2-GetImage using this ID will return ALL of the objects we expect, some of them may be blank. This is the eventual consistency of the AWS API in action. I’ve chosen to wait 60 seconds for this to happen; YMMV.
$amiID = New-EC2Image -InstanceId $instanceID.InstanceId -Description $tagDesc -Name $amiName -NoReboot:$false # Create the AMI, rebooting the instance in the process Start-Sleep -Seconds 60 # For some reason, it can take some time for subsequent calls to Get-EC2Image to return all properties, especially for snapshots. So we wait.
These next three lines are the heart of the script with respect to tagging. First, we get the image attributes via Get-EC2Image. Next, we move “down a level” by extracting the the BlockDeviceMapping object contained in the image properties. One of the properties of BlockDeviceMapping is ebs which is, itself, an object.
Since there may be more than one snapshot for any given AMI, it’s necessary to extract each of the snapshot IDs for use with New-EC2Tag separately. ForEach-Object does this elegantly and since ForEach-Object allows a script block to be specified, we can conveniently tag all our snapshots in that script block.
$amiProperties = Get-EC2Image -ImageIds $amiID # Get Amazon.EC2.Model.Image $amiBlockDeviceMapping = $amiProperties.BlockDeviceMapping # Get Amazon.Ec2.Model.BlockDeviceMapping $amiBlockDeviceMapping.ebs | ` ForEach-Object -Process {New-EC2Tag -Resources $_.SnapshotID -Tags @{ Key = "Name" ; Value = $amiName} }# Add tags to snapshots associated with the AMI using Amazon.EC2.Model.EbsBlockDevice
Here’s the full script.
<# Create an AMI from a running instance by using the instance's name tag, tag the resulting AMI and all snapshots with a meaningful tag Copyright 2014 Air11 Technology LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. #> $array = @("MANTIS01") # Name of servers to be restarted foreach ($instanceName in $array) { $instanceID = Get-EC2Instance -Filter @{name='tag:Name'; values=$instanceName} | Select -ExpandProperty instances #Get instance ID $longTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" # Get current time into a string $tagDesc = "Created by " + $MyInvocation.MyCommand.Name + " on " + $longTime # Make a nice string for the Description tag $amiName = $instanceName + " AMI " + $longTime # Make a name for the AMI $amiID = New-EC2Image -InstanceId $instanceID.InstanceId -Description $tagDesc -Name $amiName -NoReboot:$false # Create the AMI, rebooting the instance in the process Start-Sleep -Seconds 60 # For some reason, it can take some time for subsequent calls to Get-EC2Image to return all properties, especially for snapshots. So we wait $shortTime = Get-Date -Format "yyyy-MM-dd" # Shorter date for the name tag $tagName = $instanceName + " AMI " + $shortTime # Sting for use with the name TAG -- as opposed to the AMI name, which is something else and set in New-EC2Image New-EC2Tag -Resources $amiID -Tags @( @{ Key = "Name" ; Value = $tagName}, @{ Key = "Description"; Value = $tagDesc } ) # Add tags to new AMI $amiProperties = Get-EC2Image -ImageIds $amiID # Get Amazon.EC2.Model.Image $amiBlockDeviceMapping = $amiProperties.BlockDeviceMapping # Get Amazon.Ec2.Model.BlockDeviceMapping $amiBlockDeviceMapping.ebs | ` ForEach-Object -Process {New-EC2Tag -Resources $_.SnapshotID -Tags @{ Key = "Name" ; Value = $amiName} }# Add tags to snapshots associated with the AMI using Amazon.EC2.Model.EbsBlockDevice "Created AMI" + " " + $amiID + " " + $amiName | Out-File -FilePath "D:\Robocopy-logs\$instanceName-AMI.log" -Append } # End foreach
Here are two screen shots showing the results. I hope you find this useful and I look forward to your comments.
Here’s the March, 2017 updated script which automates creating an EC2 AMI from running or stopped instances only and tolerates terminated instances with an identical name tag.
<# Create an AMI from a running instance by using the instance's name tag and tag the resulting AMI and all snapshots with a meaningful tag Copyright 2017 Air11 Technology LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .SYNOPSIS Creates an AMI from a running or stopped instance. Skips terminated, pending, or shutting down instances .DESCRIPTION Creates an AMI from running or stopped instances, then tags it AND its associated snaphots for easy identification. .NOTES 2017-03-28 substantially revised to allow for duplicate instance name tags and to permit only running or stopped instances .INPUT ./CreateAMIbyName -instanceNameTag [InstanceNameTag[] ] -up [$true | $false] -note [string] -platform [ string ] $instanceNameTag must be the exact instance name tag for the instance $up (optional) uppercases lowercase name input $note is a string to be stored in the comment and log file $platform is tag:Platform info that is added to the AMI's and snapshots' tags .EXAMPLE ./CreateAmibyName -instances "instance1, instance2", -up $true, -note "This comment is stored in the AMI description" -platform "BillingApp" #> param ( [Parameter(Mandatory = $true)] [string[]]$instanceNameTag, [Parameter(Mandatory = $true)] [string]$note, [Parameter(Mandatory = $true)] [boolean]$up, [Parameter(Mandatory = $false)] [string]$platform ) Import-Module AWSPowerShell $platform = $platform.ToUpper() switch ($up) { $TRUE { $array = @($instanceNameTag.ToUpper()) } default { $array = @($instanceNameTag) } } foreach ($nameTag in $array) # Process all supplied name tags after making sure they are upper-cased. Our convention is upper-case instance name tags { $i = (Get-EC2Instance -Filter @{name ='tag:Name'; values = $nameTag}).instances # Create array of type Amazon.EC2.Model.Instance foreach ($instance in $i) # In case there are duplicated name tags, offer a choice to create an AMI only for the instances in running or stopped state { switch ($instance.state.code) { 0 { # Status is pending $instance.instanceID + " status is " + $instance.state.code + ": pending, skipped" } { ($_ -eq 16) -or ($_ -eq 80) } { # Status is running or stopped Write-Host "`nCreate AMI for $nameTag from instance $($instance.InstanceID), status = $($instance.state.name)?" -ForegroundColor Yellow "Comment to store in AMI: $note" "Platform tag to be used: $platform" $title = 'Create AMI for this instance?' $prompt = '[Y]es or [N]o?' $yes = New-Object System.Management.Automation.Host.ChoiceDescription '&Yes', 'Continues' $no = New-Object System.Management.Automation.Host.ChoiceDescription '&No', 'Exits' $options = [System.Management.Automation.Host.ChoiceDescription[]] ($yes, $no) $choice = $host.ui.PromptForChoice($title, $prompt, $options, 0) If ($choice -eq 1) { Write-Host "Instance skipped" -ForegroundColor Red Break } # End if else { $longTime = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" # Get current time into a string $tagDesc = "Created by " + $MyInvocation.MyCommand.Name + " on " + $longTime + " with comment: " + $note # Make a nice string for the AMI Description tag $amiName = $nameTag + " AMI " + $longTime # Make a name for the AMI $amiID = New-EC2Image -InstanceId $instance.InstanceId -Description $tagDesc -Name $amiName -NoReboot:$false # Create the AMI, rebooting the instance in the process Start-Sleep -Seconds 90 # Wait a few seconds just to make sure the call to Get-EC2Image will return the assigned objects for this AMI $shortTime = Get-Date -Format "yyyy-MM-dd" # Shorter date for the name tag $tagName = $nameTag + " AMI " + $shortTime # Sting for use with the name TAG -- as opposed to the AMI name, which is something else and set in New-EC2Image New-EC2Tag -Resources $amiID -Tags @(@{ Key = "Name"; Value = $tagName }, @{ Key = "Description"; Value = $tagDesc }, @{ Key = 'Platform'; Value = $platform }) # Add tags to new AMI $amiProperties = Get-EC2Image -ImageIds $amiID # Get Amazon.EC2.Model.Image $amiBlockDeviceMapping = $amiProperties.BlockDeviceMapping # Get Amazon.Ec2.Model.BlockDeviceMapping $amiBlockDeviceMapping.ebs | ` ForEach-Object -Process { New-EC2Tag -Resources $_.SnapshotID -Tags @(@{ Key = "Name"; Value = $amiName }, @{ Key = 'Platform'; Value = $platform }) } # Add tags to snapshots associated with the AMI using Amazon.EC2.Model.EbsBlockDevice Write-Host "`nCompleted instance $($instance.InstanceID), new AMI = $($amiID) " -ForegroundColor Yellow } } 32 { # Status is shutting-down $instance.instanceID + " status is " + $instance.state.code + ": shutting down,skipped" } 48 { # Status is terminated $instance.instanceID + " status is " + $instance.state.code + ": terminated, skipped" } 64 { # Status is stopping $instance.instanceID + " status is " + $instance.state.code + ": stopping, skipped" } default { Write-Error "No valid states detected for any of the instances associated with the specified name tag." } } } }
Leave a Reply