Home RSS icon My CV icon

Building Qemu-Kvm Images With Packer (Part I)
Building Rocky Linux 9 VM Images with Packer

Date: 11 April 2023

1 Introduction

We are going to create a RockyLinux image using Packer.
Creating your own VM image can significantly speed up the provisioning of new VMs whenever you need them.
Instead of repeatedly installing the operating system, we'll build VM images for Rocky Linux 9 and Ubuntu 22.04, with our own configuration, this will ensure consistency across our infrastructure.

Part 1 covers building a Rocky Linux 9 image with Packer, while Part 2 focuses on Ubuntu 22.04

2 Prerequisites

  • We need a server with KVM installed on it
  • ISO file for the image you want to build ( in our case, we are using RockyLinux9.1)

3 The Plan

  1. We are going to install Packer.
  2. Write a Packer template using HCL2.
  3. Write a kickstart configuration file for our RockyLinux system.
  4. Write a Basic shell script to automate basic configuration for our image.
  5. Build the final image

4 Installing Packer

Installing Packer is a straightforward process. Simply visit the official documentation, where you can find the appropriate version of Packer for your operating system.
I'm using Arch Linux, and Packer is already available in the Arch repos.

We will install qemu-ui-gtk as well, this will enable us to easily monitor Packer's installation of the operating system through a graphical user interface.

sudo pacman -S packer qemu-ui-gtk --noconfirm
packer --version
1.8.6

5 Write Packer Templates with HCL2

HashiCorp has developed its own configuration language, known as the HashiCorp Configuration Language (HCL), which is utilized in its suite of tools.
HCL is in version 2, and we are going to use it to create our own VM image template.

Each virtualization platform has it's own plugin which is called a builder in Packer's term.
For our Qemu platform, we'll need to use the QEMU builder plugin, which you can find on this page.

We will create a file named main.pkr.hcl, it will serve as our primary template.

touch main.pkr.hcl

5.1 Installing Packer's builder for QEMU

Add this to main.pkr.hcl

packer {
  required_plugins {
    qemu = {
      version = " >= 1.0.9"
      source  = "github.com/hashicorp/qemu"
    }
  }
}

And then, run this command

packer init main.pkr.hcl

This command is used to initiate our Packer template, which is basically downloading the necessary builder binaries.

5.2 Packer's main blocks

Packer templates consist of several built-in blocks, each serving as a container for configuration settings. The most important blocks that we are going to use here are the following:

Packer blocks

  1. source blocks contain configuration for builder plugins, including hardware and resource allocation settings such as type of hardware, RAM and CPU requirements. Each source block has a unique name that can be referenced in the build block.
  2. build blocks can reference one or more source blocks, each of which may have its own provisioners and post-processors blocks.
  3. provisioner blocks contain your provisionners, like shell scripts, Ansible playbook..etc. These blocks are nested inside of a build block.


Recently, a kernel panic has occurred while attempting to build a RockyLinux9.X image.

As noted in this issue, adding the -cpu host flag to the qemuargs section can resolve this issue.

Alright, add the following to your main.pkr.hcl file.

# Define QEMU source for rocky
source "qemu" "rocky" {
  vm_name                 = "rocky-base-image.qcow2"
  http_directory          = "./http"
  output_directory        = "./artifacts"
  iso_url                 = "<Put your ISO URL Here"
  iso_checksum            = "sha256:Put ISO checksum Here"
  format                  = "qcow2"
  accelerator             = "kvm"
  net_device              = "virtio-net"
  disk_interface          = "virtio"
  disk_size               = "25G"
  memory                  = 1024
  cpus                    = 2
  headless                = false
  boot_wait               = "5s"
  shutdown_command        = "echo admin | sudo -S -E shutdown -P now"
  ssh_username            = "admin"
  ssh_password            = "admin"
  ssh_timeout             = "60m"
  ssh_handshake_attempts  = 2000
  # (Bootstrapping with a Kickstart Config File)
  boot_command = [
    "<up><wait><tab><wait> net.ifnames=0 biosdevname=0 inst.text inst.ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/ks.cfg<enter><wait>"] 
  qemuargs = [
    [ "-m", "1024M" ],
    [ "-smp", "2" ],
    [ "-cpu", "host" ]
  ]
}

