Warum normales Deployment immer eine Lotterie ist?
\nJeder Entwickler hat mindestens einmal im Leben diesen Moment der Wahrheit erlebt: Du drückst den "Deploy"-Button und das Herz rutscht in die Hose. Was, wenn die neue Version die Datenbank lahmlegt? Was, wenn die Benutzer einen 500er-Fehler statt der Startseite sehen?
\nNormales Deployment ist wie ein Fallschirmsprung ohne Reserve. Entweder funktioniert alles perfekt, oder du verbringst die Nacht mit Logs und versuchst zu verstehen, was schiefgelaufen ist. Aber es gibt einen Weg, diesen Prozess sicher zu machen – Canary-Deployments.
\nIn diesem Artikel schauen wir uns an, wie man Canary-Deployments in Kotlin mit Ktor und einem einfachen Load-Balancer implementiert. Du lernst, neuen Code zunächst an 5% der Benutzer auszuliefern und dann den Traffic schrittweise zu erhöhen. Und ja, wir schreiben Code, der wirklich funktioniert.
\n\nWas ist ein Canary-Deployment und warum brauchst du es?
\nStell dir vor, du bist ein Kapitän. Anstatt sofort durch die Riffe zu segeln, schickst du ein kleines Boot (den Kanarienvogel) voraus. Wenn es durchkommt, folgt die gesamte Flotte. Wenn es sinkt, verlierst du nur ein Boot.
\nIn der IT-Welt sieht das so aus: Du rollst eine neue Version des Dienstes zunächst für eine kleine Gruppe von Benutzern aus. Wenn die Metriken in Ordnung sind, wird der Traffic erhöht. Wenn nicht, rollst du innerhalb von Sekunden zurück.
\nDie Vorteile liegen auf der Hand:
\n- \n
- Risikominimierung – Fehler betreffen nur 1-5% der Zielgruppe. \n
- Schnelles Feedback – Du siehst Fehler, bevor sie zur Katastrophe werden. \n
- A/B-Testing – Du kannst die Leistung der alten und neuen Version vergleichen. \n
- Zero Downtime – Benutzer bemerken den Wechsel nicht. \n
Aber es gibt auch einen Wermutstropfen: Man muss Traffic verwalten, Metriken überwachen und den Prozess automatisieren können. Kotlin mit seiner Prägnanz und den Coroutinen eignet sich hervorragend für diese Aufgabe.
\n\nArchitektur eines Canary-Deployments in Kotlin
\nLasst uns eine einfache, aber funktionale Architektur aufbauen. Wir haben:
\n- \n
- Load-Balancer – Entscheidet, welcher Prozentsatz der Anfragen an die alte Version (stable) und welcher an die neue Version (canary) geht. \n
- Zwei Service-Instanzen – stable und canary, beide in Kotlin mit Ktor geschrieben. \n
- Monitoring – Zähler für Fehler und Antwortzeiten. \n
Der Load-Balancer trifft seine Entscheidung entweder basierend auf dem Header X-Canary oder per Zufallsverteilung. Wir gehen den zweiten Weg – er ist näher an der Realität.
Code schreiben: Load-Balancer in Kotlin
\nBeginnen wir mit dem Load-Balancer. Er nimmt Anfragen entgegen und leitet sie an einen der beiden Dienste weiter. Der Einfachheit halber verwenden wir den integrierten HTTP-Client von Ktor.
\n\nimport io.ktor.client.*\nimport io.ktor.client.engine.cio.*\nimport io.ktor.client.request.*\nimport io.ktor.client.statement.*\nimport io.ktor.server.application.*\nimport io.ktor.server.engine.*\nimport io.ktor.server.netty.*\nimport io.ktor.server.response.*\nimport io.ktor.server.routing.*\nimport kotlinx.coroutines.runBlocking\nimport kotlin.random.Random\n\n// Konfiguration\nconst val STABLE_URL = "http://localhost:8081"\nconst val CANARY_URL = "http://localhost:8082"\nvar canaryPercent = 5 // Wir beginnen mit 5% Traffic\n\nfun main() {\n val client = HttpClient(CIO)\n\n embeddedServer(Netty, port = 8080) {\n routing {\n get("/api/data") {\n val useCanary = Random.nextInt(100) < canaryPercent\n val targetUrl = if (useCanary) CANARY_URL else STABLE_URL\n\n val response = runBlocking {\n client.get("$targetUrl/api/data")\n }\n\n call.respondText(\n response.bodyAsText(),\n contentType = response.contentType()\n )\n }\n\n // Endpunkt zum Ändern des Canary-Prozentsatzes\n get("/canary/set/{perc