This document covers the four per-image-type GitHub Actions workflows that build AlmaLinux OS cloud images with Packer for classic IaaS clouds:
| Workflow | Display name | Image type | Document |
|---|---|---|---|
.github/workflows/azure-build.yml |
Azure: Build Image |
azure |
this file |
.github/workflows/gencloud-build.yml |
GenericCloud: Build Image |
gencloud, gencloud_ext4 |
this file |
.github/workflows/oci-build.yml |
OCI: Build Image |
oci |
this file |
.github/workflows/opennebula-build.yml |
OpenNebula: Build Image |
opennebula |
this file |
GCP has its own workflow and is documented in BUILD_GCP.md. Vagrant boxes (libvirt, VirtualBox, VMware, Hyper-V) are documented in BUILD_VAGRANT.md. AWS AMIs are documented in AWS_AMI_BUILD_COPY_RELEASE.md.
All four workflows share:
- The same
workflow_dispatchinput shape (date stamp,version_major,self-hosted, artifact/S3/notification toggles). - The same three-job structure:
init-data→build-gh-hosted(x86_64) →start-self-hosted-runner+build-self-hosted(aarch64). - The same composite action
.github/actions/shared-steps/action.ymlthat drives the per-variant build / test / upload / notify logic. - The same
.github/scripts/resolve-image-config.shhelper that resolves Packer source names, output filenames, and S3 paths.
| Input | Type | Default | Notes |
|---|---|---|---|
date_time_stamp |
string | auto (date -u +%Y%m%d%H%M%S) |
Shared timestamp so every matrix leg produces identically dated artifacts. |
version_major |
choice | 10 |
10-kitten, 10, 9, 8. OCI excludes 10-kitten. |
self-hosted |
boolean | true |
If false, skip the aarch64 matrix entirely. |
store_as_artifact |
boolean | false |
Upload images as GitHub Actions artifacts. |
upload_to_s3 |
boolean | true |
Upload images + checksum + package list to the configured S3 bucket. |
notify_mattermost |
boolean | true |
Post a build summary to Mattermost. |
Triggered manually from the GitHub UI: Actions → <workflow name> → Run workflow.
| Type | Workflow | Output | x86_64 variants | aarch64 variants |
|---|---|---|---|---|
azure |
azure-build.yml |
.raw (Azure VHD source) |
8, 9, 10, 10-kitten |
9, 9-64k, 10, 10-64k, 10-kitten, 10-kitten-64k |
gencloud |
gencloud-build.yml |
.qcow2 (XFS root) |
8, 9, 10, 10-v2, 10-kitten, 10-kitten-v2 |
8, 9, 10, 10-kitten |
gencloud_ext4 |
gencloud-build.yml |
.qcow2 (ext4 root) |
8, 9, 10, 10-v2, 10-kitten, 10-kitten-v2 |
8, 9, 10, 10-kitten |
oci |
oci-build.yml |
.qcow2 |
8, 9, 10 |
8, 9, 10 |
opennebula |
opennebula-build.yml |
.qcow2 |
8, 9, 10, 10-v2, 10-kitten, 10-kitten-v2 |
8, 9, 10, 10-kitten |
Variant-suffix meanings:
-v2— produces an image tagged for the x86_64_v2 microarchitecture level. x86_64 only; available for AL10 / Kitten 10.-64k— produces an aarch64 image with a 64 KiB page-size kernel. Azure only, AL9+.gencloud_ext4— builds the generic cloud image with an ext4 root filesystem instead of the default XFS. Fanned out as a secondsubtypematrix leg alongsidegencloud.
Every workflow has the same three-job shape (four for gencloud-build.yml, where the matrix includes a subtype dimension):
graph TD
A[Trigger Workflow] --> B[init-data<br/>generate date_time_stamp / date_stamp]
B --> C[build-gh-hosted<br/>x86_64 matrix]
B --> D{inputs.self-hosted}
D -->|true| E[start-self-hosted-runner<br/>a1.metal aarch64]
E --> F[build-self-hosted<br/>aarch64 matrix]
D -->|false| G[skip aarch64 jobs]
C --> H[.github/actions/shared-steps]
F --> H
H --> I[resolve-image-config.sh<br/>→ packer_source / output_mask / aws_s3_path]
I --> J[Packer build]
J --> K[Locate image + SHA-256]
K --> L[Mount via qemu-nbd + verify<br/>/etc/almalinux-release + arch]
L --> M{upload_to_s3}
M -->|Yes| N[Upload image + checksum<br/>+ package list to S3]
M -->|No| O{store_as_artifact}
O -->|Yes| P[Upload as GH Artifact]
N --> Q[Summary + Mattermost]
P --> Q
Runs on ubuntu-24.04. Generates (or passes through) time_stamp (YYYYMMDDhhmmss) and date_stamp (YYYYMMDD) outputs so every matrix leg lands in the same per-build directory.
Runs on a GitHub-hosted Ubuntu 24.04 runner, or a RunsOn metal instance (c7i.metal-24xl+c7a.metal-48xl+*8gd.metal* / image=ubuntu24-full-x64) when the repository is under the AlmaLinux org. Invokes ./.github/actions/shared-steps with:
type— workflow's image type (azure/gencloud/gencloud_ext4/oci/opennebula).variant— the per-matrix variant from the fan-out, or the rawversion_majorfor Azure/OCI (neither of which fans out on x86_64).arch: x86_64.
Runs on ubuntu-24.04. Gated by if: inputs.self-hosted. When the repository is not under the AlmaLinux org, uses NextChapterSoftware/ec2-action-builder@v1.10 to provision an a1.metal EC2 instance with ec2_instance_ttl: 30, register it as a GitHub runner, and tag it for later cleanup. For AlmaLinux-org runs this job is a no-op; RunsOn provides the aarch64 instance directly.
In azure-build.yml the start-self-hosted-runner job fans out over the same aarch64 variants (9/9-64k, 10/10-64k, 10-kitten/10-kitten-64k) so there is one ephemeral runner per variant. The other three workflows start a single runner.
Runs on:
runs-on={RUN_ID}/family=a1.metal/image=almalinux-9-aarch64for AlmaLinux-org runs.- The ephemeral EC2 runner created above (targeted by
github.run_id) otherwise.
Dispatches a matrix over the aarch64 variants, then calls ./.github/actions/shared-steps with arch: aarch64 and the appropriate type / variant / subtype.
| Secret | Description |
|---|---|
AWS_ACCESS_KEY_ID |
AWS access key for S3 uploads and EC2 runner provisioning |
AWS_SECRET_ACCESS_KEY |
AWS secret key |
GIT_HUB_TOKEN |
GitHub PAT for Packer plugins and self-hosted runner registration |
MATTERMOST_WEBHOOK_URL |
Mattermost incoming webhook URL |
EC2_AMI_ID_AL9_AARCH64 |
AMI ID for the aarch64 self-hosted EC2 runner |
EC2_SUBNET_ID |
EC2 subnet for self-hosted runners |
EC2_SECURITY_GROUP_ID |
EC2 security group for self-hosted runners |
| Variable | Description |
|---|---|
AWS_REGION |
AWS region for S3 and EC2 |
AWS_S3_BUCKET |
S3 bucket for image uploads |
MATTERMOST_CHANNEL |
Mattermost channel for notifications |
Every job requests id-token: write + contents: read. id-token is used for Azure OIDC inside the Packer Azure builder and for future Workload Identity Federation hooks; contents: read is for the actions/checkout@v6 step.
Uploads are placed under:
s3://{bucket}/images/{version_major}/{release}/{type}/{timestamp}/
Examples:
s3://almalinux-cloud/images/9/9.6/azure/20260220143000/AlmaLinux-9-Azure-9.6-20260220.x86_64.raw
s3://almalinux-cloud/images/10/10.1/oci/20260220143000/AlmaLinux-10-OCI-10.1-20260220.aarch64.qcow2
s3://almalinux-cloud/images/kitten/10/gencloud_ext4/20260220143000/AlmaLinux-Kitten-GenericCloud-ext4-10-20260220.x86_64.qcow2
All uploaded objects are tagged with public=yes.
The shared action performs a minimal post-build sanity test for every cloud image (run_test is hardcoded to 'false' for these four workflows, so no Vagrant vagrant up is attempted):
- Load the
nbdkernel module. - Attach the built image using
qemu-nbd(read-only). - Mount the root partition (partition 4 on x86_64, partition 3 on aarch64).
- Verify
/etc/almalinux-releasematches the expected release string. - Verify the
almalinux-releasepackage architecture. - Extract the installed RPM package list (uploaded next to the image).
.github/scripts/resolve-image-config.sh resolves the Packer source name from type, version, arch, and variant:
qemu.almalinux-{version}-{type}-{arch} # AL 8 / 9
qemu.almalinux_{version}_{type}_{arch} # AL 10 / Kitten 10
For Azure x86_64 the legs are named e.g. qemu.almalinux-8-azure-x86_64, qemu.almalinux_10_azure_x86_64. The -64k aarch64 Azure legs follow two different naming conventions depending on the AlmaLinux version:
| Version | 64k Packer source |
|---|---|
| AL 9 | qemu.almalinux_9_azure_64k_aarch64 (64k before arch) |
| AL 10 | qemu.almalinux_10_azure_64k_aarch64 (64k before arch) |
| Kitten 10 | qemu.almalinux_kitten_10_azure_aarch64_64k (64k after arch) |
Both conventions are handled by resolve-image-config.sh; the Kitten 10 layout is preserved for backward compatibility with the existing Packer templates.
The composite action picks different binaries depending on whether the runner is Ubuntu or RHEL-family:
| Runner OS | QEMU binary | OVMF firmware |
|---|---|---|
| Ubuntu | /usr/bin/qemu-system-{arch} |
/usr/share/OVMF/OVMF_CODE_4M.fd |
| RHEL | /usr/libexec/qemu-kvm |
(default) |
- Packer build fails immediately — confirm the template source exists for the
type/variant/archtriple (see the naming rules above). Ensure the runner has/dev/kvmaccessible and enough free disk (azureespecially wants ≥30 GiB). - KVM permission denied — the workflow configures udev rules and adds the runner user to
kvm; on a truly manual runner you must do this yourself. - Cloud image test fails — verify the
nbdmodule is loadable and that the root partition number matches your arch (4 on x86_64, 3 on aarch64). A failed/etc/almalinux-releasecheck usually means Packer reused a stale partial build — cleanoutput-*/directories on manual runners. - S3 upload fails — check that
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYhaves3:PutObject+s3:PutObjectTaggingon the bucket and thatAWS_REGIONmatches the bucket region. - Self-hosted runner never starts — verify
EC2_AMI_ID_AL9_AARCH64,EC2_SUBNET_ID, andEC2_SECURITY_GROUP_ID; the subnet's AZ must supporta1.metal. The EC2 runner has a 30-minute TTL, so a long-stalled Packer build will eventually be reaped.
- BUILD_GCP.md — GCP image build pipeline.
- BUILD_VAGRANT.md — Vagrant box build pipelines (libvirt, VirtualBox, VMware, Hyper-V).
- AWS_AMI_BUILD_COPY_RELEASE.md — AWS AMI pipeline.
- Packer documentation: https://developer.hashicorp.com/packer/docs
- AlmaLinux Cloud SIG chat: https://chat.almalinux.org/almalinux/channels/sigcloud