HAProxy : configuration avancée ACL, health checks et sticky sessions

Techniques avancées HAProxy pour PrestaShop : ordonner les ACL, checks applicatifs sûrs, gérer les sticky sessions et ajouter observabilité pour une production fiable.

Schéma de configuration HAProxy avec ACL, health checks, et sticky sessions affiché dans un style holographique.

Table des matières :

  1. Contexte, versions ciblées et pièges classiques en e‑commerce
  2. ACL avancées : matcher proprement, router vite, bloquer tôt
  3. Health checks : ne pas confondre “port ouvert” et “service sain”
  4. Sticky sessions : nécessaire parfois, à éviter souvent
  5. Mettre de l’observabilité sur les décisions ACL/check/sticky (sinon vous pilotez à l’aveugle)
  6. Patterns concrets : canary/blue-green, maintenance, et compat avec conteneurs

Contexte, versions ciblées et pièges classiques en e‑commerce

Les exemples ci‑dessous partent sur HAProxy 3.2 LTS (syntaxe compatible 2.6+ pour la majorité, mais certaines features — exporter Prometheus natif, enrichissements HTTP checks — sont plus confortables en 3.x). Les configurations sont pensées pour Debian 12 / Ubuntu 24.04, avec des backends HTTP typiques d’une stack PrestaShop (Nginx/Apache + PHP-FPM 8.2/8.3) et des services annexes (API, BO, webhooks).

Si vous n’avez pas encore un binaire 3.2 proprement packagé, la compilation et l’unité systemd sont détaillées dans l’article interne HAProxy 3.2 : déploiement LTS sur Ubuntu/Debian, compilation et systemd. Pour une mise en prod, ne sautez pas l’étape « validation config » (haproxy -c -f /etc/haproxy/haproxy.cfg) ni la stratégie de reload sans coupure (master-worker, systemctl reload haproxy). En e‑commerce, le reload “propre” compte autant que l’équilibrage : un reload qui coupe des connexions au mauvais moment peut se traduire par des paniers abandonnés ou des paiements échoués.

Le piège numéro 1 en e‑commerce n’est pas le load balancing en lui‑même, c’est l’interaction entre routage L7 (ACL), checks applicatifs et état de session. Une ACL mal ordonnée peut envoyer le back-office sur le mauvais backend, un health check trop “intelligent” peut DDoS votre base, et une sticky session mal pensée masque un bug d’état partagé… jusqu’au jour où le node sticky tombe.

Une autre source d’incidents très “terrain” (souvent visible pendant les soldes / Black Friday) : les comportements anormaux de clients légitimes. Par exemple, une appli mobile mal implémentée qui retry agressivement sur /authentication ou /order peut ressembler à un bot. D’où l’intérêt de coupler : règles simples (ACL), rate limiting ciblé, et une observabilité qui permet de confirmer rapidement si vous bloquez un abus… ou une régression côté front.

« HAProxy is a free, very fast and reliable reverse-proxy offering high availability, load balancing, and proxying for TCP and HTTP-based applications. » — haproxy.org, page d’accueil : haproxy.org

ACL avancées : matcher proprement, router vite, bloquer tôt

Dans HAProxy, une ACL (Access Control List) est une expression booléenne évaluée sur des fetches (Host, path, headers, IP, SNI, méthode, etc.). Le point important en prod : les ACL ne coûtent pas toutes le même prix. Un match sur path_beg est quasiment gratuit ; un hdr_reg() sur un header volumineux (ou pire : tenter d’inspecter un body) peut plomber la latence et la CPU. La règle pratique : matcher au plus tôt sur des attributs stables (SNI/Host, méthode, path) puis affiner.

Petit mémo “perf” (utile quand la config grossit) :

Type de match Exemple Coût typique Remarque
Préfixe simple path_beg /admin faible idéal pour “router vite”
Égalité / liste hdr(host) -i shop.tld faible attention à la normalisation (ports, casse)
Regex path_reg / hdr_reg moyen à élevé à réserver aux cas nécessaires
Extraction + transformation req.hdr(host),lower faible utile pour maps + normalisation

Exemple : segmentation front / API / back-office, avec garde-fous

