Étape 5 : Load balancers HAproxy et Keepalived

Introduction

Maintenant que l’ensemble des serveurs dispose du nécessaire pour y déployer un cluster Kubernetes, on va pouvoir passer au traitement des load balancer.

Pour mémoire, comme indiqué dans la présentation de l’architecture, nous allons utiliser deux instances de gestion de charge séparées. Le première va servir à traiter les flux en provenance de l’extérieur vers les applications conteneurisées sur les nœuds du LAN, la seconde va traiter les flux en provenance de l’extérieur vers les applications conteneurisées en DMZ.

C’est un choix assumé au départ d’avoir un seul cluster pour différentes zones d’hébergement. C’est un des avantages de Kubernetes, de pouvoir dédier des nodes à tel ou tel type de fonction.

Rappel sur le réseau au sein de Kubernetes

Cela nécessite aussi d’avoir une bonne compréhension du fonctionnement réseau de Kubernetes.

Il est très important de savoir que par défaut, toutes les applications déployées sur K8S peuvent échanger les unes avec les autres.

Lorsqu’on initie un cluster, on doit choisir un fournisseur réseau, le fameux CNI (Container Network Interface). Celui-ci détermine les fonctionnalités réseau supportées avec des capacités plus ou moins avancées qu’on pourra mettre en œuvre au sein du cluster.

Mais peu importe le choix du CNI (Container Network Interface), par défaut, tout un réseau interne à K8S va se monter, permettant aux applications de se joindre les unes les autres, peu importe les IPS réelles des nœuds et leur emplacement réseau. C’est pourquoi il sera nécessaire d’employer des éléments tels que des "network policie" pour réguler le trafic en interne, surtout lorsque, comme c’est le cas ici, des nœuds sont situés en DMZ et en LAN.

Néanmoins ceci est valable uniquement de l’intérieur du cluster, de l’extérieur, aucune application n’est exposée naturellement. Pour cela il faudra se baser sur toute une logique particulière, qu’on peut appréhender de différentes manières.

Architecture cible

Dans l’architecture proposée, le premier point d’entrée pour accéder à une application du cluster est une adresse de loadbalancer qui varie en fonction de l’hébergement de l’application cible : en LAN ou en DMZ.

Ces loadbalancer auront pour mission de rerouter le trafic vers l’un des nodes qui correspond à la zone réseau voulue.

Pour ça on va exploiter HAproxy et Keepalived.

Architecture cible avec HAproxy et Keepalived

Cliquez sur l'image pour l'agrandir.

HAproxy

HAproxy est une solution open source de référence pour le loadbalancing. Il fait partie des logiciels de répartition de charge et de proxy les plus utilisés au monde, notamment pour les applications TCP et HTTP.

Logo HAproxy

Cliquez sur l'image pour l'agrandir.

HAProxy a été créé en 2000 par Willy Tarreau, un développeur français (qui est également un contributeur historique et majeur du noyau Linux). Il a conçu cet outil pour répondre aux besoins critiques de performance et de stabilité des infrastructures web qui commençaient à faire face à des pics de trafic importants au début des années 2000.

C’est donc un logiciel robuste, toujours en activité (la 3.4.3 a été publiée le 29 juillet 2026) avec un fort historique et une base de connaissance très importante.

Le risque principal autour de l’usage d’un serveur HAproxy reste le SPOF, pour Single Point of Failure, ou point de défaillance unique. Si toutes les connexions doivent transiter par ce dernier, sa défaillance entraine une coupure générale de tous les flux.

C’est pourquoi, dans l’architecture cible (et contrairement à mon ancien cookbook), ce n’est pas un serveur qui va être déployé, mais deux, par environnement (LAN/DMZ).

Chaque paire de HAproxy s’occupera d’une VIP pour Virtual IP. Cette IP va pouvoir « transiter » d’une instance HAproxy à une autre fonction de l’état des serveurs.

Si l’une des VMs rattachées à une paire est défaillante, alors qu’elle portait la gestion de l’IP virtuelle, la seconde VM va pouvoir le détecter et reprendre la gestion de l’IP en question pour continuer d’assurer le trafic.

VRRP/Keepalived

Ce mécanisme va pouvoir se faire via VRRP

VRRP (Virtual Router Redundancy Protocol) est un protocole réseau conçu pour augmenter la disponibilité et la fiabilité des chemins de routage.

Bien qu'il ait été créé à l'origine pour les routeurs physiques, il est aujourd'hui massivement utilisé dans les infrastructures web pour sécuriser les répartiteurs de charge, notamment HAProxy.

L'idée centrale du VRRP est de regrouper plusieurs instances de routage (ici HAproxy) pour qu'elles apparaissent comme un seul point d’entrée logique (virtuelle) aux yeux du réseau et des clients.

