HAProxy will be the first web-server for the HTTP client.
It will be the TLS/SSL termination. It will decide if the request is accepted or rejected,and finaly will pass the request to the upstream server or servers.
It is acting as a proxy, with load-balancing and fault-tolerance capabilities.
A frontend is an entry point. HAProxy is listening on the TCP or HTTP layer, on one or more `IP:PORT` pairs, with options regarding logs, timeouts, supported, protocols and much more.
Many frontend can coexist in an HAProxy instance,to accomodate different use cases. For example : a traditionnal web-site in one, and an API for third-parties in another one, each with their settings and configuration.
HAProxy is able to parse and process the full TCP or HTTP request. It exposes an internal API to change the request or the response, decide how to deliver them upstream or not… in a very optimized and reliable manner.
A backend is an exit point. There must be one for each group of final webservers. If there is only one server in a backend, it will deal with every request, but if there are more than one, HAProxy will balance the requests, according to an algorithm.
That's where we begin to mention load-balancing. And when we introduce som logic to deal with misbeahaving or missing servers, it becomes fault-tolerant.
Varnish va servir à mettre en cache le résultat de certaines requêtes pour ne pas avoir à solliciter le serveur applicatif final. Il va appliquer des règles plus ou moins complexes pour décider s'il met en cache,s'il sert depuis le cache…
Varnish propose par défaut un fonctionnement courant et relativement conventionnel qui nécessite généralement peu de configuration. Il y a des bonnes pratiques qui s'appliquent bien à la plupart des situations.
Les étapes clés du traitement d’une requête-réponse HTTP sont gérées par des fonctions dédiées, qu’il est possible de personnaliser et d'empiler/composer pour modifier le comportement.
Varnish expose toute la requête et la réponse aux fonctions afin de décider s’il faut modifier le contenu (ajouter/supprimer/modifier des en-têtes), servir une réponse depuis le cache ou la transmettre au serveur, mettre en cache ou pas la réponse d’un serveur…
Varnish stockant toutes les données de cache en mémoire, il est aussi important de décider selon quel algorithme décider ce qui doit être détruit lorsque la capacité maximale est atteinte…
Nous avons choisi de placer HAProxy et Varnish sur le même serveur, et de les faire communiquer par socket, mais c'est un détail et rien n'empêche de les mettre sur des serveurs différents.
Une fois la requête transmise à Varnish, il va également l'analyser pour décider s'il peut servir une réponse depuis le cache ou s'il doit la faire passer. Et s'il l'a fait passer, lors du retour il va décider s'il peut la mettre en cache.
Dans le cas le plus courant d'utilisation de Varnish, lorsqu'il doit faire passer une requête, il le fait directement au serveur d'application (lui aussi appelle ça un _backend_). Nous avons choisi de faire un peu différemment et de ne pas gérer dans Varnish la partie load-balancing et tolérance de panne, car elle est gérée de manière plus complète par HAProxy, et c'est ce que nous maitrisons le mieux.
Mais pour ne pas tomber dans une boucle, et pour ne pas refaire certaines analyses déjà faites au début, nous faisons rentrer la requête par un autre _frontend_. Celui-ci sera plus simple et aura comme principale responsabilité de choisir le _backend_ final à utiliser pour la requête. Si vous gérez des sites ou applications qui sont réparties sur des groupes de serveurs différents, il faut avoir autant de _backend_ que de groupes de serveurs.
La réponse fera ensuite le chemin inverse, en revenant à HAProxy, puis Varnish (qui décidera s'il met la réponse en cache), puis HAProxy puis le client HTTP qui a fait initialement la requête.
Même en faisant passer tout le trafic entrant (http/https) par un seul _frontend_, il est tout à fait possible d'avoir des serveurs finaux spécifiques pour chaque site, ainsi que des traitements intermédiaires spécifiques aussi.
Il n'y a pas dans HAProxy de logique ressemblant aux `VirtualHost` de Apache ou Nginx, mais on peut utiliser des primitives conditionnelles plus bas-niveau pour cela.
On crée (au moins) une ACL pour détecter si l'hôte demandée est lié à tel ou tel site, et ensuite on utilise ces ACL pour conditionnement un comportement.
Dans HAProxy, au niveau du frontend il y a une ACL qui permet de savoir si Varnish est disponible ou pas. Nous pouvons donc transmettre à Varnish s'il est dispo ou le contourner s'il est absent :
Dans leur rôle de proxy intermédiaire, HAProxy et Varnish sont vus comme des clients au niveau TCP ; entre eux, mais surtout vis-à-vis du serveur web final. Si on ne fait rien de particulier,le serveur final verra toujours l'IP d'HAProxy comme IP du client. Ça rend impossible l'application d’autorisations ou restrictions par IP. Ça rend difficile le suivi des requêtes, les statistiques…
Vous me direz qu'on a pour ça l'en-tête HTTP `X-Forwarded-For`, mais elle est invisible au niveau TCP, et sa prise en compte au niveau applicatif n'est pas toujours facile, voire possible.
Le PROXY protocol est une simple extension de TCP qui permet d'ajouter cette notion d'IP réelle du client.
Cela impose que les 2 parties de l'échange sachent gérer cette extension, mais à partir de là il n'est plus besoin de le gérer dans la couche applicative.
### Comment?
Au niveau HAProxy, nous retrouvons celà dans le backend qui transmet à Varnish, dans le frontend de retour depuis Varnish et éventuellement dans le backend qui transmet au serveur web final.
```
backend varnish
server varnish_sock /run/varnish.sock check observe layer7 maxconn 3000 inter 1s send-proxy-v2
frontend internal
bind /run/haproxy-frontend-default.sock user root mode 666 accept-proxy
backend example_com
server example-hostname 1.2.3.4:443 check observe layer4 ssl verify none send-proxy-v2
Et pour le retour vers HAProxy, c'est dans la configuration, avec l'option `proxy_header = 1` :
```
backend default {
.path = "/run/haproxy-frontend-default.sock";
.proxy_header = 1;
[…]
}
```
### Debug sans PROXY protocol
Bien que ça soit une optimisation très appréciable, elle n'est pas compatible avec de nombreux outils qui peuvent avoir besoin de se connecter à HAProxy ou Varnish de manière classique.
C'est particulièrement utile au niveau de Varnish pour faire du debug sans passer par HAProxy. On ajoute alors à la ligne de commande de démarrage un autre point d'entrée. Nous le faisons uniquement en local, sur un port inaccessible à l'extérieur :
Il devient alors très facile de faire une requête directe :
```
curl --verbose \
--resolve www.example.com:82:127.0.0.1 \
--header "X-Forwarded-Proto: https" \
http://www.example.com:82/foo/bar
```
Alors oui, curl supporte le PROXY protocol depuis laversion 7.37.0 avec l'option `--proxy-header`, mais c'est un exemple.
### Jusqu'au serveur final
Si on contrôle les serveurs web finaux et qu'il ssont compatibles, on peut aussi faire la liaison finale avec le PROXY protocol. On sera alors aussi tenté d'ajouter au serveur web un port d'écoute sans PROXY protocol, et bien protégé.
Note: si vous utilisez Apache, je vous recommande d'étudier le module "ForensicLog" qui permet facilement un debug détaillé de tous les en-têtes échangés. C'est très pratique pour débug des chaînes de proxy et voir si tout arrive bien au serveur web final. Je ne sais pas s'il existe quelque chose de similaire pour Nginx.
Toujours dans une optique de traçabilité et de facilité de debug, nous nous servons de HAProxy et Varnish pour ajouter dans les en-têtes HTTP des informations sur leur fonctionnement.
Rappelons que la norme HTTP définit des en-têtes normalisés, dont la syntaxe et l'utilisation sont bien codifiés : `Host`, `Set-Cookie`, `Cache-Control`…
Mais nous pouvons ajouter n'importe quel en-tête no standard avec un prefix `X-`. Certains sont quasi standards, d'autres sont totalement personnalisés.
Bien que nous utilisions le PROXY protocol en interne (et éventuellement vers certains serveurs finaux), nous conservons généralement l'en-tête HTTP `X-Forwarded-For`.
Ce dernier en-tête est souvent imortant lorsque la requête finale est utilisée par des framework qui impose un accès en https te ne peuvent pas s'appuyer sur le fait que la requête leur arrive au final en https.
Il est très utile de marquer une requête entrante d'un identifiant unique qui pourra être transmis d'étape en étape jusqu'au serveur final (et pourquoi pas au retour aussi).
```
frontend external
[…]
http-request set-header X-Unique-ID %[uuid()] unless { hdr(X-Unique-ID) -m found }
```
Il n'y a pas de réel consensus sur le nommage de l'en-tête. On trouve souvent `X-Unique-ID` ou `X-Request-ID`.
Sur le frontend « external » nous marquons la requête comme étant passée en étape 1 par « haproxy-external ». Les autres étapes de la requête sauront alors que la requête est entrée par là.
Lorsque réponse resortira de ce backend pour aller au client, on la marque aussi pour indiquer que l'étape 1 était « haproxy-external » en précisant si la connexion était en http ou https.
Par ailleurs, Varnish ajoute par défaut de nombreux en-têtes sur l'utilisation ou non du cache, l'âge de la ressource…
Note: La syntaxe de ces en-têtes `X-Boost-*` est encore expérimentale. Nous prévoyons de les rendre plus compacts et faciles à traiter automatiquement.
:warning: Il vaut mieux ne pas activer cela en production, mais ça peut être très utile pour permettre à un client en mode test/préprod de vérifier comment se comporte le proxy.
Lorsque le site ou l'application web finale dispose de plusieurs serveurs pour gérer les requêtes, on peut prendre en charge directement dans HAProxy la répartition de charge.
Lorsque c'est possible, on fait de préférence un `round-robin` sur les serveurs web.
Lorsque l'application gère mal les sessions ou des aspects bloquants, on met un serveur en actif et les autres en backup, pour basculer sur un secours si le primaire n'est plus disponible.
### Côté HAProxy
Pour éviter que HAProxy + Varnish ne soient un « Single Point Of Failure », nous avons mis en place 2 serveurs différents dans 2 réseaux différents et nous faisons un round-robin DNS en amont.
Cette approche n'est pas la plus avancée, car en cas de panne d'un serveur il faut intervenir au niveau DNS. Cependant elle a le mérite de la simplicité et les pannes complète de machines virtuelles tournant sur de la virtualisation redondante sont heureusement très rares. En revanche, ce système nous permet pas mal de souplesse.
Il serait aussi possible de faire du « actif-passif » en mettant une IP flottante (keepalived/vrrp) en amont de deux serveurs, pour avoir une bascule automatique sur le secondaire en cas de panne du primaire.
Ces approches permettent une reprise d'activité quasi immédiate en cas de panne, mais elles impliquent plus de ressources et une topologie réseau particulière.
## Fonctionnalités complémentaires
### Filtrage TCP
Dans le frontend « external » qui gère le trafic entrant, nous pouvons vérifier si le client doit être immédiatement rejeté, par exemple selon son adresse IP :
```
frontend external
[…]
# Reject the request at the TCP level if source is in the denylist
tcp-request connection reject if { src -f /etc/haproxy/deny_ips }
```
Cela ne remplace pas un vrai firewall, mais ça permet de facilement exclure des client au niveau TCP (couche 4).
### Filtrage HTTP
En analysant la requête au niveau HTTP (couche 7), on peut filtrer de manière beaucoup plus fine.
Par exemple, si un site est passé en mode maintenance (détaillé plus loin), on peut contourner ce mode maintenance pour une liste d'IP particulière qui pourra tout de même consulter le site et vérifier par exemple l'état des opérationsde maintenance.
Pour les outils locaux de monitoring (exemple: Munin) ou pour les challenges ACME, il peut être utile de renvoyer sur un serveur web local (exemple: Apache ou Nginx), au lieu de renvoyer sur Varnish ou les serveurs web appliatifs.
```
frontend external
[…]
# Is the request coming for the server itself (stats…)
Pour une coupure par site, il faut définir un backend spécial pour ce site et activer l'utilisation de ce backend pour les requêtes. On retrouve notre ACL par domaine.