Voici un frontend HTTP/HTTPS qui route en fonction du host et du path, et refuse tout ce qui ressemble à un scan basique. On utilise aussi capture request header pour diagnostiquer sans activer un log en mode verbeux sur toute la flotte.

frontend fe_https
  bind :443 ssl crt /etc/haproxy/certs alpn h2,http/1.1
  mode http
  option httplog
  log global

  # Hygiène : ne logguez pas des tokens / cookies en clair.
  capture request header Host len 60
  capture request header User-Agent len 120

  # ACL de base
  acl host_shop hdr(host) -i shop.example.com
  acl host_api  hdr(host) -i api.example.com

  acl is_api_path path_beg /v1/ /v2/
  acl is_bo_path  path_beg /admin /backoffice

  # Blocage simple : méthodes exotiques et chemins de scans
  acl bad_method method -m reg ^(TRACE|TRACK|CONNECT)$
  acl bad_path   path_reg -i ^/(\.git|\.env|wp-admin|phpmyadmin|\.svn)
  http-request deny deny_status 405 if bad_method
  http-request deny deny_status 404 if bad_path

  # Routage
  use_backend be_api  if host_api  || is_api_path
  use_backend be_bo   if host_shop is_bo_path
  default_backend be_shop

Trois points à noter : (1) l’ordre des use_backend est critique : HAProxy prend la première règle qui matche ; (2) hdr(host) doit être normalisé (attention aux ports Host: example.com:443) si vous avez des clients non standards — utilisez hdr(host),lower + un match -m end si besoin ; (3) pour PrestaShop, séparer BO et front est une protection simple contre un BO qui devient un hotspot CPU (indexation, modules mal écrits, back-office lourd).

Un ajout souvent utile en environnement réel : refuser les requêtes sans Host en HTTP/1.1 (souvent du trafic parasite) et poser une base d’hygiène sur les headers :

  acl missing_host req.hdr(Host) -m len 0
  http-request deny deny_status 400 if missing_host

Maps : sortir le routage du code, garder le contrôle

Dès que vous avez plus de 3–4 domaines, coder des dizaines d’ACL devient une anti‑maintenance. HAProxy fournit les maps : des fichiers clé→valeur rechargés sans redéployer toute votre logique. Exemple : router par Host vers un backend nommé.

# /etc/haproxy/maps/host2be.map
shop1.example.com be_shop1
shop2.example.com be_shop2
api.example.com   be_api

frontend fe_https
  # ...
  use_backend %[req.hdr(host),lower,map(/etc/haproxy/maps/host2be.map,be_fallback)]

En environnement agence/multi‑boutique, les maps évitent de multiplier les if fragiles et réduisent le risque “je déploie une boutique et je casse les autres”. Le fallback explicite (be_fallback) est volontaire : si un host inconnu arrive, vous pouvez servir une page neutre, ou répondre 421/404.

  • Normaliser une fois : si vous enchaînez plusieurs maps/ACL sur le host, stockez-le dans une variable de transaction, pour éviter la duplication et rendre le debug lisible.
  • Mise à jour contrôlée : les maps peuvent être mises à jour via le runtime (socket admin) selon vos process, mais gardez une source de vérité (fichier versionné) pour éviter les dérives.

Exemple de normalisation :

  http-request set-var(txn.host) req.hdr(host),lower
  use_backend %[var(txn.host),map(/etc/haproxy/maps/host2be.map,be_fallback)]

Rate limiting ciblé avec stick-table (auth, checkout)

Pour une boutique PrestaShop, la surface d’attaque principale est souvent /authentication + endpoints AJAX. HAProxy sait faire du rate limiting sans WAF complet via stick-table.

frontend fe_https
  # Table : 10 minutes, suivi du taux de requêtes par IP
  stick-table type ip size 200k expire 10m store http_req_rate(10s)

  acl is_login path_beg /connexion /authentication
  http-request track-sc0 src if is_login
  acl abuse_login sc_http_req_rate(0) gt 20
  http-request deny deny_status 429 if is_login abuse_login

