Aller au contenu
Le répartiteur de charge en profondeur

Le répartiteur de charge en profondeur

300 Concevoir ⏱ 1 h 30 cloudscalewayhttptls

À la fin, vous saurez

  • Ordonner des ACL pour rediriger, filtrer par adresse ou par chemin, et n'accepter que le trafic d'Edge Services
  • Router un même frontend vers plusieurs backends selon l'hôte ou le chemin, avec plusieurs certificats
  • Transmettre l'adresse réelle du client à l'application et décider à qui faire confiance
  • Régler délais, files d'attente et nouvelles tentatives pour des requêtes longues et des WebSocket
  • Retirer un serveur sans couper ses utilisateurs, et dimensionner le répartiteur avec ses métriques
  • Concevoir une exposition qui survit à la perte d'une zone malgré un répartiteur zonal

Prérequis

Testé avec flask 3.1.3 gunicorn 26.2.0 scaleway-cli 2.62.0 , vérifié le 5 octobre 2026

Pourquoi

Le répartiteur lb-signalements a été créé au cours Le cloud : les fondamentaux avec le strict nécessaire : un backend vers les deux instances, une vérification de santé sur /sante, un frontend HTTP et un frontend HTTPS. Six mois plus tard, la liste des demandes s'est allongée :

  • le frontend 80 sert encore l'application en clair, alors que tout doit passer en HTTPS ;
  • la page d'administration /admin ne doit être joignable que depuis le réseau de Lyneko ;
  • une nouvelle version de l'API, servie par d'autres instances, doit répondre sous /v2/ pendant la migration des métropoles ;
  • les journaux de l'application montrent toutes les requêtes venant de la même adresse, celle du répartiteur, ce qui rend impossible de retrouver un abus ;
  • le tableau de bord en temps réel de la leçon 9 se déconnecte régulièrement ;
  • depuis la mise en place d'Edge Services (leçon 11), le répartiteur reste joignable en direct, ce qui permet de contourner le pare-feu applicatif ;
  • et une question revient à chaque revue d'architecture : que devient Signalements si la zone fr-par-1, où vit le répartiteur, tombe ?

Chacune de ces demandes a une réponse dans les réglages du répartiteur. Cette leçon les passe en revue, dans l'ordre où une requête les rencontre.

Les concepts

