Kamal Proxy, le proxy HTTP minimal pour des déploiements sans interruption
(github.com/basecamp)- Kamal Proxy est un petit proxy HTTP qui s’exécute devant une application web et qui est conçu pour basculer un déploiement vers de nouvelles instances sans interrompre le trafic en cours
- Les nouvelles instances sont enregistrées avec
kamal-proxy deploy, et le proxy ne route le nouveau trafic vers elles qu’après validation des health checks HTTP - La commande de déploiement attend que tout le trafic ait quitté les anciennes instances ; une fois qu’elle s’est terminée avec succès, on peut supprimer les anciennes instances sans interrompre les requêtes en cours
- Il fournit le routage basé sur l’hôte, le routage basé sur le chemin, le TLS automatique et la spécification manuelle de certificats TLS afin d’exploiter plusieurs applications derrière un seul proxy
- Kamal Proxy a été conçu comme une partie de l’outil de déploiement Kamal, mais peut aussi être utilisé seul ou comme composant d’autres outils de déploiement
Ce que fait Kamal Proxy
- Kamal Proxy est un petit proxy HTTP conçu pour simplifier l’orchestration de déploiements sans interruption
- En exécutant une application web derrière Kamal Proxy, il est possible de déployer des changements sans interrompre le trafic en cours
- Aucun mécanisme de coopération particulier n’est nécessaire côté application pour que cela fonctionne
- Kamal Proxy a été conçu pour fonctionner comme une partie de Kamal, qui inclut le packaging en conteneur et le provisioning
- Il peut aussi être utilisé seul ou comme partie d’un autre outil de déploiement
Exécution et flux de déploiement
- L’instance du proxy se lance avec la commande
kamal-proxy run - Il n’y a pas de fichier de configuration ; des options peuvent être passées si les valeurs par défaut ne conviennent pas à l’application
- Le port HTTP par défaut est
80 - Pour l’exécuter sur un autre port, utiliser
kamal-proxy run --http-port 8080 - L’ensemble des options est disponible avec
kamal-proxy help run
- Le port HTTP par défaut est
- Pour envoyer le trafic vers l’application web, on déploie les instances de l’application sur le proxy
- Les instances cibles sont indiquées au format
hostname:port- Exemple :
kamal-proxy deploy service1 --target web-1:3000 - Cette commande enregistre
web-1:3000sous le nom de serviceservice1
- Exemple :
Mécanisme de bascule sans interruption
- Lorsqu’une nouvelle instance est enregistrée, Kamal Proxy exécute immédiatement un health check HTTP pour vérifier qu’elle est accessible et opérationnelle
- Si le health check réussit, il commence à router le nouveau trafic vers la nouvelle instance
- Si la nouvelle instance ne devient pas healthy dans le délai imparti, la commande
deployinterrompt le déploiement et renvoie un code de sortie non nul - Chaque déploiement transfère tout le trafic des instances précédemment déployées vers la nouvelle instance
- La commande
deployattend ensuite que tout le trafic ait quitté les anciennes instances avant de se terminer- Une fois la commande terminée avec succès, on peut supprimer les anciennes instances sans couper les requêtes en cours
- Comme la nouvelle instance ne reçoit du trafic que lorsqu’elle est healthy, et que les anciennes instances ne sont supprimées qu’une fois totalement drainées, le déploiement se fait sans interruption
Configuration des health checks
- Le health check par défaut envoie une requête
GETsur/upune fois par seconde pour chaque service - Une réponse
200est considérée comme un état healthy - Pour adapter les health checks à l’application, utiliser les options de
deploy--health-check-path--health-check-port--health-check-timeout--health-check-interval
- Exemple de changement du chemin du health check :
kamal-proxy deploy service1 --target web-1:3000 --health-check-path web/index.html
- Si le endpoint de health check est exposé sur un port différent du service principal :
kamal-proxy deploy service1 --target web-1:3000 --health-check-port 8080
Fonctions de routage
-
Routage basé sur l’hôte
- Le routage basé sur l’hôte permet à une seule instance de Kamal Proxy de router le trafic vers plusieurs applications sur le même serveur
- Un hôte spécifique peut être indiqué au moment du déploiement
kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com- Les instances déployées de cette façon ne reçoivent que le trafic destiné à l’hôte indiqué
- En attribuant un hôte distinct à chaque application, il est possible d’exécuter plusieurs applications sur le même serveur sans conflit de ports
- Un hôte donné ne peut router qu’un seul service à la fois
- Si
service1utilise déjàapp1.example.com, une erreur est renvoyée siservice2est déployé avec le même hôte - Après suppression de
service1,service2peut être déployé sur ce même hôte
-
Routage basé sur le chemin
- Le routage basé sur le chemin sert aux applications qui répartissent le trafic entre différents services selon le chemin de la requête
- Les services peuvent être montés sous différents préfixes de chemin
- Exemple pour envoyer les requêtes commençant par
/apiversweb-1:kamal-proxy deploy service1 --target web-1:3000 --path-prefix=/api - Exemple pour envoyer le reste vers
web-2:kamal-proxy deploy service2 --target web-2:3000 - Par défaut, le préfixe de chemin est supprimé avant la transmission vers l’upstream
- Une requête vers
/api/users/123est transmise àweb-1sous la forme/users/123 - Pour transmettre le chemin d’origine tel quel, préciser
--strip-path-prefix=false
Prise en charge de TLS
- Kamal Proxy peut obtenir et renouveler automatiquement des certificats TLS pour les applications
- Le TLS automatique s’active en ajoutant l’option
--tlslors du déploiementkamal-proxy deploy service1 --target web-1:3000 --host app1.example.com --tls
- Le TLS automatique nécessite la définition d’un hôte
- C’est une condition destinée à empêcher les demandes malveillantes de certificats pour des noms d’hôtes arbitraires
- Avec le routage basé sur le chemin, l’option TLS doit être définie sur le chemin racine
- Les services déployés sur d’autres chemins du même hôte utilisent la configuration TLS définie sur le chemin racine
- Lorsqu’il faut installer un certificat TLS obtenu manuellement, une autorité de certification interne ou un Cloudflare origin certificate, il est possible d’indiquer directement les chemins du certificat et de la clé privée
--tls-certificate-path cert.pem--tls-private-key-path key.pem
Variables d’environnement et build
- Dans des environnements comme l’exécution dans un conteneur Docker, les options de
runpeuvent être définies via des variables d’environnement kamal-proxy runlit la valeur de chaque option depuis les variables d’environnement si elles sont définieskamal-proxy run --http-port 8080HTTP_PORT=8080 kamal-proxy run
- Si un nom de variable d’environnement entre en conflit avec l’environnement existant, on peut le distinguer en ajoutant le préfixe
KAMAL_PROXY_KAMAL_PROXY_HTTP_PORT=8080 kamal-proxy run
- Le build local se fait avec
makedans un environnement Go fonctionnel - Pour construire avec un conteneur Docker, utiliser
make docker - Une configuration Docker Compose permettant de tester les commandes du proxy se trouve dans le dossier
example
1 commentaires
Avis sur Hacker News
C’est étrange d’avoir choisi deploy comme nom d’action
Le terme est déjà surchargé, ce qui risque de créer de la confusion du genre « l’app a-t-elle été déployée ? ou déployée sur le proxy ? »
Ils auraient pu utiliser
bind,intercept, ou simplementproxy, donc je ne comprends pas pourquoideployDe nos jours, il est facile de faire du déploiement sans interruption avec d’autres serveurs aussi, donc le choix d’en faire une application distincte complète est intéressant
Par exemple, avec une app + un proxy web prenant en charge les sockets Unix, il suffit de déplacer un fichier pour obtenir du sans interruption
C’est atomique, et on peut aussi envoyer des requêtes de préchauffage avec
curlCréer tout un système d’enregistrement paraît excessif
Kamal est un outil de déploiement utilisé pour économiser des millions de dollars en passant du cloud à son propre matériel (https://basecamp.com/cloud-exit)
S’il y a un article de référence, j’aimerais en apprendre davantage
Cela semble surtout viser à traiter le problème fondamental des coupures de trafic lors du remplacement de conteneurs de service avec Docker Swarm
Nous avons rencontré le même problème chez Cloud 66 en construisant un serveur JAMStack ; nous avons utilisé Caddy au lieu d’un proxy maison, et Traefik aurait probablement aussi très bien convenu
Je ne sais pas pourquoi Kamal a choisi Swarm plutôt que k8s ou k3s, mais la complexité doit bien se loger quelque part et ne peut pas être cachée ; ils semblent donc avoir fini par créer leur propre proxy
Je ne l’ai pas essayé moi-même, mais je suis assez sceptique : ils risquent de devoir courir sans cesse après la prise en charge des WebSockets, de SSE, de HTTP/3, des diverses compressions et du chiffrement
Cela ressemble à un exemple de proxy inverse parti d’une idée simple, mais qui finit par absorber peu à peu la complexité qu’il voulait éviter
Je pense qu’un système d’exécution large et extensible a besoin de concurrents, mais Kamal ressemble à un retour en arrière vers la complexité qu’il cherchait à fuir
Les capacités et la flexibilité de Kubernetes comme framework de gestion d’état désiré n’en ressortent que davantage
On peut continuer à utiliser Docker tout en utilisant Caddy, Traefik ou Envoy comme proxy
Kubernetes lui-même les utilise souvent comme contrôleurs d’ingress
À première vue, il ne semble pas utiliser Swarm
docker run; il n’y a ni Swarm ni orchestrationC’est essentiellement un outil simple qui remplace Capistrano
Le principe de base d’un service d’orchestration de conteneurs est de gérer les connexions et les déploiements blue/green via un contrôleur d’ingress
Plus précisément, contrôleur d’ingress est le nom donné à ce rôle, assumé depuis des années, voire des décennies, par d’innombrables proxys inverses pour ce cas d’usage
J’aimerais qu’on explique brièvement comment fonctionne en général un déploiement sans interruption
J’imagine une structure où deux versions de l’app tournent en même temps et où le nouveau trafic est routé vers la nouvelle version
Mais si l’on utilise une seule base de données et que la nouvelle version de l’app apporte une modification de schéma, le script de migration va modifier le schéma au démarrage, alors que l’ancienne version de l’app s’attend à l’ancien schéma
Je me demande comment cela se gère
Beaucoup de frameworks exécutent les migrations lors du déploiement du code, mais en pratique il vaut mieux contrôler manuellement le moment de leur exécution
Chaque version du code doit fonctionner à la fois avec le schéma actuel et avec le schéma futur après migration, ce qui revient en fait à écrire du code rétrocompatible
Le flux devient : « déployer le nouveau code qui fonctionne avec le schéma actuel comme avec le futur schéma → vérifier → exécuter la migration → vérifier → nettoyer le code gérant l’ancien schéma disparu »
C’est le coût supplémentaire à payer pour faire du déploiement sans interruption ou du déploiement progressif, et les grandes entreprises procèdent généralement ainsi
C’est encore plus vrai si l’on cible des utilisateurs d’une même région et que l’on peut choisir une plage horaire calme pour déployer
Deux versions de l’app peuvent s’exécuter en même temps
https://xata.io/blog/multi-version-schema-migrations
Le load balancer commence à accepter des connexions sur Server2 et n’en accepte plus de nouvelles sur Server1
Ensuite, lorsque toutes les connexions de Server1 sont fermées, on déconnecte Server1
Il peut s’agir de serveurs différents, ou de plusieurs workers sur un même serveur
Pendant cette période, comme l’indiquent les autres réponses, les migrations doivent être rétrocompatibles
Kamal 2 est actuellement en RC (https://github.com/basecamp/kamal/releases) et, comme il prendra en charge le SSL automatique, il devrait permettre de faire tourner facilement plusieurs apps avec Kamal sur un même serveur : c’est prometteur.
autocert, qui ressemble à une implémentation minimale.C’est fragile, et les limites d’émission de Let’s Encrypt ainsi que les limites de certificats simultanés rendent difficile le scaling horizontal des instances de proxy.
Caddy/Certmagic résout ce problème en écrivant les données dans un stockage partagé : un seul certificat est émis, puis toutes les instances le réutilisent et se coordonnent via ce stockage.
Il n’y a pas non plus de bascule vers un autre émetteur, de contournement des limites d’émission, ni de prise en charge d’ARI.
Le fait de retenir les requêtes jusqu’à ce que l’upstream devienne disponible est aussi bien géré par Caddy si l’on configure
try_durationettry_intervalsurreverse_proxy.Le traitement des en-têtes de proxy ne tient pas non plus compte des IP de confiance : si on l’active, quelqu’un peut définir
X-Forwarded-Foret usurper une IP.Heureusement, c’est désactivé par défaut, mais il n’y a même pas d’avertissement.
Je comprends que l’objectif soit la simplicité, mais dans son état actuel ce n’est pas assez mûr, et il y a trop de meilleures options pour que j’aie envie de l’utiliser en pratique.
Je ne vois pas bien comment on est censé l’utiliser, et j’ai l’impression de rater quelque chose.
L’exemple lance 4 réplicas du service
web.Si l’on peut déployer sur un de ces réplicas, comme
example-web-1, pour créer le service, je ne vois pas à quoi servent les 3 autres réplicas.Pour mettre à jour
webavec un déploiement sans interruption, il me semble qu’il faudrait exécuter la commande de build sur le serviceweb, démarrer ce service d’une manière ou d’une autre, puis lancer deploy vers la nouvelle cible.Mais si l’on exécute
docker compose up --build --force-recreate web, les réplicas existants sont arrêtés, ce qui rend tout cela inutile.Je n’ai pas vérifié si ce proxy répartit la charge entre les différents enregistrements renvoyés par les requêtes DNS, mais au moins avec un load balancing basé sur VIP, cela devrait fonctionner comme prévu.
La partie concernant la mise à jour est moins claire.
L’idée attendue est peut-être de lancer, dans le même réseau, un service portant un autre nom, puis d’exécuter
kamal-proxy deploy.Cela pourrait consister à mettre un numéro de version dans le nom du service ; et si l’on veut pouvoir revenir rapidement en arrière, garder l’ancienne version à chaud a aussi du sens.
[0]: https://docs.docker.com/reference/compose-file/deploy/#endpo...
k8s rollout restart deployment.Ou alors basculer le DNS ou le routeur entre deux backends.
Je me demande si cela implémente le pattern de mise en pause du trafic.
Le proxy arrête en pratique le trafic pendant quelques secondes : les requêtes entrantes semblent simplement prendre quelques secondes de plus que d’habitude, mais elles aboutissent après un court délai.
Pendant ces quelques secondes, on peut exécuter une petite migration de DB, ou un changement d’infrastructure bloquant un peu plus complexe qui peut se terminer en moins de 5 secondes.
Ça paraît assez risqué, et même après plusieurs années dans une entreprise de serveurs d’applications, je n’avais jamais entendu parler de ce pattern.
Cela dit, venant du cofondateur de Django, il a sans doute vu pas mal de déploiements, donc difficile de balayer ça d’un revers de main.
Je me souviens en avoir parlé autrefois sur ce sujet.
Je me demande où ils ont expliqué pourquoi ils ont choisi de créer leur propre proxy au lieu d’utiliser Traefik ou un autre outil éprouvé.
Le contexte expliquant pourquoi ils ont changé et décidé de l’implémenter eux-mêmes se trouve dans la PR.
https://github.com/basecamp/kamal/pull/940
Mais je ne crois pas qu’ils aient publié de comparaison officielle avec autre chose que Traefik.
Je me demande s’il existe un moyen de configurer les timeouts.
https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
https://blog.cloudflare.com/exposing-go-on-the-internet/
C’est du NIH. Rien à ajouter.