DEV Community

Ana Villar
Ana Villar

Posted on

Building an OpenShift 4.18 Cluster from Scratch: Part 2 – Generating Ignition Configs & VM Prep

Note: This is Part 2 of a multi-part series.

In Part 1, we built the Utilities Server—DNS, DHCP, TFTP, HTTP, and HAProxy. Now it's time to prepare the actual OpenShift deployment artifacts.

This part is all about Ignition: what it is, how to generate it, and how to feed it to our VMs so they can boot into Red Hat CoreOS and join the cluster automatically.

We'll also cover a critical—and poorly documented—step: disabling Secure Boot on libvirt VMs to allow iPXE chainloading. Without this, your nodes will simply refuse to boot.

Note that all the tasks mentioned in this blog are performed in the utilities server.

Let's get into it.

1. Download OpenShift RHCOS images

To deploy OpenShift, we need to download the Red Hat CoreOS (formerly Red Hat Enterprise Linux CoreOS) images. It's the immutable, container-optimized operating system that OpenShift uses on all cluster nodes—bootstrap, masters, and workers alike.

RHCOS images (kernel, initramfs, rootfs) are the three core artifacts needed to network-boot RHCOS: the kernel is the Linux kernel that initializes the hardware, the initramfs is a minimal in-memory filesystem that loads early in the boot process and handles loading drivers and fetching the rootfs, and the rootfs is the full RHCOS filesystem that gets loaded into RAM and serves as the base operating system before Ignition takes over.

Those files can be found in this OpenShift mirror site.

At the time I deployed the cluster, I checked what was the latest available 4.18 version, which happened to be 4.18.27. That is the reason you are going to see some folders called "oc41827". But you can name them as you prefer, but ensure you review the commands and configuration files accordantly.

First, let's create the folders to hold those files.

$ sudo mkdir -p /var/www/html/oc41827/{ignition,rhcos}
Enter fullscreen mode Exit fullscreen mode

Define some variables to ease the download process.

$ ocp_maj=4.18;rhcos_ver=4.18.27
$ mirror=https://mirror.openshift.com/pub/openshift-v4
$ baseurl=${mirror}/dependencies/rhcos/${ocp_maj}/${rhcos_ver}
$ cd /var/www/html/oc1827/rhcos/
Enter fullscreen mode Exit fullscreen mode

And download the files

$ sudo wget ${baseurl}/rhcos-${rhcos_ver}-x86_64-live-rootfs.x86_64.img
$ sudo wget ${baseurl}/rhcos-${rhcos_ver}-x86_64-live-kernel-x86_64
$ sudo wget ${baseurl}/rhcos-${rhcos_ver}-x86_64-live-initramfs.x86_64.img
Enter fullscreen mode Exit fullscreen mode

Note that on version 4.20, the kernel file name is rhcos-4.20.0-x86_64-live-kernel.x86_64, while on 4.18 is rhcos-4.18.27-x86_64-live-kernel-x86_64. Note the difference between .x86 and -x86.

2. Downloading the OpenShift Tools

We need the tools to generate the cluster configuration, which are the OpenShift Installer & OpenShift Client.

They can be found in the OpenShift mirror site. At the time I was deploying this cluster, the latest 4.18 version available was 4.18.37.

Let's download them.

$ sudo su
# mirror="https://mirror.openshift.com/pub/openshift-v4/clients"
# wget ${mirror}/ocp/4.18.37/openshift-client-linux-4.18.37.tar.gz
# wget ${mirror}/ocp/4.18.37/openshift-install-linux-4.18.37.tar.gz
Enter fullscreen mode Exit fullscreen mode

Extract both, move binaries to /usr/bin/ and ensure we have some helping aliases

# tar -xvf openshift-client-linux-4.18.37.tar.gz -C /usr/bin/
# tar -xvf openshift-install-linux-4.18.37.tar.gz -C /usr/bin/
# oc completion bash > /etc/bash_completion.d/openshift
# openshift-install completion bash > /etc/bash_completion.d/openshift-install
# exit
$ source /etc/bash_completion.d/openshift
$ source /etc/bash_completion.d/openshift-install
Enter fullscreen mode Exit fullscreen mode