C’est basique mais efficace pour réduire le bruit (bots) sans impacter les clients normaux. Les limites dépendent de votre trafic : sur une boutique à 5–10 req/s, 20 req/10s sur /authentication par IP est déjà agressif. Et si vous êtes derrière un CDN/WAF, trackez l’IP réelle (par ex. src devient l’IP du CDN) : utilisez http-request set-src req.hdr_ip(CF-Connecting-IP) ou équivalent, mais uniquement si vous maîtrisez le réseau en amont (sinon n’importe qui peut forger le header).

Astuce “e‑commerce” : sur certaines attaques, répondre 429 immédiatement est bien, mais ralentir peut être encore plus rentable (moins de pics). HAProxy propose http-request tarpit (même logique de condition) pour faire perdre du temps à un bot sans surcharger vos backends — à utiliser avec parcimonie et en mesurant l’impact sur les connexions.

Health checks : ne pas confondre “port ouvert” et “service sain”

Un health check HAProxy pilote l’état UP/DOWN des serveurs et influence directement le load balancing. En e‑commerce, un TCP check “port 80 ouvert” est insuffisant : un PHP-FPM saturé, un pool DB à bout, ou un disque plein donnent souvent un Nginx qui accepte la connexion mais renvoie des 502/504.

HTTP checks réalistes, mais pas destructeurs

Le bon compromis est un endpoint léger (pas de session, pas de cart) qui teste le minimum vital : accès PHP + éventuellement un ping DB très simple. L’erreur classique : pointer le check sur / (page home) et déclencher du rendu Smarty, des hooks, des calls externes… ce qui crée une charge artificielle exactement quand ça va mal.

backend be_shop
  mode http
  balance roundrobin
  option httpchk GET /healthz HTTP/1.1\r\nHost:\ shop.example.com
  http-check expect status 200

  default-server inter 2s fall 3 rise 2 slowstart 10s
  server web1 10.0.0.11:80 check
  server web2 10.0.0.12:80 check

inter 2s fall 3 rise 2 signifie : un serveur est marqué DOWN après ~6s d’échecs consécutifs, et remonte après ~4s de succès. slowstart évite qu’un serveur qui remonte prenne immédiatement 50% du trafic (utile après un redéploiement ou un warmup OPcache).

Mini-checklist pour un /healthz “safe” (côté app ou Nginx) :

  • réponse 200/500 très rapide (objectif : < 50 ms en local) ;
  • aucune session / aucun cookie nécessaire ;
  • pas d’appel à un service externe (paiement, ERP, API marketing) ;
  • éventuellement un test DB minimal (ex : SELECT 1) mais avec un timeout strict ;
  • un cache HTTP désactivé pour éviter des faux positifs.

Si vos backends sont en HTTPS (LB→backend), pensez à activer la vérification TLS côté HAProxy (check-ssl, SNI si nécessaire). Un check qui ne valide pas la couche TLS peut laisser passer un certificat expiré ou un mauvais vhost côté backend.

Checks L4, L7 et “observe” : choisir selon le type de panne

Pour des backends non HTTP (Redis, RabbitMQ, etc.), vous êtes sur du tcp-check. Pour HTTP, privilégiez L7. HAProxy peut aussi “observer” les erreurs réelles (observe layer7) pour dégrader un serveur qui sert des 5xx sans que le check ne les voie (par exemple un endpoint /healthz qui retourne 200 alors que l’app est partiellement cassée).

backend be_api
  mode http
  balance leastconn
  option httpchk GET /healthz
  http-check expect status 200

  # Détection de comportements anormaux sur le trafic réel
  observe layer7
  error-limit 50

  server api1 10.0.1.11:8080 check on-marked-down shutdown-sessions
  server api2 10.0.1.12:8080 check on-marked-down shutdown-sessions

on-marked-down shutdown-sessions est souvent appliqué côté server pour forcer la fermeture des sessions quand un nœud est marqué DOWN ; c’est brutal mais utile pour les API : vous cassez les connexions vers un nœud malade pour accélérer la convergence. Sur checkout, ça peut être trop violent si vous n’avez pas de retry côté client.