# Define build process
build {
  sources = ["source.qemu.rocky"]
  # (Execute shell scripts)
  provisioner "shell" {
    scripts               = ["../scripts/configs.sh"]
    expect_disconnect     = true
  }
}

In the build block, we can reference our source block by it's name source.qemu.rocky.

We also used a provisioner block calling the configs.sh shell script.

6 Kickstart file

Packer initiates its HTTP server at boot time to serve configuration files, we'll create a directory named http and store our Kickstart configuration file within. Then, we can reference the Kickstart file with the boot command.

mkdir http

Create a kickstart file with the following content, (or any content you want).

# Global settings
cdrom                              # Specify installation media type
lang en_US.UTF-8                   # Set language and character encoding
keyboard us                        # Set keyboard layout

# Network settings
network --bootproto=dhcp --device=eth0 --nameserver=10.10.0.2,10.10.0.3 --noipv6 --activate --onboot=on

# User settings
rootpw --plaintext admin           # Set root password
user --name=admin --plaintext --password admin   # Create a user account

timezone Africa/Algeria            # Set timezone
bootloader --timeout=1 --location=mbr --append="net.ifnames=0 biosdevname=0"   # Configure bootloader
text                               # Use text mode install
skipx                              # Do not configure X Window System
zerombr                            # Clear master boot record
clearpart --all --initlabel        # Clear all existing partitions
autopart --nohome --nolvm --noboot # Automatically partition disk

# System service settings
firewall --enabled                 # Enable firewall
selinux --enforcing                # Enable SELinux in enforcing mode
firstboot --disabled               # Disable Initial Setup on first boot
reboot --eject                     # Reboot system after installation
services --enabled="NetworkManager,sshd,chronyd"   # Enable specified services

# Package installation settings
%packages --ignoremissing --excludedocs
openssh-clients
sudo
vim
bash-completion
selinux-policy-devel
wget
nfs-utils
net-tools
tar
bzip2
deltarpm
rsync
dnf-utils
redhat-lsb-core
elfutils-libelf-devel
-fprintd-pam
-intltool
-iwl*-firmware
-microcode_ctl
%end

# Post installation settings
%post
#
# Sudo configuration
echo 'Defaults:admin !requiretty' > /etc/sudoers.d/admin
echo '%admin ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/admin
chmod 440 /etc/sudoers.d/admin

# SSH configuration
echo "PubkeyAcceptedKeyTypes=+ssh-rsa" >> /etc/ssh/sshd_config
/usr/bin/systemctl enable sshd

# Update all packages
/usr/bin/yum -y update

%end

7 Shell script for Provisionning

The path of the shell script is ../scripts/configs.sh

This is a basic shell script that creates a file called /etc/banner with a greeting message, enable the Banner option and restarts the sshd service to apply the new configuration.
Feel free to add any additional configuration you see fit.

#!/bin/bash
# We need to use `tee` command, because redirection ">" doesn't pass sudo privileges 
cat << EOF | sudo tee /etc/banner
    ┌────────────────────────┐
    │!! Welcome to Homelab !!│
    └────────────────────────┘
EOF
# Enable `Banner` option
sudo sed -i 's/^#Banner none/Banner \/etc\/banner/' /etc/ssh/sshd_config
# Restart sshd service
sudo systemctl restart sshd


8 Building the image

Let's first validate our template.

packer validate .
The configuration is valid.

And then build.

