Building Custom Ubuntu AMIs with Packer and Ansible Link to heading

In today’s cloud-native world, having consistent and automated infrastructure deployment is crucial. One of the key components in this process is creating custom Amazon Machine Images (AMIs) that are pre-configured with all necessary software and settings. In this post, we’ll explore how to build custom Ubuntu AMIs using Packer and Ansible, two powerful tools in the DevOps ecosystem.

Why Custom AMIs? Link to heading

Custom AMIs offer several advantages:

  • Consistent environment across deployments
  • Faster instance launch times
  • Pre-installed software and configurations
  • Security hardening out of the box
  • Reduced configuration drift

Project Overview Link to heading

Our project builds an AWS AMI using Packer and configures it with Ansible. The resulting AMI is based on Ubuntu 22.04 LTS and includes:

  • System updates and security patches
  • User management (creates an jenkins user with sudo access)
  • Common system packages and security tools

Prerequisites Link to heading

Before we begin, ensure you have:

  • AWS account with appropriate permissions
  • Packer installed
  • Ansible installed
  • AWS CLI configured with credentials
  • SSH public key at ~/.ssh/id_rsa.pub

Project Structure Link to heading

packer-ansible/
├── ubuntu-ami.pkr.hcl      # Packer template
├── ansible/
│   ├── playbook.yml        # Main Ansible playbook
│   └── roles/
│       ├── system_updates/     # System update role
│       │   └── tasks/
│       │       └── main.yml
│       ├── user_management/    # User management role
│       │   └── tasks/
│       │       └── main.yml
│       └── package_installation/ # Package installation role
│           └── tasks/
│               └── main.yml

Packer Configuration Link to heading

The Packer template (ubuntu-ami.pkr.hcl) defines our build process:

