Build an AWS AMI with Packer and Use It in Terraform
Why build images #
An AMI with your software already installed starts in seconds and is the same every time. Packer automates the build: it launches a temporary EC2 instance, runs your provisioners, creates the AMI and deletes the instance. Terraform then launches servers from it. This is immutable infrastructure: instead of changing a server, you replace it with a new image.
You need an AWS account with credentials configured (aws sts get-caller-identity must work) and the AWS CLI.
Install Packer #
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install -y packer
packer version
The template #
packer {
required_plugins {
amazon = {
version = ">= 1.3.0"
source = "github.com/hashicorp/amazon"
}
}
}
variable "region" {
type = string
default = "us-east-1"
}
locals {
timestamp = regex_replace(timestamp(), "[- TZ:]", "")
}
source "amazon-ebs" "ubuntu" {
ami_name = "web-${local.timestamp}"
instance_type = "t3.micro"
region = var.region
ssh_username = "ubuntu"
source_ami_filter {
filters = {
name = "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"] # Canonical
}
tags = {
Name = "web"
Role = "web"
}
}
build {
sources = ["source.amazon-ebs.ubuntu"]
provisioner "shell" {
inline = [
"sudo apt-get update -y",
"sudo apt-get install -y nginx",
"echo '<h1>Built with Packer</h1>' | sudo tee /var/www/html/index.html",
"sudo systemctl enable nginx",
]
}
}sourcesays how to build: a temporary EBS-backed instance from the latest official Ubuntu image.provisioner "shell"installs NGINX. For real projects use an Ansible provisioner (provisioner "ansible") and keep the playbook in Git.- The name has a timestamp because AMI names must be unique.
Build #
packer init .
packer fmt .
packer validate .
packer build web.pkr.hcl
init downloads the plugin, validate checks the template, and build takes a few minutes. The last lines show the new AMI id. The build needs permissions to create and delete instances, key pairs, security groups and AMIs (see AWS IAM).
Use the AMI in Terraform #
Look up the most recent image with a data source instead of copying the id:
data "aws_ami" "web" {
most_recent = true
owners = ["self"]
filter {
name = "name"
values = ["web-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.web.id
instance_type = "t3.micro"
tags = {
Name = "web"
}
}When you build a new image and run terraform apply, the AMI id changes and Terraform replaces the instance. Use lifecycle { create_before_destroy = true } to avoid downtime (see lifecycle meta-argument).
Clean up #
Old AMIs and their snapshots cost money. Deregister them when you no longer need them:
aws ec2 describe-images --owners self --query 'Images[].[ImageId,Name,CreationDate]' --output table
aws ec2 deregister-image --image-id ami-0123456789abcdef0
Then delete the snapshots listed under EBS. Automate this with a lifecycle policy.
Next steps #
Run the build in a pipeline: GitHub Actions with AWS OIDC shows how to authenticate without long-lived keys.