
Hinter den Kulissen
Mehr KI und noch mehr Preistransparenz – das «Hackfest» liefert Ideen und Ergebnisse
von Martin Jungfer
Preview-Deployments sind immer so eine Sache, vor allem, wenn man sie nicht aufräumt. Was Preview-Deployments überhaupt sind und wie wir sie in Form gebracht haben, erzähle ich euch hier.
Wer mit Deployments in irgendeiner Form zu tun hat, weiss, dass man diese auch gelegentlich mal updaten muss, und wer updated, will auch testen können (und auch wer nicht will, muss trotzdem, aber das ist ein anderes Thema). Bei Frontend-Belangen kommt die Schwierigkeit hinzu, dass oftmals mehrere unterschiedliche Änderungen gleichzeitig getestet werden müssen, weshalb wir nicht einfach das bestehende Test-Deployment überschreiben dürfen. Dafür gibt’s die Preview-Deployments. Dabei handelt es sich um Frontend-Deployments, die zum Testen einer spezifischen Änderung da sind, und die mitunter in grösserer Anzahl parallel laufen müssen. Damit Nicht-Tech-Spezialisten sich diese anschauen können, ohne dafür einen Kurs besuchen zu müssen, sollten sie auch einfach im Browser anzusteuern sein.
Bei uns laufen all diese Deployments auf Kubernetes, spezifischer: Auf Azure Kubernetes Service (AKS), was uns einige Vorteile bringt, aber uns nicht alles abnimmt. GKE alias Google Kubernetes Engine haben wir übrigens auch im Einsatz, aber nicht für diesen Use Case. Insbesondere das Aufräumen macht auch AKS nicht freiwillig, was bei regulären Deployments (wo neuere Versionen ältere überschreiben) kein Problem ist, aber bei Frontend-Previews, die sich eben gerade nicht gegenseitig überschreiben dürfen und darum bisweilen in unvorhersagbarer Anzahl präsent sind, eben schon. Die sollten nach Gebrauch wieder ordentlich weggeräumt werden. Aber davon wollten wir schon als Kinder nichts wissen, und weil IT-Leute im Grunde genommen lediglich grossgewachsene Achtjährige sind, darf von uns hier auch nicht allzu viel erwartet werden. Wir hatten also eine Menge Deployments am Start, und mit ihnen die zugewandten Ressourcentypen Service und Ingress, und niemanden, der regelmässig aufräumt. Könnten wir wenigstens die Pods, die für CPU- und RAM-Verbrauch verantwortlich sind, auf null halten, bis sie gebraucht werden?
Horizontal Pod Autoscaler to the rescue! Wenn das denn nur so einfach wäre… der skaliert nämlich nicht auf null, bzw. skaliert er ein Deployment mit null Replicas nicht hoch, weil er dazu mindestens einen Pod bräuchte, dessen CPU- und RAM-Verbrauch er messen kann. Wir mussten also alle Preview-Deployments mit mindestens einem Pod am Laufen halten, für den Fall, dass jemand sich das anschauen will – und unsere Frontend-Teams deployen sehr fleissig. Das kostet Ressourcen, die anderen Deployments dann nicht zur Verfügung stehen, und die man durch mehr oder grössere Nodes ausgleichen muss – geschenkt gibt es die aber freilich nicht. Eine andere Lösung musste also her, und nach etwas Recherche-Arbeit fanden wir sie in Knative.
Red Hat sagt: kay-nay-tiv. Wir benutzen die Serving-Komponente davon. Für Eventing setzen wir stattdessen auf KEDA, aber das ist eine andere Geschichte und soll ein andermal erzählt werden – auch Features wie Traffic Splitting benutzen wir aktuell nicht.
Grundsätzlich sehen Knative-Servings ganz ähnlich aus wie die handelsübliche Trias Deployment – Service – Ingress, setzen aber auf eigene Custom Resource Definitions und sind in der Lage, von null zu skalieren, sobald Requests eintreffen. Das erlaubt uns, mal grundsätzlich von allen Pull Requests ein Preview Deployment zu builden und auszurollen, ohne Systemressourcen dafür reservieren zu müssen – unabhängig davon, ob das dann von irgendjemandem angeschaut wird oder nicht. Ein dedizierter, Knative-eigener Ingress-Controller namens Kourier hilft ausserdem dabei, die Requests zu der gewünschten Instanz zu routen, ohne manuell etwas einstellen zu müssen.
So sieht ein reguläres Deployment aus:
Und so eines, das mit Knative erstellt worden ist:
Selbstverständlich sollen sich unsere Feature-Teams nicht mit Knative oder anderen infrastrukturverwandten Themen mehr herumschlagen müssen als nötig. Wir, Team Bender, sind eines von mehreren Platform-Engineering-Teams, und nebst Kubernetes auch mit dafür zuständig, den Feature-Teams das Deployen so komfortabel wie möglich zu machen. Darum haben wir uns schon vor einiger Zeit mal daran gemacht, eine ganze Sammlung von Helm-Charts anzulegen, die alle schlimmlichen Details so weit abstrahieren, dass jedes Team nur ein einfaches values.yaml-File mit den wichtigsten Angaben in seinem Repository haben muss.
Das Bereitstellen von Knative war im Wesentlichen eine zweigeteilte Sache:
Der Kourier-Ingress-Controller hat uns dann in Zusammenarbeit mit Wildcard-DNS dabei geholfen, jedes Preview-Deployment unter seiner Pull-Request-ID zur Verfügung zu stellen, und los konnte die Sache gehen.
Probleme nicht, aber Herausforderungen. Kourier als zusätzlicher Ingress-Controller hat uns etwas Zusatzaufwand gekostet. Grundsätzlich setzen wir auf Nginx. Da hätten wir es schon bevorzugt, dies auch bei Knative so zu handhaben, da Ingress Rules oftmals controller-spezifisch konfiguriert werden müssen. Weil unsere Preview-Deployments alle eine Wildcard-Subdomain verwenden, die ansonsten für nichts anderes gebraucht wird, konnten wir den Traffic ziemlich einfach zu Kourier leiten, ohne dass sich Nginx hier zuständig hätte fühlen müssen.
Leider aber haben Knative-Deployments an einigen Stellen etwas andere Performance-Charakteristika als reguläre Deployments, so dass wir sie nur eingeschränkt für Lasttests nutzen können. Wir haben den Kourier-Controller in Verdacht, der auch an einigen Stellen buffert, wo das der Nginx nicht tut, aber mehr als Kaffeesatzlesen ist das an dieser Stelle nicht. Zum Glück herrscht in einer IT-Abteilung an Kaffeesatz selten Mangel.
Ein weitere Herausforderung war, dass Knative keine hostPath-Mounts unterstützt. Datadog, das wir als Monitoring-Lösung einsetzen, benötigt diese aber, um seine Config in jeden Pod mounten zu können, und so sind wir für den Moment metrik- und alertlos, was Preview-Deployments betrifft. Reguläre Deployments benutzen kein Knative, darum ist das halb so wild, auch wenn allfällige Probleme hier schon zu erkennen natürlich schön wäre. Die Details sind noch ein bisschen nebulös, aber eine grundlegende Vorstellung, wie das zu lösen wäre, haben wir schon.
Die grössere Herausforderung war das oben erwähnte Aufräumen. Das macht nämlich auch Knative nicht von selbst.
Knative erstellt für jedes Preview-Deployment mehrere Services, damit ältere und neuere Versionen nebeneinander laufen können. Und weil wir, was das Aufräumen betrifft, mit unterschiedlicher Motivation zu Werke gehen, sind im Frontend-Namespace irgendwann mal tausende von K8s-Ressourcen herumgelegen. Kubernetes findet das nicht sonderlich gut, weil dann in diesem Namespace kein Service seine Pods mehr findet, bis man mal seine innere Marie Kondō beschwört, und tut, was getan werden muss. Gemerkt haben wir das, weil auch die regulären Test-Frontend-Deployments nicht mehr liefen, mit denen sich Knative denselben Namespace teilt – nicht die beste Idee, wie wir feststellen mussten. Offensichtlich war es nicht genug, einfach nur die Anzahl Pods auf null zu halten.
Also haben wir Shell-Skripte gebaut (etwas altschul, aber immer noch zweckmässig), die von regelmässig laufenden Pipelines ausgeführt werden, und die alle Preview-Deployments, die älter sind als 14 Tage ebenso automatisch löschen wie alle, deren Pull Requests abgeschlossen worden sind. So sind unsere Namespaces immer wieder schön sauber, und Kubernetes hat keine Mühe mehr, seiner Arbeit nachzugehen.
Ein kurzer Blick in den Namespace (mit grosszügiger Unterstützung von kubectl und grep) zeigt, dass aktuell (Stand 01.06.2023, 12:52) 43 Preview-Deployments dort wohnen. Preview-Pods sind es deren drei. 40 Deployments laufen also momentan mit null Pods, wo es vor Knative jeweils mindestens einer gewesen wäre. Die meisten Nodes in unserem Test-Cluster verfügen über 16 CPU-Cores und 32 GiB RAM – all diese Preview-Pods, die nicht laufen, weil wir sie nicht brauchen, sparen uns rund einen halben Node. In der Praxis ist das eher ein ganzer Node. Das klingt nicht nach viel, ist für uns aber doch eine spürbare Einsparung und wahrscheinlich auch nur die Spitze des Eisbergs: All die Pods, die zu den Preview-Deployments gehört hätten, die wir nicht mehr sehen, weil wir sie regelmässig aufräumen, deren Zahl dürfte noch einiges höher gewesen sein.
Zur Illustration hier zwei aufschlussreiche Diagramme, die anhand der Anzahl Pods über die Zeit unsere Einsparungen anschaulich darstellen:
Ich weiss, da sind nicht allzuviele Details drin, aber ich habe immerhin die Achsen beschriftet.
Natürlich sind wir bei der Implementation von Knative noch lange nicht am Ende angelangt. Manch eine der erwähnten Herausforderungen liegt immer noch zum Anpacken bereit, einiges könnte noch performanter sein, und wer weiss, ob wir nicht schon morgen eine bessere Alternative zu Knative finden.
Hattet ihr schon mit ähnlichen Anwendungsfällen zu tun? Wie habt ihr’s gelöst, benutzt ihr Knative oder was ganz anderes? Ideen, Anmerkungen, Fragen, die Lottozahlen von morgen – gern alles in die Kommentare!