And now you can test them. You should see something similar to the following lines

$ oc version
Client Version: 4.18.37
Kustomize Version: v5.4.2
$ openshift-install version
openshift-install 4.18.37
built from commit 488926dc2c95d96460ee9939929a76ed23e1c596
release image quay.io/openshift-release-dev/ocp-release@sha256:9b7068aa6f6087c2f0a7cefa241c5dbb0ede0efaad783607dff0da98cac432d2
release architecture amd64
$
Enter fullscreen mode Exit fullscreen mode

3. Generating installation configuration file

In order to deploy the OpenShift nodes, we need to generate the Ignition files. Those are JSON-based configuration specifications that tell RHCOS exactly how to configure itself on first boot—partitioning disks, writing network configs, embedding the pull secret, and registering the node with the OpenShift cluster. Think of them as the "first-run setup script" that transforms a blank RHCOS machine into a functioning OpenShift node.

To generate those files, we can runs the OpenShift installer, which asks for the necessary cluster information and then creates the installation configuration file install-config.yaml accordingly.

Instead, I have a install-config.yaml template I used to create the Kubernetes manifests and the ignition configuration files for the bootstrap node bootstrap.ign, control plane nodes master.ign, and compute nodes worker.ign.

install-config.yaml

Note the importance of the correct contents of this file, as it defines the OpenShift cluster.

apiVersion: v1
baseDomain: internal.local
compute:
- hyperthreading: Enabled
  name: worker
  replicas: 3
controlPlane:
  hyperthreading: Enabled
  name: master
  replicas: 3
metadata:
  name: oc41827
networking:
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  networkType: OVNKubernetes
  serviceNetwork:
  - 172.30.0.0/16
platform:
  none: {}
fips: false
pullSecret: 'CHANGEME'
sshKey: 'CHANGEMETOO'
Enter fullscreen mode Exit fullscreen mode
What It Defines Why It Matters
Cluster Identity (metadata.name, baseDomain) Determines all DNS names (api.., *.apps..)
Networking (clusterNetwork, serviceNetwork) Ensures pods, services, and node IPs don't conflict with each other or existing infrastructure
Topology (node replicas, control plane size) Sets the minimum resources needed for high availability (3 masters, at least 1 worker)
Authentication (pullSecret, sshKey) Allows nodes to pull container images from Red Hat registries and enables emergency SSH access
Platform (platform: none: {}) Tells the installer we're doing bare metal/external provisioning, not cloud integration

As you can see, there are two pieces of information are essential before we can use install-config.yaml that we need to change.

Pull Secret

Log into the OpenShift console using this link, or navigate to Red Hat OpenShift, under Get started with OpenShift select Create Cluster, Select Datacenter, then Bare Metal (x86_64), and next select Full Control.

Under Pull secret, press Download the pull secret. This file authenticates your cluster with Red Hat's registries.

Save it and convert it into a one-liner json file. Create a working directory from where the OpenShift deployment will be run, for example:

$ mkdir oc41827upi
Enter fullscreen mode Exit fullscreen mode

and then

$ python3 -m json.tool pull-secret > pull-secret.json
$ cat pull-secret.json | jq . -c > ~/oc41827upi/pull-secret-online.json
Enter fullscreen mode Exit fullscreen mode

SSH key

You'll need an SSH key pair to access the nodes from the utilities server.

$ ssh-keygen -t rsa -b 4096 -N '' -f .ssh/oc41827upi
Enter fullscreen mode Exit fullscreen mode

Make note of the public key path—we'll reference it in the install config.

Update install-config.yaml

Now it is time to get all the information together:

cat <<EOF > ~/oc41827upi/install-config.yaml
apiVersion: v1
baseDomain: internal.local
compute:
- hyperthreading: Enabled
  name: worker
  replicas: 3
controlPlane:
  hyperthreading: Enabled
  name: master
  replicas: 3
