Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Router par noms et chemins avec ACL et maps

Par mis à jour le

Améliorer cette page

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.

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_backend et default_backend en 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.

La leçon suppose que HAProxy est installé et que vous lisez un fichier haproxy.cfg.

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.

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_lue

Cet 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.

FetchCe qu'il retientPiège
pathLe chemin exact, ni plus ni moins/api/ n'est pas /api
path_begTout chemin qui commence par le motifRetient les voisins qui partagent le début du nom
path_endTout chemin qui finit par le motifUtile pour une extension, pas pour une branche
path_dirUn 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_regUne expression régulièrePuissant, 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.

Arbre de décision pour une requête boutique, chemin /panierarchive. La règle 1 consulte la map des noms : si elle nomme l'hôte, la requête part vers le backend de la map et l'évaluation s'arrête, sans que les règles 2 et 3 soient lues. Sinon la règle 2 teste le préfixe /panier, puis la règle 3 teste /api/v2/, puis le default_backend.oui, on s'arrêtenonoui, on s'arrêtenonoui, on s'arrêtenonRequêteboutique, /panierarchiveRègle 1la map nomme-t-elle le Host ?Règle 2le chemin commence-t-il par /panier ?Règle 3le chemin commence-t-il par /api/v2/ ?default_backendBackend de la mapBackend panierRefus 404
Une règle qui a toujours une réponse en tête ferme la porte à toutes les suivantes. Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

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.

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 nom
blog.pub.lab.test blog
wiki.pub.lab.test wiki

Il 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 :

Fenêtre de terminal
echo 'show map' | sudo socat -t2 stdio /run/haproxy/admin.sock
echo 'get map /etc/haproxy/maps/hotes.map blog.pub.lab.test' | sudo socat -t2 stdio /run/haproxy/admin.sock
echo 'set map /etc/haproxy/maps/hotes.map blog.pub.lab.test wiki' | sudo socat -t2 stdio /run/haproxy/admin.sock

show 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.

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 :

Fenêtre de terminal
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.

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.

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.

  • La première règle use_backend gagnante arrête l'évaluation ; les http-request passent avant tous les use_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, or donne un OU.
  • Un préfixe n'est pas un chemin : path_beg /docs retient /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_backend calculé (%[...]) dont le nom ne correspond à aucun backend se rabat sur default_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 map n'é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.

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

8 questions
8 min.
70% requis

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

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn