3 Punkte von GN⁺ 2024-09-22 | 1 Kommentare | Auf WhatsApp teilen
  • Kamal Proxy ist ein kleiner HTTP-Proxy, der vor Webanwendungen läuft und dafür entwickelt wurde, Deployments auf neue Instanzen umzuschalten, ohne laufenden Traffic zu unterbrechen
  • Neue Instanzen werden mit kamal-proxy deploy registriert; der Proxy routet neuen Traffic erst dann an die jeweilige Instanz, wenn sie einen HTTP-Health-Check bestanden hat
  • Der Deployment-Befehl wartet, bis der gesamte Traffic der vorherigen Instanz abgeflossen ist, sodass die alte Instanz nach erfolgreicher Rückkehr ohne Abbruch laufender Requests entfernt werden kann
  • Für den Betrieb mehrerer Anwendungen über einen einzelnen Proxy bietet er hostbasiertes Routing, pfadbasiertes Routing, automatisches TLS sowie die Angabe manueller TLS-Zertifikate
  • Kamal Proxy wurde als Teil des Deployment-Tools Kamal entwickelt, kann aber auch eigenständig oder als Bestandteil anderer Deployment-Tools verwendet werden

Was Kamal Proxy macht

  • Kamal Proxy ist ein kleiner HTTP-Proxy, der die Koordination von Zero-Downtime-Deployments vereinfachen soll
  • Wenn Webanwendungen hinter Kamal Proxy ausgeführt werden, lassen sich Änderungen deployen, ohne laufenden Traffic zu unterbrechen
  • Dafür ist keine besondere Mitwirkung der Anwendung erforderlich
  • Kamal Proxy ist dafür ausgelegt, als Teil von Kamal zu arbeiten, das Container-Packaging und Provisioning umfasst
  • Er kann auch eigenständig oder als Teil anderer Deployment-Tools eingesetzt werden

Ablauf von Ausführung und Deployment

  • Eine Proxy-Instanz wird mit dem Befehl kamal-proxy run gestartet
  • Es gibt keine Konfigurationsdatei; Optionen können angegeben werden, wenn die Standardwerte nicht zur Anwendung passen
    • Der Standard-HTTP-Port ist 80
    • Für den Betrieb auf einem anderen Port: kamal-proxy run --http-port 8080
    • Alle Optionen sind unter kamal-proxy help run einsehbar
  • Um Traffic an eine Webanwendung zu senden, wird eine Anwendungsinstanz beim Proxy deployt
  • Die Zielinstanz wird im Format hostname:port angegeben
    • Beispiel: kamal-proxy deploy service1 --target web-1:3000
    • Dieser Befehl registriert web-1:3000 unter dem Servicenamen service1

Verfahren für Zero-Downtime-Umschaltung

  • Sobald eine neue Instanz registriert ist, führt Kamal Proxy sofort einen HTTP-Health-Check aus, um Erreichbarkeit und Funktionsfähigkeit zu prüfen
  • Wenn der Health-Check erfolgreich ist, beginnt er, neuen Traffic an die neue Instanz zu routen
  • Wird die neue Instanz nicht innerhalb einer angemessenen Zeit healthy, bricht der Befehl deploy das Deployment ab und gibt einen Exit-Code ungleich 0 zurück
  • Jedes Deployment übergibt den gesamten Traffic der zuvor deployten Instanz an die neue Instanz
  • Der Befehl deploy wartet, bis der Traffic auf der vorherigen Instanz vollständig abgeflossen ist, und kehrt erst dann zurück
    • Nach erfolgreicher Rückkehr kann die vorherige Instanz entfernt werden, ohne laufende Requests abzubrechen
  • Da neue Instanzen nur im healthy-Zustand Traffic erhalten und die vorherige Instanz erst nach vollständigem Draining entfernt wird, erfolgt das Deployment mit Zero Downtime

Health-Check-Konfiguration

  • Der Standard-Health-Check sendet für jeden Service einmal pro Sekunde einen GET-Request an /up
  • Eine Antwort mit 200 gilt als healthy
  • Um den Health-Check an die Anwendung anzupassen, werden deploy-Flags verwendet
    • --health-check-path
    • --health-check-port
    • --health-check-timeout
    • --health-check-interval
  • Beispiel zum Ändern des Health-Check-Pfads:
    • kamal-proxy deploy service1 --target web-1:3000 --health-check-path web/index.html
  • Wenn der Health-Endpunkt auf einem anderen Port als der Hauptservice bereitgestellt wird:
    • kamal-proxy deploy service1 --target web-1:3000 --health-check-port 8080

