<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[OKD on XCP-ng]]></title><description><![CDATA[<p dir="auto">Hey All, I recently deployed OKD on XCP-ng and wanted to share the runbook that I wrote to go along with the installation incase anyone else was looking at Redhat like Yeah Nah....</p>
<h1>OKD 5 UPI Deployment Runbook on XCP-ng</h1>
<p dir="auto">User-Provisioned Infrastructure · CentOS Stream CoreOS (SCOS) · Rocky Linux Helper · DNS/DHCP</p>
<p dir="auto">This guide documents the validated step-by-step procedure to deploy OKD 5 (CentOS Stream CoreOS - SCOS) on User-Provisioned Infrastructure (UPI) using XCP-ng, a Rocky Linux Helper VM, and an external DNS/DHCP server.</p>
<h1>1. Environment Architecture &amp; IP Allocations</h1>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Role</th>
<th>Hostname</th>
<th>IP Address</th>
<th>vCPU</th>
<th>RAM</th>
<th>Disk</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>DNS / DHCP</td>
<td>services.domain.int</td>
<td>10.100.20.248</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>Shared Infrastructure</td>
</tr>
<tr>
<td>Helper / LB</td>
<td>helper.okd.domain.int</td>
<td>10.100.3.9</td>
<td>2</td>
<td>4 GB</td>
<td>30 GB</td>
<td>Rocky Linux (HAProxy + Apache)</td>
</tr>
<tr>
<td>Bootstrap</td>
<td>bootstrap.okd.domain.int</td>
<td>10.100.3.3</td>
<td>4</td>
<td>16 GB</td>
<td>60 GB</td>
<td>Temporary VM (Destroyed post-install)</td>
</tr>
<tr>
<td>Control Plane 1</td>
<td>fedora-cp-1.okd.domain.int</td>
<td>10.100.3.4</td>
<td>4</td>
<td>16 GB</td>
<td>60 GB</td>
<td>Master Node 1</td>
</tr>
<tr>
<td>Control Plane 2</td>
<td>fedora-cp-2.okd.domain.int</td>
<td>10.100.3.5</td>
<td>4</td>
<td>16 GB</td>
<td>60 GB</td>
<td>Master Node 2 (Bind MAC in DHCP)</td>
</tr>
<tr>
<td>Control Plane 3</td>
<td>fedora-cp-3.okd.domain.int</td>
<td>10.100.3.6</td>
<td>4</td>
<td>16 GB</td>
<td>60 GB</td>
<td>Master Node 3</td>
</tr>
<tr>
<td>Compute 1</td>
<td>fedora-cmp-1.okd.domain.int</td>
<td>10.100.3.7</td>
<td>2–4</td>
<td>8 GB</td>
<td>60 GB</td>
<td>Worker Node 1</td>
</tr>
<tr>
<td>Compute 2</td>
<td>fedora-cmp-2.okd.domain.int</td>
<td>10.100.3.8</td>
<td>2–4</td>
<td>8 GB</td>
<td>60 GB</td>
<td>Worker Node 2</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>Subnet:</strong> 10.100.0.0/16</p>
<p dir="auto"><strong>Base Domain:</strong> domain.int</p>
<p dir="auto"><strong>Cluster Name:</strong> okd</p>
<h2>Sizing Notes</h2>
<p dir="auto"><strong>Worker count:</strong> A 2-worker topology is suitable for non-HA workloads. Control plane nodes are set to non-schedulable in this guide, so workloads requiring 3-way quorum or strict pod anti-affinity (for example IBM MQ Native HA, OpenShift Data Foundation / Ceph, Elasticsearch, Kafka) will leave their third replica Pending. If deploying such workloads, provision a third compute node (fedora-cmp-3) and add it to the DNS, DHCP, and HAProxy ingress configuration.</p>
<p dir="auto"><strong>Worker memory:</strong> 8 GB per compute node is suitable for an idle or lightly loaded lab cluster. OKD system pods (OVN-Kubernetes, ingress routers, monitoring, image registry) consume a substantial share of that, so 16 GB per compute node is recommended if you plan to test multi-container operators or stateful applications.</p>
<h1>2. Network, DHCP, and DNS Configuration</h1>
<h2>DHCP Reservations</h2>
<p dir="auto">To avoid IP drift on a flat /16 network, map the virtual MAC addresses of all 6 cluster VMs and the Helper VM to their assigned static IPs inside your network DNS/DHCP Server before powering them on.</p>
<h2>Forward DNS Records (domain.int or okd.domain.int zone)</h2>
<p dir="auto">Add the following A records:</p>
<pre><code class="language-text">helper.okd.domain.int. A 10.100.3.9
api.okd.domain.int. A 10.100.3.9
api-int.okd.domain.int. A 10.100.3.9
*.apps.okd.domain.int. A 10.100.3.9
bootstrap.okd.domain.int. A 10.100.3.3
fedora-cp-1.okd.domain.int. A 10.100.3.4
fedora-cp-2.okd.domain.int. A 10.100.3.5
fedora-cp-3.okd.domain.int. A 10.100.3.6
fedora-cmp-1.okd.domain.int. A 10.100.3.7
fedora-cmp-2.okd.domain.int. A 10.100.3.8
</code></pre>
<h2>Reverse DNS Zone (3.100.10.in-addr.arpa)</h2>
<p dir="auto">Configure a reverse lookup zone in network DNS. Entries must map the host octet to the FQDN with a trailing dot:</p>
<pre><code class="language-text">$ORIGIN 3.100.10.in-addr.arpa.
$TTL 86400
3.100.10.in-addr.arpa. IN SOA ns.3.100.10.in-addr.arpa. mail.3.100.10.in-addr.arpa. (
    1 43200 180 1209600 10800
)
3.100.10.in-addr.arpa. IN NS ns.domain.int.
3 IN PTR bootstrap.okd.domain.int.
4 IN PTR fedora-cp-1.okd.domain.int.
5 IN PTR fedora-cp-2.okd.domain.int.
6 IN PTR fedora-cp-3.okd.domain.int.
7 IN PTR fedora-cmp-1.okd.domain.int.
8 IN PTR fedora-cmp-2.okd.domain.int.
9 IN PTR helper.okd.domain.int.
</code></pre>
<h2>NTP &amp; Clock Synchronization</h2>
<p dir="auto">CoreOS and etcd are sensitive to clock drift. Significant clock skew between control plane VMs can cause etcd to lose quorum and the installation to fail with gRPC dial timeouts. Confirm that you network DHCP server hands out a valid NTP server (DHCP Option 42) to all cluster VMs, or otherwise ensure all nodes sync to the same time source.</p>
<h1>3. Helper VM Configuration</h1>
<p dir="auto">Log into the Rocky Linux Helper VM (10.100.3.9) to configure services.</p>
<h2>Package Installation</h2>
<pre><code class="language-bash">sudo dnf install -y epel-release
sudo dnf install -y haproxy httpd bind-utils wget curl jq tar helm
</code></pre>
<h2>SELinux &amp; Firewall Rules</h2>
<pre><code class="language-bash"># Allow HAProxy to proxy arbitrary network ports
sudo setsebool -P haproxy_connect_any 1

# Open API, Ingress, Machine Config, and HTTP installation ports
sudo firewall-cmd --permanent --add-port={80,443,6443,22623,8080}/tcp
sudo firewall-cmd --reload
</code></pre>
<h2>Apache (httpd) Hosting Setup</h2>
<p dir="auto">Run Apache on port 8080 to leave ports 80 and 443 open for HAProxy ingress routing.</p>
<pre><code class="language-bash">sudo sed -i 's/^Listen 80/Listen 8080/' /etc/httpd/conf/httpd.conf
sudo mkdir -p /var/www/html/okd
sudo chown -R apache:apache /var/www/html/okd
sudo chmod -R 755 /var/www/html/okd
sudo systemctl enable --now httpd
</code></pre>
<h2>HAProxy Configuration</h2>
<p dir="auto">Replace <strong>/etc/haproxy/haproxy.cfg</strong> with:</p>
<pre><code class="language-text">global
    log 127.0.0.1 local2
    chroot /var/lib/haproxy
    pidfile /var/run/haproxy.pid
    maxconn 4000
    user haproxy
    group haproxy
    daemon
    stats socket /var/lib/haproxy/stats

defaults
    mode tcp
    log global
    option tcplog
    option dontlognull
    retries 3
    timeout queue 1m
    timeout connect 10s
    timeout client 1m
    timeout server 1m
    timeout check 10s
    maxconn 3000

# --- Kubernetes API (Port 6443) ---
frontend okd_k8s_api_fe
    bind *:6443
    default_backend okd_k8s_api_be

backend okd_k8s_api_be
    balance roundrobin
    server bootstrap 10.100.3.3:6443 check
    server fedora-cp-1 10.100.3.4:6443 check
    server fedora-cp-2 10.100.3.5:6443 check
    server fedora-cp-3 10.100.3.6:6443 check

# --- Machine Config Server (Port 22623) ---
frontend okd_machine_config_fe
    bind *:22623
    default_backend okd_machine_config_be

backend okd_machine_config_be
    balance roundrobin
    server bootstrap 10.100.3.3:22623 check
    server fedora-cp-1 10.100.3.4:22623 check
    server fedora-cp-2 10.100.3.5:22623 check
    server fedora-cp-3 10.100.3.6:22623 check

# --- Ingress Router HTTP (Port 80) ---
frontend okd_http_ingress_fe
    bind *:80
    default_backend okd_http_ingress_be

backend okd_http_ingress_be
    balance roundrobin
    server fedora-cmp-1 10.100.3.7:80 check
    server fedora-cmp-2 10.100.3.8:80 check

# --- Ingress Router HTTPS (Port 443) ---
frontend okd_https_ingress_fe
    bind *:443
    default_backend okd_https_ingress_be

backend okd_https_ingress_be
    balance roundrobin
    server fedora-cmp-1 10.100.3.7:443 check
    server fedora-cmp-2 10.100.3.8:443 check
</code></pre>
<p dir="auto">Validate and start HAProxy:</p>
<pre><code class="language-bash">sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl enable --now haproxy
</code></pre>
<h1>4. OKD Binaries &amp; Ignition Generation</h1>
<p dir="auto">Execute these commands as the admin user (<strong>&lt;admin-user&gt;</strong>) on the Helper VM.</p>
<h2>Download Release Binaries</h2>
<pre><code class="language-bash">cd ~
export OKD_VER="5.0.0-okd-scos.2"
wget https://github.com/okd-project/okd/releases/download/${OKD_VER}/openshift-client-linux-${OKD_VER}.tar.gz
wget https://github.com/okd-project/okd/releases/download/${OKD_VER}/openshift-install-linux-${OKD_VER}.tar.gz
tar -xvf openshift-client-linux-${OKD_VER}.tar.gz
tar -xvf openshift-install-linux-${OKD_VER}.tar.gz
sudo mv oc kubectl openshift-install /usr/local/bin/
</code></pre>
<h2>Retrieve the Official Live SCOS ISO</h2>
<p dir="auto">Fetch the exact installer-matched image URL:</p>
<pre><code class="language-bash">openshift-install coreos print-stream-json | jq -r '.architectures.x86_64.artifacts.metal.formats.iso.disk.location'
</code></pre>
<p dir="auto">Upload this downloaded .iso to your XCP-ng storage repository.</p>
<h2>Generate SSH Key</h2>
<pre><code class="language-bash">[ -f ~/.ssh/id_ed25519 ] || ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
</code></pre>
<h2>Create install-config.yaml</h2>
<pre><code class="language-bash">mkdir -p ~/okd-install
cd ~/okd-install
cat &lt;&lt;EOF &gt; install-config.yaml
apiVersion: v1
baseDomain: domain.int
metadata:
  name: okd
compute:
- hyperthreading: Enabled
  name: worker
  replicas: 0
controlPlane:
  hyperthreading: Enabled
  name: master
  replicas: 3
networking:
  networkType: OVNKubernetes
  clusterNetwork:
  - cidr: 10.128.0.0/14
    hostPrefix: 23
  serviceNetwork:
  - 172.30.0.0/16
platform:
  none: {}
# Note: Because OKD pulls exclusively from public community container registries, this dummy pull secret works out of the box with no modifications.
pullSecret: '{"auths":{"fake":{"auth":"aWQ6cGFzc3dvcmQ="}}}'
sshKey: '$(cat ~/.ssh/id_ed25519.pub)'
EOF

# Back up the install configuration
cp install-config.yaml install-config.yaml.bak
</code></pre>
<p dir="auto"><strong>Note:</strong> compute replicas is set to 0 because on User-Provisioned Infrastructure (platform: none) there is no cloud provider to create machines. The two worker VMs are provisioned manually and join the cluster through the CSR approvals in Section 7.</p>
<h2>Generate Manifests &amp; Ignition Files</h2>
<pre><code class="language-bash"># 1. Create manifests
openshift-install create manifests --dir=~/okd-install

# 2. Prevent general user workloads on master nodes
sed -i 's/mastersSchedulable: true/mastersSchedulable: false/g' ~/okd-install/manifests/cluster-scheduler-02-config.yml

# 3. Create Ignition configs
openshift-install create ignition-configs --dir=~/okd-install

# 4. Publish to Apache
sudo cp ~/okd-install/*.ign /var/www/html/okd/
sudo chown -R apache:apache /var/www/html/okd
sudo chmod 644 /var/www/html/okd/*.ign
sudo restorecon -Rv /var/www/html/okd/

# 5. Sanity check HTTP server
curl -s -I http://10.100.3.9:8080/okd/bootstrap.ign | head -n 1
</code></pre>
<h1>5. VM Provisioning &amp; Installation on XCP-ng</h1>
<p dir="auto">Create the 6 VMs in XCP-ng / Xen Orchestra matching the specifications from Section 1. Attach the SCOS Live ISO to each VM.</p>
<p dir="auto"><strong>Important:</strong> Power on only the bootstrap VM and the three control plane VMs at this stage. Leave the two compute VMs powered off until Section 7.</p>
<p dir="auto">The worker.ign file is only a lightweight pointer that tells each worker to fetch its real configuration from <a href="https://api-int.okd.domain.int:22623/config/worker" target="_blank" rel="noopener noreferrer nofollow ugc">https://api-int.okd.domain.int:22623/config/worker</a>. While the bootstrap node is still behind HAProxy on port 22623, it can only serve control plane configurations and returns HTTP 500 for worker requests. Booting the workers after the bootstrap node has been removed means they only reach healthy control plane nodes and pull their configuration on the first attempt.</p>
<p dir="auto">Once each VM boots into the live environment (network device <strong>enX0</strong> pulls DHCP), run <strong>coreos-installer</strong>. Verify your local virtual disk name with <strong>lsblk</strong> (<strong>/dev/xvda</strong> on XCP-ng virtual disks):</p>
<h3>On bootstrap (10.100.3.3):</h3>
<pre><code class="language-bash">sudo coreos-installer install /dev/xvda --ignition-url=http://10.100.3.9:8080/okd/bootstrap.ign --insecure-ignition
sudo reboot
</code></pre>
<h3>On fedora-cp-1, cp-2, and cp-3 (10.100.3.4–6):</h3>
<pre><code class="language-bash">sudo coreos-installer install /dev/xvda --ignition-url=http://10.100.3.9:8080/okd/master.ign --insecure-ignition
sudo reboot
</code></pre>
<p dir="auto">Make sure the VMs boot from their virtual hard drives on reboot rather than looping back into the ISO.</p>
<h1>6. Bootstrap Handoff</h1>
<h2>Monitor Bootstrap</h2>
<p dir="auto">From the Helper VM:</p>
<pre><code class="language-bash">openshift-install wait-for bootstrap-complete --dir=$HOME/okd-install --log-level=info
</code></pre>
<h2>Decommission the Bootstrap VM</h2>
<p dir="auto">When wait-for bootstrap-complete outputs:</p>
<pre><code>INFO It is now safe to remove the bootstrap resources
</code></pre>
<ol>
<li>Power off and delete the bootstrap VM in XCP-ng.</li>
<li>Remove the bootstrap entry from both backend okd_k8s_api_be (port 6443) and backend okd_machine_config_be (port 22623) in /etc/haproxy/haproxy.cfg, leaving only the control plane servers:</li>
<li>Reload HAProxy:</li>
</ol>
<pre><code class="language-text">backend okd_k8s_api_be
    balance roundrobin
    server fedora-cp-1 10.100.3.4:6443 check
    server fedora-cp-2 10.100.3.5:6443 check
    server fedora-cp-3 10.100.3.6:6443 check

backend okd_machine_config_be
    balance roundrobin
    server fedora-cp-1 10.100.3.4:22623 check
    server fedora-cp-2 10.100.3.5:22623 check
    server fedora-cp-3 10.100.3.6:22623 check
</code></pre>
<pre><code class="language-bash">sudo systemctl reload haproxy
</code></pre>
<h1>7. Worker Nodes, Approvals &amp; Cluster Finalization</h1>
<h2>Boot and Install the Worker Nodes</h2>
<p dir="auto">Now power on fedora-cmp-1 and fedora-cmp-2 with the SCOS Live ISO attached. Once each boots into the live environment, run the installer (verify the disk name with lsblk first):</p>
<h3>On fedora-cmp-1 and cmp-2 (10.100.3.7–8):</h3>
<pre><code class="language-bash">sudo coreos-installer install /dev/xvda --ignition-url=http://10.100.3.9:8080/okd/worker.ign --insecure-ignition
sudo reboot
</code></pre>
<p dir="auto">After the reboot (booting from the virtual hard drive, not the ISO), the workers pull the ~450KB configuration from the control plane nodes through HAProxy, write their partitions, and reboot.</p>
<h2>Approve Worker CSRs</h2>
<p dir="auto">Worker nodes require manual Certificate Signing Request (CSR) approvals before joining the cluster. The kubelets first request client join certificates, then roughly 30 to 60 seconds later request serving certificates, so the approval command must be run repeatedly. The loop below runs it every 30 seconds for about 10 minutes and covers both phases.</p>
<p dir="auto">Run on the Helper VM:</p>
<pre><code class="language-bash">export KUBECONFIG=$HOME/okd-install/auth/kubeconfig

# Approve client join certificates, then serving certificates
for i in $(seq 1 20); do
  oc get csr -o go-template='{{range .items}}{{if not .status}}{{.metadata.name}}{{"\n"}}{{end}}{{end}}' | xargs --no-run-if-empty oc adm certificate approve
  sleep 30
done
</code></pre>
<p dir="auto">If any node is still missing afterwards, run the loop again.</p>
<h2>Validate Node Status</h2>
<p dir="auto">Check that all 5 nodes report Ready:</p>
<pre><code class="language-bash">oc get nodes
</code></pre>
<h2>Complete Installation</h2>
<p dir="auto">Run the final installer watcher to monitor cluster operators:</p>
<pre><code class="language-bash">openshift-install wait-for install-complete --dir=$HOME/okd-install --log-level=info
</code></pre>
<p dir="auto">When finished, the terminal outputs the Web Console URL (<strong><a href="https://console-openshift-console.apps.okd.domain.int" target="_blank" rel="noopener noreferrer nofollow ugc">https://console-openshift-console.apps.okd.domain.int</a></strong>) and the <strong>kubeadmin</strong> password.</p>
<h2>Preserve Kubeconfig for the Admin User</h2>
<p dir="auto">The KUBECONFIG export above only lasts for the current shell session. Copy the kubeconfig so oc commands keep working after logging out and back in to the Helper VM:</p>
<pre><code class="language-bash">mkdir -p ~/.kube
cp ~/okd-install/auth/kubeconfig ~/.kube/config
chmod 600 ~/.kube/config
</code></pre>
<h1>8. Post-Install Configuration</h1>
<p dir="auto">On UPI with platform: none there is no cloud provider, so the cluster is not ready for general use when the installer exits. Two Day-1 items must be configured: the internal image registry and a default StorageClass.</p>
<h2>Configure the Image Registry</h2>
<p dir="auto">The Image Registry Operator starts in a Removed state because no cloud object storage is available. Check it:</p>
<pre><code class="language-bash">oc get configs.imageregistry.operator.openshift.io cluster -o jsonpath='{.spec.managementState}'
# Returns: Removed
</code></pre>
<p dir="auto">Until this is addressed, internal builds and local image pushes will fail. For a lab or other non-production environment, use ephemeral storage (image data is lost if the registry pod restarts):</p>
<pre><code class="language-bash">oc patch configs.imageregistry.operator.openshift.io cluster --type merge \
  -p '{"spec":{"managementState":"Managed","storage":{"emptyDir":{}}}}'
</code></pre>
<p dir="auto">Once the default StorageClass below is in place, the registry can be moved to a persistent volume claim instead.</p>
<h2>Provision a Default StorageClass (NFS)</h2>
<p dir="auto">A fresh UPI cluster has no StorageClasses, so any PersistentVolumeClaim will remain Pending. This lab example uses the nfs-subdir-external-provisioner on the storage appliance / NAS  (example path: 10.100.20.248:/volume1/okd).</p>
<p dir="auto"><strong>Prerequisites:</strong> create the NFS export on your storage appliance (SAN/NAS), allow the cluster node IPs (10.100.3.0/24), and set the squash option so the provisioner can write to the share (usually no_root_squash).</p>
<pre><code class="language-bash">oc new-project nfs-provisioner
oc adm policy add-scc-to-user hostmount-anyuid -z nfs-subdir-external-provisioner -n nfs-provisioner

helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --namespace nfs-provisioner \
  --set nfs.server=10.100.20.248 \
  --set nfs.path=/volume1/okd \
  --set storageClass.name=nfs-client \
  --set storageClass.defaultClass=true
</code></pre>
<p dir="auto">Verify, and mark the class as default manually if it is not already:</p>
<pre><code class="language-bash">oc get storageclass
oc annotate storageclass nfs-client storageclass.kubernetes.io/is-default-class="true" --overwrite
</code></pre>
]]></description><link>https://xcp-ng.org/forum/topic/12535/okd-on-xcp-ng</link><generator>RSS for Node</generator><lastBuildDate>Fri, 16 Oct 2026 21:46:07 GMT</lastBuildDate><atom:link href="https://xcp-ng.org/forum/topic/12535.rss" rel="self" type="application/rss+xml"/><pubDate>Fri, 16 Oct 2026 16:36:36 GMT</pubDate><ttl>60</ttl></channel></rss>