Voici les concepts clés :

  • L'IP Virtuelle (VIP) : Les instances du groupe partagent une seule adresse IP virtuelle (et une adresse MAC virtuelle associée). C'est vers cette VIP que le trafic client est envoyé.
  • Les rôles (Master / Backup) : Au sein du cluster VRRP, une machine est élue MASTER et gère activement le trafic de la VIP. Les autres machines sont en BACKUP et attendent.
  • Le hearbeat : Le MASTER envoie en permanence des messages d'annonce pour signaler au groupe qu'il est toujours opérationnel.
  • Le basculement (Failover) : Si les serveurs BACKUP cessent de recevoir les annonces du MASTER (suite à un crash matériel, un problème réseau ou un redémarrage), le nœud BACKUP ayant la priorité la plus haute s'attribue instantanément la VIP et devient le nouveau MASTER. Ce basculement est souvent transparent pour les utilisateurs finaux.

Pour implémenter VRRP, on a retenu l’usage de Keepalived

Logo Keepalived

Cliquez sur l'image pour l'agrandir.

Keepalived est également un logiciel de routage open-source écrit en C, conçu pour fournir des fonctionnalités simples et robustes de haute disponibilité et de répartition de charge spécifiquement pour les infrastructures Linux.

Ici aussi, l’origine est française, puisqu' il a été créé en 2001 par Alexandre Cassen. Le projet est profondément lié à l'écosystème Linux et évolue en étroite collaboration avec les développements de la pile réseau du noyau (notamment Netfilter et IPVS).

Keepalived s'articule autour de trois modules principaux qui en font un outil redoutable pour la résilience réseau :

  • Implémentation complète de VRRP : C'est le chef d'orchestre de la haute disponibilité. Keepalived gère l'annonce des messages, l'élection des rôles MASTER/BACKUP, et l'attribution dynamique de l'IP virtuelle (VIP) sur l'interface réseau de la machine active.
  • Vérification d'état (Health Checking) : Il surveille en permanence la santé des services. Keepalived peut exécuter des scripts locaux pour vérifier que le processus HAProxy tourne correctement. S'il détecte que le service HAProxy local a planté, Keepalived se rétrograde lui-même, forçant le basculement de la VIP vers le nœud BACKUP.
  • Pilotage de LVS (Load Balancing couche 4) : Bien qu'il soit très souvent utilisé uniquement pour sa fonction VRRP en amont d'un HAProxy, Keepalived intègre nativement de quoi configurer LVS. Il permet ainsi de faire de la répartition de charge TCP/UDP extrêmement rapide, car traitée directement au niveau du noyau Linux (Kernel space), sans remonter dans la couche applicative (User space).

Principes

Concernant l’implémentation de l’ensemble, l’objectif est résumé dans le tableau suivant :

Zone Noeud VIP VRID Backend
LAN prdnlbhap501 / 502 192.168.10.187/24 51 workers LAN, NodePorts 30080-30443 80/443
DMZ prdnlbhap510 / 511 192.168.5.187/24 52 workers DMZ, NodePorts 31080-31443 80/443

Keepalived assurera la disponibilité de l’adresse virtuelle extérieure (quel nœud porte la VIP). HAProxy assurera la répartition du trafic (quel worker K8S reçoit la requête).

À noter que l'API Kubernetes (TCP 6443) n’est pas dans le scope. Elle sera traitée par kube-vip sur les control planes (voir l’architecture présentée en étape 0). Nos load balancer HAProxy ne doivent servir qu’au trafic applicatif.

Un mot aussi sur la notion de VRID qui apparait dans le tableau. Il s’agit du « Virtual Router ID ». C’est l’identifiant du routeur virtuel au sens VRRP. Chaque instance qui participe au routage doit être rattachée à ce même identifiant. Comme dans notre cas, on a deux pairs de loabalancer à gérer, on retient un VRID par pair. Étant donné qu’un pare-feu existe entre chacune de ces paires, on aurait pu choisir un VRID identique, mais c’est plus simple de les différentier pour une meilleure compréhension.

Voici également quelques spécificités associées à la configuration cible

  • On travaillera en mode TCP pas HTTP. La terminaison TLS (chiffrement) sera assurée par Traefik, qui se positionnera derrière les loads balancer (ce point sera vu plus tard). Les instances HAProxy ne doivent faire que passe-plat. Pour ceux qui aiment le réseau, nos HAProxy resteront sur la couche 4 du modèle OSI soit la couche Transport.
  • Contrairement à d’autres schémas, pour VRRP on utilisera unicast pas multicast. VRRP peut s'employer avec plus que seulement deux instances et travailler avec tout un groupe de routeurs, le multicast est donc plus efficace pour ce besoin. Mais dans notre cas, étant donné qu’on travaille par pair de HAproxy, on peut se permettre de cibler les communications sans avoir à diffuser les informations sur tout le réseau. D’un point de vue sécurité, ça permet de filtrer sur les IPS participant au routage.

Résilience supportée

Il est important aussi de comprendre les mécanismes qu’on va mettre en place pour sécuriser les paires HA.

Plusieurs niveaux de surveillance vont être appliqués.