metadata:
  name: oc41827
networking:
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  networkType: OVNKubernetes
  serviceNetwork:
  - 172.30.0.0/16
platform:
  none: {}
fips: false
pullSecret: '$(cat ~/oc41827upi/pull-secret.json)'
sshKey: '$(cat ~/.ssh/oc41827upi.pub)'
EOF
Enter fullscreen mode Exit fullscreen mode

⚠️ Important: The metadata.name (oc41827) becomes part of your cluster's FQDN. Make sure this matches the DNS records we created in Part 1 (api.oc41827.internal.local).

🔒 Sensitive Data: The pullSecret and sshKey fields are populated at generation time. Once you run openshift-install create manifests, these values are embedded in the Ignition files. Do not commit install-config.yaml to a public repository without redacting these fields.

Key Fields Explained

Field Value Why
baseDomain internal.local Matches the DNS zone from Part 1
metadata.name oc41827 Cluster identifier, part of the FQDN
platform none: {} Platform-agnostic (bare metal / VM)
networkType OVNKubernetes Default SDN for 4.18
replicas (master) 3 Minimum for HA control plane
replicas (worker) 3 Adjust as needed for your lab, minimum 1

4. Generating Manifests & Ignition Files

Now we transform the blueprint into machine-readable configuration. This is a one-way operation: once you generate manifests, you can make targeted edits, but after Ignition files are created, the config is frozen.

Step 1: Create Manifests

$ cd ~/oc41827upi
$ openshift-install create manifests --dir=.
Enter fullscreen mode Exit fullscreen mode

This creates two directories:

  • manifests/ — YAML definitions for cluster resources
  • openshift/ — Internal OpenShift configuration files

💡 Optional: At this stage, you can modify files in manifests/ to customize the cluster before Ignition generation. Common modifications include adjusting default node settings or removing the infrastructure components you don't need. For this lab, we'll proceed with defaults.

Step 2: Generate Ignition Files

$ openshift-install create ignition-configs --dir=.
Enter fullscreen mode Exit fullscreen mode

This produces three critical files:

File Target
bootstrap.ign The temporary bootstrap node
master.ign Each control plane node
worker.ign Each compute node

These Ignition files contain the complete initial configuration for each node type: disk partitioning, systemd units, network settings, and the embedded pull secret. They are the single source of truth that tells RHCOS how to become an OpenShift node.

5. Serving Ignition Files via Apache

Copy the Ignition files to the web server

sudo cp ~/oc41827upi/bootstrap.ign /var/www/html/oc41827/ignition/
sudo cp ~/oc41827upi/master.ign /var/www/html/oc41827/ignition/
sudo cp ~/oc41827upi/worker.ign /var/www/html/oc41827/ignition/
Enter fullscreen mode Exit fullscreen mode

Set permissions

