Keepalived is a routing software written in C that can be used to setup load balancing and high availiability for Linux machines.

NOTE 1: For the sake of simplicity, in this exampe we will consider a cluster of two CentOS Stream virtual machines with firewalld and iptables disabled.

NOTE 2: virtualization host is Fedora 33 server with libvirt and qemu/kvm.

CentOS Stream

Install keepalived:

$ dnf install keepalived

Add nginx repo to the system: repo.
It will be use to check that keepalived is actually working:

$ dnf install nginx
$ systemctl enable --now nginx

Configure keepalived:

$ vi /etc/keepalived/keepalived.conf
---
vrrp_script chk_nginx {
  script "/etc/keepalived/scripts/nginx-check.sh"
  interval 2 # run script every 2 seconds
  weight 2 # add 2 points if OK
}

vrrp_instance VI_1 {
  interface enp2s0 # interface to monitor
  state MASTER # MASTER on host1, BACKUP on host2
  virtual_router_id 51
  priority 101 # MASTER 101, BACKUP 100
  advert_int 1
  #nopreempt # decomment to not have the VIP go back to master when it goes back online
  authentication {
    auth_type PASS
    auth_pass myPass # maximum 8 chars
  }
  virtual_ipaddress {
    10.10.0.12/24 # virtual ip address
  }
  track_script {
    chk_nginx
  }
}

Also add a script to check if nginx is alive and well:

$ mkdir /etc/keepalived/scripts
$ vi /etc/keepalived/scripts/nginx-check.sh
---
#!/bin/sh

if [ -z "`pidof nginx`" ]; then
  exit 1
fi
---
$ chmod 755 /etc/keepalived/scripts/nginx-check.sh

libvirt and qemu/KVM caveats

In my case I have two virtual machine each configured with one MACVTAP virtual network adapter.
Apparently libvirt MACVTAP network interfaces doesn’t actually pass all traffic going from host to guests and vice versa.
This unfortunate behavior makes it impossible for keepalived to receive packets, meaning that both machines will try to elavate themselves to MASTER status.
To avoid this conflict, add trustGuestRxFilters='yes' parameter to the virtual network adapter config:

$ virsh edit <vm_name>
---
    ...
    <interface type='direct' trustGuestRxFilters='yes'>
        ...
    </interface>
    ...