export PACKER_LOG=1 && packer build .
Packer building process -- output
2023/04/11 20:13:04 [INFO] Packer version: 1.8.6 [go1.20.1 linux amd64]
2023/04/11 20:13:04 Detected xdg config directory from env var: /home/zakaria/.config
2023/04/11 20:13:04 [TRACE] discovering plugins in /usr/bin
2023/04/11 20:13:04 Detected xdg config directory from env var: /home/zakaria/.config
2023/04/11 20:13:04 [TRACE] discovering plugins in /home/zakaria/.config/packer/plugins
2023/04/11 20:13:04 [DEBUG] Discovered plugin: qemu = /home/zakaria/.config/packer/plugins/github.com/hashicorp/qemu/packer-plugin-qemu_v1.0.9_x5.0_linux_amd64
2023/04/11 20:13:04 [INFO] found external [-packer-default-plugin-name-] builders from qemu plugin
2023/04/11 20:13:04 [TRACE] discovering plugins in .
2023/04/11 20:13:04 [INFO] PACKER_CONFIG env var not set; checking the default config file path
2023/04/11 20:13:04 [INFO] PACKER_CONFIG env var set; attempting to open config file: /home/zakaria/.packerconfig
2023/04/11 20:13:04 [WARN] Config file doesn't exist: /home/zakaria/.packerconfig
2023/04/11 20:13:04 Detected xdg config directory from env var: /home/zakaria/.config
2023/04/11 20:13:04 [INFO] Setting cache directory: /home/zakaria/.cache/packer
2023/04/11 20:13:04 Detected xdg config directory from env var: /home/zakaria/.config
2023/04/11 20:13:04 [TRACE] listing potential installations for "github.com/hashicorp/qemu" that match " >= 1.0.9". plugingetter.ListInstallationsOptions{FromFolders:[]string{"/usr/bin/packer", ".", "/home/zakaria/.config/packer/plugins"}, BinaryInstallationOptions:plugingetter.BinaryInstallationOptions{APIVersionMajor:"5", APIVersionMinor:"0", OS:"linux", ARCH:"amd64", Ext:"", Checksummers:[]plugingetter.Checksummer{plugingetter.Checksummer{Type:"sha256", Hash:(*sha256.digest)(0xc000a92200)}}}}
2023/04/11 20:13:04 [TRACE] Found the following "github.com/hashicorp/qemu" installations: [{/home/zakaria/.config/packer/plugins/github.com/hashicorp/qemu/packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 v1.0.9}]
2023/04/11 20:13:04 [INFO] found external [-packer-default-plugin-name-] builders from qemu plugin
2023/04/11 20:13:04 [TRACE] Starting external plugin /home/zakaria/.config/packer/plugins/github.com/hashicorp/qemu/packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 start builder -packer-default-plugin-name-
2023/04/11 20:13:04 Starting plugin: /home/zakaria/.config/packer/plugins/github.com/hashicorp/qemu/packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 []string{"/home/zakaria/.config/packer/plugins/github.com/hashicorp/qemu/packer-plugin-qemu_v1.0.9_x5.0_linux_amd64", "start", "builder", "-packer-default-plugin-name-"}
2023/04/11 20:13:04 Waiting for RPC address for: /home/zakaria/.config/packer/plugins/github.com/hashicorp/qemu/packer-plugin-qemu_v1.0.9_x5.0_linux_amd64
2023/04/11 20:13:04 packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 plugin: 2023/04/11 20:13:04 Plugin address: unix /tmp/packer-plugin309220793
2023/04/11 20:13:04 packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 plugin: 2023/04/11 20:13:04 Waiting for connection...
2023/04/11 20:13:04 Received unix RPC address for /home/zakaria/.config/packer/plugins/github.com/hashicorp/qemu/packer-plugin-qemu_v1.0.9_x5.0_linux_amd64: addr is /tmp/packer-plugin309220793
2023/04/11 20:13:04 packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 plugin: 2023/04/11 20:13:04 Serving a plugin connection...
2023/04/11 20:13:04 packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 plugin: 2023/04/11 20:13:04 [TRACE] starting builder -packer-default-plugin-name-
2023/04/11 20:13:04 packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 plugin: 2023/04/11 20:13:04 use specified accelerator: kvm
2023/04/11 20:13:04 [TRACE] Starting internal plugin packer-provisioner-shell
2023/04/11 20:13:04 Starting plugin: /usr/bin/packer []string{"/usr/bin/packer", "plugin", "packer-provisioner-shell"}
2023/04/11 20:13:04 Waiting for RPC address for: /usr/bin/packer
2023/04/11 20:13:05 packer-provisioner-shell plugin: [INFO] Packer version: 1.8.6 [go1.20.1 linux amd64]
2023/04/11 20:13:05 packer-provisioner-shell plugin: Detected xdg config directory from env var: /home/zakaria/.config
2023/04/11 20:13:05 packer-provisioner-shell plugin: [INFO] PACKER_CONFIG env var not set; checking the default config file path
2023/04/11 20:13:05 packer-provisioner-shell plugin: [INFO] PACKER_CONFIG env var set; attempting to open config file: /home/zakaria/.packerconfig
2023/04/11 20:13:05 packer-provisioner-shell plugin: [WARN] Config file doesn't exist: /home/zakaria/.packerconfig
2023/04/11 20:13:05 packer-provisioner-shell plugin: Detected xdg config directory from env var: /home/zakaria/.config
2023/04/11 20:13:05 packer-provisioner-shell plugin: [INFO] Setting cache directory: /home/zakaria/.cache/packer
2023/04/11 20:13:05 packer-provisioner-shell plugin: args: []string{"packer-provisioner-shell"}
2023/04/11 20:13:05 packer-provisioner-shell plugin: Detected xdg config directory from env var: /home/zakaria/.config
2023/04/11 20:13:05 packer-provisioner-shell plugin: Plugin address: unix /tmp/packer-plugin771583659
2023/04/11 20:13:05 packer-provisioner-shell plugin: Waiting for connection...
2023/04/11 20:13:05 Received unix RPC address for /usr/bin/packer: addr is /tmp/packer-plugin771583659
2023/04/11 20:13:05 packer-provisioner-shell plugin: Serving a plugin connection...
2023/04/11 20:13:05 Build debug mode: false
2023/04/11 20:13:05 Force build: false
2023/04/11 20:13:05 On error: 
2023/04/11 20:13:05 Waiting on builds to complete...
2023/04/11 20:13:05 Starting build run: qemu.rocky
2023/04/11 20:13:05 Running builder: 
2023/04/11 20:13:05 [INFO] (telemetry) Starting builder qemu.rocky
qemu.rocky: output will be in this color.