Routing-Funktionen

  • Hostbasiertes Routing

    • Mit hostbasiertem Routing kann eine einzelne Kamal-Proxy-Instanz Traffic an mehrere Anwendungen auf demselben Server routen
    • Beim Deployment kann ein bestimmter Host angegeben werden
    • kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com
    • Eine so deployte Instanz erhält nur Traffic für den angegebenen Host
    • Wenn für jede Anwendung ein eigener Host angegeben wird, können mehrere Anwendungen ohne Port-Konflikte auf demselben Server laufen
    • Ein bestimmter Host kann zu einem Zeitpunkt nur an einen Service geroutet werden
    • Wenn service1 bereits app1.example.com verwendet, wird ein Fehler zurückgegeben, wenn service2 mit demselben Host deployt wird
    • Nachdem service1 entfernt wurde, kann service2 auf denselben Host deployt werden
  • Pfadbasiertes Routing

    • Pfadbasiertes Routing wird für Anwendungen verwendet, die Traffic je nach Request-Pfad auf unterschiedliche Services aufteilen
    • Services können unter verschiedenen Pfadpräfixen gemountet werden
    • Beispiel für Requests, die mit /api beginnen und an web-1 gesendet werden: kamal-proxy deploy service1 --target web-1:3000 --path-prefix=/api
    • Beispiel für den restlichen Traffic an web-2: kamal-proxy deploy service2 --target web-2:3000
    • Standardmäßig wird das Pfadpräfix entfernt, bevor der Request an den Upstream weitergegeben wird
    • Ein Request an /api/users/123 wird als /users/123 an web-1 weitergeleitet
    • Um den ursprünglichen Pfad unverändert weiterzugeben, wird --strip-path-prefix=false angegeben

TLS-Unterstützung

  • Kamal Proxy kann TLS-Zertifikate für Anwendungen automatisch beziehen und erneuern
  • Automatisches TLS wird aktiviert, indem beim Deployment das Flag --tls hinzugefügt wird
    • kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com --tls
  • Für automatisches TLS muss ein Host angegeben werden
    • Dies ist eine Bedingung, um zu verhindern, dass Zertifikate böswillig für beliebige Hostnamen angefordert werden
  • Beim pfadbasierten Routing muss die TLS-Option auf dem Root-Pfad gesetzt werden
    • Services, die unter anderen Pfaden desselben Hosts deployt sind, verwenden die TLS-Einstellungen des Root-Pfads
  • Wenn manuell bezogene TLS-Zertifikate, eine eigene Zertifizierungsstelle oder ein Cloudflare Origin Certificate installiert werden müssen, können Zertifikatsdatei und Pfad zum privaten Schlüssel direkt angegeben werden
    • --tls-certificate-path cert.pem
    • --tls-private-key-path key.pem

Umgebungsvariablen und Build

  • In Umgebungen wie Docker-Containern können run-Optionen als Umgebungsvariablen angegeben werden
  • kamal-proxy run liest Optionswerte aus Umgebungsvariablen, sofern diese gesetzt sind
    • kamal-proxy run --http-port 8080
    • HTTP_PORT=8080 kamal-proxy run
  • Wenn Namen von Umgebungsvariablen mit der bestehenden Umgebung kollidieren, kann zur Unterscheidung das Präfix KAMAL_PROXY_ verwendet werden
    • KAMAL_PROXY_HTTP_PORT=8080 kamal-proxy run
  • Ein lokaler Build wird in einer funktionierenden Go-Umgebung mit make durchgeführt
  • Für den Build in einem Docker-Container wird make docker verwendet
  • Eine Docker-Compose-Konfiguration zum Ausprobieren der Proxy-Befehle befindet sich im Ordner example