Un premier niveau via VRRP. Chaque membre d’une paire va émettre une annonce à destination de son voisin (unicast). Si aucune des trois annonces ne répond, l’instance émettrice va considérer son pair comme hors service. Dans le cas du serveur HAproxy de BACKUP, il passera MASTER. A noter qu'en production, il n'est pas rare d'avoir des liens de heartbeat dédiés.

Ce premier niveau va couvrir les cas de panne suite à un nœud éteint, un kernel panic, une coupure réseau et autre panne franche.

Un second niveau plus avancé va être mis en place à l’aide d’un script. Celui-ci va permettre de surveiller l’état du service HAproxy sur le nœud lui-même. Cela permettra d’établir la perte du service et de couvrir le cas où un nœud est UP (réponse à l’annonce VRRP), mais avec un service HAproxy en erreur.

Ce second niveau permettra de traiter les problèmes liés à HAproxy.

Enfin, les serveurs HAproxy, eux-mêmes surveilleront leur backend. Ce n’est pas lié aux bascules de VIP, mais à l’envoi des paquets reçu uniquement au nœud du cluster K8S capable de traiter la demande. Ce contrôle se fera via un simple check sur les ports TCP des nodes K8S.

En résumé

Panne Détection VIP déplacé après
Nœud MASTER (HAproxy) coupé Silence VRRP 3,6s environ
HAproxy HS Via script chk_haproxy (2 tentative toutes les 2s) 4s environ
Worker Traefik HS Tcp check Pas de bascule de la VIP, mais éjection du nœud HS au bout de 6s environ

Cela pourrait être amélioré par la suite, par exemple le check de HAproxy qui ne s’attarde que sur l’état du service HAproxy et pas forcément sur sa saturation ou autres problèmes plus subtil. Mais c’est déjà un bon début.

Déploiement via Ansible

Comme pour les étapes précédentes, le but va être d’utiliser Ansible pour déployer et configurer les briques HAproxy et Keepalived

Avant de continuer, je vous suggère de jeter un œil, ou de relire, l’article de l’étape 3 sur Ansible et la logique sous-jacente.

Playbook

Concernant ansible, la première chose est de faire appel au playbook xkub_20_haproxy.yml dont voici le contenu:

---
# Paires HAProxy + keepalived (VIP LAN et VIP DMZ).
- name: Load balancers applicatifs LAN et DMZ
  hosts: nlb_haproxy_lan:nlb_haproxy_web
  become: true
  roles:
    - xkub_haproxy_keepalived

playbooks/xkub_20_haproxy.yml

L’objectif est d’associer le rôle xkub_haproxy_keepalived aux serveurs de l’inventaire présent dans les groupes nlb_haproxy_lan et nlb_haproxy_web.

En effet, contrairement aux étapes précédentes, ici, ce ne sont que les serveurs correspondant à la nomenclature prdnlbhap qui nous intéressent et qui ne sont pas membre du cluster K8S.

Role

Attardons-nous sur le rôle xkub_haproxy_keepalived

Comme d’habitude dans le dossier default, au niveau du fichier main.yml, on va retrouver les variables propres à ce rôle.

---
# Rôle xkub_haproxy_keepalived — valeurs par défaut.
# Les variables de zone (haproxy_zone, haproxy_vip, haproxy_backend_*,
# haproxy_peer_group, xkub_haproxy_keepalived_router_id) sont définies dans
# inventory/group_vars/nlb_haproxy_{lan,web}.yml.

xkub_haproxy_keepalived_packages:
  - haproxy
  - keepalived
  - psmisc # fournit killall, utilisé par le check keepalived

# Interface portant la VIP. Sur Rocky Linux 10 virtualisé XCP-ng, les cartes
# xen-netfront sont nommées enX0 — à vérifier sur les VMs (`ip -br a`).
xkub_haproxy_keepalived_vrrp_interface: enX0

# Le MASTER de chaque paire est le premier hostname (tri alphabétique) du
# groupe : prdnlbhap501 (LAN) et prdnlbhap510 (DMZ).
xkub_haproxy_keepalived_is_master: "{{ inventory_hostname == (groups[haproxy_peer_group] | sort | first) }}"
xkub_haproxy_keepalived_priority: "{{ 150 if xkub_haproxy_keepalived_is_master | bool else 100 }}"

# Proxy protocol vers Traefik (send-proxy) : désactivé tant que Traefik n'est
# pas configuré pour l'accepter — activer les deux côtés ensemble.
xkub_haproxy_keepalived_use_proxy_protocol: false

# Timeouts repris de l'existant PhotonOS
xkub_haproxy_keepalived_timeout_client: 30s
xkub_haproxy_keepalived_timeout_server: 30s
xkub_haproxy_keepalived_timeout_connect: 30s

roles/xkub_haproxy_keepalived/defaults/main.yml

L’enjeu principal est d’installer les paquets, notamment haproxy et keepalived, via la variable xkub_haproxy_keepalived_packages. Étant donné qu’on opère sur une infrastructure XCP-ng, le nom des interfaces à utiliser sur les VMs est enX0.

