API load balancers
Keepalived and HAProxy configuration and validation.
Scope: This VIP is for the Kubernetes API only. Ingress traffic is handled by Ingress-NGINX exposed through MetalLB.
Kubernetes load balancer setup (dev env)
- Using VRRP (Virtual Router Redundancy Protocol).
- The information in this section includes two load balancers, master and backup, however, only the master needs to run in the dev environment if you are low on resources.
- Install haproxy and keepalived, noting that keepalived will go to loaded state not running state.
sudo apt install haproxy keepalived -y
- The
router_idis local only and doesn't affect the VRRP protocol. It is mostly used to identify messages and should be unique per server. I use the hostname. - The Virtual IP for my cluster is
192.168.1.200. - For all load balancers change the state to BACKUP and let priority decide
- If you have multiple k8s clusters in your environment ensure that the
virtual_router_idis unique for each cluster as it is used for election. I use the last octet of the VIP. - The vrrp interface will depend on your environment, in my dev virtual environment it is:
enp0s5.
Master load balancer: lb-1
-
Edit keepalived configuration:
sudo vi /etc/keepalived/keepalived.confglobal_defs {router_id lb-1script_user rootenable_script_security}vrrp_script check_apiserver {script "/etc/keepalived/check_apiserver.sh"interval 3fall 5rise 2weight -150}vrrp_instance VI_1 {state BACKUPinterface enp0s5virtual_router_id 200priority 254advert_int 1preempt_delay 10virtual_ipaddress {192.168.1.200/24}track_script {check_apiserver}garp_master_repeat 5garp_master_refresh 10notify_master "/etc/keepalived/status_capture.sh MASTER"notify_backup "/etc/keepalived/status_capture.sh BACKUP"notify_fault "/etc/keepalived/status_capture.sh FAULT"}
Backup load balancer: dev-lb-v2
-
Edit keepalived configuration:
vi /etc/keepalived/keepalived.confglobal_defs {router_id dev-lb-v2script_user rootenable_script_security}vrrp_script check_apiserver {script "/etc/keepalived/check_apiserver.sh"interval 3fall 5rise 2weight -150}vrrp_instance VI_1 {state BACKUPinterface enp0s5virtual_router_id 200priority 200advert_int 1virtual_ipaddress {192.168.1.200/24}track_script {check_apiserver}garp_master_repeat 5garp_master_refresh 10notify_master "/etc/keepalived/status_capture.sh MASTER"notify_backup "/etc/keepalived/status_capture.sh BACKUP"notify_fault "/etc/keepalived/status_capture.sh FAULT"}
Create scripts on all load balancers
-
Check API server script - fail if the local HAProxy listener isn't returning HTTP 200:
sudo vi /etc/keepalived/check_apiserver.sh#!/bin/shset -euVIP="192.168.1.200/24"IFACE="enp0s5"URL_LOCAL="https://localhost:8443/healthz"URL_VIP="https://192.168.1.200:8443/healthz"try_curl() {curl --silent --show-error --fail --max-time 4 --insecure "$1" -o /dev/null}retry() {url="$1"; n=0until try_curl "$url"; don=$((n+1)); [ "$n" -ge 3 ] && return 1sleep 1done}# Optional: only run curls if 8443 is actually listeningss -Htl "( sport = :8443 )" | grep -q . || { echo "*** 8443 not listening" >&2; exit 1; }retry "$URL_LOCAL" || { echo "*** HAProxy local listener unhealthy: $URL_LOCAL" >&2; exit 1; }if ip -4 addr show dev "$IFACE" | grep -q " $VIP\\b"; thenretry "$URL_VIP" || { echo "*** HAProxy VIP path unhealthy: $URL_VIP" >&2; exit 1; }fi -
Check keepalived status script - log to syslog:
sudo vi /etc/keepalived/status_capture.sh#!/usr/bin/env bashset -euo pipefailROLE="${1:-UNKNOWN}"MSG="$(date '+%Y-%m-%d %H:%M:%S'): The load balancer instance on $(hostname) is currently marked ${ROLE}"# Write a small status fileSTATUS_FILE="/var/run/load-balancer-status"umask 022echo "$MSG" > "$STATUS_FILE"# Also send to sysloglogger -t keepalived-notify -- "$MSG"# Optional: echo for journalecho "$MSG" -
Health check k8s script:
sudo vi /etc/haproxy/haproxy.cfggloballog /dev/log local0log /dev/log local1 noticechroot /var/lib/haproxystats socket /run/haproxy/admin.sock mode 660 level adminstats timeout 30suser haproxygroup haproxydaemon# Optional / irrelevant for TCP pass-through, but harmless to leaveca-base /etc/ssl/certscrt-base /etc/ssl/privatessl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-ticketsdefaultslog globalmode tcpoption tcplogoption dontlognulltimeout connect 10stimeout client 30stimeout server 30serrorfile 400 /etc/haproxy/errors/400.httperrorfile 403 /etc/haproxy/errors/403.httperrorfile 408 /etc/haproxy/errors/408.httperrorfile 500 /etc/haproxy/errors/500.httperrorfile 502 /etc/haproxy/errors/502.httperrorfile 503 /etc/haproxy/errors/503.httperrorfile 504 /etc/haproxy/errors/504.httpfrontend apiserverbind *:8443default_backend apiserverbackend apiserveroption httpchk GET /healthzhttp-check expect status 200balance roundrobin# Do a TLS handshake for the health check, skip cert verify (self-signed)server master-1 192.168.1.203:6443 check check-ssl verify none -
Ensure both scripts can execute
sudo chmod u+x /etc/keepalived/check_apiserver.shsudo chmod u+x /etc/keepalived/status_capture.sh -
Ensure HAProxy can always start with floating VIP
echo 'net.ipv4.ip_nonlocal_bind=1'|sudo tee -a /etc/sysctl.conf -
Ensure the settings persist across reboots:
sudo sysctl -p
Confirm keepalived and haproxy are running on all load balancers
- Start and status checks
sudo service keepalived startsudo service haproxy startsudo service keepalived statussudo service haproxy status
- Reboot the load balancers and check logs:
journalctl -u haproxy -n 20 --no-pagerOct 03 14:33:09 lb-1 haproxy[1689]: [NOTICE] (1689) : haproxy version is 2.8.5-1ubuntu3.3Oct 03 14:33:09 lb-1 haproxy[1689]: [NOTICE] (1689) : path to executable is /usr/sbin/haproxyOct 03 14:33:09 lb-1 haproxy[1689]: [WARNING] (1689) : Former worker (5195) exited with code 0 (Exit)Oct 04 00:55:33 lb-1 systemd[1]: Stopping haproxy.service - HAProxy Load Balancer...Oct 04 00:55:33 lb-1 haproxy[1689]: [WARNING] (1689) : Exiting Master process...Oct 04 00:55:33 lb-1 haproxy[1689]: [ALERT] (1689) : Current worker (6064) exited with code 143 (Terminated)Oct 04 00:55:33 lb-1 haproxy[1689]: [WARNING] (1689) : All workers exited. Exiting... (0)Oct 04 00:55:33 lb-1 systemd[1]: haproxy.service: Deactivated successfully.Oct 04 00:55:33 lb-1 systemd[1]: Stopped haproxy.service - HAProxy Load Balancer.Oct 04 00:55:33 lb-1 systemd[1]: haproxy.service: Consumed 31.879s CPU time, 39.0M memory peak, 0B memory swap peak.-- Boot a2259a0db8b043a3b0a4b465ec31df29 --Oct 04 00:55:43 lb-1 systemd[1]: Starting haproxy.service - HAProxy Load Balancer...Oct 04 00:55:43 lb-1 haproxy[770]: [NOTICE] (770) : New worker (846) forkedOct 04 00:55:43 lb-1 systemd[1]: Started haproxy.service - HAProxy Load Balancer.Oct 04 00:55:43 lb-1 haproxy[770]: [NOTICE] (770) : Loading success.Oct 04 00:55:43 lb-1 haproxy[846]: [WARNING] (846) : Server apiserver/master-1 is DOWN, reason: Layer4 connection problem, info: "Connection refused", check duration: 1ms. 0 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.Oct 04 00:55:43 lb-1 haproxy[846]: [ALERT] (846) : backend 'apiserver' has no server available!Oct 04 00:55:43 lb-1 haproxy[846]: Server apiserver/master-1 is DOWN, reason: Layer4 connection problem, info: "Connection refused", check duration: 1ms. 0 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.Oct 04 00:55:43 lb-1 haproxy[846]: Server apiserver/master-1 is DOWN, reason: Layer4 connection problem, info: "Connection refused", check duration: 1ms. 0 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.Oct 04 00:55:43 lb-1 haproxy[846]: backend apiserver has no server available!Oct 04 00:55:43 lb-1 haproxy[846]: backend apiserver has no server available! - At this stage, before the cluster is deployed, we are looking for any log entries that indicate mis-configuration, possible errors to be resolved include:
sendmsg()/writev() failed in logger #1: Permission denied (errno=13)
Warning: Before the cluster is deployed, the backend will show no server available and that is expected.