Building Qemu KVM Images with Packer

Packer is a tool that enables us to create identical machine images for multiple platforms from a single source template.

We are going to build a Rocky 8 image using Packer.

Why Packer?

It’s fast. Deploying KVM guests with Packer-built images takes less time than provisioning servers using PXE boot.

We still use PXE boot to provision physical hosts (KVM hypervisors), but virtual guests are deployed using Packer images.


You need to have a server with KVM installed on it. We use our Kubernetes homelab in this article.

Install Packer

Install Packer on an existing RHEL-based KVM hypervisor:

$ sudo yum install -y yum-utils
$ sudo yum-config-manager --add-repo
$ sudo yum install -y packer

Build Rocky 8 Image with Qemu Packer Builder

We will use the Qemu Packer builder which is able to create KVM virtual machine images.

First, we need to define a builder configuration for Rocky 8. Builders are responsible for creating machines and generating images for various platforms.

Create a file rocky8.json with the following content:

  "variables": {
    "cpu": "2",
    "ram": "2048",
    "name": "rocky",
    "disk_size": "32768",
    "version": "8",
    "iso_checksum_type": "sha256",
    "iso_urls": "",
    "iso_checksum": "5a0dc65d1308e47b51a49e23f1030b5ee0f0ece3702483a8a6554382e893333c",
    "headless": "true",
    "config_file": "rocky8-packer-ks.cfg",
    "ssh_username": "root",
    "ssh_password": "packer"
  "builders": [
      "name": "{{user `name`}}{{user `version`}}",
      "type": "qemu",
      "format": "qcow2",
      "accelerator": "kvm",
      "qemu_binary": "/usr/libexec/qemu-kvm",
      "net_device": "virtio-net",
      "disk_interface": "virtio",
      "disk_cache": "none",
      "qemuargs": [
          "{{user `ram`}}M"
          "{{user `cpu`}}"
      "ssh_wait_timeout": "30m",
      "http_directory": ".",
      "ssh_username": "{{user `ssh_username`}}",
      "ssh_password": "{{user `ssh_password`}}",
      "iso_urls": "{{user `iso_urls`}}",
      "iso_checksum": "{{user `iso_checksum`}}",
      "boot_wait": "40s",
      "boot_command": [
        " net.ifnames=0 biosdevname=0 inst.text inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/http/{{user `config_file`}}"
      "disk_size": "{{user `disk_size`}}",
      "disk_discard": "unmap",
      "disk_compression": true,
      "headless": "{{user `headless`}}",
      "shutdown_command": "sudo /usr/sbin/shutdown -h now",
      "output_directory": "artifacts/qemu/{{user `name`}}{{user `version`}}"

We do not use any post-processors in this article, but we do on GitHub.

Packer can start an HTTP server to serve Kickstart files at boot time. That’s something that we want to take advantage of. Create an http directory:

$ mkdir http

Create a Rocky 8 Kickstart file ./http/rocky8-packer-ks.cfg with the following content:

# Use network installation
url --url=""
repo --name="AppStream" --baseurl=""
# Disable Initial Setup on first boot
firstboot --disable

# Use text mode install
# Keyboard layouts
keyboard --vckeymap=gb --xlayouts='gb'
# System language
lang en_GB.UTF-8
# SELinux configuration
selinux --enforcing
# Firewall configuration
firewall --enabled --ssh
# Do not configure the X Window System

# Network information
network --bootproto=dhcp --device=eth0 --nameserver=, --noipv6 --activate
network --hostname=rocky8.localdomain

# System authorisation information
auth --useshadow --passalgo=sha512
# Root password
rootpw packer
# System timezone
timezone Europe/London --isUtc

ignoredisk --only-use=vda
# System bootloader configuration
bootloader --location=mbr --timeout=1 --boot-drive=vda
# Clear the Master Boot Record
# Partition clearing information
clearpart --all --initlabel
# Reboot after installation

# Disk partitioning information
part /boot --fstype="xfs" --ondisk=vda --size=1024 --label=boot --asprimary
part pv.01 --fstype="lvmpv" --ondisk=vda --size=31743
volgroup vg_os pv.01
logvol /tmp  --fstype="xfs" --size=1024 --label="lv_tmp" --name=lv_tmp --vgname=vg_os
logvol /  --fstype="xfs" --size=30716 --label="lv_root" --name=lv_root --vgname=vg_os

# dnf group info minimal-environment
# Exclude unnecessary firmwares

%addon com_redhat_kdump --disable --reserve-mb='auto'

sed -i 's/^.*requiretty/#Defaults requiretty/' /etc/sudoers
sed -i 's/rhgb //' /etc/default/grub
# SSHD PermitRootLogin and enable the service
sed -i "s/#PermitRootLogin yes/PermitRootLogin yes/g" /etc/ssh/sshd_config
/usr/bin/systemctl enable sshd
# Update all packages
/usr/bin/yum -y update

pwpolicy root --minlen=10 --minquality=1 --notstrict --nochanges --notempty
pwpolicy user --minlen=10 --minquality=1 --notstrict --nochanges --emptyok
pwpolicy luks --minlen=10 --minquality=1 --notstrict --nochanges --notempty

Build a Rocky 8 image:

$ PACKER_LOG=1 packer build ./rocky8.json

Monitoring Build Process

Packer uses VNC, therefore we can check its window with a VNC client, e.g.:

$ vncviewer -shared

Deploy KVM Guests from Packer Images

Copy the virtual machine image that was created by Packer:

$ sudo cp --sparse=always ./artifacts/qemu/rocky8/packer-rocky8 /var/lib/libvirt/images/rocky8.qcow2

Provision a new Rocky 8 KVM guest:

$ sudo virt-install \
  --name rocky8 \
  --network bridge=br0,model=virtio,mac=C0:FF:EE:D0:5E:40 \
  --disk path=/var/lib/libvirt/images/rocky8.qcow2,size=32 \
  --ram 2048 \
  --vcpus 2 \
  --os-type linux \
  --os-variant centos7.0 \
  --sound none \
  --rng /dev/urandom \
  --virt-type kvm \
  --import \
  --wait 0


Leave a Reply

Your email address will not be published.