Petite particularité, on n’activera pas de suite le suivi des instances Traefik coté HAProxy, puisqu’elles n’existent pas encore.

Dossier handlers

Côté dossier handlers maintenant, le contenu du fichier main est le suivant

---
- name: Reload haproxy
  ansible.builtin.systemd_service:
    name: haproxy
    state: reloaded

- name: Restart keepalived
  ansible.builtin.systemd_service:
    name: keepalived
    state: restarted

roles/xkub_haproxy_keepalived/handlers/main.yml

Ce sont les commandes de redémarrage pour HAproxy et keepalived. Rien de spécial ici.

Les tasks

On peut enchainer sur le dossier task, ou le fichier main.yml ne fait qu’afficher l’enchainement des taches à réaliser en faisant référence aux autres yamls.

---
# Rôle xkub_haproxy_keepalived — paires de load balancers applicatifs LAN et DMZ.
# Frontends 80/443 sur la VIP → NodePorts Traefik des workers de la zone.
- name: Installation des paquets
  ansible.builtin.import_tasks: 01_install.yml
  tags:
    - xkub
    - xkub.haproxy_keepalived
    - xkub.haproxy_keepalived.install

# Le pare-feu est ouvert AVANT le démarrage des services : dans l'autre sens,
# le premier déploiement laisse une fenêtre où keepalived échange du VRRP encore
# bloqué, ce qui produit une double élection MASTER transitoire (les deux nœuds
# se croient seuls). Ouvrir d'abord puis démarrer est aussi le sens sûr du point
# de vue sécurité : on n'expose jamais un service avant sa règle.
- name: Politique firewalld
  ansible.builtin.import_tasks: 04_firewalld.yml
  tags:
    - xkub
    - xkub.haproxy_keepalived
    - xkub.haproxy_keepalived.firewalld

- name: Configuration HAProxy
  ansible.builtin.import_tasks: 02_haproxy.yml
  tags:
    - xkub
    - xkub.haproxy_keepalived
    - xkub.haproxy_keepalived.haproxy

- name: Configuration keepalived (VIP)
  ansible.builtin.import_tasks: 03_keepalived.yml
  tags:
    - xkub
    - xkub.haproxy_keepalived
    - xkub.haproxy_keepalived.keepalived

roles/xkub_haproxy_keepalived/tasks/main.yml

Notez que l'ordre des tâches est important. On commence par installer les paquets, puis on ouvre le pare-feu, ensuite on configure HAproxy et enfin Keepalived. C'est pourquoi finalement j'ai du basculer l'éxécution du fichier 04_firewalld.yml, intialement prévu pour etre lancé à la fin, avant 02_haproxy.yml

Il faudrait procéder à une rénumerotation des fichiers pour être cohérent

Le premier fichier 01_install.yml traite de l’installation des composants

---
- name: Installer haproxy, keepalived et psmisc
  ansible.builtin.dnf:
    name: "{{ xkub_haproxy_keepalived_packages }}"
    state: present
    update_cache: true

# Le BACKUP doit pouvoir binder la VIP qu'il ne porte pas encore : sans ce
# sysctl, haproxy refuse de démarrer sur le nœud passif.
- name: Autoriser le bind sur une IP non locale (VIP)
  ansible.posix.sysctl:
    name: net.ipv4.ip_nonlocal_bind
    value: "1"
    sysctl_file: /etc/sysctl.d/90-haproxy.conf
    state: present
    reload: true

# SELinux reste ENFORCING (règle 11) : on autorise explicitement haproxy à se
# connecter aux ports non standards (NodePorts 30000-32767) via le booléen dédié.
- name: Autoriser haproxy à joindre les NodePorts (SELinux)
  ansible.posix.seboolean:
    name: haproxy_connect_any
    state: true
    persistent: true

roles/xkub_haproxy_keepalived/tasks/01_install.yml

On installe Haproxy et Keepalived sur chaque serveur ciblé.

Il est nécessaire d’intervenir sur le systctl des VMs, car sans cela, un serveur BACKUP coté HAproxy ne pourrait démarrer son service, car la VIP auquelle il serait rattaché ne serait pas encore disponible (VRRP associe l'IP (la VIP) uniquement sur le MASTER, on autorise donc le kernel à exécuter HAproxy même si l'IP qu'il doit utiliser n'est pas disponible sur le serveur de BACKUP).

Pour des questions de sécurité, on ne retire pas le support de SElinux, on autorise par contre HAproxy à exploiter toute sorte de ports, autres que ceux autorisés par défaut par SElinux.

On passe à 02_haproxy.yml

---
# Un seul template pour les deux zones : la zone est portée par les variables
# de groupe (nlb_haproxy_lan / nlb_haproxy_web) — plus de variantes _lan/_dmz.
- name: Déployer la configuration HAProxy
  ansible.builtin.template:
    src: haproxy.cfg.j2
    dest: /etc/haproxy/haproxy.cfg
    owner: root
    group: root
    mode: "0644"
    validate: haproxy -c -f %s
  notify: Reload haproxy

