How to create a custom AlmaLinux cloud image
Using cloud images in your virtualization environment can make spinning up a new VM easier and quicker than having to install it from scratch every time you want a new VM. Cloud Images are often used by cloud platform providers and they come with a tool called cloud-init pre-installed.
cloud-init allows you to inject settings, like SSH keys, network setting or even install packages on the first start up of the new VM. Which is quite useful when you for example mange your infrastructure with terraform/openTofu like I do. For this purpose I use cloud images of AlmaLinux as my preferred flavor of Linux. AlmaLinux itself offers prepared cloud ready images but they are not updated frequently and so after a while you spend the first couple minutes always updating and rebooting your new VM since it's base image is a couple weeks or month old.
But AlmaLinux also offers on their github an environment to build your own images and even customize them.
If you are only interested in having an always up-to-date image you could just clone the Repo as is and build it once a week or once a month and then update your VM template and be happy. But since I use XCP-ng as my virtualization platform and use XenOrchestra as orchestration tool I wanted to pre-install the Xen guest tools so I don't have to do it afterwards on each VM in an extra step despite I am doing it via Ansible anyways.
Preparing your build environment
Since I am using a Fedora system to build the image for this I am going to post the steps I do on this very system. For your environment you may need to use some different sources for the tools used.
First we clone the repository:
git clone https://github.com/AlmaLinux/cloud-images.git
Then we need to install packer from hashicorp, for Fedora or other RHEL based systems we can do
sudo dnf config-manager addrepo --from-repofile=https://rpm.releases.hashicorp.com/fedora/hashicorp.repo
sudo dnf -y install packer
Now we go into our project directory and prepare the Python virtual environment.
For that we first create a virtual environment in a hidden directory in out project called .venv
.
Then we activate this environment. The .
at the start is the POSIX variant of the source
command and will execute the commands in .venv/bin/activate
to activate our virtual environment.
Finally we install all the requirements to build and pre-configure the image.
cd cloud-images
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
Packer can also be used in docker here you might want to refer to their official documentation.
But that's basically it and we are good to go. Now you just could change into the directory created when cloning the project and execute packer with following command
packer build -only=qemu.almalinux-9-gencloud-x86_64 .
And packer will build a cloud ready AlmaLinux Image with latest patches installed for x86_64 architecture. You can find the image in the output directory
❯ ls -lh output-almalinux-9-gencloud-x86_64
total 515M
-rw-r--r--. 1 bufanda bufanda 518M Apr 16 14:19 AlmaLinux-9-GenericCloud-9.5-20250416.x86_64.qcow2
-rw-r-----. 1 bufanda bufanda 128K Apr 16 14:18 efivars.fd
Customizing the image
To customize the image we start by copying some files.
cp almalinux-9-gencloud.pkr.hcl almalinux-9-xcp-ng.pkr.hcl
cp ansible/gencloud.yml ansible/xcp-ng.yml
cp -r ansible/roles/gencloud_guest/ ansible/roles/xen_guest/
Now that we have our base to do some modifications let's prepare this image to run on XCP-ng and have the xe-guest-utilities-latest
installed so we get metrics in XenOrchestra or can send a reboot or stop command to shutdown the VM safely.
Now open the file almalinux-9-xcp-ng.pkr.hcl
in your favorite editor and at least replace the string almalinux-9-gencloud
with almalinux-9-xcp-ng
. So we have new objects we can reference in pacer with. Then look for the string playbook_file
and replace the generic cloud playbook with our own playbook we copied earlier.
Any other changes that may still refer to the generic cloud image can be change if you want to but the object name is the bare minimum we have to do here at the moment.
Then we will adapt the play book ansible/xcp-ng.yml
and for the time being all we need to here is to replace the gencloud_guest role with the xen_guest role we also copied earlier.
Now we will adapt the xen_guest ansible role to install the xe-guest-utilies-latest
package. So we edit ansible/roles/xen_guest/tasks/main.yml
. Here we are looking for the Task Install additional packages
and the following task before it
...
# Optimizations
- name: Install epel repository for guest utilites support
ansible.builtin.dnf:
name:
- epel-release
state: present
- name: Install additional packages
...
In the Install additional packages
task itself we replace the qemu-guest-agent
with the xe-guest-utilities-latest
package and that's it. Now we are ready to build our own customized cloud ready image for XCP-ng.
Now we run packer to build our image as follows
packer build -only=qemu.almalinux-9-xcp-ng-x86_64 .
And depending on the host machine 5 minutes or so later we have a qcow2 image waiting to be imported into XCP-ng. How we do this and how we replace an image on an already existing template I will go over in another blog post.