2023/04/11 20:13:05 packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 plugin: 2023/04/11 20:13:05 Qemu path: /usr/bin/qemu-system-x86_64, Qemu Image path: /usr/bin/qemu-img
qemu.rocky: Retrieving ISO
qemu.rocky: Trying https://download.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9.1-x86_64-minimal.iso
2023/04/11 20:13:05 packer-plugin-qemu_v1.0.9_x5.0_linux_amd64 plugin: 2023/04/11 20:13:05 Acquiring lock for: https://download.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9.1-x86_64-minimal.iso?checksum=sha256%3A750c373c3206ae79784e436cc94fffc122296cf1bf8129a427dcd6ba7fac5888 (/home/zakaria/.cache/packer/8f4d630bc056b35e6243168c126713b9dad68ffd.iso.lock)
==> qemu.rocky: Trying https://download.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9.1-x86_64-minimal.iso?checksum=sha256%3A750c373c3206ae79784e436cc94fffc122296cf1bf8129a427dcd6ba7fac5888
qemu.rocky: Rocky-9.1-x86_64-minimal.iso 412.40 KiB / 1.48 GiB [>-----------------------------------------------------------------------------------------------------------------------]   0.03% 5h25m58s
...
...

After the ISO file is downloded, an interface will pop up from which you can follow the automated installation.

Congratulations, you have built your first image using Packer!

Creative Commons License

Copyright © 2023 Zakaria Kebairia
Content licensed CC-BY-SA 4.0 unless otherwise noted.