- name: Activer et démarrer HAProxy
  ansible.builtin.systemd_service:
    name: haproxy
    state: started
    enabled: true

roles/xkub_haproxy_keepalived/tasks/02_haproxy.yml

Ici les choses sont basiques, puisqu’on demande simplement à récupérer le template de configuration HAproxy (qu’on va voir juste après) pour le copier sur les serveurs et redémarrer le service HAproxy (via les commandes définies dans le handler) pour prendre en compte cette nouvelle config.

On passe à 03_keepalived.yml

---
# VRRP en unicast entre les deux membres de la paire (pas d'auth_pass : le
# mécanisme VRRPv2 est faible et déconseillé ; l'unicast + le filtrage
# firewalld limitent la surface). MASTER = prdnlbhap501 / prdnlbhap510.
- name: Déployer la configuration keepalived
  ansible.builtin.template:
    src: keepalived.conf.j2
    dest: /etc/keepalived/keepalived.conf
    owner: root
    group: root
    mode: "0600"
  notify: Restart keepalived

- name: Activer et démarrer keepalived
  ansible.builtin.systemd_service:
    name: keepalived
    state: started
    enabled: true

roles/xkub_haproxy_keepalived/tasks/03_keepalived.yml

Même combat pour keepalived, on récupère la conf (qu’on va voir également juste après) et redémarrage du service dans la foulée.

Enfin on termine par 04_firewalld.yml

---
# firewalld reste actif : on ouvre 80/443 (frontends) et le
# protocole VRRP entre les membres de la paire.
- name: S'assurer que firewalld est actif
  ansible.builtin.systemd_service:
    name: firewalld
    state: started
    enabled: true

- name: Ouvrir les services HTTP/HTTPS
  ansible.posix.firewalld:
    service: "{{ item }}"
    permanent: true
    immediate: true
    state: enabled
  loop:
    - http
    - https

# VRRP est restreint aux SEULES IP des pairs de la paire, jamais ouvert à
# l'ensemble du sous-réseau . C'est la
# contrepartie du choix de ne pas utiliser d'`auth_pass` : l'authentification
# VRRPv2 étant faible par conception, le filtrage réseau est la vraie mesure
# de protection. Sans `source address`, n'importe quel hôte du même L2 pourrait
# annoncer une priorité supérieure, s'emparer de la VIP et intercepter le
# trafic applicatif de la zone. La liste des pairs est dérivée du même groupe
# d'inventaire que les `unicast_peer` de keepalived.conf.j2 : les deux restent
# donc cohérents par construction.
- name: Autoriser le protocole VRRP depuis les pairs de la paire uniquement
  ansible.posix.firewalld:
    rich_rule: >-
      rule family="ipv4"
      source address="{{ hostvars[item].ansible_host }}/32"
      protocol value="vrrp" accept
    permanent: true
    immediate: true
    state: enabled
  loop: "{{ groups[haproxy_peer_group] | difference([inventory_hostname]) | sort }}"

# Nettoyage de l'ancienne règle non restreinte, laissée en place par les runs
# antérieurs au durcissement : sans cette tâche, elle survivrait indéfiniment
# dans la configuration permanente de firewalld et continuerait d'autoriser
# VRRP depuis n'importe quelle source.
- name: Retirer l'ancienne règle VRRP sans restriction de source
  ansible.posix.firewalld:
    rich_rule: rule protocol value="vrrp" accept
    permanent: true
    immediate: true
    state: disabled

roles/xkub_haproxy_keepalived/tasks/04_firewalld.yml

Le but est d’ouvrir les ports nécessaires sur les serveurs et d’autoriser le trafic propre à HAproxy et au protocole VRRP.

C’est ici qu’on joue sur le côté unicast pour que les annonces VRRPs ne se fassent qu’entre les IPS des serveurs HAproxy appartenant à la même paire. On joue sur les variables d’inventaire pour obtenir les bonnes règles.

Les templates

On passe au dossier le plus intéressant, soit le dossier templates

On y retrouve deux fichiers

haproxy.cfg.j2

# {{ ansible_managed }}
# HAProxy zone {{ haproxy_zone | upper }} — VIP {{ haproxy_vip }}
# Frontends 80/443 -> NodePorts Traefik {{ haproxy_zone }} (décision D2).
# L'API Kubernetes (6443) n'est pas frontée ici : kube-vip (décision D1).

global
    log /dev/log local0
    maxconn 4000
    user haproxy
    group haproxy
    daemon

defaults
    mode tcp
    log global
    option tcplog
    option dontlognull
    timeout client  {{ xkub_haproxy_keepalived_timeout_client }}
    timeout server  {{ xkub_haproxy_keepalived_timeout_server }}
    timeout connect {{ xkub_haproxy_keepalived_timeout_connect }}

frontend fe_traefik_{{ haproxy_zone }}_http
    bind {{ haproxy_vip }}:80
    default_backend bk_traefik_{{ haproxy_zone }}_http

