# Build an AWS AMI with Packer and Use It in Terraform

> Install Packer, build a custom Ubuntu AMI with a shell provisioner, tag it and look it up in Terraform to launch EC2 instances that boot ready to serve.

- Source: https://www.itwonderlab.com/packer-build-aws-ami/
- Published: 2026-09-19
- Updated: 2026-09-19
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## Why build images

An [AMI](https://www.itwonderlab.com/aws-ami/) with your software already installed starts in seconds and is the same every time. [Packer](https://www.itwonderlab.com/packer/) automates the build: it launches a temporary EC2 instance, runs your provisioners, creates the AMI and deletes the instance. [Terraform](https://www.itwonderlab.com/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](https://www.itwonderlab.com/aws-cli-cheat-sheet/).

## Install Packer

```bash
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

```hcl title="web.pkr.hcl"
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",
    ]
  }
}
```

- `source` says how to build: a temporary EBS-backed instance from the latest official Ubuntu image.
- `provisioner "shell"` installs [NGINX](https://www.itwonderlab.com/nginx/). For real projects use an [Ansible](https://www.itwonderlab.com/ansible/) provisioner (`provisioner "ansible"`) and keep the playbook in Git.
- The name has a timestamp because AMI names must be unique.

## Build

```bash
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](https://www.itwonderlab.com/aws-iam/)).

## Use the AMI in Terraform

Look up the most recent image with a [data source](https://www.itwonderlab.com/terraform-data-sources-remote-state/) instead of copying the id:

```hcl title="main.tf"
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](https://www.itwonderlab.com/terraform-lifecycle-meta-argument/)).

## Clean up

Old AMIs and their snapshots cost money. Deregister them when you no longer need them:

```bash
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](https://www.itwonderlab.com/aws-ebs/). Automate this with a lifecycle policy.

## Next steps

Run the build in a pipeline: [GitHub Actions with AWS OIDC](https://www.itwonderlab.com/terraform-github-actions-aws-oidc/) shows how to authenticate without long-lived keys.