Note de terrain : observe layer7 est particulièrement utile quand une API est “semi-OK” (par exemple : /healthz répond 200 mais certains endpoints renvoient 500 à cause d’un cache corrompu ou d’une migration incomplète). En revanche, si vos 5xx sont principalement dus à des timeouts DB, il faut aussi regarder les timeouts HAProxy (timeout connect, timeout server) pour éviter que le proxy ne “pile” des connexions en attente.

Agent-check : quand le LB doit écouter l’OS/app plutôt que “pinguer”

Pour les scénarios où la santé dépend d’un signal interne (file d’attente trop longue, maintenance applicative, node drain), l’agent-check est plus fiable qu’un endpoint HTTP. Le serveur expose une ligne (up, down, poids, etc.) sur un port dédié ; HAProxy adapte le poids/état.

backend be_shop
  option httpchk GET /healthz
  server web1 10.0.0.11:80 check agent-check agent-port 9000
  server web2 10.0.0.12:80 check agent-check agent-port 9000

En pratique, un petit daemon local (ou systemd unit + script) peut renvoyer down si, par exemple, le disque est à 99%, si PHP-FPM dépasse un seuil de max_children saturés, ou si vous êtes en phase de déploiement. C’est un pattern propre pour faire du drain sans couper le service.

Un compromis intéressant en e‑commerce : renvoyer un poids réduit (au lieu de down) pendant un warmup (OPcache froid, caches applicatifs vides), afin de laisser le nœud respirer tout en restant dans la rotation.

Sticky sessions : nécessaire parfois, à éviter souvent

Les sticky sessions (affinité) forcent un client à retomber sur le même backend. On les utilise quand l’application n’externalise pas l’état (sessions sur disque local, cache local, panier non partagé, uploads temporaires). Dans un monde idéal, vous externalisez en Redis/Memcached et vous désactivez la sticky. Dans le monde réel PrestaShop, on tombe encore régulièrement sur des sessions PHP stockées localement (ou des modules qui écrivent des fichiers temporaires dans un volume non partagé).

Le problème : la sticky masque les défauts d’architecture. Tant que tout va bien, OK ; quand un serveur tombe, vous perdez une partie des sessions (et donc des paniers, logins BO, etc.). Avant de mettre de la sticky “par défaut”, auditez votre stack : stockage session, cache applicatif, médias, génération de PDF, webhooks. Sur PrestaShop, l’article PrestaShop : optimiser performance via cache Smarty, CCC et CDN est un bon point de départ pour distinguer ce qui est cacheable vs stateful.

Règle de décision rapide (pragmatique) :

  • si vous avez sessions PHP + cache + médias sur du partagé (ou externalisés) → la sticky devient optionnelle ;
  • si votre BO déclenche des traitements lourds (imports, indexations) → séparer BO/front est souvent plus rentable que “coller” des sessions ;
  • si vous dépendez d’un stockage local (sessions, tmp) et que vous ne pouvez pas corriger vite → sticky temporaire, mais documentée (et plan de sortie).

Sticky par cookie HAProxy (méthode la plus propre en HTTP)

Le schéma standard : HAProxy injecte un cookie technique (SRV) avec l’identifiant du serveur. L’app n’a rien à gérer. C’est généralement le meilleur compromis.

backend be_shop
  mode http
  balance roundrobin

  cookie SRV insert indirect nocache
  server web1 10.0.0.11:80 check cookie w1
  server web2 10.0.0.12:80 check cookie w2

indirect évite de renvoyer le cookie au backend (il reste côté client), et nocache limite les effets avec des caches intermédiaires. Attention : si vous avez du HTTP/2 + multiplexing, la sticky reste valide, mais la réutilisation de connexion côté client peut biaiser certaines métriques de “sessions”.

  • si vous utilisez déjà des cookies applicatifs sensibles, gardez un nom clairement technique (SRV, HAPSRV) et évitez toute confusion avec la session ;
  • en cas de suppression de serveur, prévoyez le comportement attendu (ex : cookie SRV insert vs rewrite), pour éviter des boucles où le client revient avec un cookie pointant vers un nœud disparu.

Sticky via stick-table : contrôler, expirer, limiter

Pour des usages plus fins (ou quand vous ne voulez pas d’un cookie LB), utilisez stick-table. Exemple : stickiness par IP source, avec expiration courte.

