Router avec HAProxy, c'est décider pour chaque requête quel backend la sert, et surtout quelle requête n'en sert aucun. Cette leçon s'adresse à une personne qui sait déjà installer HAProxy et lire un frontend : elle apprend à écrire des ACL (règles de test sur la requête), à les ordonner, à consulter une map (table clé-valeur) pour les noms de domaine, puis à refuser ce qui n'a pas de route. Le fil rouge est un piège réel : quatre règles chacune justes, qui ensemble envoient une requête au mauvais endroit, et qu'aucune lecture ligne à ligne ne dénonce.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »La leçon va de la règle isolée à la table de routage prouvée.
- Écrire une ACL (un test nommé sur la requête) avec un fetch (qui extrait une valeur), un converter (qui la transforme) et une condition.
- Ordonner
use_backendetdefault_backenden sachant que la première règle gagnante arrête tout. - Refuser un nom inconnu ou une branche de chemins sans atteindre un backend.
- Router par nom avec une map, et la modifier à chaud par le socket d'administration.
- Prouver chaque route chez le backend qui a réellement servi la requête.
Prérequis
Section intitulée « Prérequis »La leçon suppose que HAProxy est installé et que vous lisez un fichier haproxy.cfg.
- Installer HAProxy, lire
frontendetbackend, valider avechaproxy -c: Installer HAProxy et lire ses sections de configuration. - Lire une requête HTTP, son
Host, son chemin et sa chaîne de requête : Lire HTTP avant de configurer un proxy. - Le lab tourne sur Debian 13, avec HAProxy 3.4.7 (la branche LTS) installé depuis le dépôt
trixie-backports-3.4dehaproxy.debian.net. Les faits de cette leçon sont ceux de cette version.
Comment HAProxy choisit-il un backend ?
Section intitulée « Comment HAProxy choisit-il un backend ? »HAProxy évalue les règles d'un frontend dans un ordre précis et s'arrête à la première qui désigne un backend ; une requête qu'aucune règle ne retient tombe dans default_backend, ou n'est servie par personne s'il n'y en a pas. Comprendre cet ordre est l'essentiel de la leçon, parce que toutes les erreurs de routage en viennent.
Un frontend est la section de la configuration qui reçoit les requêtes des clients ; un backend est le groupe de serveurs qui les sert. Dans un frontend en mode http, trois familles de directives se succèdent. Les règles http-request passent d'abord : elles peuvent refuser (deny), rediriger ou réécrire une requête. Viennent ensuite les règles use_backend, évaluées dans l'ordre où vous les écrivez ; la première dont la condition est vraie choisit le backend, et les suivantes ne sont plus lues. Reste default_backend, qui s'applique quand aucune use_backend n'a retenu la requête. Cette leçon suppose des backends en bonne santé : ce qui se passe quand l'un d'eux tombe (sondes, retrait, drainage) est l'objet de Choisir la répartition et mesurer les sondes.
Retenez la conséquence : l'ordre entre deux directives du même type compte, alors que l'ordre entre types différents est fixé par HAProxy, quelle que soit la position dans le fichier. Une règle http-request deny écrite à la fin d'un frontend s'exécute quand même avant tout use_backend.
Une précision sur la source : le manuel de la branche 3.4 ne dit que « evaluated in their declaration order » à l'intérieur d'une famille de règles. L'ordre entre familles (les http-request avant tous les use_backend) vient du code de HAProxy, pas d'une phrase du manuel. Écrivez donc vos refus avant les use_backend : le fichier se lit alors dans l'ordre réel d'exécution.
Qu'est-ce qu'une ACL, un fetch et un converter ?
Section intitulée « Qu'est-ce qu'une ACL, un fetch et un converter ? »Une ACL (Access Control List) est un test nommé sur la requête : un fetch extrait une valeur, un converter la transforme éventuellement, et un motif la compare. On la déclare par acl <nom> <fetch> <motif>, puis on la cite dans une condition if <nom>.
Un fetch (« échantillon ») va chercher une information : path donne le chemin de l'URL, req.hdr(host) l'en-tête Host, src l'adresse du client, method la méthode HTTP. Un converter transforme cette valeur avant la comparaison, par exemple lower pour passer en minuscules. On l'enchaîne avec une virgule : req.hdr(host),lower. Le motif est ce à quoi on compare.
Les conditions combinent les ACL. Deux ACL côte à côte se lisent en ET (if a b), le mot or donne un OU (if a or b), et le point d'exclamation une négation (if !a). Une ACL peut aussi porter le même nom sur plusieurs lignes : les lignes s'ajoutent en OU, ce qui permet de lister plusieurs motifs sous un seul nom.
frontend public bind :80
acl hote_blog req.hdr(host),lower -m str blog.pub.lab.test acl chemin_docs path_beg /docs/ acl methode_lue method GET HEAD
use_backend docs if hote_blog chemin_docs methode_lueCet exemple est volontairement différent du lab : trois ACL, une condition qui les combine en ET. Lisez la dernière ligne comme une phrase : « envoyer vers docs si l'hôte est blog.pub.lab.test, et que le chemin commence par /docs/, et que la méthode est GET ou HEAD ». L'option -m str demande une comparaison de chaîne exacte ; sans elle, HAProxy applique la méthode par défaut du fetch.
Quel critère de chemin choisir : exact, préfixe ou répertoire ?
Section intitulée « Quel critère de chemin choisir : exact, préfixe ou répertoire ? »Un préfixe n'est pas un chemin : path_beg /panier retient aussi /panierarchive, qui n'a rien à voir avec /panier. Le critère de comparaison décide de ce que la règle attrape, et le choix se fait selon ce qu'on veut vraiment dire.
| Fetch | Ce qu'il retient | Piège |
|---|---|---|
path | Le chemin exact, ni plus ni moins | /api/ n'est pas /api |
path_beg | Tout chemin qui commence par le motif | Retient les voisins qui partagent le début du nom |
path_end | Tout chemin qui finit par le motif | Utile pour une extension, pas pour une branche |
path_dir | Un ou plusieurs répertoires complets du chemin, délimités par des barres, situés n'importe où | /images/png/logo/32x32.png correspond à /png/logo ou à images/png, mais pas à png : ce n'est pas un préfixe, et le motif peut retenir un chemin d'une tout autre branche |
path_reg | Une expression régulière | Puissant, plus coûteux, plus difficile à relire |
Prenons une branche /docs à ne pas confondre avec /docs-archive. path_beg /docs attrape les deux. Le motif /docs/, avec la barre finale, écarte /docs-archive mais n'attrape plus /docs seul. Pour couvrir à la fois la ressource et sa branche, on déclare deux motifs sous le même nom d'ACL, le chemin exact et le préfixe avec barre. Pour borner une branche à partir de la racine, path_beg avec barre finale convient ; path_dir laisserait passer, vers votre backend, des chemins qui contiennent le même nom plus profond. Comparez toujours la règle aux chemins voisins que votre service expose, pas seulement au chemin que vous avez en tête.
Deux autres sources d'écart. Les chemins sont sensibles à la casse : /Docs/ n'est pas /docs/, sauf si on ajoute l'option -i à l'ACL. Et le Host peut porter un port (blog.pub.lab.test:8080), ce que req.hdr(host) renvoie tel quel : une map qui ne connaît que le nom sans port ne le trouvera pas. Normalisez avec lower, et au besoin retirez le port avec le converter field(1,:).
Pourquoi l'ordre des règles décide-t-il de tout ?
Section intitulée « Pourquoi l'ordre des règles décide-t-il de tout ? »Quand deux règles use_backend peuvent retenir la même requête, seule la première compte ; une règle large placée en tête rend toutes les suivantes inatteignables, même si elles sont écrites, lisibles et justes. C'est la faute que le lab fait trouver, et c'est la plus difficile à voir parce qu'aucune ligne n'est fausse.
Le cas typique est la map avec valeur par défaut. Une règle use_backend qui consulte une map et lui donne un nom de repli a toujours une réponse : elle désigne un backend pour n'importe quelle requête. Placée avant les exceptions de chemin, elle les prive de toute chance de s'exécuter. Le remède est de placer les exceptions les plus étroites en premier, puis les règles larges, et de n'écrire de règle qui a toujours une réponse qu'à la fin.
Le lab exige donc la conjonction de deux mesures : le trafic légitime est servi, et l'exception de chemin est respectée. La première moitié seule ne prouve rien, car elle est satisfaite en ne configurant rien du tout. Une exception de chemin ne prime sur la table par défaut que par l'ordre des use_backend, et c'est cet ordre que la seconde moitié contrôle.
Une méthode simple pour relire une table de routage : classez les règles de la plus spécifique à la plus générale, puis demandez, pour chaque règle, « quelle requête arrive jusqu'ici ? ». Si la réponse est « aucune », la règle est morte. Ce raisonnement est aussi celui que le dossier du lab vous demande d'écrire.
Comment refuser une requête sans qu'elle touche un backend ?
Section intitulée « Comment refuser une requête sans qu'elle touche un backend ? »Un refus se fait par une règle http-request deny, qui passe avant toute règle qui route, ou par un backend de refus sans serveur qui répond lui-même l'erreur. Un refus qui atteint quand même un backend n'est pas un refus : c'est une route qui répond une erreur après avoir déjà consommé une requête chez lui.
Le geste le plus direct est http-request deny deny_status 404 if <condition>. Il s'évalue avant les use_backend, comme vu plus haut, donc il ne dépend pas de la place où vous l'écrivez par rapport à eux. Le statut 403 (interdit) dit « je sais ce que vous demandez, c'est refusé » ; le 404 (introuvable) dit « cela n'existe pas ». Le lab accepte l'un ou l'autre : choisissez-en un et tenez-le.
Il faut distinguer trois situations de refus. Une branche à refuser (/api/v2/ dans le lab, qui n'est pas déployée : la router vers l'API la ferait répondre comme si elle existait) se traite par une condition sur le chemin. Un nom inconnu se traite par la valeur de repli de la map, qui doit désigner un backend de refus, ou par un default_backend de refus. Et le chemin imprévu sous un nom connu n'est pas refusé : la table donne à chaque nom une route par défaut, et c'est elle qui s'applique. Attention à ce qu'on prétend : le lab refuse les noms sans route et la branche /api/v2/, pas tout chemin non listé.
Que se passe-t-il sans default_backend ? Une requête qu'aucune règle ne retient n'a pas de backend : HAProxy répond 503 Service Unavailable, et le manuel de use_backend (branche 3.4) le dit expressément pour un frontend sans default_backend. C'est un défaut de décision plus qu'un refus voulu, et le test du lab n'accepte que 403 ou 404. Écrivez un refus explicite plutôt que de compter sur l'absence de règle.
Comment router par nom avec une map ?
Section intitulée « Comment router par nom avec une map ? »Une map est un fichier de paires « clé valeur » que HAProxy charge au démarrage et interroge avec le converter map, ce qui sort la table de routage de la configuration. Quand vous avez dix noms au lieu de deux, dix lignes d'ACL et de use_backend deviennent dix lignes de données, relisibles et modifiables sans toucher aux règles.
Le fichier est du texte brut, une paire par ligne, avec des commentaires en # :
# nom publié -> backend par défaut de ce nomblog.pub.lab.test blogwiki.pub.lab.test wikiIl se consulte par map(<fichier>,<repli>) appliqué à une valeur extraite de la requête. La règle use_backend %[req.hdr(host),lower,map(/etc/haproxy/maps/hotes.map,refus)] lit l'en-tête Host, le met en minuscules, cherche le résultat dans le fichier et obtient un nom de backend. Si la clé est absente, le converter renvoie la valeur de repli, ici refus, qui doit être un backend existant. La notation %[ ... ] signifie que le nom du backend est calculé au moment de la requête, et c'est là le piège : si le nom calculé ne correspond à aucun backend, HAProxy n'évalue plus aucune autre règle et applique default_backend, sans erreur à la validation de la configuration (haproxy -c). Une faute de frappe dans la map ou dans le repli passe donc la validation et se découvre en production.
Un backend désactivé ou non publié se comporte autrement : la règle use_backend qui le retient est ignorée, et l'évaluation continue avec les règles suivantes.
La map porte la route par défaut de chaque nom ; les exceptions de chemin restent des règles. C'est une répartition saine : la donnée qui change souvent (les noms) vit dans le fichier, la logique rare (les exceptions) dans la configuration. Un nom absent de la table n'a pas de route, donc la ligne qui consulte la map doit désigner un repli, et ce repli doit refuser.
Un piège fréquent : une map décorative. Elle est chargée, mais une ACL placée avant elle tranche déjà le nom. La map existe et ne décide de rien, et on s'en aperçoit le jour où on la modifie sans effet. Le lab le mesure : il change une entrée à chaud et exige que le routage suive.
Comment modifier une map à chaud, et que devient le changement ?
Section intitulée « Comment modifier une map à chaud, et que devient le changement ? »Le socket d'administration modifie la map dans la mémoire du processus, tout de suite et sans rechargement ; mais le rechargement relit le fichier, donc le changement à chaud disparaît s'il n'est pas aussi écrit dans le fichier. La mémoire et le fichier sont deux versions de la même table, et seul le fichier survit.
Les commandes passent par socat sur le socket UNIX du lab :
echo 'show map' | sudo socat -t2 stdio /run/haproxy/admin.sockecho 'get map /etc/haproxy/maps/hotes.map blog.pub.lab.test' | sudo socat -t2 stdio /run/haproxy/admin.sockecho 'set map /etc/haproxy/maps/hotes.map blog.pub.lab.test wiki' | sudo socat -t2 stdio /run/haproxy/admin.sockshow map liste les fichiers chargés, get map lit une clé, set map change la valeur d'une clé existante. Pour une clé nouvelle, on utilise add map ; attention, il n'écrase pas : appliqué à une clé déjà présente, il crée un doublon au lieu de la modifier, ce que seul set map fait. Pour retirer une clé, del map. Ces commandes ne produisent aucun fichier : je ne montre pas de sortie, car elle dépend de votre table.
Le test du lab joue ce scénario en deux moitiés. Il change une entrée par le socket et vérifie que le routage suit, ce qui prouve que la map décide vraiment. Puis il recharge HAProxy et vérifie que le routage est revenu à ce que dit le fichier. La seconde moitié montre pourquoi un changement à chaud qu'on veut garder se recopie dans le fichier : le rechargement relit le fichier, et la valeur d'origine revient. Dans le lab, le changement est celui du test : il ne doit pas se retrouver dans le fichier, sinon le routage ne revient pas après le rechargement. En exploitation, c'est l'inverse : un changement voulu, que vous avez fait en mémoire, se recopie dans le fichier pour que le rechargement le conserve.
La règle d'usage est donc la suivante : utilisez le changement à chaud pour agir vite (couper un nom, rediriger pendant un incident), puis écrivez la même modification dans le fichier, puis rechargez avec systemctl reload haproxy. Un changement qui n'existe qu'en mémoire est une dette qui s'efface au prochain rechargement, ou au prochain redémarrage.
Comment prouver qu'une route est la bonne ?
Section intitulée « Comment prouver qu'une route est la bonne ? »Une route se prouve chez le backend, jamais dans la réponse rendue : la réponse peut avoir l'air normale alors qu'elle vient du mauvais service. Un default_backend panier répond 200 avec un corps plausible pour n'importe quel nom. Seul le backend sait s'il a servi la requête.
Le lab fait donc compter à chaque backend les requêtes qu'il a servies, et expose ce compteur sur la boucle locale (127.0.0.1). La validation lit les compteurs avant et après chaque requête, et exige un delta de 1 chez le backend attendu et de 0 chez tous les autres. Pour un refus, elle exige un statut 403 ou 404 et aucun compteur qui bouge : un refus qui touche un backend n'est pas un refus. Chaque requête porte un jeton unique en chaîne de requête, que HAProxy ne compte pas dans path : il rend la ligne de journal reconnaissable sans changer la décision.
Trois familles de cas se testent. Les chemins voisins : /panier, /panier/ajouter, /panierarchive et /panier-public pour la règle du préfixe. Les noms imprévus, tirés au hasard à chaque exécution : une liste écrite dans le test se ferait traiter nommément, alors qu'on veut que tout nom absent de la table soit refusé. Et les variantes de casse : un Host en majuscules doit atteindre la même route que sa version en minuscules, ce que lower garantit.
Vous pouvez faire la même mesure à la main. Lisez le compteur de chaque backend tel que le lab le met à disposition, envoyez une requête avec un en-tête Host choisi, relisez les compteurs :
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: blog.pub.lab.test' 'http://127.0.0.1/docs/?jeton=essai1'Cette commande vous donne le statut ; la preuve est la différence des compteurs de vos backends avant et après. Un statut 200 seul ne prouve rien.
Que doit contenir le dossier de décision ?
Section intitulée « Que doit contenir le dossier de décision ? »Le dossier explique l'ordre de vos décisions et tranche les deux cas qu'aucune règle ne résout d'elle-même : un préfixe qui attrape un voisin, et une branche à refuser plutôt qu'à router. Il est relu par un humain, et son raisonnement n'est pas noté : le test vérifie seulement qu'il existe, qu'il fait plus d'une ligne par cas, et qu'il traite ces deux sujets.
Un bon dossier est une suite de réponses courtes à des questions que votre relecteur se poserait. Dans quel ordre les règles sont-elles évaluées, et pourquoi cet ordre ? Quelle requête ferait mentir chaque règle si on la déplaçait ? Quel chemin voisin avez-vous testé, et qu'avez-vous mesuré ? Pourquoi refuser plutôt que router une branche non déployée ? Un dossier qui énumère des mots-clés apprend à écrire des mots-clés ; un dossier qui raconte la décision apprend à raisonner.
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab vous donne un HAProxy en service, quatre backends sains, un socket d'administration et une table de routage fausse de quatre façons qui sont chacune juste à elle seule. Vous la réparez pour que chaque combinaison de nom et de chemin atteigne le backend de la recette, y compris les chemins voisins ; vous faites en sorte qu'un nom imprévu n'atteigne aucun backend ; vous montrez que la map décide et qu'un changement à chaud ne survit pas au rechargement ; puis vous rédigez le dossier. La validation lit les compteurs des backends, pas les réponses.
À retenir
Section intitulée « À retenir »- La première règle
use_backendgagnante arrête l'évaluation ; leshttp-requestpassent avant tous lesuse_backend, quelle que soit leur place dans le fichier. - Une ACL combine un fetch (
path,req.hdr(host)), un converter (lower) et un motif ; deux ACL dans une condition se lisent en ET,ordonne un OU. - Un préfixe n'est pas un chemin :
path_beg /docsretient/docs-archive. Comparez toujours aux chemins voisins, et gardez la casse en tête (-i). - Une map avec valeur de repli a toujours une réponse : placée en tête, elle rend inatteignable tout ce qui la suit.
- Un
use_backendcalculé (%[...]) dont le nom ne correspond à aucun backend se rabat surdefault_backend, sans erreur à la validation : testez chaque nom, repli compris. - Un refus ne doit toucher aucun backend ; un nom imprévu se refuse par le repli de la map, une branche non déployée par une condition sur le chemin.
- Un changement de map à chaud (
set map) vit en mémoire ; le rechargement relit le fichier, donc le changement se recopie dans le fichier.add mapn'écrase pas : il crée un doublon. - Une exception de chemin ne prime sur la table que par l'ordre des
use_backend; le lab exige le trafic légitime servi et l'exception respectée. - Une route se prouve chez le backend par un compteur qui bouge de 1 chez lui et de 0 ailleurs, jamais par la réponse rendue.
- Un nom connu garde sa route par défaut pour un chemin imprévu : le refus porte sur les noms et sur les branches nommées.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Choisir la répartition et mesurer les sondes
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications