Create an EC2 AMI using instance name tags via PowerShell

AWS EC2 AMI tags (click to enlarge)
AWS EC2 AMI tags (click to enlarge)

[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.

Tagging-an-EC2-AMI
Tagged EC2 AMI (Click to enlarge)
Tagging an AMI's snapshots in EC2
Tags in associated AMI snapshots (Click to enlarge)
 

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."
            }
        }
    }
}

Posted

in

, , ,

by

Comments

8 responses to “Create an EC2 AMI using instance name tags via PowerShell”

  1. Suraj Makhija Avatar

    Hi Alex,

     

    The snapshot completes and then gives error : “The request must contain the parameter resourceIdSet”

    and under snapshot, it doesnot give the backupdate.

    Thanks,

    Suraj Makhija

    1. Alex Neihaus Avatar
      Alex Neihaus

      When I get a comment like this, I always wonder if the poster is asking for support or telling me I have a bug.

      Mostly, I think it’s the former. But since this post is popular and quite old (almost four years!), I decided to give the more recent script (from March, 2017) a tough test: I wanted to see if it worked using PowerShell.NetCore cmdlets running pwsh 6.0.1 on macOS High Sierra.

      As you can see, it worked perfectly. The only changes I needed to make were to include Set-AWSCredential -ProfileName [stored profile] and Set-DefaultAWSRegion.

      (Click to enlarge the image.)

  2. Chris Avatar
    Chris

    Hi Alex,

    Thank you for posting both these scripts.

    I have tried the first one but get errors. I think that is because the script is not compatible with the latest version of AWS Powershell?

    I would like to try your second script as it is a newer version but I do not want to run it against all instances. Is there a way to specify a single instance in the second script as in the first?

     

    Many Thanks,

    Chris

    1. Alex Neihaus Avatar
      Alex Neihaus

      Hi, Chris.

      This script should run on any version of PowerShell later than PowerShell 3.

      Sure, there’s a way you can test either script against a limited set of instances. You can modify it any way you like and/or use the Tag Editor to undo whatever happens.

      Good luck.

  3. Bruce Noe Avatar
    Bruce Noe

    For sure running. But I could see wanting to see all owned instances. Id be ore than happy to help in any way.

  4. Alex Neihaus Avatar
    Alex Neihaus

    Thanks for the suggestion, Bruce!

    It’s an interesting idea. One question: would you want all instances in the current account listed? Or would it be just running instances?

    As I discovered, it makes a difference. Did you see this post about EC2.Model.Filter? If I were to do this for my own use, I would present only running instances. But I’d like to know what you think.

    I’ll give this a go…but can’t promise how long it’ll take. I public versions of scripts that I write for clients in my spare time.

  5. Bruce Noe Avatar
    Bruce Noe

    Any chance to modify this to use out-gridview and select the machines you want to create an AMI for? Similiar to your new de-register script?

  6. […] I wrote a PowerShell script that uses an EC2 instance’s Name tag to create an Amazon Machine Image (AMI) of that running […]

Leave a Reply

Your email address will not be published. Required fields are marked *