$ sudo chmod +r /var/www/html/oc41827/ignition/*
Enter fullscreen mode Exit fullscreen mode

Verify they are accessible:

$ curl -I http://192.168.110.20/oc41827/ignition/bootstrap.ign
Enter fullscreen mode Exit fullscreen mode

Expected: HTTP/1.1 200 OK

6. Creating the boot.ipxe Menu

Now we create the iPXE boot script to include OpenShift entries. Each node type points to the same kernel and rootfs but fetches a different Ignition file via the coreos.inst.ignition_url kernel argument.

Here's the structure for the menu entries in your boot.ipxe (/var/www/html/boot.ipxe):

#!ipxe
dhcp

set base http://192.168.110.20/
set oc41827 http://192.168.110.20/oc41827/

:bootstrap
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/bootstrap.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:master
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/master.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:worker
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/worker.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot
Enter fullscreen mode Exit fullscreen mode

⚠️ Disk Device Name: coreos.inst.install_dev=vda assumes your VMs use virtio disks. If your VMs use SATA or SCSI, change this to sda accordingly.

7. Testing the environment

Before we continue, I would recommend to create a test virtual machine and to check if it can be installed using iPXE. This step can be bypassed, but it can give us the confidence that all is ready for the OpenShift deployment.

I am adding here two examples, RHEL and Ubuntu

RHEL 10

Previously, I copied the RHEL 10 iso image in the utilities server in a folder called /isos. Let's mount it.

$ ls /isos
rhel-10.1-x86_64-dvd.iso
$ sudo mkdir -v /mnt/dvd
mkdir: created directory '/mnt/dvd'
$ sudo mount -v /isos/rhel-10.1-x86_64-dvd.iso /mnt/dvd
mount: /mnt/dvd: WARNING: source write-protected, mounted read-only.
mount: /dev/loop0 mounted on /mnt/dvd.
$
Enter fullscreen mode Exit fullscreen mode

Let´s copy now the contents of the iso image to our Apache server folder.

$ sudo mkdir /var/www/html/rhel10
$ sudo cp -R /mnt/dvd/* /var/www/html/rhel10/
$ sudo restorecon -Rv /var/www/html
$
Enter fullscreen mode Exit fullscreen mode

Ubuntu 24.04 and 26.04

Similarly as with RHEL, I copied the isos to the server in the /isos folder.

Place their contents into the web server:

$ sudo mkdir -p /var/www/html/ubuntuserver/24.04
$ sudo mkdir -p /var/www/html/ubuntuserver/26.04
$ sudo mount -o loop /isos/ubuntu-24.04.4-live-server-amd64.iso /mnt/dvd
mount: /mnt/dvd: WARNING: source write-protected, mounted read-only.
$ sudo cp -a /mnt/dvd/. /var/www/html/ubuntuserver/24.04/
$ sudo umount /mnt/dvd
$ sudo mount -o loop /isos/ubuntu-26.04-live-server-amd64.iso /mnt/dvd
mount: /mnt/dvd: WARNING: source write-protected, mounted read-only.
$ sudo cp -a /mnt/dvd/. /var/www/html/ubuntuserver/26.04/
$ sudo umount /mnt/dvd
Enter fullscreen mode Exit fullscreen mode

Correct SELinux:

$ sudo restorecon -Rv /var/www/html
Enter fullscreen mode Exit fullscreen mode

Update the iPXE boot file

This is the full contents of the ipxe boot file.

/var/www/html/boot.ipxe

#!ipxe
dhcp

set base http://192.168.110.20/
set rhel10 http://192.168.110.20/rhel10/
set ubuntuserver2404 http://192.168.110.20/ubuntuserver/24.04/
set ubuntuserver2604 http://192.168.110.20/ubuntuserver/26.04/
set oc41827 http://192.168.110.20/oc41827/

menu iPXE menu
item redhat    Install RHEL 10 interactively
item ubuntuserver2404  Install Ubuntu Server 24.04.4 LTS interactively
item ubuntuserver2604  Install Ubuntu Server 26.04 LTS interactively
item bootstrap  Install OpenShift 4.18.27 Bootstrap
item master  Install OpenShift 4.18.27 Master Node
item worker  Install OpenShift 4.18.27 Worker Node
item shell      Drop to iPXE shell
choose --default shell --timeout 1500000 target && goto ${target}

:redhat
kernel ${rhel10}images/pxeboot/vmlinuz ip=dhcp inst.repo=${rhel10} inst.stage2=${rhel10} quiet
initrd ${rhel10}images/pxeboot/initrd.img
boot

:ubuntuserver2404
kernel ${ubuntuserver2404}casper/vmlinuz ip=dhcp url=${ubuntuserver2404}
initrd ${ubuntuserver2404}casper/initrd
boot

:ubuntuserver2604
kernel ${ubuntuserver2604}casper/vmlinuz ip=dhcp url=${ubuntuserver2604}
initrd ${ubuntuserver2604}casper/initrd
boot

:bootstrap
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/bootstrap.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:master
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/master.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:worker
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/worker.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot

:shell
shell
Enter fullscreen mode Exit fullscreen mode

8. Disabling Secure Boot on VMs

This is the step that cost me several hours of debugging. If your libvirt VMs have Secure Boot enabled, iPXE will fail to chainload. The UEFI firmware rejects the unsigned iPXE binary, and your VMs won't boot.

Those steps are to be done in your hypervisor, where all the virtual machines are being hosted (the utilities server, your test virtual machine to boot from ipxe, the OpenShift nodes).

If you are using a different hypervisor, check their documentation to disable Secure Boot. Here I am explaining how I managed to do it for RHEL virtualization.

Dump the VM definition as XML

Replace test by the name of your virtual machine.

$ sudo virsh dumpxml test > test.xml
Enter fullscreen mode Exit fullscreen mode

Edit the VM definition

Using your preferred editor (for example vim or nano), edit the file test.xml.

Find the section and change:

  <vcpu placement='static'>2</vcpu>
  <os firmware='efi'>
    <type arch='x86_64' machine='pc-q35-rhel10.0.0'>hvm</type>
    <firmware>
      <feature enabled='yes' name='enrolled-keys'/>
      <feature enabled='yes' name='secure-boot'/>
    </firmware>
    <loader readonly='yes' secure='yes' type='pflash' format='raw'>/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd</loader>
    <nvram template='/usr/share/edk2/ovmf/OVMF_VARS.secboot.fd' templateFormat='raw' format='raw'>/var/lib/libvirt/qemu/nvram/test_VARS.fd</nvram>
  </os>
  <features>
    <acpi/>
    <apic/>
    <smm state='on'/>
  </features>
Enter fullscreen mode Exit fullscreen mode

to:

  <vcpu placement='static'>2</vcpu>
  <os>
    <type arch='x86_64' machine='pc-q35-rhel10.0.0'>hvm</type>
    <loader readonly='yes' type='pflash' format='raw'>/usr/share/edk2/ovmf/OVMF_CODE.fd</loader>
    <nvram template='/usr/share/edk2/ovmf/OVMF_VARS.fd' templateFormat='raw' format='raw'>/var/lib/libvirt/qemu/nvram/test_VARS.fd</nvram>
  </os>
  <features>
    <acpi/>
    <apic/>
    <smm state='off'/>
  </features>
Enter fullscreen mode Exit fullscreen mode

🔑 Key Changes:

  • Replace <os firmware='efi'> by <os>
  • Remove the full section starting with <firmware> to </firmware>
  • At the line starting with <loader, remove secure='yes' and replace OVMF_CODE.secboot.fd by OVMF_CODE.fd
  • At the line starting with <nvram, replace OVMF_VARS.secboot.fd by OVMF_VARS.fd
  • And replace the line <smm state='on'/> by <smm state='off'/>

Update the VM definition

Update the configuration of the virtual machine in your host:

$ sudo virsh define test.xml
Enter fullscreen mode Exit fullscreen mode

9. Verification

After disabling Secure Boot on the VM, power on the VM and connect to its console:

$ sudo virsh start test --console
Enter fullscreen mode Exit fullscreen mode

or using your Cockpit interface.

You should now see the iPXE menu appear. If it loads the boot.ipxe script correctly, and you can install the OS into your test VM, you're ready for deployment.

Part 2 Recap

Let's review what we accomplished:

✅ Downloaded OpenShift installer and CLI tools
✅ Secured the pull secret and SSH key
✅ Wrote install-config.yaml matching our DNS/DHCP from Part 1
✅ Generated manifests and Ignition files
✅ Published Ignition files and RHCOS images via Apache
✅ Updated boot.ipxe with OpenShift boot entries
✅ Disabled Secure Boot on all VMs to enable iPXE chainloading

Everything is staged. The Ignition files are on the web server, the iPXE menu is updated, and the VMs can boot.

🔜 Coming Up in Part 3: The Deployment Lifecycle

  • Powering on the bootstrap node and monitoring logs
  • Waiting for bootstrap-complete and removing the bootstrap node
  • Approving CSRs for master and worker nodes
  • Verifying cluster health with oc commands

Stay tuned!

Top comments (0)