Le chemin d'une requête

    flowchart LR
  C["Client"] --> F["Frontend<br/>port, certificats"]
  F --> A["ACL<br/>dans l'ordre"]
  A -->|"allow ou fin de liste"| R["Routes<br/>hôte, chemin, SNI"]
  A -->|"deny"| X["403"]
  A -->|"redirect"| Y["301/302..."]
  R --> B1["Backend par défaut"]
  R --> B2["Autre backend"]
  B1 --> S1["Serveurs<br/>vérifiés par health check"]
  
  • Le frontend écoute sur un port, avec zéro, un ou plusieurs certificats, et désigne un backend par défaut.
  • Les ACL du frontend s'appliquent dans l'ordre de leur index (0 d'abord, d'après l'aide de la CLI). Chacune a une action (allow, deny, redirect), conditionnelle ou non. La documentation insiste sur deux règles : la première ACL qui s'applique décide, et les suivantes ne sont pas évaluées ; une requête qui arrive au bout de la liste sans qu'aucune ACL ne s'applique passe. Pour une liste blanche, il faut donc terminer par un deny sans condition.
  • Les routes choisissent, pour un frontend, un autre backend que celui par défaut : selon l'en-tête Host ou le début du chemin pour un backend HTTP, selon le SNI (Server Name Indication, le nom demandé par le client pendant la négociation TLS) pour un backend TCP.
  • Le backend applique son algorithme de répartition, ses délais, ses nouvelles tentatives, et n'envoie qu'aux serveurs que la vérification de santé juge sains.

Les conditions d'une ACL

Une ACL conditionnelle combine au plus deux filtres, qui doivent correspondre tous les deux :

  • un filtre IP : une liste d'adresses ou de blocs, IPv4 ou IPv6, côté client ;
  • un filtre HTTP, disponible seulement si le backend est en HTTP : début du chemin (path_begin), fin du chemin (path_end), expression régulière sur le chemin (regex), ou valeurs d'un en-tête (http_header_match).

S'y ajoute une condition propre à Scaleway, ips_edge_services, que la description de l'API présente comme restreignant les connexions à celles qui viennent d'Edge Services. L'option invert inverse la condition (« si pas de correspondance »).

L'adresse réelle du client

Un répartiteur en mode HTTP ouvre sa propre connexion vers le serveur : l'application voit l'adresse du répartiteur comme adresse source. C'est le comportement d'un mandataire (proxy), pas d'une traduction d'adresses (voir La traduction d'adresses). Deux moyens de transmettre l'adresse d'origine :

X-Forwarded-ForProtocole PROXY
OùUn en-tête HTTP ajouté à la requêteUn en-tête placé avant les données, au début de la connexion TCP
PourBackends HTTPBackends TCP ou HTTP
FormatUne liste d'adresses, une par intermédiairev1 en texte, v2 en binaire (avec des variantes qui transmettent des informations TLS)
Côté serveurL'application ou son cadre lit l'en-têteLe serveur doit attendre cet en-tête, sinon il rejette la connexion

La documentation de Scaleway rappelle qu'un backend HTTP transmet déjà X-Forwarded-For sans rien activer, et que le protocole PROXY sert surtout aux backends TCP. Dans les deux cas, la question décisive est la confiance : un client peut envoyer lui-même un en-tête X-Forwarded-For mensonger. L'application ne doit croire que les valeurs ajoutées par des intermédiaires qu'elle connaît, et dans le nombre exact où elle les attend.

Les délais

Un répartiteur applique plusieurs délais distincts, dont la CLI 2.62 donne les valeurs par défaut :

DélaiOùDéfautSens
timeout-clientfrontend5 minInactivité maximale côté client
timeout-connectbackend5 sTemps pour établir la connexion vers un serveur
timeout-serverbackend5 minTemps maximal qu'a un serveur pour traiter une requête
timeout-tunnelbackend15 minInactivité maximale d'un WebSocket établi ; l'emporte sur les deux précédents
timeout-queuebackend5 s (doc.)Attente maximale d'une requête quand tous les serveurs sont à leur maximum

Un tableau de bord en temps réel qui ne reçoit rien pendant quinze minutes est coupé par timeout-tunnel. Ce n'est pas un défaut : un répartiteur qui garderait indéfiniment des connexions inactives finirait par épuiser ses ressources. La correction est côté application : envoyer un message de maintien (ping) plus souvent que le délai.

Protéger les serveurs

Trois réglages du backend évitent qu'un pic n'écrase les instances :

  • max-connections limite le nombre de requêtes simultanées par serveur ; au-delà, le répartiteur met en file, pendant au plus timeout-queue, puis répond 503. Il vaut mieux refuser vite que laisser gunicorn empiler des requêtes qu'il ne servira qu'après leur expiration côté client.
  • max-retries et redispatch-attempt-count : nouvelles tentatives de connexion (pas de requête) en cas d'échec, sur le même serveur ou sur un autre.
  • on-marked-down-action=shutdown_sessions coupe les connexions en cours vers un serveur dès qu'il est déclaré en panne, au lieu de les laisser se terminer.

Et une page de secours : failover-host désigne un site statique dans un bucket, servi quand aucun serveur n'est sain, plutôt qu'une erreur 503 brute.

Tailles et limites

La page des limites de la documentation, au 5 octobre 2026, donne pour chaque taille la bande passante, le nombre maximal de connexions simultanées et la taille de tampon par connexion :

TailleBande passanteConnexions simultanéesTampon
LB-S200 Mbit/s20 00016 Ko
LB-M500 Mbit/s50 00032 Ko
LB-L1 Gbit/s160 00064 Ko
LB-XL4 Gbit/s3 000 000128 Ko

L'aide de la CLI 2.62, elle, énumère les types LB-S, LB-GP-M et LB-GP-L ; la liste réelle et disponible dans une zone s'obtient avec scw lb lb-types list. Quelques limites à retenir : la documentation précise que le nombre de connexions est un maximum théorique, le processeur saturant souvent avant ; qu'un répartiteur ne peut pas tenir plus de 50 000 sessions TCP vers un même couple serveur et port (au-delà, ajouter des serveurs) ; et que la taille du tampon limite la taille des en-têtes en HTTP : un cookie démesuré peut faire échouer des requêtes sur un LB-S.

Zonal, mais pas fragile

Un répartiteur vit dans une zone. La FAQ de Scaleway décrit un mécanisme de bascule : si l'instance qui porte le répartiteur tombe, un remplaçant est déployé automatiquement et l'IP flexible y est redirigée. Ce mécanisme protège de la panne d'une machine, pas de la perte de la zone entière.

Pour survivre à la perte d'une zone, il faut donc un second répartiteur dans une autre zone, et un moyen d'aiguiller les clients vers celui qui fonctionne. Deux contraintes documentées compliquent le schéma :

  • l'IP flexible d'un répartiteur ne peut pas en être détachée après la création, et ne peut servir qu'à un autre répartiteur de la même zone : on ne déplace pas une adresse d'une zone à l'autre ;
  • chaque répartiteur n'a qu'une adresse IPv4 publique et au plus une IPv6.

L'aiguillage se fait donc par le DNS, avec un enregistrement à vérification de santé (leçon 10), au prix du délai de cache, ou par un service en amont qui connaît les deux répartiteurs (Edge Services avec deux backends, dont le routage multiple est en bêta au 5 octobre 2026).

En pratique

Les identifiants des objets sont lus une fois au début ; toutes les commandes visent la zone fr-par-1. Elles ont été vérifiées avec l'aide de la CLI 2.62.

$ Z=fr-par-1
$ LB=$(scw lb lb list name=lb-signalements zone=$Z -o json | jq -r '.[] | select(.name=="lb-signalements") | .id')
$ BE=$(scw lb backend list lb-id=$LB zone=$Z -o json | jq -r '.[] | select(.name=="sig-app") | .id')
$ FE_HTTP=$(scw lb frontend list lb-id=$LB zone=$Z -o json | jq -r '.[] | select(.name=="http") | .id')
$ FE_HTTPS=$(scw lb frontend list lb-id=$LB zone=$Z -o json | jq -r '.[] | select(.name=="https") | .id')

Le filtre name= des commandes list cherche une sous-chaîne ; le select de jq garde le nom exact.

Rediriger HTTP vers HTTPS

Sur le frontend 80, une seule ACL, sans condition, qui remplace le schéma :

$ scw lb acl create frontend-id=$FE_HTTP name=vers-https index=0 \
    action.type=redirect action.redirect.type=scheme action.redirect.target=https \
    action.redirect.code=301 zone=$Z
  • action.redirect.type=scheme et target=https gardent l'hôte, le chemin et les paramètres, et changent seulement le schéma.
  • 301 est une redirection permanente, que les navigateurs mémorisent. Faites d'abord l'essai avec 302, puis passez en 301 quand tout fonctionne : une 301 erronée reste dans les caches des navigateurs.
  • Aucune condition n'est donnée : l'ACL s'applique à tout le trafic du frontend.

N'accepter que le trafic d'Edge Services

Sur le frontend 443, deux ACL : autoriser Edge Services, refuser le reste.

$ scw lb acl create frontend-id=$FE_HTTPS name=edge-seulement index=5 \
    action.type=allow match.ips-edge-services=true zone=$Z
$ scw lb acl create frontend-id=$FE_HTTPS name=refus-par-defaut index=10 \
    action.type=deny zone=$Z

L'ACL d'index 10, sans condition, rend la liste fermée par défaut ; laisser de la place entre les index (0 à 4 restent libres) permet d'insérer des règles avant elles, ce que fait la section suivante. Avant de l'appliquer, vérifiez que le nom public passe bien par Edge Services : sinon, cette ACL coupe tout le service. Testez ensuite que l'adresse IP du répartiteur, appelée en direct, répond désormais 403.

Restreindre l'administration

/admin ne doit répondre qu'au réseau de Lyneko (adresse de documentation 198.51.100.0/24 ici). L'ACL se place avant celle qui autorise Edge Services, puisque la première qui s'applique décide. Une première idée, à ne pas appliquer, serait une seule règle inversée :

$ scw lb acl create frontend-id=$FE_HTTPS name=admin-interne index=0 \
    action.type=deny match.http-filter=path_begin match.http-filter-value.0=/admin \
    match.ip-subnet.0=198.51.100.0/24 match.invert=true zone=$Z

Lisez-la comme une phrase : refuser si le chemin commence par /admin et que la condition sur l'adresse n'est pas remplie. Mais attention à invert : la documentation précise que, pour une ACL à deux filtres, la même inversion s'applique aux deux (« ni l'un ni l'autre »). La règle ci-dessus refuse donc les requêtes qui ne correspondent ni au chemin, ni à l'adresse, ce qui n'est pas du tout l'intention. Avec des filtres combinés, préférez deux règles simples :

$ scw lb acl create frontend-id=$FE_HTTPS name=admin-autorise index=0 \
    action.type=allow match.http-filter=path_begin match.http-filter-value.0=/admin \
    match.ip-subnet.0=198.51.100.0/24 zone=$Z
$ scw lb acl create frontend-id=$FE_HTTPS name=admin-refuse index=1 \
    action.type=deny match.http-filter=path_begin match.http-filter-value.0=/admin zone=$Z

La première laisse passer /admin depuis le réseau interne, la seconde refuse /admin à tous les autres ; le reste du trafic continue vers l'ACL d'Edge Services. (Le recours à l'ACL pour l'administration ne dispense pas d'une authentification forte dans l'application.) Et un détail qui compte : derrière Edge Services, l'adresse vue par le répartiteur est celle d'Edge Services, pas celle de l'administrateur ; pour que le filtrage par adresse fonctionne, l'administration doit être joignable par un chemin qui ne passe pas par Edge Services, ou être filtrée par l'application sur l'adresse transmise, ce qui demande la confiance décrite plus bas.

Listez le résultat et relisez l'ordre :

$ scw lb acl list frontend-id=$FE_HTTPS zone=$Z -o json | jq -r '.[] | [.index, .name, .action.type] | @tsv' | sort -n

Un second backend pour /v2/

$ BE_V2=$(scw lb backend create lb-id=$LB name=sig-api-v2 \
    forward-protocol=http forward-port=8000 forward-port-algorithm=roundrobin \
    sticky-sessions=none server-ip.0=172.16.20.21 server-ip.1=172.16.20.22 \
    health-check.port=8000 health-check.http-config.uri=/sante \
    health-check.http-config.method=GET health-check.http-config.code=200 \
    health-check.check-delay=5s health-check.check-timeout=2s health-check.check-max-retries=3 \
    zone=$Z -o json | jq -r .id)
$ scw lb route create frontend-id=$FE_HTTPS backend-id=$BE_V2 \
    match.path-begin=/v2/ zone=$Z

Les requêtes dont le chemin commence par /v2/ partent vers les nouvelles instances, les autres vers le backend par défaut du frontend. Les ACL s'appliquent avant les routes : /v2/admin est concerné par les règles d'administration.

Plusieurs noms, plusieurs certificats

Une métropole veut accéder à Signalements sous son propre nom, signalements.metropole.example. Le frontend accepte une liste de certificats ; le répartiteur choisit celui qui correspond au nom demandé par le client (SNI) :

$ CERT2=$(scw lb certificate create lb-id=$LB name=metropole \
    letsencrypt-common-name=signalements.metropole.example zone=$Z -o json | jq -r .id)
$ scw lb frontend update $FE_HTTPS name=https inbound-port=443 backend-id=$BE \
    certificate-ids.0=<certificat existant> certificate-ids.1=$CERT2 zone=$Z

La commande update attend les champs obligatoires du frontend (nom, port, backend) : reprenez leurs valeurs actuelles, sinon vous les modifiez sans le vouloir. Un certificat Let's Encrypt exige que le nom résolve déjà vers l'adresse du répartiteur ; si le nom passe par Edge Services, c'est Edge Services qui présente le certificat au client, et ce certificat-là se règle dans le pipeline. Une route match.host-header=signalements.metropole.example permet ensuite, si nécessaire, d'envoyer ce nom vers un backend différent.

Transmettre l'adresse du client à Signalements

Signalements est servi par gunicorn derrière un répartiteur en HTTP, lui-même derrière Edge Services. L'en-tête X-Forwarded-For arrive donc avec une ou plusieurs adresses ajoutées par les intermédiaires. Gunicorn ne remplace pas lui-même l'adresse distante par cet en-tête : son réglage --forwarded-allow-ips ne concerne, d'après sa documentation, que les en-têtes qui signalent une requête HTTPS (X-Forwarded-Proto). C'est le cadre de l'application qui doit le faire. En Flask, avec le composant de Werkzeug prévu à cet effet :

from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)
# Nombre d'intermédiaires de confiance qui ajoutent une valeur à X-Forwarded-For.
# À établir par l'observation (voir ci-dessous), jamais à deviner.
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2, x_proto=1)

