The most surprising thing about building machine images is how much of your infrastructure’s future stability and deployment speed is decided in these early, often manual, steps.
Let’s build a simple Ubuntu 22.04 image for AWS using Packer. Imagine you want a base AMI that’s ready for your application servers – SSH enabled, a few essential packages installed, and ready to go.
First, you need a Packer template. This is a JSON or HCL file that tells Packer what to build and how.
{
"builders": [
{
"type": "amazon-ebs",
"region": "us-east-1",
"source_ami_filter": {
"filters": {
"name": "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20230120",
"virtualization-type": "hvm",
"root-device-type": "ebs"
},
"most_recent": true
},
"instance_type": "t3.micro",
"ssh_username": "ubuntu",
"ami_name": "my-first-packer-image-{{timestamp}}"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"sudo apt-get update",
"sudo apt-get install -y nginx git"
]
}
]
}
This template has two main sections: builders and provisioners.
The builders section defines where and how the image will be created.
-
type: "amazon-ebs"tells Packer we’re building an Amazon Machine Image (AMI) using EBS. -
region: "us-east-1"is the AWS region. -
source_ami_filteris crucial: it finds the base AMI we’ll start from. We’re looking for the latest Ubuntu 22.04 LTS (Jammy Jellyfish) server image.most_recent: trueensures we get the newest one available. -
instance_type: "t3.micro"specifies the EC2 instance type Packer will launch temporarily to build the image. This is a small, cheap instance for building. -
ssh_username: "ubuntu"is the default username for Ubuntu AMIs. -
ami_name: "my-first-packer-image-{{timestamp}}"sets the name of the resulting AMI.{{timestamp}}is a Packer variable that injects the current time, making each build unique.
The provisioners section defines what happens after the base image is launched and before it’s turned into an AMI.
type: "shell"means we’ll run shell commands.inlineis an array of commands to execute. Here, we’re updating the package list and installing Nginx and Git.
To run this, save the JSON above as ubuntu-nginx.json. Then, from your terminal, execute:
packer init .
packer build ubuntu-nginx.json
packer init downloads the necessary plugins (like the amazon-ebs builder). packer build then orchestrates the entire process:
- Packer finds the
source_ami_filterand launches at3.microEC2 instance from the latest matching Ubuntu 22.04 AMI. - Packer waits for the instance to become SSH-accessible.
- Packer connects to the instance via SSH using the
ssh_usernameand executes the commands in theprovisionerssection. - Once the provisioners complete successfully, Packer stops the EC2 instance.
- Packer creates a new AMI from the instance’s root EBS volume.
- Packer terminates the temporary EC2 instance.
You’ll see output in your terminal showing each step. When it’s done, you’ll have a new AMI in your us-east-1 AWS account, ready to launch.
The real power comes when you go beyond simple package installs. You can use file provisioners to upload configuration files, run complex scripts, integrate with configuration management tools like Ansible or Chef, and even trigger security scans.
What most people don’t realize is how Packer handles secrets. You absolutely should not hardcode AWS access keys or other sensitive information directly in your Packer templates. Instead, Packer integrates with environment variables (like AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) or can be configured to use IAM roles when running on EC2, which is the most secure approach.
After building your first image, the next logical step is to automate the entire lifecycle, perhaps by integrating Packer into a CI/CD pipeline that triggers builds on code changes or scheduled intervals.