backend be_bo
  mode http
  balance leastconn

  stick-table type ip size 50k expire 30m
  stick on src

  server bo1 10.0.0.21:80 check
  server bo2 10.0.0.22:80 check

C’est utile pour un back-office utilisé par une équipe interne (IP stables) et où la session est fragile. Mais en B2C, stick on src est souvent mauvais : NAT opérateurs mobiles, CGNAT, proxies d’entreprise → plusieurs clients partagent la même IP et se retrouvent collés au même backend (effet “hotspot” involontaire).

Si vous tenez à une stick-table en B2C, une alternative est de “sticker” sur une valeur stable côté client (cookie LB, ou identifiant anonyme généré) plutôt que l’IP.

Sticky sur cookie applicatif : faisable, rarement robuste en PrestaShop

Techniquement, vous pouvez “sticker” sur un cookie applicatif (ex : PHPSESSID). PrestaShop, lui, utilise une cookie “PrestaShop-” (nom variable selon le contexte, le shop, etc.). Ça rend la règle plus fragile si vous ne contrôlez pas précisément le nom.

Si vous tenez à le faire, cherchez un fetch supportant une regex sur le nom de cookie (selon version, req.cook_reg() ou équivalent), puis stick on cette valeur. Testez à blanc sur staging, car une regex mal écrite sur chaque requête peut coûter cher en CPU.

En clair : si votre objectif est la fiabilité, préférez cookie HAProxy ou, mieux, stateless via sessions partagées (Redis) — la sticky devient alors un mécanisme de confort, pas un prérequis.

Mettre de l’observabilité sur les décisions ACL/check/sticky (sinon vous pilotez à l’aveugle)

HAProxy peut exposer une page stats, un socket admin, et un exporter Prometheus. Pour un environnement e‑commerce, l’important n’est pas d’avoir “des métriques”, c’est de lier métriques ↔ décisions : combien de requêtes ont été deny par ACL ? combien de serveurs ont flap ? combien de sessions ont été reset lors d’un marked-down ?

Exporter Prometheus natif et alerting utile

En 3.x, l’exporter Prometheus est simple via http-request use-service prometheus-exporter.

frontend fe_metrics
  bind :8404
  mode http

  # Restreindre aux IPs d’observabilité
  acl allowed src 10.0.10.0/24
  http-request deny unless allowed

  http-request use-service prometheus-exporter if { path /metrics }

Une fois les métriques scrapées, vous pouvez définir des alertes (taux de 5xx, backends DOWN, saturation de queue). Pour la partie PromQL + alertmanager, référez-vous à l’article interne Prometheus : configuration des alertes, métriques et requêtes PromQL.

« Prometheus is an open-source systems monitoring and alerting toolkit originally built at SoundCloud. » — Prometheus docs : prometheus.io

Côté sécurité (souvent négligée) : protégez /metrics comme un endpoint interne. Une fuite de métriques peut exposer des noms de backends, des volumes de trafic, voire des patterns d’attaque. La restriction IP est un minimum ; si vous avez des scrapers multi-sites, envisagez aussi un bind sur une interface dédiée ou un réseau d’admin.

Logs structurés : capturer le minimum utile (et rien de sensible)

Côté logs, le piège est de vouloir tout capturer (cookies, query string, payload) : vous allez créer un problème RGPD et un incident sécurité. Concentrez-vous sur : status, bytes, Tq/Tw/Tc/Tr/Tt, backend/server, et quelques headers non sensibles (Host, User-Agent, X-Request-ID). En incident, ces timings permettent de trancher vite : attente en queue LB (Tw), connexion backend (Tc), temps de réponse (Tr).

Si vous faites déjà du tracing applicatif, propagez un X-Request-ID généré au LB pour corréler LB ↔ Nginx ↔ app. HAProxy sait générer et injecter :

frontend fe_https
  http-request set-header X-Request-ID %[unique-id]

Pour rendre unique-id vraiment exploitable, définissez le format global (afin d’éviter des IDs vides ou non uniques selon les cas) :

global
  unique-id-format %{+X}o\ %ci:%cp_%fi:%fp_%Ts_%rt:%pid
  unique-id-header X-Request-ID