x_for=2 fait prendre à Werkzeug la deuxième valeur en partant de la fin de la liste, en ignorant tout ce qui précède, c'est-à-dire ce qu'aurait pu inventer le client. La documentation de Werkzeug avertit que faire confiance à des valeurs venues du client plutôt que d'un mandataire est une faille. La bonne valeur dépend de la chaîne réelle : établissez-la en journalisant, sur une requête de test, l'en-tête X-Forwarded-For brut reçu par l'application, et comptez les adresses ajoutées par Edge Services et par le répartiteur. Une valeur trop grande lit une adresse fournie par le client ; une valeur trop petite lit l'adresse d'un intermédiaire.

Le protocole PROXY est l'autre voie, utile si le backend passe en TCP (par exemple pour faire terminer TLS par les instances elles-mêmes). Gunicorn le prend en charge depuis sa version 24.1, en v1, en v2 ou en détection automatique :

$ scw lb backend update $BE name=sig-app forward-protocol=tcp forward-port=8000 \
    forward-port-algorithm=roundrobin sticky-sessions=none \
    proxy-protocol=proxy_protocol_v2 zone=$Z

et côté instance, dans l'unité systemd :

ExecStart=/opt/signalements/venv/bin/gunicorn --bind 172.16.20.11:8000 \
    --proxy-protocol v2 --proxy-allow-from 172.16.20.0/22 app:app