frontend fe_traefik_{{ haproxy_zone }}_https
    bind {{ haproxy_vip }}:443
    default_backend bk_traefik_{{ haproxy_zone }}_https

backend bk_traefik_{{ haproxy_zone }}_http
    option tcp-check
{% for host in groups[haproxy_backend_group] | sort %}
    server {{ host.split('.')[0] }} {{ hostvars[host].ansible_host }}:{{ haproxy_backend_port_http }} check
{% endfor %}

backend bk_traefik_{{ haproxy_zone }}_https
    option tcp-check
{% for host in groups[haproxy_backend_group] | sort %}
    server {{ host.split('.')[0] }} {{ hostvars[host].ansible_host }}:{{ haproxy_backend_port_https }} check{{ ' send-proxy' if xkub_haproxy_keepalived_use_proxy_protocol | bool else '' }}
{% endfor %}

roles/xkub_haproxy_keepalived/templates/haproxy.cfg.j2

C’est le template associé à la configuration de HAproxy. Il va s’appuyer sur les variables du rôle et sur le nom des hosts récupéré dans l’inventaire Ansible.

La conf est assez simple. On retrouve l’usage du mode « tcp » comme expliqué précédemment.

Ensuite on définit les interfaces de frontend, soit l’interface qui va exploiter la VIP VRRP gérée par Keealived.

Ensuite on définit les serveurs de backend, soit les nodes K8S vers lesquels renvoyer les requêtes.

Grâce à la notion de variable et d’inventaire coté Ansible le contenu de ce fichier sera dynamiquement rempli à la copie vers les serveurs cibles en tenant compte de l’emplacement réseau du serveur (LAN/DMZ)

C’est au tour de la configuration de Keepalived keepalived.conf.j2:

# {{ ansible_managed }}
# keepalived zone {{ haproxy_zone | upper }} — VIP {{ haproxy_vip }}/{{ haproxy_vip_prefix }}
global_defs {
    router_id {{ inventory_hostname.split('.')[0] }}
}

# La VIP ne reste sur ce nœud que si haproxy tourne
vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    fall 2
    rise 2
}

vrrp_instance VI_{{ haproxy_zone | upper }} {
    state {{ 'MASTER' if xkub_haproxy_keepalived_is_master | bool else 'BACKUP' }}
    interface {{ xkub_haproxy_keepalived_vrrp_interface }}
    virtual_router_id {{ xkub_haproxy_keepalived_router_id }}
    priority {{ xkub_haproxy_keepalived_priority }}
    advert_int 1
    unicast_src_ip {{ ansible_host }}
    unicast_peer {
{% for host in groups[haproxy_peer_group] | sort if host != inventory_hostname %}
        {{ hostvars[host].ansible_host }}
{% endfor %}
    }
    virtual_ipaddress {
        {{ haproxy_vip }}/{{ haproxy_vip_prefix }}
    }
    track_script {
        chk_haproxy
    }
}

roles/xkub_haproxy_keepalived/templates/keepalived.conf.j2

Ici aussi elle est rendue dynamique grâce aux variables du rôle et de l’inventaire.

On détermine l’identifiant du routeur (router_id) à l’intérieur de la paire en se basant sur le nom du serveur qui hébergera la configuration.

La section « vrrp_script chk_haproxy » permet de définir les commandes de contrôles à jouer pour checker l’état du service HAproxy. On appel pas un script externe on définit directement les commandes à utiliser:

"/usr/bin/killall -0 haproxy"
  interval 2
  fall 2
  rise 2

Puis on définit le VRID avec la notion de MASTER et de BACKUP pour indiquer à VRRP quel doit être la priorité pour chaque serveur dans la paire HA. Cette notion est importante, car par défaut, on va attribuer une priorité plus élevée au premier serveur de chaque pair pour définir qui est MASTER en situation nominal.

Le tour d’horizon est bouclé, il ne reste plus qu’à appliquer

Mise en œuvre

Pour ça on se rend dans WSL puis dans l’arborescence Ansible pour lancer l’exécution du playbook

ansible-playbook playbooks/xkub_20_haproxy.yml
Lancement du playbook Ansible xkub_20_haproxy.yml depuis WSL

Cliquez sur l'image pour l'agrandir.

Chaque serveur parmi les quatre qui nous intéresse va être traité et les différents composants installés avec leur configuration associés.

(Comme d’habitude on n’hésitera pas à relancer le playbook plusieurs fois pour s’assurer qu’il est rejouable à souhait)

PLAY RECAP du playbook xkub_20_haproxy.yml rejoué : changed=0 sur les quatre load balancers

Cliquez sur l'image pour l'agrandir.

Si tout va bien, on devrait pouvoir retrouver sur chacun des serveurs la configuration HAproxy et Keepalived en place.

Controles et tests

Contenu des fichiers de configuration

Vérifions sur les serveurs associé à la paire du lan, soit prdnlbhap501/prdnlbhap502

Voici la configuration HAproxy sur chaque node (/etc/haproxy/haproxy.cfg)

  • Sur prdnlbhap501 soit le MASTER
