3 points par GN⁺ 2024-09-22 | 1 commentaires | Partager sur WhatsApp
  • 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
  • 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:3000 sous le nom de service service1

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 deploy interrompt 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 deploy attend 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 GET sur /up une fois par seconde pour chaque service
  • Une réponse 200 est 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 service1 utilise déjà app1.example.com, une erreur est renvoyée si service2 est déployé avec le même hôte
    • Après suppression de service1, service2 peut ê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 /api vers web-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/123 est transmise à web-1 sous 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 --tls lors du déploiement
    • kamal-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 run peuvent être définies via des variables d’environnement
  • kamal-proxy run lit la valeur de chaque option depuis les variables d’environnement si elles sont définies
    • kamal-proxy run --http-port 8080
    • HTTP_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 make dans 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

 
GN⁺ 2024-09-22
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 simplement proxy, donc je ne comprends pas pourquoi deploy

    • « Déployé » signifie qu’elle est en cours d’exécution et enregistrée auprès du proxy
    • Si le trafic entrant vient du proxy, je ne vois pas ce que deploy pourrait vouloir dire d’autre que « le trafic du proxy commence à être dirigé vers la nouvelle instance de l’app »
  • De 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 curl
    Créer tout un système d’enregistrement paraît excessif

    • Ce n’est qu’une toute petite partie de Kamal (https://kamal-deploy.org)
      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)
    • J’aimerais bien que tu expliques un peu plus cette approche
      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

    • Kamal donne l’impression d’être construit sur le postulat que « Kubernetes est trop complexe », puis, à partir de cette justification, de recréer une bonne partie de ce que fait Kubernetes
      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
    • Tu sembles mélanger orchestration et proxy
      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
    • Je ne sais pas où tu as vu que Kamal utilisait Docker Swarm
      À première vue, il ne semble pas utiliser Swarm
    • Kamal utilise de simples commandes docker run ; il n’y a ni Swarm ni orchestration
      C’est essentiellement un outil simple qui remplace Capistrano
    • Si c’est bien la raison, on a l’impression qu’il n’y a même pas eu de recherche très basique sur le sujet
      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

    • La première étape consiste à séparer les migrations du déploiement
      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 »
    • Les migrations doivent être rétrocompatibles, de sorte que le schéma de base de données puisse prendre en charge les deux versions de l’app
      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
    • Les autres réponses expliquent les méthodes à employer lorsqu’un vrai déploiement sans interruption est nécessaire, mais pour la plupart des organisations et la plupart des migrations, le temps d’arrêt dû aux migrations de DB est pratiquement indistinguable de zéro
      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
    • Je ne l’ai pas essayé moi-même, mais Xata semble avoir conçu une solution assez propre pour les migrations de DB, au moins avec Postgres
      Deux versions de l’app peuvent s’exécuter en même temps
      https://xata.io/blog/multi-version-schema-migrations
    • Oui, à un moment donné, les deux versions doivent tourner simultanément
      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.

    • Il utilise le paquet 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_duration et try_interval sur reverse_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-For et 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 web avec un déploiement sans interruption, il me semble qu’il faudrait exécuter la commande de build sur le service web, 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.

    • Pour la première question, les autres réplicas sont load-balancés par Docker via un VIP ou en renvoyant plusieurs IP dans les requêtes DNS[0].
      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...
    • Je ne vois tout simplement pas pourquoi ne pas utiliser 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.

    • Je me demande si quelqu’un a déjà fait ça dans un vrai environnement de production.
      Ç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.
    • Honnêtement, ça ressemble au pattern vivre dangereusement.
    • Caddy fait ça.
      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é.

    • En fait, avant cette préversion “v2.0.0”, ils utilisaient Traefik.
      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
    • J’ai essayé de résumer un peu les raisons ici : https://nts.strzibny.name/kamal-proxy/
      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.