Build an Ubuntu Pro AMI using Packer

This guide will provide instructions for using Packer to create your own golden image of Ubuntu Pro. You’ll be able to customise your Packer template file, while keeping the the Ubuntu Pro activation mechanism intact.

We’ll be using Ubuntu Pro 20.04 for this guide, but the method is equally applicable to other Pro versions.

Note

  • For Ubuntu Pro FIPS, it is better to use a pre-enabled FIPS image from the Marketplace to avoid unnecessary additional steps.

  • For Ubuntu LTS, you can use this method with a small change as explained at the end of the Define provisioners section below.

Basic setup

Assuming you already have an AWS account you’ll need to install Packer and create your API credentials. If you need a FIPS AMI, you’ll also have to subscribe to the Ubuntu Pro listing on the Marketplace.

Install Packer

If you have an Ubuntu workstation, run:

sudo apt install packer

For other platforms, use the appropriate install Packer instructions.

Subscribe to Ubuntu Pro (Only needed for FIPS AMI)

In the AWS Marketplace search for ‘Ubuntu Pro FIPS’. Select Ubuntu Pro FIPS 20.04 LTS > Continue to Subscribe > Accept Terms and wait for the activation to complete. You only get charged when you run an instance of Ubuntu Pro.

If you skip this step and proceed with the build, you can still subscribe later when an error leads you to the Marketplace product page.

Create AWS Credentials

Follow the instructions in the Programmatic access section of the IAM page for creating credentials. For a list of the minimal set of permissions needed in your IAM user or role policy, refer to IAM permissions needed for Packer.

Save your credentials, they should be similar to:

Secret: AKIAIOSFODNN7EXAMPLE
Access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

Create a Packer template

Our Packer template file will consist of variables, builders and provisioners residing at the same root level of the structure. For a deeper understanding of the structure and the key components of a Packer JSON file, refer to the Packer website.

Add variables

Add the saved AWS credentials as variables to a blank packer.json file:

{
   "variables": {
      "aws_access_key": "YOUR_ACCESS_KEY",
      "aws_secret_key": "YOUR_SECRET_KEY"
   },
}

Define a builder

Add details such as image type, region, base image and owner ID in the builders component:

"builders": [
   {
     "type": "amazon-ebs",
     "access_key": "{{user `aws_access_key`}}",
     "secret_key": "{{user `aws_secret_key`}}",
     "region": "us-east-1",
     "instance_type": "t2.micro",
     "ami_name": "My-Ubuntu-Pro-20.04-{{timestamp}}",

To add the source image, we can use Packer’s image search functionality - source_ami_filter. To get the latest EC2 AMI with name = ubuntu-pro-server*20.04-amd64* and owner = Canonical (where * is a wildcard), use:

"source_ami_filter": {
   "filters": {
         "virtualization-type": "hvm",
         "name": "ubuntu-pro-server*20.04-amd64*",
         "root-device-type": "ebs"
      },
   "owners": ["099720109477"],
   "most_recent": true
},

If you are using the FIPS listings from AWS Marketplace, then use 679593333241 as the owner ID (AWS’ Marketplace ID). Finally, complete the builders component by adding the username that’ll be used to access the instance during provisioning:

     "ssh_username": "ubuntu"
   }
]

Define provisioners

The provisioners component is used to specify things that have to be installed and their configuration details. This specification can be done via inline shell commands, bash scripts or even using configuration tools such as Ansible, Chef and Puppet. Some of the typical use cases that can be configured are - hardening the image, configuring Active Directory login, adding specific configurations for management and compliance, installing software and copying plain files.

Irrespective of the use-cases and tools being used, there are two sets of commands that have to be included - one at the beginning and the other at the end. These are needed to ensure the smooth functioning of Ubuntu Pro.

The first set used at the beginning includes a cloud-init status --wait command. Using an “inline shell”, it will look like:

"provisioners": [
  {
    "type": "shell",
    "inline": [
      "cloud-init status --wait",
      "sudo apt-get update && sudo apt-get upgrade -y"
    ]
  },

This wait command will tell the script to wait for the completion of all initialisation processes, including the Ubuntu Pro activation. If you skip this command, you may face errors during the build process, since the Ubuntu Pro client needs to change configurations and repositories right after booting up.

The second set of commands are included at the end and are used to perform clean-up:

  {
    "type": "shell",
    "inline": [
      "sudo ua detach --assume-yes",
      "sudo rm -rf /var/log/ubuntu-advantage.log",
      "sudo cloud-init clean --machine-id"
    ]
  }
]

These commands remove information that is specific to the instance being used to build the image, such as the machine ID and the token generated by Ubuntu Pro. Doing this ensures that a generic AMI is created, and each time you spin up a new instance from this AMI, you’ll have a “fresh start” with no duplication of unique information.

The rest of your personalised scripts or provisioning tools should go in between these two sets of commands. These two sets of commands can also be included directly within your script or provisioning tool.

Note

For an Ubuntu LTS AMI (i.e. for all non-Pro versions), you can remove the cloud-init status --wait command from the first set and exclude the second set completely. These commands are not needed since they are Pro specific. However, including them will not lead to any errors and following this guide as it is will also work fine.

Build the AMI

The complete sample packer.json looks like:

{
   "variables": {
      "aws_access_key": "YOURACCESSKEY",
      "aws_secret_key": "YOURSECRETKEY"
   },
   "builders": [
      {
         "type": "amazon-ebs",
         "access_key": "{{user `aws_access_key`}}",
         "secret_key": "{{user `aws_secret_key`}}",
         "region": "us-east-1",
         "instance_type": "t2.micro",
         "ami_name": "packer-base-ubuntu-{{timestamp}}",
         "source_ami_filter": {
               "filters": {
                  "virtualization-type": "hvm",
                  "name": "ubuntu-pro-server*20.04-amd64*",
                  "root-device-type": "ebs"
               },
            "owners": ["099720109477"],
            "most_recent": true
         },
         "ssh_username": "ubuntu"
      }
   ],
   "provisioners": [
      {
         "type": "shell",
         "inline": [
            "cloud-init status --wait",
            "sudo apt-get update && sudo apt-get upgrade -y"
         ]
      },
      {
         "type": "shell",
         "scripts": ["my_script.sh"]
      },
      {
         "type": "shell",
         "inline": [
            "sudo ua detach --assume-yes",
            "sudo rm -rf /var/log/ubuntu-advantage.log",
            "sudo cloud-init clean --machine-id"
         ]
      }
   ]
}

Build the AMI by running Packer with the JSON file:

packer build packer.json

Once this process finishes, you should get the AMI ID of your newly created golden image.

Create a VM using the AMI

In the EC2 console, navigate to Launch instance > My AMIs and select the new AMI. Follow the wizard to configure options such as instance type, disk, security groups, roles, key-pair etc. Remember to use an instance type with the same architecture as that of the newly created AMI (AMD64 was used in this guide).

Log in to the instance and run:

sudo ua status --wait

The results should show that the machine is attached to a Pro subscription and has esm-apps, esm-infra and livepatch enabled.