Socket admin : diagnostiquer et agir sans redéployer

Activez le socket (local) pour inspecter l’état et agir (désactiver un serveur, changer un poids, vider une table sticky) :

global
  stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners

Exemples :

  • echo "show stat" | socat stdio /run/haproxy/admin.sock pour exporter l’état.
  • echo "disable server be_shop/web2" | socat stdio /run/haproxy/admin.sock pour drainer un nœud.
  • echo "clear table be_bo" | socat stdio /run/haproxy/admin.sock pour purger une stick-table (à faire avec prudence).

Astuce opérationnelle : documentez 3–4 commandes “réflexe” (drain, enable, clear table, show errors) dans votre runbook. Le jour où un nœud se met à flapper, la vitesse d’exécution vaut plus qu’un long post-mortem.

Patterns concrets : canary/blue-green, maintenance, et compat avec conteneurs

Le cœur de HAProxy est stable, mais votre prod ne l’est pas : déploiements fréquents, migrations DB, invalidations de cache, périodes de soldes. La configuration “avancée” n’est pas celle qui a 200 lignes d’ACL, c’est celle qui supporte ces événements sans improvisation.

Canary par header ou cookie, sans casser le SEO

Un canary simple : une fraction de trafic ou un header interne (par exemple envoyé par votre réseau interne ou votre QA) route vers un pool be_shop_canary. Attention à ne pas mélanger canary et SEO : ne routez pas Googlebot aléatoirement sur une version différente.

frontend fe_https
  acl is_canary hdr(X-Canary) -i 1
  acl is_bot hdr_sub(User-Agent) -i Googlebot

  use_backend be_shop_canary if is_canary !is_bot
  default_backend be_shop

Ça donne un contrôle explicite : seuls les clients “opt-in” passent en canary. Si vous voulez du pourcentage, HAProxy propose des fetches pseudo-aléatoires (rand()) mais évitez de le faire pour des utilisateurs non authentifiés si l’app est stateful.

En e‑commerce, une variante courante est le canary par cookie QA (posée via un outil interne) plutôt que par header, ce qui évite que des proxies intermédiaires ne suppriment le header.

Mode maintenance : répondre vite et proprement

Plutôt que de couper Nginx ou de renvoyer des 502, servez une 503 avec un contenu statique côté LB. C’est une vraie optimisation : vous protégez vos backends et vous contrôlez la réponse.

frontend fe_https
  acl maintenance_file  -f /etc/haproxy/maintenance.enabled
  http-request return status 503 content-type "text/html" lf-string "<html><body>Maintenance</body></html>" if maintenance_file

Vous pouvez aller plus loin avec errorfile 503 /etc/haproxy/errors/503.http, un message plus explicite, et un header Retry-After (utile pour les clients et certains robots). Le comportement HTTP 503 est défini dans la spec HTTP (RFC 9110) : RFC 9110

L’objectif n’est pas “faire joli”, c’est d’avoir une réponse déterministe pendant une migration ou un incident : même code HTTP, même contenu, même timing, et pas de charge backend.

Déploiements conteneurisés : stabilité des IPs et discovery

Si vous orchestrez via Docker Compose en prod (ce qui arrive encore, même si ce n’est pas l’outil le plus fun à opérer), faites attention aux IPs qui bougent et aux redémarrages. Pour des patterns propres (réseaux, healthchecks, dépendances), l’article interne Docker Compose : orchestrer des services multi-conteneurs en production donne une base.

Pour HAProxy, deux approches : (1) backends en DNS (service name) + resolvers HAProxy, ou (2) templating (server-template) si vous avez un naming stable. Le point clé : ne laissez pas des IPs hardcodées si votre orchestration les renouvelle.

Enfin, si vous combinez conteneurs + sticky sessions, soyez prudent : un redeploy qui recrée des conteneurs (et donc des endpoints) peut invalider brutalement l’affinité. Si vous n’avez pas encore externalisé les sessions, planifiez des déploiements “drain → arrêt” plutôt qu’un remplacement immédiat.


Références externes utiles (docs officielles) :


À lire aussi