--proxy-allow-from limite les adresses dont gunicorn accepte un en-tête PROXY (par défaut, seulement la boucle locale). Les deux réglages vont ensemble : un serveur qui attend le protocole PROXY rejette les connexions qui ne l'envoient pas, et la documentation de Scaleway cite ce désaccord comme une cause fréquente d'erreurs 400. Pensez aussi à la vérification de santé (check-send-proxy dans update-healthcheck), sinon tous les serveurs sont déclarés en panne. Pour Signalements, la voie X-Forwarded-For en HTTP suffit.

Les WebSocket du tableau de bord

$ scw lb backend update $BE name=sig-app forward-protocol=http forward-port=8000 \
    forward-port-algorithm=roundrobin sticky-sessions=none \
    timeout-tunnel=1h timeout-server=30s zone=$Z
  • timeout-tunnel=1h laisse une heure d'inactivité à une connexion WebSocket établie ; le tableau de bord envoie de toute façon un message de maintien toutes les trente secondes, ce qui rend la valeur peu critique.
  • timeout-server=30s raccourcit, pour les requêtes ordinaires, le délai de cinq minutes par défaut : une requête de l'API qui n'a pas répondu en trente secondes ne répondra plus utilement, et la garder ouverte occupe un processus gunicorn.

Les mêmes réglages valent pour le backend /v2/. Gunicorn a lui-même un délai (--timeout, trente secondes par défaut) au-delà duquel il tue un processus bloqué : gardez-le cohérent avec celui du répartiteur.