# Ansible managed
# HAProxy zone LAN — VIP 192.168.10.187
# Frontends 80/443 -> NodePorts Traefik lan (décision D2).
# L'API Kubernetes (6443) n'est pas frontée ici : kube-vip (décision D1).

global
    log /dev/log local0
    maxconn 4000
    user haproxy
    group haproxy
    daemon

defaults
    mode tcp
    log global
    option tcplog
    option dontlognull
    timeout client  30s
    timeout server  30s
    timeout connect 30s

frontend fe_traefik_lan_http
    bind 192.168.10.187:80
    default_backend bk_traefik_lan_http

frontend fe_traefik_lan_https
    bind 192.168.10.187:443
    default_backend bk_traefik_lan_https

backend bk_traefik_lan_http
    option tcp-check
    server prdk8smin501 192.168.10.171:30080 check
    server prdk8smin502 192.168.10.172:30080 check

backend bk_traefik_lan_https
    option tcp-check
    server prdk8smin501 192.168.10.171:30443 check
    server prdk8smin502 192.168.10.172:30443 check

prdnlbhap501 — /etc/haproxy/haproxy.cfg

  • Sur prdnlbhap502 soit le BACKUP
# Ansible managed
# HAProxy zone LAN — VIP 192.168.10.187
# Frontends 80/443 -> NodePorts Traefik lan (décision D2).
# L'API Kubernetes (6443) n'est pas frontée ici : kube-vip (décision D1).

global
    log /dev/log local0
    maxconn 4000
    user haproxy
    group haproxy
    daemon

defaults
    mode tcp
    log global
    option tcplog
    option dontlognull
    timeout client  30s
    timeout server  30s
    timeout connect 30s

frontend fe_traefik_lan_http
    bind 192.168.10.187:80
    default_backend bk_traefik_lan_http

frontend fe_traefik_lan_https
    bind 192.168.10.187:443
    default_backend bk_traefik_lan_https

backend bk_traefik_lan_http
    option tcp-check
    server prdk8smin501 192.168.10.171:30080 check
    server prdk8smin502 192.168.10.172:30080 check

backend bk_traefik_lan_https
    option tcp-check
    server prdk8smin501 192.168.10.171:30443 check
    server prdk8smin502 192.168.10.172:30443 check

prdnlbhap502 — /etc/haproxy/haproxy.cfg

En fait pour HAproxy, la configuration est identique, puisque chaque serveur doit pouvoir opérer les mêmes fonctions. C'est davantage au niveau de Keepalived que ça se joue pour savoir qui doit porter la VIP.

Voici le contenu de la configuration (/etc/keepalived/keepalived.conf)

  • Sur prdnlbhap501 soit le MASTER
# Ansible managed
# keepalived zone LAN — VIP 192.168.10.187/24
global_defs {
    router_id prdnlbhap501
}

# La VIP ne reste sur ce nœud que si haproxy tourne
vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    fall 2
    rise 2
}

vrrp_instance VI_LAN {
    state MASTER
    interface enX0
    virtual_router_id 51
    priority 150
    advert_int 1
    unicast_src_ip 192.168.10.185
    unicast_peer {
        192.168.10.186
    }
    virtual_ipaddress {
        192.168.10.187/24
    }
    track_script {
        chk_haproxy
    }
}

prdnlbhap501 — /etc/keepalived/keepalived.conf

  • Sur prdnlbhap502 soit le Backup
# Ansible managed
# keepalived zone LAN — VIP 192.168.10.187/24
global_defs {
    router_id prdnlbhap502
}

# La VIP ne reste sur ce nœud que si haproxy tourne
vrrp_script chk_haproxy {
    script "/usr/bin/killall -0 haproxy"
    interval 2
    fall 2
    rise 2
}

vrrp_instance VI_LAN {
    state BACKUP
    interface enX0
    virtual_router_id 51
    priority 100
    advert_int 1
    unicast_src_ip 192.168.10.186
    unicast_peer {
        192.168.10.185
    }
    virtual_ipaddress {
        192.168.10.187/24
    }
    track_script {
        chk_haproxy
    }
}

prdnlbhap502 — /etc/keepalived/keepalived.conf

La différence est subtile, mais on peut remarquer que le nœud MASTER (501) dispose d’une priorité de 150 alors que le BACKUP (502) dispose d’une priorité de 100. C’est de cette manière que, par défaut, l'IP de la VIP 192.168.10.187 va être portée et active sur prdnlbhap501.

On remarquera aussi les adresses d’unicast utilisées pour diffuser le heartbeat qui correspondent aux adresses IP réelless des serveurs.

Je ne vais pas détailler les configurations en DMZ sur les serveurs prdnlbhap510/prdnlbhap511

C’est exactement la même chose, mais avec la plage d’IP du réseau de DMZ (192.168.5.x).

Tests de bascule

Maintenant que tout cela est fonctionnel.

C’est le moment de tester.

Déjà, utilisons Ansible pour lister l’état des IP sur les interfaces. Pour cela on peut utiliser la commande

