Skip to content

Instantly share code, notes, and snippets.

What would you like to do?

Deploying OKD using libvirt

For this environment, we'll be using these hostname/IP combinations:

  • helper =
  • bootstrap =
  • controlplane-0 =
  • controlplane-0 =
  • controlplane-0 =
  • worker-0 =
  • worker-1 =
  1. Configure libvirt network and storage

    Using your tool of choice, configure the libvirt network. You can create a new one or modify the default if desired:

    • Assign a name, e.g. okd
    • Mode = NAT
    • IPv4 config
      • Choose any subnet that doesn't overlap with your external network. I'll be using
      • Disable DHCP
      • DNS domain name - do not use your regular domain name, I'm using okd.lan

    After creating the new libvirt network, we need to inform the local DNS resolver of how to find the domain. With Fedora 33, systemd-resolved is used, so we need to use resovlectl to configure it.

    # change these values to match your environment
    #   virbr1 = the bridge interface to the libvirt network
    #   okd.lan = the domain you chose
    # = the reverse subnet you're using
    sudo resolvectl domain virbr1 '~okd.lan' ''
    sudo resolvectl default-route virbr1 false
    sudo resolvectl dns virbr1
    # verify settings
    resolvectl domain
    resolvectl dns

    If needed, create a storage pool for where you'll be storing the VMs. The control plane nodes, in particular, need low latency storage, e.g. SSD or NVMe.

  2. Create and configure helper node

    The helper node will provide DNS and DHCP via dnsmasq, an http server, and load balancing via haproxy. Choose any OS you like, I'll be using Fedora Server. Create the VM (1 CPU, 1 GiB memory), install the OS, apply updates. Use a static IP address, I'm using

    SSH to the helper node for the following steps.

    • Install Podman, haproxy, and dnsmasq

      dnf -y install podman haproxy dnsmasq
    • Configure dnsmasq

      cat << EOF > /etc/dnsmasq.d/okd.conf
      # OKD required
      # create node entries
      # enable and start dnsmasq
      systemctl enable --now dnsmasq
    • Configure haproxy

      cat << EOF > /etc/haproxy/haproxy.cfg
        log local2
        chroot      /var/lib/haproxy
        pidfile     /var/run/
        maxconn     4000
        user        haproxy
        group       haproxy
        stats socket /var/lib/haproxy/stats
        mode                    tcp
        log                     global
        option                  httplog
        option                  dontlognull
        option http-server-close
        option forwardfor       except
        option                  redispatch
        retries                 3
        timeout http-request    10s
        timeout queue           1m
        timeout connect         10s
        timeout client          10m
        timeout server          10m
        timeout http-keep-alive 10s
        timeout check           10s
        maxconn                 3000
      listen stats
        bind :9000
        mode http
        stats enable
        stats uri /
        monitor-uri /healthz
      frontend openshift-api-server
        bind *:6443
        default_backend openshift-api-server
        option tcplog
      backend openshift-api-server
        balance source
        server bootstrap check
        server controlplane0 check
        server controlplane1 check
        server controlplane2 check
      frontend machine-config-server
          bind *:22623
          default_backend machine-config-server
          option tcplog
      backend machine-config-server
          balance source
          server bootstrap check
          server controlplane0 check
          server controlplane1 check
          server controlplane2 check
      frontend ingress-http
          bind *:80
          default_backend ingress-http
          option tcplog
      backend ingress-http
          mode http
          balance source
          server controlplane0-http-router check
          server controlplane1-http-router check
          server controlplane2-http-router check
          server worker0-http-router check
          server worker1-http-router check
      frontend ingress-https
          bind *:443
          default_backend ingress-https
          option tcplog
      backend ingress-https
          balance source
          server controlplane0-https-router check
          server controlplane1-https-router check
          server controlplane2-https-router check
          server worker0-https-router check
          server worker1-https-router check
      # enable and start haproxy
      systemctl enable --now haproxy
    • Configure an http server using podman

      The FCOS rootfs image used in this step is here.

      # start the httpd container on port 8080
      podman run -d \
       --restart=unless-stopped \
       -p 8080:80 \
       -v /var/www/html:/usr/local/apache2/htdocs \
  3. Download and place OKD resources

    From the libvirt host, download the following resources:

    From the OKD release page on GitHub, the openshift-client and openshift-install packages.

    Un-gzip and move the binaries to /usr/local/bin:

    # download
    # unpack
    tar xzf openshift-install-linux-4.6.0-0.okd-2021-02-14-205305.tar.gz
    tar xzf openshift-client-linux-4.6.0-0.okd-2021-02-14-205305.tar.gz
    # place
    sudo mv openshift-install oc kubectl /usr/local/bin

    From the helper node:

    Links for the most recent binaries to download are here.

    # organizational directories
    mkdir -p /var/www/html/{install,ignition}
    # download the kernel image
    mv fedora-coreos-33.20210212.20.1-live-kernel-x86_64 /var/www/html/install/kernel
    # download the initramfs image
    mv fedora-coreos-33.20210212.20.1-live-initramfs.x86_64.img /var/www/html/install/initramfs.img
    # download the rootfs image
    mv fedora-coreos-33.20210212.20.1-live-rootfs.x86_64.img /var/www/html/install/rootfs.img
    # set permissions for all
    chmod 444 /var/www/html/install/*
  4. Create install-config.yaml

    From the libvirt host.

    Substitue your values for the SSH key and adjust others as needed, e.g. networking.machineNetwork.cidr.

    mkdir ~/okd && cd ~/okd
    # a real pull secret is not needed for OKD
    PULLSECRET='{"auths":{"fake":{"auth": "bar"}}}'
    # use the path for your public key
    SSHKEY=$(cat ~/.ssh/id_*.pub)
    cat << EOF > install-config.yaml
    apiVersion: v1
    baseDomain: okd.lan
    - architecture: amd64
      hyperthreading: Enabled
      name: worker
      replicas: 0
      architecture: amd64
      hyperthreading: Enabled
      name: master
      replicas: 3
      creationTimestamp: null
      name: cluster
      - cidr:
        hostPrefix: 23
      - cidr:
      networkType: OVNKubernetes
      none: {}
    publish: External
    pullSecret: '$PULLSECRET'
    sshKey: |
  5. Create ignition files

    From the libvirt host

    This will be done in two phases so that the control plane can be marked unschedulable.

    # create a working directory
    cd ~/okd && mkdir cluster && cp install-config.yaml cluster/
    # generate manifests
    openshift-install create manifests --dir=cluster
    # set the control plane non-schedulable
    sed -i 's/mastersSchedulable: true/mastersSchedulable: false/' cluster/manifests/cluster-scheduler-02-config.yml
    # generate ignition files
    openshift-install create ignition-configs --dir=cluster
    # copy the ignition files to the helper node
    scp cluster/*.ign user@

    You may need to adjust permissions for the files on the helper node so that the containerized web server can access them.

  6. Create and configure the VMs

    From the libvirt host.

    # create the disk images, set the directory according to your host
    for NODE in bootstrap controlplane-0 controlplane-1 controlplane-2 worker-0 worker-1; do
      sudo qemu-img create -f qcow2 /var/lib/libvirt/okd-images/$NODE.qcow2 120G
    # set permissions
    sudo chown qemu:qemu /var/lib/libvirt/okd-images/*
    sudo chmod 600 /var/lib/libvirt/okd-images/*
    # organization is good
    mkdir -p node-configs
    wget && sudo mv kernel /var/lib/libvirt/boot/
    wget && sudo mv initramfs.img /var/lib/libvirt/boot/
    sudo chown qemu:qemu /var/lib/libvirt/boot/kernel
    sudo chown qemu:qemu /var/lib/libvirt/boot/initramfs.img
    # where to find the install files needed for direct kernel boot of the VMs
    KERNEL_ARGS=' rd.neednet=1 coreos.inst.install_dev=/dev/vda'
    # set static IP configuration
    IP_STR='ip=192.168.110.NODEIP:: nameserver='
    IP_bootstrap=$(echo $IP_STR | sed 's/NODEIP/60/;s/NODENAME/bootstrap/')
    IP_controlplane0=$(echo $IP_STR | sed 's/NODEIP/61/;s/NODENAME/controlplane-0/')
    IP_controlplane1=$(echo $IP_STR | sed 's/NODEIP/62/;s/NODENAME/controlplane-1/')
    IP_controlplane2=$(echo $IP_STR | sed 's/NODEIP/63/;s/NODENAME/controlplane-2/')
    IP_worker0=$(echo $IP_STR | sed 's/NODEIP/65/;s/NODENAME/worker-0/')
    IP_worker1=$(echo $IP_STR | sed 's/NODEIP/66/;s/NODENAME/worker-1/')
    # bootstrap ignition location
    # create the bootstrap machine
    sudo virt-install \
      --virt-type kvm \
      --ram 12188 \
      --vcpus 4 \
      --os-variant fedora-coreos-stable \
      --disk path=/var/lib/libvirt/okd-images/bootstrap.qcow2,device=disk,bus=virtio,format=qcow2 \
      --noautoconsole \
      --vnc \
      --network network:okd \
      --boot hd,network \
      --install kernel=${KERNEL},initrd=${INITRD},kernel_args_overwrite=yes,kernel_args="${KERNEL_ARGS} ${IP_bootstrap} ${BOOTSTRAP_IGNITION}" \
      --name bootstrap \
      --print-xml 1 > node-configs/bootstrap.xml
    # set the ignition location for the control plane nodes
    # define the nodes, set values according to your host and desired outcome
    for NODE in controlplane-0 controlplane-1 controlplane-2; do
      # jiggery pokery to get the IP string via variable reference
      ipvarname="IP_$(echo $NODE | sed 's/-//')"
      sudo virt-install \
        --virt-type kvm \
        --ram 12188 \
        --vcpus 4 \
        --os-variant fedora-coreos-stable \
        --disk path=/var/lib/libvirt/okd-images/$NODE.qcow2,device=disk,bus=virtio,format=qcow2 \
        --noautoconsole \
        --vnc \
        --network network:okd \
        --boot hd,network \
        --install kernel=${KERNEL},initrd=${INITRD},kernel_args_overwrite=yes,kernel_args="${KERNEL_ARGS} ${!ipvarname} ${CONTROL_IGNITION}" \
        --name $NODE \
        --print-xml 1 > node-configs/$NODE.xml
    # set the worker ignition location
    for NODE in worker-0 worker-1; do
      ipvarname="IP_$(echo $NODE | sed 's/-//')"
      sudo virt-install \
        --virt-type kvm \
        --ram 8192 \
        --vcpus 2 \
        --os-variant fedora-coreos-stable \
        --disk path=/var/lib/libvirt/okd-images/$NODE.qcow2,device=disk,bus=virtio,format=qcow2 \
        --noautoconsole \
        --vnc \
        --network network:okd \
        --boot hd,network \
        --install kernel=${KERNEL},initrd=${INITRD},kernel_args_overwrite=yes,kernel_args="${KERNEL_ARGS} ${!ipvarname} ${WORKER_IGNITION}" \
        --name $NODE \
        --print-xml 1 > node-configs/$NODE.xml
    # define each of the VMs
    for VM in `ls node-configs/`; do
      sudo virsh define node-configs/$VM
      # for some reason libvirt doesn't like the kernel and initd location, set it forcefully
      sudo virt-xml $VM --edit \
        --xml ./os/kernel=$KERNEL \
        --xml ./os/initrd=$INITRD
  7. Install FCOS

    Now we need to power on each of the VMs and let them boot the first time to install FCOS:

    for VM in bootstrap controlplane-0 controlplane-1 controlplane-2 worker-0 worker-1; do
      sudo virsh start $VM
      # optionally, add a sleep here to not overwhelm the storage
      #sleep 30

    The VMs will start, install FCOS, then power off. After they have powered off, we need to adjust the settings to boot from the drive as normal

    for VM in bootstrap controlplane-0 controlplane-1 controlplane-2 worker-0 worker-1; do
      sudo virt-xml $VM --edit \
        --xml ./on_reboot=restart \
        --xml xpath.delete=./os/kernel \
        --xml xpath.delete=./os/initrd \
        --xml xpath.delete=./os/cmdline 
  8. Finish deploying

    Finally, start the VMs so that OKD can finish deploying.

    for VM in bootstrap controlplane-0 controlplane-1 controlplane-2 worker-0 worker-1; do
      sudo virsh start $VM

    Monitor the progress, and complete the deployment, using these commands, from the libvirt host.

    cd ~/okd
    # bootstrap
    openshift-install wait-for bootstrap-complete --log-level=debug --dir=cluster
    # turn off bootstrap when it's done
    sudo virsh destroy bootstrap
    # connect to the cluster
    export KUBECONFIG=~/okd/cluster/auth/kubeconfig
    # approve CSRs
    watch -n 5 oc get csr
    # when there are two CSRs pending, approve them
    oc get csr -o go-template='{{range .items}}{{if not .status}}{{}}{{"\n"}}{{end}}{{end}}' | xargs oc adm certificate approve
    # two additional CSRs will be requested shortly after, repeat the commands above to approve them
    # when the CSRs are approved, wait for the install to complete
    openshift-install wait-for install-complete --log-level=debug --dir=cluster



for VM in bootstrap controlplane-0 controlplane-1 controlplane-2 worker-0 worker-1; do
  sudo virsh destroy $VM
  sudo virsh undefine --domain $VM

sudo rm /var/lib/libvirt/okd-images/bootstrap.qcow2
sudo rm /var/lib/libvirt/okd-images/controlplane*.qcow2
sudo rm /var/lib/libvirt/okd-images/worker*.qcow2
Copy link

nontw commented Oct 21, 2021

Hi Andrew, thanks for writing this up. I'm trying to follow along and understand more here. One correction that you made in the video is:

sudo resolvectl domain virbr1 '~okd.lan' ''
sudo resolvectl default-route virbr1 false
sudo resolvectl dns virbr1

The last command should be because you're having resolvectl send dns queries to the helper node.

Copy link

berndgz commented May 1, 2022

Hi Andrew, thanks for writing the gist up.
I'm trying to follow along but stuck between point 1 & 2.
I'm struggling for weeks and find some solutions for different issues.
I hope the following posts can help someone to save time and get more understanding in how it works.

When booting up the helper node, dnsmasq fails to start because of the error: unknown interface enp1s0
It seems that NetworkManager has not brought the Ethernet interface up in time, so it fails to start.
To start dnsmasq manually after logging in is possible.

Using 'bind-dynamic' instead of 'bind-interfaces' allows start of dnsmasq even when 'interface=enp1s0' is still down.
As soon as it is up, it will bind the interface and listen on it.

[root@helper ~]# vi /etc/dnsmasq.conf
# If you want dnsmasq to listen for DHCP and DNS requests only on
# specified interfaces (and the loopback) give the name of the
# interface (eg eth0) here.
# Repeat the line for more than one interface.
interface=enp1s0                                                            <- uncomment and modify line
# Listen only on localhost by default
# Or you can specify which interface _not_ to listen on
# Or which to listen on by address (remember to include if
# you use this.)
# If you want dnsmasq to provide only DNS service on an interface,
# configure it as shown above, and then use the following line to
# disable DHCP and TFTP on it.
# Serve DNS and DHCP only to networks directly connected to this machine.
# Any interface= line will override it.
# On systems which support it, dnsmasq binds the wildcard address,
# even when it is listening on only some interfaces. It then discards
# requests that it shouldn't reply to. This has the advantage of
# working even when interfaces come and go and change address. If you
# want dnsmasq to really bind only the interfaces it is listening on,
# uncomment this option. About the only time you may need this is when
# running another nameserver on the same machine.
# To listen only on localhost and do not receive packets on other
# interfaces, bind only to lo device. Comment out to bind on single
# wildcard socket.
#bind-interfaces                                                            <- comment out line
bind-dynamic                                                                <- insert line

[root@helper ~]# dnsmasq --test
dnsmasq: syntax check OK.

[root@helper ~]# systemctl restart dnsmasq

When booting up the helper node, dnsmasq fails to start because of the systemd-resolved is started with preference and has port 53 already in use.

Disable the local DNS stub listener from systemd-resolved with the setting 'DNSStubListener=no'.

[root@helper ~]# vi /etc/systemd/resolved.conf
#  This file is part of systemd.
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.
# Entries in this file show the compile time defaults.
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
# See resolved.conf(5) for details

#FallbackDNS= 2606:4700:4700::1111 2001:4860:4860::8888 2606:4700:4700::1001 2001:4860:4860::8844
DNSStubListener=no               <- uncomment and modify line

[root@helper ~]# systemctl restart systemd-resolved

When rebooting the host machine the domain 'okd.lan' libvirt network can not be reached due to missing settings in the resolver.

Persistent the local DNS resolver settings with a libvirt network hook script.

qemu-kvm:~ # vi /etc/libvirt/hooks/network

# After the libvirt network is started, up & running, this script is called.
# ''
# By default, guests that are connected via a virtual network with <forward mode='nat'/>
# can make any outgoing network connection they like.
# Incoming connections are allowed from the host,
# and from other guests connected to the same libvirt network,
# but all other incoming connections are blocked by iptables rules.
# ''
# The && is an 'and' comparison operator.
# The : command has a useful side-effect, it always returns a successful exit status.
# If the evaluation exits with an error, the || : will force a zero exit status.
# This ensures that in the event of an error, the process is not interrupted.
# Change these values to match your environment!
# - okd = the libvirt network name
# - virbr1 = the bridge interface name to the libvirt network
# - okd.lan = the domain name you chose
# - = the reverse subnet you're using
# - = helper node with dnsmasq running
# Important NOTE!
# For a hook script to be utilized,
# it must have its execute bit set (e.g. chmod +x /etc/libvirt/hooks/network),
# and must be present when the libvirt daemon is started.

if [ "${1}" = "okd" ] && [ "${2}" = "started" ]; then

    # we need to inform the local DNS resolver of how to find the domain.
    # to make the settings permanent, it must be set in the hook script.
    /usr/bin/resolvectl domain virbr1 '~okd.lan' '' || :
    /usr/bin/resolvectl default-route virbr1 false || :
    /usr/bin/resolvectl dns virbr1 || :

    # to connect guests in the virtual network with clients from the host network
    # kick '-D' the block rules and add '-A' some allow rules for all other incoming connections.
    # to delete rules by specification you can use the rule list output (iptables -S) for help.
    # don't do this in production environments!
    /sbin/iptables -D LIBVIRT_FWI -o virbr1 -j REJECT --reject-with icmp-port-unreachable || :
    /sbin/iptables -D LIBVIRT_FWO -i virbr1 -j REJECT --reject-with icmp-port-unreachable || :
    /sbin/iptables -A LIBVIRT_FWI -o virbr1 -j ACCEPT || :
    /sbin/iptables -A LIBVIRT_FWO -i virbr1 -j ACCEPT || :


qemu-kvm:~ # chmod +x /etc/libvirt/hooks/network

qemu-kvm:~ # reboot

The host machine can not resolve the domain 'okd.lan' libvirt network because it can not find the resolver.

systemd-resolve listens on loopback interface.
Include 'DNS=' in the - Network configuration.

qemu-kvm:~ # ss -tulpn | grep resolve
udp   UNCONN 0      0*    users:(("systemd-resolve",pid=621,fd=19))
udp   UNCONN 0      0*    users:(("systemd-resolve",pid=621,fd=17))
udp   UNCONN 0      0  *    users:(("systemd-resolve",pid=621,fd=11))
udp   UNCONN 0      0               [::]:5355         [::]:*    users:(("systemd-resolve",pid=621,fd=13))
tcp   LISTEN 0      4096*    users:(("systemd-resolve",pid=621,fd=12))
tcp   LISTEN 0      4096*    users:(("systemd-resolve",pid=621,fd=20))
tcp   LISTEN 0      4096*    users:(("systemd-resolve",pid=621,fd=18))
tcp   LISTEN 0      4096            [::]:5355         [::]:*    users:(("systemd-resolve",pid=621,fd=14))

qemu-kvm:~ # vi /etc/systemd/network/

DNS=            <- insert DNS entry

qemu-kvm:~ # systemctl restart systemd-networkd

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment