AWS Find AMIs | Boto3 Script

Photo by Taylor Vick on Unsplash

AWS Find AMIs | Boto3 Script

Overview

Capture a list of available AWS AMIs for a specified region and OS version with my latest boto3 script. This Python script will output a color-coded table listing AMIs that have been released within the last 30 days with the owner alias set to Amazon. The table includes columns for AMI ID, Name, Architecture type and Virtualization type. Output is sorted by Architecture type (x86_64, arm64, etc), then sorted by AMI names which include release dates.

Sample output against us-west-2 region for a list of debian-11* AMI's:

Purpose

For my particular use case, I have a dev environment where AMIs for various OS versions and instance types need to be built for testing purposes. I also have a fleet of production instances that all run the same OS version and machine type using a custom AWS Terraform module. I'm still able to use this same Terraform module for my dev infrastructure however, I've created a unique variable to be able to assign a specific AMI to certain dev resources. The user_specified_ami var is what I'm using to assign my AMIs. I've applied the coalesce function to the "ami" resource which forces this value to return as precedent over the next sequential data.aws_ami source as long as there's a string assigned to this variable. It looks like this in the aws_instance resource block of the module:

ami = coalesce(var.user_specified_ami, data.aws_ami.prod_os.id)

Then in my configuration module, I can simply assign the user_specified_ami an ID from the script output.

Script Functionality

Link to the script: https://github.com/jksprattler/aws-tools/blob/main/scripts/aws_ec2_find_amis.py

Usage

Here's a look at the options available from the -h/--help output:

❯ python aws_ec2_find_amis.py --help
usage: aws_ec2_find_amis.py [-h] region os_version

Find AMIs based on region and OS

positional arguments:
  region      AWS region
  os_version  OS version

options:
  -h, --help  show this help message and exit

Here's an example of entering an OS version not set in the valid_os_versions list of elements:

❯ python aws_ec2_find_amis.py us-west-2 debian
Invalid OS version 'debian'. Available OS versions:
amzn2
debian-11
ubuntu-jammy
Windows_Server-2019-English

Here's an example of entering an invalid AWS region:

❯ python aws_ec2_find_amis.py us-west debian-11
Invalid region 'us-west'. Available regions:
af-south-1
ap-south-1
eu-north-1
eu-west-3
eu-south-1
eu-west-2
eu-west-1
ap-northeast-3
ap-northeast-2
me-south-1
ap-northeast-1
ca-central-1
sa-east-1
ap-east-1
ap-southeast-1
ap-southeast-2
eu-central-1
ap-southeast-3
us-east-1
us-east-2
us-west-1
us-west-2

Code Explanation

Inside the main function, it calls the parse_arguments() function to parse the user input arguments and assigns them to the region and os_version variables. See the above help output for example.

def main():
    """
    Parse user arguments, assign region and os values
    Lookup latest available AMI's and output them to color-coded table
    """
    region, os_version = parse_arguments()
    validate_inputs(region, os_version)
    find_amis(region, os_version)

Here's the parse_arguments() function :

def parse_arguments():
    """ Parse args """
    parser = argparse.ArgumentParser(description='Find AMIs based on region and OS')
    parser.add_argument('region', type=str, help='AWS region')
    parser.add_argument('os_version', type=str, help='OS version')
    args = parser.parse_args()
    return args.region, args.os_version

Next, it checks if the provided inputs for the region and os_version arguments are valid with the validate_inputs(region, os_version) function.

def validate_inputs(region, os_version):
    """ Validate region and OS version if provided"""
    if region:
        available_regions = get_available_regions()
        if region not in available_regions:
            print(f"Invalid region '{region}'. Available regions:")
            for regions in available_regions:
                print(regions)
            sys.exit()

    if os_version:
        validate_os_version(os_version)

This will perform a conditional check for the region input against the get_available_regions() function which performs an API call using the EC2 client to describe the AWS regions:

def get_available_regions():
    """
    Get a list of available AWS regions
    """
    ec2_client = boto3.client('ec2')
    response = ec2_client.describe_regions()
    regions = [region['RegionName'] for region in response['Regions']]
    return regions

Then it performs a conditional check for the os_version input against the validate_os_version(os_version) function which checks if the provided versions starts with any of the elements in the valid_os_versions list:

def validate_os_version(os_version):
    """ Check if the provided OS version is valid """
    valid_os_versions = ['amzn2', 'debian-11', 'ubuntu-jammy', 'Windows_Server-2019-English']
    for version in valid_os_versions:
        if os_version.startswith(version):
            return
    print(f"Invalid OS version '{os_version}'. Available OS versions:")
    for version in valid_os_versions:
        print(version)
    sys.exit()

Finally, the find_amis(region, os_version) function is executed. The API of the EC2 client is called to describe images hosted in the specified region and filtered based on the provided OS version and AMI alias owner, Amazon:

def find_amis(region, os_version):
    """
    Output a list of the latest Amazon owned AMI's for the specified
    region and OS version in color-coded Table format
    """

    ec2_client = boto3.client('ec2', region_name=region)

    response = ec2_client.describe_images(
        Filters=[
            {'Name': 'name', 'Values': ['*'+os_version+'*']},
            {'Name': 'owner-alias', 'Values': ['amazon']}
        ]
    )

    amis = response['Images']

    # Sort the AMIs by architecture type and name
    sorted_amis = sorted(amis, key=lambda x: (x['Architecture'], x['Name']))
    latest_amis = []
    for ami in sorted_amis:
        creation_date = datetime.datetime.strptime(ami['CreationDate'], "%Y-%m-%dT%H:%M:%S.%fZ")
        if creation_date >= DATE_THRESHOLD:
            latest_amis.append(ami)

    table = []
    table_headers = ["AMI ID", "Name", "Architecture", "VirtualizationType"]
    for ami in latest_amis:
        table.append([
            f"{Fore.CYAN}{ami['ImageId']}{Style.RESET_ALL}",
            ami['Name'],
            f"{Fore.GREEN}{ami['Architecture']}{Style.RESET_ALL}",
            ami['VirtualizationType']
        ])
    print(tabulate(table, headers=table_headers, tablefmt="grid"))

The response contains a list of AMIS matching these filters and is sorted by the Architecture type. Further sorting is done by AMI name which contains release dates. Only AMIs with release dates in the last 30 days are provided.

Next, a table is generated by creating an empty list called table to store the AMI information for tabulation. As the script iterates over the sorted AMIs, the requested data for the AMI ID, Name, Architecture and Virtualization Type are appended to the table list. The ImageID and the Architecture values have color formatting applied using the Fore class from the colorama library. I added this feature to help with the readability of the table output.

Conclusion

Now I'm able to get these AMI values much more quickly with this handy script rather than crafting an aws ec2 describe-images command and parsing through the output. Hope you found this useful! What kind of use cases do you have for capturing and assigning AMIs?