Protéger les instances pendant un pic

$ scw lb backend update $BE name=sig-app forward-protocol=http forward-port=8000 \
    forward-port-algorithm=roundrobin sticky-sessions=none \
    max-connections=8 timeout-queue=3s max-retries=2 redispatch-attempt-count=1 \
    on-marked-down-action=shutdown_sessions failover-host=signalements-maintenance.s3-website.fr-par.scw.cloud \
    zone=$Z

Chaque instance a deux processus gunicorn de quelques fils d'exécution : huit requêtes simultanées par instance est un ordre de grandeur, à affiner par un test de charge. Au-delà, les requêtes attendent trois secondes au plus, puis reçoivent 503, ce qui vaut mieux qu'une file sans fin. failover-host désigne le domaine de site web du bucket (format bucket.s3-website.région.scw.cloud, sans https://, comme le précise la documentation), qui affiche une page de maintenance si plus aucun serveur n'est sain.

Le frontend a aussi une protection : connection-rate-limit limite le nombre de nouvelles connexions par seconde (0 pour désactiver). C'est un garde-fou grossier, à régler bien au-dessus du trafic normal.

Mettre à jour sans couper

Pour remplacer sig-app-1 (version 1.3.0 de l'application, ou correctif du système), on la retire du pool, on attend que ses requêtes en cours se terminent, puis on la remplace :

$ scw lb backend remove-servers $BE server-ip.0=172.16.20.11 zone=$Z
$ sleep 35
$ # ... remplacement de l'instance (cours Le cloud : les fondamentaux, leçon 4) ...
$ scw lb backend add-servers $BE server-ip.0=172.16.20.31 zone=$Z
$ scw lb backend list-statistics lb-id=$LB zone=$Z

L'attente couvre le timeout-server de trente secondes. La dernière commande montre l'état de chaque serveur vu par la vérification de santé ; le nouveau ne reçoit du trafic qu'une fois déclaré sain. Avec deux instances seulement, Signalements tourne sur une seule le temps de l'opération : c'est un argument pour une troisième instance, ou pour le groupe d'instances de la leçon 2.

Activer les journaux d'accès

La documentation de Cockpit précise que les journaux d'accès ne sont pas collectés par défaut, et qu'ils s'activent par l'API (la console ne le propose pas) :

$ scw lb frontend update $FE_HTTPS name=https inbound-port=443 backend-id=$BE \
    certificate-ids.0=<certificat existant> certificate-ids.1=$CERT2 \
    enable-access-logs=true zone=$Z

Chaque ligne donne la date, l'adresse et le port du client, le frontend, le backend et le serveur choisis, puis la requête et le code de réponse. C'est l'outil pour comprendre un 503 ou repérer une adresse abusive ; c'est aussi une donnée personnelle (l'adresse IP), soumise à une durée de conservation.

Un répartiteur privé pour les services internes

Le service interne qui génère les rapports mensuels n'a rien à faire sur Internet. Un répartiteur privé n'a pas d'adresse publique et n'écoute que sur ses réseaux privés (huit au plus) :

$ LB_INT=$(scw lb lb create name=lb-rapports type=LB-S assign-flexible-ip=false zone=$Z -w -o json | jq -r .id)
$ scw lb private-network attach lb-id=$LB_INT private-network-id=<id de pn-signalements> zone=$Z

La documentation précise que l'accessibilité (publique ou privée) ne peut plus être changée après la création, qu'un répartiteur privé n'accepte pas de certificat Let's Encrypt (seulement des certificats importés), et qu'il est joignable dans le réseau privé sous le nom <nom du répartiteur>.<nom du réseau>.internal.

Survivre à la perte de fr-par-1

Créez un second répartiteur identique en fr-par-2, avec les mêmes backends (les instances des deux zones sont sur le même réseau privé régional), puis aiguillez par le DNS :

$ scw dns record add example.com name=signalements-ha type=A data=203.0.113.10 ttl=60 \
    http-service-config.ips.0=203.0.113.10 http-service-config.ips.1=203.0.113.20 \
    http-service-config.url=https://signalements-ha.example.com/sante \
    http-service-config.must-contain=ok http-service-config.strategy=all

La stratégie all renvoie les deux adresses saines, dans un ordre aléatoire : les clients se répartissent, et beaucoup de clients HTTP essaient l'adresse suivante quand la première ne répond pas. Le prix est le double du coût du répartiteur, et une configuration à maintenir à l'identique sur deux objets (une raison de plus de la décrire en code, cours Terraform et OpenTofu : les fondamentaux).

Sous le capot

Pourquoi l'ordre des ACL piège si souvent. Le modèle est celui d'un pare-feu à première correspondance : la première règle qui s'applique gagne. Mais contrairement à beaucoup de pare-feux, la politique par défaut, en fin de liste, est d'accepter. Une liste « autoriser A, autoriser B » n'interdit rien : tout ce qui n'est ni A ni B passe aussi. Seule une règle finale de refus sans condition ferme la liste.

Ce que fait le protocole PROXY. Au tout début de la connexion vers le serveur, avant le premier octet de la requête (ou de la négociation TLS), le répartiteur écrit une courte structure : en v1, une ligne de texte de la forme PROXY TCP4 <adresse client> <adresse destination> <port client> <port destination> ; en v2, un en-tête binaire qui commence par une signature de douze octets, suivi d'extensions facultatives (informations TLS, par exemple). Le serveur lit cette structure, en tire l'adresse du client, puis traite le reste comme une connexion ordinaire. Si le serveur ne s'y attend pas, il prend cette structure pour le début de la requête, et répond 400 ou ferme la connexion.

Pourquoi un pic sature le répartiteur avant les connexions. Chaque nouvelle connexion TLS coûte une négociation cryptographique, et chaque requête HTTP doit être analysée. La documentation note qu'un débit élevé de nouvelles connexions consomme plus de processeur qu'un grand nombre de connexions stables, et que le chiffrement en HTTP est la configuration la plus coûteuse. D'où l'intérêt du maintien de connexion côté clients, et d'Edge Services en amont, dont le cache absorbe une partie des requêtes.

Les 50 000 sessions par serveur. Pour se connecter à un serveur, le répartiteur choisit un port source local ; avec un seul couple adresse et port de destination, le nombre de ports disponibles limite le nombre de connexions simultanées vers ce serveur, la même contrainte que l'épuisement des ports éphémères décrit dans le cours TCP/IP. Ajouter des serveurs, ou des ports, répartit la charge sur plus de couples.

Pièges courants

Une ACL d'autorisation qui ne ferme rien. « allow 198.51.100.0/24 » seule ne bloque personne. Il faut la règle finale deny.

invert sur deux filtres. L'inversion s'applique aux deux filtres ensemble : « ni l'adresse, ni le chemin ». Pour « ce chemin, sauf depuis ces adresses », deux règles simples sont plus sûres qu'une règle combinée inversée.

Filtrer par adresse derrière un intermédiaire. Derrière Edge Services, le répartiteur voit l'adresse d'Edge Services, pas celle du client ; une ACL par adresse de client ne fonctionne plus, sauf sur l'en-tête X-Forwarded-For (filtre d'en-tête), qui n'est fiable que si l'intermédiaire écrase ce qu'envoie le client.

Protocole PROXY d'un seul côté. Activé sur le répartiteur et pas sur gunicorn (ou l'inverse), il produit des 400 ou des connexions fermées sur toutes les requêtes, et la vérification de santé déclare tous les serveurs en panne.

ProxyFix avec une valeur au hasard. x_for trop grand donne à un client le pouvoir de choisir son adresse ; trop petit, toutes les requêtes semblent venir du même intermédiaire. On l'établit en regardant l'en-tête réellement reçu.

Une mise à jour de frontend qui efface un certificat. scw lb frontend update remplace la liste des certificats par celle donnée. Oublier certificate-ids.0 retire le premier certificat. Lisez l'objet avec scw lb frontend get avant de le modifier.

Un LB-S dimensionné pour la moyenne. 20 000 connexions simultanées et 200 Mbit/s suffisent largement en temps normal ; un pic après une intempérie, avec des photos téléversées, peut atteindre la bande passante. La documentation décrit des coupures de plusieurs minutes quand la limite de connexions est atteinte. Surveillez les métriques, et redimensionnez (scw lb lb migrate) avant la saison des orages plutôt que pendant.

Sécurité

  • Fermer l'accès direct au répartiteur quand un intermédiaire (Edge Services) porte le WAF : sinon, le WAF se contourne en appelant l'adresse IP.
  • Ne jamais faire confiance à X-Forwarded-For sans compter les intermédiaires. Une application qui limite le débit, journalise ou autorise par adresse sur la base de la première valeur de l'en-tête obéit au client.
  • Chiffrer jusqu'aux instances si le réseau privé ne suffit pas : le répartiteur peut parler TLS aux serveurs (ssl-bridging) en vérifiant leur certificat. Dans un réseau privé dédié, beaucoup d'équipes s'en passent ; la décision dépend du modèle de menace et des exigences du client (cours Cloud souverain).
  • Les journaux d'accès contiennent des adresses IP, donc des données personnelles : durée de conservation fixée et documentée.
  • Le répartiteur privé réduit la surface d'attaque des services internes : aucun port public à protéger.
  • Les droits : quelqu'un qui peut modifier les ACL ou les routes peut ouvrir l'administration au monde ou détourner un nom vers un autre backend. Réservez ces droits, et versionnez la configuration.

En production

  • Décrivez le répartiteur en code dès qu'il a plus de deux ACL : l'ordre, les inversions et les champs obligatoires des mises à jour sont des sources d'erreurs que la relecture d'un fichier attrape mieux qu'une suite de commandes.
  • Suivez les métriques du répartiteur dans Cockpit (leçon 14) : connexions, débit, processeur, codes de réponse par backend, état des serveurs. La documentation signale que Scaleway fournit des alertes gérées quand une limite approche.
  • Testez la charge avant les pics prévisibles, avec un outil comme k6 ou vegeta, sur la préproduction, en observant à la fois le répartiteur et les instances. On y découvre le bon max-connections, le bon nombre d'instances, et la taille de répartiteur nécessaire.
  • Prévoyez la zone de secours selon l'objectif de disponibilité réel (leçon 3 du cours précédent) : un second répartiteur double le coût de cette brique, et ne sert que si les instances, la base et le réseau survivent aussi à la perte de la zone.
  • Chez Lyneko, le trafic des applications sur Kapsule entre par un répartiteur créé par Kubernetes pour l'ingress Traefik ; les ACL et les routes y sont rarement nécessaires, Traefik faisant le routage par hôte et par chemin.

Exercices

1. Ordonner les ACL (niveau 300). Sur le frontend 443, on veut : rediriger /ancien-formulaire vers /signalements/nouveau ; refuser /admin sauf depuis 198.51.100.0/24 ; n'accepter que le trafic d'Edge Services ; refuser le reste. Écrivez la liste des ACL (index, action, conditions), et expliquez pourquoi l'ordre choisi fonctionne.

Solution

Index 0 : redirect (type location, cible https://{{host}}/signalements/nouveau, code 301), condition chemin path_begin /ancien-formulaire. Index 1 : allow, conditions chemin path_begin /admin et adresse 198.51.100.0/24. Index 2 : deny, condition chemin path_begin /admin. Index 3 : allow, condition ips_edge_services. Index 10 : deny sans condition. La redirection vient en premier pour s'appliquer quelle que soit l'origine ; l'administration est traitée avant l'autorisation générale d'Edge Services, sinon Edge Services laisserait tout passer, /admin compris ; le refus final ferme la liste. Limite : si l'administration passe par Edge Services, l'adresse vue est celle d'Edge Services, et l'ACL d'index 1 ne laisse passer personne ; il faut alors exposer l'administration autrement (nom dédié non mis derrière Edge Services, réseau privé et bastion), ou filtrer dans l'application.

2. L'adresse qui ment (niveau 300). Une application derrière un seul répartiteur HTTP utilise ProxyFix(x_for=1). Un collègue propose x_for=3 « pour être sûr de ne rien manquer ». Un client envoie X-Forwarded-For: 10.0.0.1, 10.0.0.2. Quelle adresse l'application retient-elle dans chaque cas, et quel est le risque ?

Solution

Le répartiteur ajoute l'adresse réelle du client (disons 203.0.113.50) à la fin : l'application reçoit 10.0.0.1, 10.0.0.2, 203.0.113.50. Avec x_for=1, Werkzeug prend la dernière valeur, 203.0.113.50 : correct. Avec x_for=3, il prend la troisième en partant de la fin, 10.0.0.1, une valeur choisie par le client : celui-ci peut se faire passer pour n'importe qui, contourner une limitation de débit ou un filtre par adresse, et fausser les journaux. Le nombre doit être exactement celui des intermédiaires de confiance.

3. La zone perdue (niveau 300). fr-par-1 est indisponible pendant deux heures. Décrivez ce qui arrive à Signalements (a) avec l'architecture du cours précédent, (b) avec deux répartiteurs et l'enregistrement DNS à vérification de santé de cette leçon. Qu'est-ce qui reste à vérifier pour que (b) tienne vraiment ?

Solution

(a) Le répartiteur, en fr-par-1, est injoignable : le service est arrêté, même si sig-app-2 en fr-par-2 fonctionne. (b) La vérification de santé retire 203.0.113.10 ; après le TTL de 60 secondes, les résolveurs ne renvoient plus que 203.0.113.20, et le répartiteur de fr-par-2 sert le trafic vers sig-app-2. À vérifier : que la base de données survit (une base en un seul nœud en fr-par-1 arrête tout, quelle que soit l'exposition), que sig-app-2 tient seule la charge (dimensionnement), que la passerelle publique, zonale, n'est pas indispensable au fonctionnement (sortie Internet des instances de fr-par-2), et que les dépendances externes (files de messages, stockage objet, régionaux) sont disponibles. Et le tester, en coupant volontairement un répartiteur en préproduction.

Récapitulatif

  • Une requête traverse frontend, ACL, routes, backend ; les ACL s'appliquent par index croissant, la première qui s'applique décide, et la fin de liste accepte : une liste blanche se termine par un deny.
  • Les ACL redirigent (HTTP vers HTTPS par changement de schéma), filtrent par adresse, chemin ou en-tête, et peuvent n'accepter que le trafic d'Edge Services ; invert porte sur l'ensemble des filtres.
  • Les routes envoient vers d'autres backends selon l'hôte, le début du chemin ou le SNI ; un frontend porte plusieurs certificats.
  • L'adresse du client arrive par X-Forwarded-For (HTTP) ou par le protocole PROXY (TCP ou HTTP, à activer des deux côtés) ; on ne fait confiance qu'au nombre exact d'intermédiaires connus.
  • Les délais (timeout-server, timeout-tunnel pour les WebSocket), la protection des serveurs (max-connections, timeout-queue, page de secours) et les journaux d'accès se règlent par backend et par frontend.
  • Un répartiteur est zonal : bascule automatique sur une autre machine de la zone, mais pas d'une zone à l'autre, et une IP flexible qui ne change pas de zone. Survivre à la perte d'une zone demande deux répartiteurs et un aiguillage par le DNS ou en amont.

Pour aller plus loin

  • La page des ACL de Scaleway, dont les exemples couvrent la plupart des combinaisons.
  • La spécification du protocole PROXY publiée par HAProxy, courte et précise.
  • La documentation de Werkzeug sur ProxyFix, et celle de votre cadre si ce n'est pas Flask.
  • La leçon 13, pour relier plusieurs réseaux privés et donner au répartiteur privé des clients dans d'autres réseaux.
  • Le cours Reverse proxies et répartition de charge, pour HAProxy et Nginx que l'on exploite soi-même.
Voir ma constellation →

Sources