Kamal Proxy: ein minimaler HTTP-Proxy für Zero-Downtime-Deployments
(github.com/basecamp)- 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 deployregistriert; 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 rungestartet - 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 runeinsehbar
- Der Standard-HTTP-Port ist
- Um Traffic an eine Webanwendung zu senden, wird eine Anwendungsinstanz beim Proxy deployt
- Die Zielinstanz wird im Format
hostname:portangegeben- Beispiel:
kamal-proxy deploy service1 --target web-1:3000 - Dieser Befehl registriert
web-1:3000unter dem Servicenamenservice1
- Beispiel:
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
deploydas 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
deploywartet, 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
200gilt 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
service1bereitsapp1.example.comverwendet, wird ein Fehler zurückgegeben, wennservice2mit demselben Host deployt wird - Nachdem
service1entfernt wurde, kannservice2auf 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
/apibeginnen und anweb-1gesendet 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/123wird als/users/123anweb-1weitergeleitet - Um den ursprünglichen Pfad unverändert weiterzugeben, wird
--strip-path-prefix=falseangegeben
TLS-Unterstützung
- Kamal Proxy kann TLS-Zertifikate für Anwendungen automatisch beziehen und erneuern
- Automatisches TLS wird aktiviert, indem beim Deployment das Flag
--tlshinzugefügt wirdkamal-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 runliest Optionswerte aus Umgebungsvariablen, sofern diese gesetzt sindkamal-proxy run --http-port 8080HTTP_PORT=8080 kamal-proxy run
- Wenn Namen von Umgebungsvariablen mit der bestehenden Umgebung kollidieren, kann zur Unterscheidung das Präfix
KAMAL_PROXY_verwendet werdenKAMAL_PROXY_HTTP_PORT=8080 kamal-proxy run
- Ein lokaler Build wird in einer funktionierenden Go-Umgebung mit
makedurchgeführt - Für den Build in einem Docker-Container wird
make dockerverwendet - Eine Docker-Compose-Konfiguration zum Ausprobieren der Proxy-Befehle befindet sich im Ordner
example
1 Kommentare
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,interceptoder einfachproxyverwenden können, deshalb ist unklar, warum ausgerechnetdeploygewählt wurde.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
curlauch Warm-up-Requests schicken.Ein Registrierungssystem dafür zu bauen wirkt übertrieben.
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).
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.
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.
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.
So wie es aussieht, nutzt es Swarm nicht.
docker run-Befehle und hat kein Swarm und keine Orchestrierung.Im Grunde ist es ein einfaches Tool als Ersatz für Capistrano.
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.
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“.
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.
Besonders dann, wenn man Nutzer in nur einer Region bedient und einen ruhigen Zeitraum für das Deployment wählen kann.
Dabei können zwei Versionen der App gleichzeitig laufen.
https://xata.io/blog/multi-version-schema-migrations
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
autocert, das eher einer Minimalimplementierung nahekommtEs 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_proxytry_durationundtry_intervalsetztAuch die Verarbeitung von Proxy-Headern berücksichtigt keine vertrauenswürdigen IPs; wenn man sie aktiviert, kann jemand mit gesetztem
X-Forwarded-Fordie IP fälschenGut 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
webWenn man auf einem dieser Replikate, etwa
example-web-1, deployen und damit den Dienst bereitstellen kann, ist mir unklar, was die übrigen 3 Replikate tunUm
webzu aktualisieren und ein Deployment ohne Downtime zu erreichen, müsste man vermutlich im Dienstwebden Build-Befehl ausführen, diesen Dienst irgendwie starten und dann ein Deploy auf das neue Ziel ausführenWenn man jedoch
docker compose up --build --force-recreate webausführt, werden die bestehenden Replikate beendet, wodurch das alles sinnlos wirdIch 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 deployauszuführenVielleicht 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...
k8s rollout restart deploymentverwenden sollteOder 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
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
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
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
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