ansible nlb_haproxy_lan:nlb_haproxy_web -b -m shell -a "ip -br a show enX0"
Sortie Ansible de ip -br a show enX0 sur les quatre load balancers : seuls les nœuds MASTER portent la VIP

Cliquez sur l'image pour l'agrandir.

On remarque en sortie que seuls les nodes MASTER disposent de deux IPs puisqu’ils ont leur IP réelle et l’IP de la VIP

Ce qui correspond bien à notre situation nominale.

C’est quelque chose qu’on peut vérifier directement sur les serveurs (ip addr)

ip addr sur prdnlbhap501 : la VIP 192.168.10.187 apparait en adresse secondaire de enX0

Cliquez sur l'image pour l'agrandir.

ip addr sur prdnlbhap502 : le nœud BACKUP ne porte que son adresse réelle 192.168.10.186

Cliquez sur l'image pour l'agrandir.

Testons maintenant la résilience de l’ensemble.

Scénario d’arrêt brutale d’un serveur HAproxy

On commence par faire un arrêt brutal du serveur « prdnlbhap501 » soit le MASTER sur la paire LAN.

Pour évaluer la coupure, on ping en permanence l’IP de VIP. On s’aperçoit alors qu’un paquet est perdu, juste le temps que le BACKUP, soit prdnlbhap502, récupère la gestion de la VIP

Arrêt brutal de prdnlbhap501 : un seul paquet perdu sur le ping continu de la VIP 192.168.10.187

Cliquez sur l'image pour l'agrandir.

On peut le confirmer en faisant un « ip addr » sur ce serveur pour se rendre compte que maintenant la VIP est bien rattaché à ce dernier.

ip addr sur prdnlbhap502 après bascule : la VIP 192.168.10.187 est désormais portée par le nœud BACKUP

Cliquez sur l'image pour l'agrandir.

Scénario perte du service HAproxy

Utilisons maintenant la paire en DMZ pour simuler l’autre scénario, soit la perte de HAproxy, mais sans éteindre le node.

ip addr sur prdnlbhap510 : le MASTER de la paire DMZ porte la VIP 192.168.5.187

Cliquez sur l'image pour l'agrandir.

Pour cela on stoppe simplement le service HAproxy sur prdnlbhap510. Ici aussi, via un ping continu, on observe une perte d'un paquet.

Arrêt du service haproxy sur prdnlbhap510 et perte d'un paquet sur le ping de la VIP 192.168.5.187

Cliquez sur l'image pour l'agrandir.

C’est bien le serveur prdnlbhap511 qui a récupéré la VIP

ip addr sur prdnlbhap511 : la VIP 192.168.5.187 a basculé sur le nœud BACKUP de la DMZ

Cliquez sur l'image pour l'agrandir.

Dans cette situation, l’interface réelle de prdnlbhap510 est toujours active et répond. Cependant, puisque HAProxy n’est plus opérationnel, le contrôle de keepalived surveillant HAProxy a incité prdnlbhap510 à abandonner l’adresse IP virtuelle (VIP), qui a alors été transférée vers prdnlbhap511 (en réalité, les priorités ont été modifiées pour que prdnlbhap511 devienne MASTER).

Si maintenant on rétablit le service HAproxy, sur prdnlbhap510 alors la VIP va revenir sur ce dernier.

Service haproxy relancé sur prdnlbhap510 : la VIP 192.168.5.187 revient sur le nœud MASTER

Cliquez sur l'image pour l'agrandir.

C’est d’ailleurs un point qu’on n’a pas précisé, mais la configuration est en mode rollback. Si les conditions permettent à nouveau d’activer le MASTER, alors la VIP sera automatiquement rétablie vers celui-ci.

Conclusion

Nous voilà arrivés au bout de cet article. L’usage de Ansible nous a permis d’automatiser une fois de plus la configuration des composants nécessaires, tout en me servant de base pour expliquer le fonctionnement et la logique derrière HAproxy/Keepalived. Là aussi l’IA telle que décrit en début de cookbook m’a permis d’accélérer l’écriture des yamls et des configurations, mais je n’ai pas pour autant perdu la compréhension du sujet.

Kubernetes est une chose, mais il est important de savoir que le plus souvent des composants tiers sont tout de même nécessaires pour traiter de la mise en ligne d’un cluster K8S. Ces éléments sont aussi critiques que les nodes Kubernetes eux-mêmes et il faut les déployer en connaissance de cause avec le bon niveau de résilience.

Disposer de plusieurs serveurs au sein d’un cluster K8S pour une bonne disponibilité n’est pas efficace si les composants externes utiles à l’accès aux applications du cluster ne sont pas résilients eux-mêmes.

Le duo HAproxy/Keepalived est un standard reconnu et robuste, il faut juste les implémenter intelligemment et en lien avec nos besoins.

Maintenant qu’on a traité de ces problématiques, on va pouvoir poursuivre sur le déploiement de Kubernetes à proprement parler dans le prochain article à paraitre.