1 Kommentare

 
GN⁺ 2024-09-22
Hacker-News-Kommentare
  • Es ist seltsam, ausgerechnet deploy als Aktionsnamen gewählt zu haben.
    Der Begriff ist ohnehin schon überladen, daher könnte Verwirrung entstehen wie: „Wurde die App deployt? Oder zum Proxy deployt?“
    Man hätte auch bind, intercept oder einfach proxy verwenden können, deshalb ist unklar, warum ausgerechnet deploy gewählt wurde.

    • „Deployt“ kann man so verstehen, dass es läuft und beim Proxy registriert ist.
    • Wenn eingehender Traffic vom Proxy kommt, wofür sonst sollte deploy stehen, außer dass „der Proxy-Traffic beginnt zur neuen App-Instanz zu fließen“?
  • Heutzutage kann man Zero-Downtime-Deployments auch auf anderen Servern recht einfach umsetzen, deshalb ist die Entscheidung interessant, dafür gleich eine eigene ganze App zu bauen.
    Zum Beispiel können App+Web-Proxy-Setups, die Unix-Sockets unterstützen, Zero-Downtime einfach durch das Verschieben von Dateien erreichen.
    Das ist atomar, und man kann mit curl auch Warm-up-Requests schicken.
    Ein Registrierungssystem dafür zu bauen wirkt übertrieben.

    • Das ist nur ein sehr kleiner Teil von Kamal(https://kamal-deploy.org).
      Kamal ist ein Deployment-Tool, das genutzt wurde, um von der Cloud auf eigene Hardware umzuziehen und dabei Millionen von Dollar zu sparen(https://basecamp.com/cloud-exit).
    • Es wäre schön, wenn du diesen Ansatz etwas genauer erklären könntest.
      Falls es dazu einen guten Artikel gibt, würde ich gern mehr darüber lernen.
  • Das wirkt vor allem wie ein Versuch, das grundlegende Problem zu lösen, dass beim Ersetzen von Service-Containern in Docker Swarm Traffic unterbrochen wird.
    Bei Cloud 66 hatten wir dasselbe Problem beim Bauen von JAMStack-Servern, haben statt eines eigenen Proxys Caddy verwendet, und Traefik wäre vermutlich ebenfalls gut geeignet gewesen.
    Warum Kamal sich für Swarm statt k8s oder k3s entschieden hat, weiß ich nicht, aber Komplexität muss irgendwo untergebracht werden und lässt sich nicht verstecken, daher scheint man am Ende bei einem eigenen Proxy gelandet zu sein.
    Ich habe es selbst nicht benutzt, aber ich bin ziemlich skeptisch, weil man wohl ständig WebSockets, SSE, HTTP/3 sowie Unterstützung für verschiedene Komprimierungs- und Verschlüsselungsverfahren hinterherlaufen muss.

    • Kamal wirkt, als sei es auf der Annahme aufgebaut, dass „Kubernetes zu komplex ist“, und rekonstruiert von dieser Rechtfertigung aus einen beträchtlichen Teil dessen, was Kubernetes ohnehin macht.
      Es sieht aus wie ein Fall, in dem ein anfangs einfach gehaltener Reverse Proxy am Ende doch immer mehr von der Komplexität aufnimmt, die man eigentlich vermeiden wollte.
      Ich finde, breit einsetzbare und skalierbare Laufzeitsysteme brauchen Konkurrenz, aber Kamal wirkt wie Arbeit, die rückwärts in genau die Komplexität hineinläuft, die es vermeiden wollte.
      Dadurch treten die Fähigkeiten und die Flexibilität von Kubernetes als Framework für Desired-State-Management eher noch deutlicher hervor.
    • Es wirkt, als würden hier Orchestrierung und Proxying miteinander vermischt.
      Man kann weiter Docker nutzen und für den Proxy trotzdem Caddy, Traefik oder Envoy einsetzen.
      Auch Kubernetes verwendet diese am Ende oft als Ingress Controller.
    • Ich weiß nicht, wo du gelesen hast, dass Kamal Docker Swarm verwendet.
      So wie es aussieht, nutzt es Swarm nicht.
    • Kamal verwendet normale docker run-Befehle und hat kein Swarm und keine Orchestrierung.
      Im Grunde ist es ein einfaches Tool als Ersatz für Capistrano.
    • Falls das der Grund ist, wirkt es, als wäre zu diesem Thema nicht einmal ganz grundlegende Recherche gemacht worden.
      Die Basis von Container-Orchestrierungsdiensten ist, Verbindungen und Blue/Green-Deployments über Ingress Controller zu handhaben.
      Genauer gesagt ist Ingress Controller die Rollenbezeichnung, die vielen Reverse Proxys gegeben wurde, die diesen Anwendungsfall schon seit Jahren, teils seit Jahrzehnten, abdecken.
  • Es wäre hilfreich, wenn jemand kurz erklären könnte, wie Zero-Downtime-Deployments üblicherweise funktionieren.
    Ich vermute, dass dabei zwei Versionen der App gleichzeitig laufen und neuer Traffic zur neuen Version geroutet wird.
    Wenn man aber eine einzelne Datenbank nutzt und die neue App-Version ein Schema-Update mitbringt, wird beim Start das Migrationsskript das Schema ändern, während die alte App-Version noch das bisherige Schema erwartet.
    Mich würde interessieren, wie man damit umgeht.

    • Der erste Schritt ist, Migrationen und Deployments zu trennen.
      Viele Frameworks führen Migrationen bei Code-Deployments aus, aber in der Praxis ist es besser, den Ausführungszeitpunkt manuell zu steuern.
      Jede Code-Version muss sowohl mit dem aktuellen Schema als auch mit dem zukünftigen Schema nach den Migrationen funktionieren und wird dadurch faktisch abwärtskompatibler Code.
      Der Ablauf ist dann: „Neuen Code deployen, der sowohl mit aktuellem als auch zukünftigem Schema funktioniert → prüfen → Migrationen ausführen → prüfen → Code entfernen, der das alte Schema noch berücksichtigt“.
    • Migrationen müssen abwärtskompatibel sein, sodass das Datenbankschema beide Versionen der App unterstützen kann.
      Das sind die zusätzlichen Kosten, die man für Zero-Downtime- oder Rolling-Deployments zahlen muss, und größere Unternehmen machen das in der Regel so.
    • Wie andere Antworten erklären, gibt es Methoden, wenn man wirklich echte Zero-Downtime-Deployments braucht, aber in den meisten Organisationen und bei den meisten Migrationen ist die Ausfallzeit durch DB-Migrationen praktisch nicht von null zu unterscheiden.
      Besonders dann, wenn man Nutzer in nur einer Region bedient und einen ruhigen Zeitraum für das Deployment wählen kann.
    • Ich habe es selbst nicht benutzt, aber Xata scheint zumindest für Postgres eine ziemlich elegante Lösung für DB-Migrationen gebaut zu haben.
      Dabei können zwei Versionen der App gleichzeitig laufen.
      https://xata.io/blog/multi-version-schema-migrations
    • Genau, zu einem bestimmten Zeitpunkt müssen beide Versionen gleichzeitig laufen.
      Der Load Balancer beginnt dann, Verbindungen auf Server2 anzunehmen, und Server1 bekommt keine neuen Verbindungen mehr.
      Sobald danach alle Verbindungen auf Server1 geschlossen sind, wird Server1 getrennt.
      Das können unterschiedliche Server sein oder mehrere Worker auf einem einzelnen Server.
      Während dieses Zeitraums müssen die Migrationen, wie in den anderen Antworten beschrieben, abwärtskompatibel sein.
  • Kamal 2 ist derzeit ein RC(https://github.com/basecamp/kamal/releases), und es klingt vielversprechend, dass automatisches SSL unterstützt wird, sodass sich mehrere Apps auf einem Server mit Kamal leicht betreiben lassen

    • Es nutzt das Paket autocert, das eher einer Minimalimplementierung nahekommt
      Es ist fragil, und wegen der Ausstellungsgrenzen von Let's Encrypt sowie der Grenzen für gleichzeitige Zertifikate ist es schwierig, Proxy-Instanzen horizontal zu skalieren
      Caddy/Certmagic lösen dieses Problem, indem sie Daten in einen gemeinsamen Speicher schreiben, nur ein Zertifikat ausstellen und alle Instanzen es über den Speicher wiederverwenden und koordinieren lassen
      Es gibt auch keinen Ersatz-Aussteller, keine Umgehung von Ausstellungsgrenzen und keine ARI-Unterstützung
      Das Zurückhalten von Anfragen, bis der Upstream verfügbar wird, lässt sich bei Caddy ebenfalls gut handhaben, wenn man für reverse_proxy try_duration und try_interval setzt
      Auch die Verarbeitung von Proxy-Headern berücksichtigt keine vertrauenswürdigen IPs; wenn man sie aktiviert, kann jemand mit gesetztem X-Forwarded-For die IP fälschen
      Gut ist immerhin, dass dies standardmäßig deaktiviert ist, aber es gibt auch keine Warnung
      Ich verstehe, dass Einfachheit das Ziel ist, aber im aktuellen Zustand wirkt es unausgereift, und es gibt zu viele bessere Alternativen, als dass ich es in der Praxis einsetzen wollte
  • Ich verstehe nicht ganz, wie man das verwenden soll, und habe wohl etwas übersehen
    Das Beispiel startet 4 Replikate des Dienstes web
    Wenn man auf einem dieser Replikate, etwa example-web-1, deployen und damit den Dienst bereitstellen kann, ist mir unklar, was die übrigen 3 Replikate tun
    Um web zu aktualisieren und ein Deployment ohne Downtime zu erreichen, müsste man vermutlich im Dienst web den Build-Befehl ausführen, diesen Dienst irgendwie starten und dann ein Deploy auf das neue Ziel ausführen
    Wenn man jedoch docker compose up --build --force-recreate web ausführt, werden die bestehenden Replikate beendet, wodurch das alles sinnlos wird

    • Die anderen Replikate in der ersten Frage werden von Docker per Load Balancing bedient, entweder über ein VIP oder dadurch, dass bei DNS-Anfragen mehrere IPs zurückgegeben werden[0]
      Ich habe nicht geprüft, ob dieser Proxy über mehrere bei DNS-Anfragen zurückgegebene Records balanciert, aber zumindest bei VIP-basiertem Load Balancing sollte es wie erwartet funktionieren
      Der zweite Teil zur Aktualisierung ist weniger klar
      Möglicherweise ist gemeint, einen Dienst mit anderem Namen im selben Netzwerk zu starten und dann kamal-proxy deploy auszuführen
      Vielleicht in der Art, Versionsnummern in Dienstnamen zu verwenden, und wenn man schnell zurückrollen möchte, ergibt es auch Sinn, die alte Version warm zu halten
      [0]: https://docs.docker.com/reference/compose-file/deploy/#endpo...
    • Ich sehe nicht, warum man nicht einfach k8s rollout restart deployment verwenden sollte
      Oder man schaltet DNS bzw. den Router zwischen zwei Backends um
  • Ich frage mich, ob das das Muster Traffic anhalten implementiert
    Also dass der Proxy den Traffic für ein paar Sekunden effektiv stoppt, sodass eingehende Anfragen ein paar Sekunden länger als üblich zu dauern scheinen, aber nach kurzer Verzögerung abgeschlossen werden
    In diesen paar Sekunden könnte man kleine DB-Migrationen oder etwas komplexere blockierende Infrastrukturänderungen ausführen, die innerhalb von 5 Sekunden abgeschlossen werden können

    • Ich frage mich, ob das jemand schon in einer echten Produktionsumgebung gemacht hat
      Es klingt ziemlich riskant, und obwohl ich einige Jahre bei einem App-Server-Unternehmen gearbeitet habe, höre ich zum ersten Mal von diesem Muster
      Andererseits ist es schwer, es einfach abzutun, wenn es von einem Django-Mitgründer kommt, der sicher viele Deployments gesehen hat
    • Ehrlich gesagt klingt das nach dem Muster gefährlich leben
    • Caddy macht das
      Ich meine, ich erinnere mich, früher schon einmal Gespräche zu diesem Thema gesehen zu haben
  • Ich frage mich, wo erklärt wurde, warum man sich entschied, einen eigenen Proxy zu bauen, statt Traefik oder ein anderes bewährtes Tool zu verwenden

    • Tatsächlich wurde bis zu diesem „v2.0.0“-Prerelease Traefik verwendet
      Im PR gibt es Kontext dazu, warum man umgestiegen ist und sich für eine Eigenimplementierung entschieden hat
      https://github.com/basecamp/kamal/pull/940
    • Ich habe hier versucht, die Gründe etwas zusammenzufassen https://nts.strzibny.name/kamal-proxy/
      Aber ich glaube nicht, dass es jemals offiziell einen Vergleich mit anderen Optionen außer Traefik gab
  • Ich frage mich, ob es eine Möglichkeit gibt, Timeouts zu setzen
    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/

  • NIH. Mehr gibt es dazu nicht zu sagen