packer {
  required_plugins {
    amazon = {
      version = ">= 1.2.8"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "source_ami" {
  type    = string
  default = "ami-0c7217cdde317cfec" # Ubuntu 22.04 LTS
}

source "amazon-ebs" "ubuntu" {
  ami_name      = "ubuntu-22-04-ansible-{{timestamp}}"
  instance_type = "t2.micro"
  region        = var.aws_region
  source_ami    = var.source_ami
  ssh_username  = "ubuntu"

  tags = {
    Name = "Ubuntu 22.04 with Ansible"
  }
}

build {
  sources = ["source.amazon-ebs.ubuntu"]

  provisioner "ansible" {
    playbook_file = "./ansible/playbook.yml"
    extra_arguments = [
      "--extra-vars", "ansible_python_interpreter=/usr/bin/python3"
    ]
  }
}

Ansible Configuration Link to heading

The Ansible playbook (playbook.yml) orchestrates the configuration:

---
- name: Configure Ubuntu 22.04 AMI
  hosts: all
  become: true
  roles:
    - system_updates
    - user_management
    - package_installation

The playbook uses 3 simple roles:

  1. system_updates: Handles system updates and security patches
  2. user_management: Creates and configures jenkins users
  3. package_installation: Installs common system packages

Let’s look at the main tasks for each role:

# system_updates/tasks/main.yml
---
- name: Update apt package index
  apt:
    update_cache: yes
    cache_valid_time: 3600

- name: Upgrade all packages
  apt:
    upgrade: dist
    force_apt_get: yes

- name: Install security updates
  apt:
    upgrade: yes
    update_cache: yes
    cache_valid_time: 3600

# user_management/tasks/main.yml
---
- name: Create jenkins user
  user:
    name: jenkins
    groups: sudo
    shell: /bin/bash
    state: present
    create_home: yes

- name: Set up authorized key for jenkins user
  authorized_key:
    user: jenkins
    state: present
    key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

- name: Allow sudo without password
  lineinfile:
    path: /etc/sudoers
    line: "jenkins ALL=(ALL) NOPASSWD:ALL"
    validate: 'visudo -cf %s'

# package_installation/tasks/main.yml
---
- name: Install common system packages
  apt:
    name:
      - htop
      - vim
      - curl
      - wget
      - git
      - unzip
      - python3-pip
    state: present
    update_cache: yes

Building the AMI Link to heading

To build the AMI:

packer build ubuntu-ami.pkr.hcl
amazon-ebs.ubuntu: output will be in this color.

==> amazon-ebs.ubuntu: Prevalidating any provided VPC information
==> amazon-ebs.ubuntu: Prevalidating AMI Name: ubuntu-22-04-ansible-1744517631
    amazon-ebs.ubuntu: Found Image ID: ami-0c7217cdde317cfec
==> amazon-ebs.ubuntu: Creating temporary keypair: packer_67fb39ff-3511-3eef-3ff1-297fb3c3b3c9
==> amazon-ebs.ubuntu: Creating temporary security group for this instance: packer_67fb3a01-ac73-b9e3-c24f-12ac4428be83
==> amazon-ebs.ubuntu: Authorizing access to port 22 from [0.0.0.0/0] in the temporary security groups...
==> amazon-ebs.ubuntu: Launching a source AWS instance...
    amazon-ebs.ubuntu: Instance ID: i-082d0913508638938
==> amazon-ebs.ubuntu: Waiting for instance (i-082d0913508638938) to become ready...
==> amazon-ebs.ubuntu: Using SSH communicator to connect: 3.87.68.167
==> amazon-ebs.ubuntu: Waiting for SSH to become available...
==> amazon-ebs.ubuntu: Connected to SSH!
==> amazon-ebs.ubuntu: Provisioning with Ansible...
    amazon-ebs.ubuntu: Setting up proxy adapter for Ansible....
==> amazon-ebs.ubuntu: Executing Ansible: ansible-playbook -e packer_build_name="ubuntu" -e packer_builder_type=amazon-ebs --ssh-extra-args '-o IdentitiesOnly=yes' --extra-vars ansible_python_interpreter=/usr/bin/python3 -e ansible_ssh_private_key_file=/var/folders/gs/pzy2ccf56fz14fkhrcmjhph00000gp/T/ansible-key705286474 -i /var/folders/gs/pzy2ccf56fz14fkhrcmjhph00000gp/T/packer-provisioner-ansible4253597519 /Users/adamvu/stem-cd-integ-bot/packer-ansible/ansible/playbook.yml
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: PLAY [Configure Ubuntu 22.04 AMI] **********************************************
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: TASK [Gathering Facts] *********************************************************
    amazon-ebs.ubuntu: ok: [default]
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: TASK [system_updates : Update apt package index] *******************************
    amazon-ebs.ubuntu: changed: [default]
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: TASK [system_updates : Upgrade all packages] ***********************************
    amazon-ebs.ubuntu: changed: [default]
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: TASK [system_updates : Install security updates] *******************************
    amazon-ebs.ubuntu: ok: [default]
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: TASK [user_management : Create jenkins user] *************************************
    amazon-ebs.ubuntu: changed: [default]
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: TASK [user_management : Set up authorized key for jenkins user] ******************
    amazon-ebs.ubuntu: changed: [default]
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: TASK [user_management : Allow sudo without password] ***************************
    amazon-ebs.ubuntu: changed: [default]
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: TASK [package_installation : Install common system packages] *******************
    amazon-ebs.ubuntu: changed: [default]
    amazon-ebs.ubuntu:
    amazon-ebs.ubuntu: PLAY RECAP *********************************************************************
    amazon-ebs.ubuntu: default                    : ok=8    changed=6    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    amazon-ebs.ubuntu:
==> amazon-ebs.ubuntu: Stopping the source instance...
    amazon-ebs.ubuntu: Stopping instance
==> amazon-ebs.ubuntu: Waiting for the instance to stop...
==> amazon-ebs.ubuntu: Creating AMI ubuntu-22-04-ansible-1744517631 from instance i-082d0913508638938
    amazon-ebs.ubuntu: AMI: ami-01ba4796a04182845
==> amazon-ebs.ubuntu: Waiting for AMI to become ready...
==> amazon-ebs.ubuntu: Skipping Enable AMI deprecation...
==> amazon-ebs.ubuntu: Adding tags to AMI (ami-01ba4796a04182845)...
==> amazon-ebs.ubuntu: Tagging snapshot: snap-081aa13668138e0b1
==> amazon-ebs.ubuntu: Creating AMI tags
    amazon-ebs.ubuntu: Adding tag: "Name": "Ubuntu 22.04 with Ansible"
==> amazon-ebs.ubuntu: Creating snapshot tags
==> amazon-ebs.ubuntu: Terminating the source AWS instance...
==> amazon-ebs.ubuntu: Cleaning up any extra volumes...
==> amazon-ebs.ubuntu: No volumes to clean up, skipping
==> amazon-ebs.ubuntu: Deleting temporary security group...
==> amazon-ebs.ubuntu: Deleting temporary keypair...
Build 'amazon-ebs.ubuntu' finished after 10 minutes 5 seconds.

==> Wait completed after 10 minutes 5 seconds

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs.ubuntu: AMIs were created:
us-east-1: ami-01ba4796a04182845

The build process:

  1. Launches a temporary EC2 instance
  2. Applies Ansible configurations
  3. Creates an AMI from the configured instance
  4. Cleans up temporary resources

Conclusion Link to heading

Building custom AMIs with Packer and Ansible provides a robust foundation for your infrastructure. This approach:

  • Ensures consistency across environments
  • Automates the build process
  • Reduces manual configuration
  • Improves security posture
  • Speeds up deployment times