Die Aufgaben eines Programmierers zur Erstellung von Cloud–native-Anwendungen sind durch die veränderte Laufzeitumgebung vielfältiger, als es bei klassischen Umgebungen bisher der Fall war. In diesem Artikel wird gezeigt, wie man sich das Leben als Cloud-Entwickler erleichtern kann.
Damit eine Anwendung in Kubernetes betrieben werden kann, muss sie vorher als Docker Image paketiert werden, um sie anschließend in Kubernetes deployen zu können. Für das Kubernetes Deployment sind verschiedene Manifest-Dateien notwendig, mit denen die erforderlichen Kubernetes-Objekte angelegt werden. Erst jetzt kann mit dem Test der Anwendung in der Cloud-Umgebung begonnen werden. Bei Bugs oder neuen Features der Anwendung wiederholt sich dieser Kreislauf.
In den letzten Jahren ist eine Menge an Open-Source-Tools entstanden, die einem die Arbeit als Cloud-Entwickler erleichtern sollen. Einige dieser Tools wurden leider bereits wieder eingestellt. Um nun einen hilfreichen Mix an Tools zu finden, wird dieser Artikel die einzelnen Teilschritte aufgreifen und anhand exemplarisch ausgewählter Tools zeigen, wie man sich das Leben als Cloud-Entwickler komfortabler gestalten kann. Dabei steht die prototypische Entwicklung einer neuen Cloud-Anwendung im Vordergrund, d. h. es wird auf eine passenden CI/CD-Pipeline verzichtet. Was aber nicht bedeuten soll, dass eine solche Pipeline nicht notwendig wäre. Fast alle der vorgestellten Tools können ebenso gut in einer CI/CD-Pipeline eingesetzt werden, womit sie eine gute Basis für den Aufbau einer entsprechenden Pipeline sein können.
IMMER AUF DEM NEUESTEN STAND BLEIBEN?
Dann abonnieren Sie unseren Newsletter und erhalten Sie Updates zum Event, Infos über neu erschienene Blogartikel und weitere Themen rund um Softwarearchitektur
Einzelschritte bei der Cloud-Entwicklung
Der typische Ablauf bei der Entwicklung einer Cloud–native Anwendung besteht aus den folgenden Teilschritten:
-
Entwicklung der Anwendung
-
Erstellung des Docker Image
-
Kubernetes Deployment
-
Test und Debugging der Anwendung in Kubernetes
Während der Entwicklungsphase wiederholen sich diese Teilschritte zyklisch und fordern vom Entwickler einiges an Wissen und Erfahrung, um die notwendigen Teilergebnisse zu erstellen. Das Aufgabenspektrum reicht dabei von der Erstellung eines sogenannten Dockerfiles, aus dem das Docker Image erstellt werden kann (oder eine proprietäre Form der Definition des gewünschten Docker Image), bis hin zur Erstellung der passenden Kubernetes-Objekte in Form von Manifest-Dateien.
Auch der Betrieb der Anwendung in der Cloud bringt einiges an neuen Anforderungen mit sich, wie beispielsweise das veränderte Routing, die Verwendung von Environment-Variablen zur Konfiguration der Anwendung oder den Einsatz von Kubernetes Volumes zur Speicherung von Daten. Erst wenn die Zwischenergebnisse all dieser Teilschritte korrekt ineinandergreifen, funktioniert die Cloud-Anwendung fehlerfrei. Werden diese Schritte in einer frühen Projektphase manuell ausgeführt, birgt der Ablauf großes Potenzial an Fehlerquellen. Oft wird ein Teilschritt vergessen, wie beispielsweise das erneute Erstellen des Docker Image auf Basis der geänderten Anwendung. Diesen Fehler erkennt man dann erst wieder beim nächsten Test der Anwendung in der Cloud-Umgebung, da das Docker Image noch die alte Version der Anwendung enthalten hat. Der Einsatz nachfolgender Tools kann bei der Fehlervermeidung sehr hilfreich sein, was sich durchaus positiv auf die sogenannte Developer Experience auswirkt.
Cloud First?
In Anlehnung an die Bedeutung des Begriffs Cloud First, d. h. die Programmierung einer Anwendung mit sehr starkem Fokus auf den Betrieb in der Cloud, sollte sich der Entwickler auch möglichst frühzeitig mit den Erfordernissen der Cloud-Plattform auseinandersetzen. Zum einen muss der neue Ablauf in Fleisch und Blut übergehen, zum anderen muss man sich mit den neu zu erstellenden Artefakten auseinandersetzen. Um eine Anwendung in Kubernetes betreiben zu können, sind als Minimum ein Kubernetes Service und ein Kubernetes Deployment anhand der passenden Manifest-Dateien zu erstellen.
Jede Laufzeitumgebung hat ihre Besonderheiten, das gilt natürlich auch für Kubernetes. Je früher man sich damit vertraut macht, desto weniger wird man in kritischen Situationen davon überrascht werden. Auch der Test und das Debugging der Anwendung sollten möglichst früh in der Cloud stattfinden, um sicherzustellen, dass keine versteckten Abhängigkeiten zu Tools oder Einstellungen der lokalen Entwicklungsumgebung existieren. Seinen lokalen Entwicklungsrechner analog der Cloud-Umgebung einzurichten, ist schwierig, wenn nicht sogar unmöglich. Der einzige Ansatz, der hier empfohlen werden kann, ist der Einsatz einer lokalen Kubernetes-Umgebung. Mit
-
Minikube [1]
-
Docker Desktop [2]
-
Minishift [3]
-
CodeReady Containers [4]
existieren einige Umgebungen, die auf einem ausreichend starken Entwicklungsrechner betrieben werden können. Die ersten beiden Vertreter der Liste basieren auf reinem Kubernetes, die beiden letzten Umgebungen auf OpenShift. Minishift sollte zum Einsatz kommen, wenn OpenShift < 4.x benötigt wird, und CodeReady Containers, das seit Oktober 2019 existiert, muss bei OpenShift > Version 4.x verwendet werden. All diese lokalen Cloud-Umgebungen bieten sehr gute Dienste für den Fall, dass man keinen Zugriff auf einen echten Kubernetes-Cluster hat oder einfach mal schnell lokal etwas ausprobieren will.
Codegenerierung
Wer erste Erfahrungen mit der Java-basierten Cloud-Entwicklung machen will, kann sich mit Spring Initializr [5] oder mit MicroProfile Starter [6] ein Grundgerüst für die Cloud-Anwendung generieren. Bei Spring Initializr bekommt man leider nur sehr wenig Code-Snippets generiert, dafür kann man zwischen Maven und Gradle auswählen. MicroProfile Starter hingegen generiert für jede ausgewählte MicroProfile-Spezifikation einen passenden Sample-Code. Leider basieren die generierten Projekte nur auf Maven. Obwohl MicroProfile nur als Beta verfügbar ist, kann der Einsatz trotzdem empfohlen werden.
Erzeugung des Docker Image
Nachdem die Basis für die Cloud-Entwicklung gelegt worden ist, kommt nun der erste neue Schritt: die Erstellung des Docker Image. Hierfür müssen ein paar Entscheidungen vorab getroffen werden:
-
Darf der Docker Daemon zur Erzeugung des Docker Image eingesetzt werden?
-
Welche Basis soll für die Beschreibung des Docker Image verwendet werden? Das klassische Docker File oder eine proprietäre toolbasierte Beschreibung?
-
In welcher Build-Umgebung erfolgt die Erzeugung des Docker Image? In der Cloud-basierten CI/CD-Pipeline oder auf einem dedizierten Build-Server?
-
Soll die Erstellung des Docker Image im Anschluss an den Build-Prozess der eigentlichen Anwendung ebenfalls mit Maven/Gradle oder mittels eines separaten Tools erfolgen?
All diese Fragen sollten im Hinblick auf die gewünschte Build-Pipeline möglichst zu Anfang eines Projekts geklärt werden.
Die Verwendung des Docker Daemon bringt Vor- und Nachteile mit sich. Zum einen stellt er das passende REST API für Docker zur Verfügung, was es den Toolherstellern leichter macht, mit Docker zu kommunizieren. Auf der anderen Seite hat dieser Ansatz auch ein paar Nachteile. Sollten mehrere Build-Prozesse parallel ablaufen, scheitert das daran, dass der Docker Daemon nicht skaliert. Der weitaus gravierendere Nachteil ist aber ein Securityproblem des Docker Daemon, da er einen TCP Socket öffnet und dazu Root-Rechte benötigt. Docker hat deswegen mit dem Buildkit [7] eine eigene Initiative gestartet, um die Nachteile des bisherigen, auf dem Docker Daemon basierenden Builds zu beheben. Bei der Auswahl des richtigen Werkzeugs kommt aber noch ein anderer Aspekt zum Tragen, nämlich die Frage, auf welcher Basis man das Docker Image beschreiben will.
Soll die Definition mit dem klassischen Dockerfile oder mit einer proprietären Beschreibungssyntax, wie sie einem das Tool vorgibt, erstellt werden? Demjenigen, dem dieser zusätzliche Indirektionsschritt nichts ausmacht, hat damit mehr Wahlmöglichkeiten bei der Suche nach dem passenden Werkzeug. Meist finden die ersten Lernschritte jedoch auf Basis des originalen Dockerfiles statt. Wenn man dessen Aufbau verstanden hat, ist ein weiterer Transformationsschritt in Richtung Toolsyntax nötig. Auch die Umgebung, in der das Docker Image erstellt werden soll, hat einen Einfluss auf die Entscheidung, welches Werkzeug das passende ist. Will man den vorhandenen Build-Server um den zusätzlichen Schritt der Docker-Image-Erstellung erweitern oder soll der Build-Prozess in der Cloud stattfinden? Diese Frage ist nicht leicht zu beantworten, da die meisten Projekte nicht auf der grünen Wiese anfangen und somit Altlasten mit sich bringen.
Für den ersten prototypischen Ansatz ist es sinnvoll, das Docker Image erst einmal mit Maven oder Gradle erstellen zu lassen. Leider zeigt ein Blick in die Historie der Docker-Maven-Plug-ins ein trauriges Bild. Von anfänglich mehr als zehn existierenden Open-Source-Projekten sind heute nur noch zwei Vertreter übriggeblieben: docker-maven-plugin [8] und jib-maven-plugin [9]. Auch bei Gradle sieht die Bilanz nicht viel besser aus. Hier existieren auch nur zwei oder drei Plug-ins: Docker-Gradle-Plug-in [10], jib-gradle-plugin [11] und Gradle-Docker-Plug-in [12]. Das zuletzt genannte Tool sucht, laut Hinweis auf der entsprechenden GitHub-Seite, händeringend Entwickler zur Unterstützung der eigenen Open-Source-Community.
Die gute Nachricht an dieser Stelle: Auf dem Markt der Standalone-Tools zur Erstellung des Docker Image hat sich in letzter Zeit einiges getan. Alle größeren Softwarefirmen haben mittlerweile ein passendes Tool im Angebot. All diese Tools wurden mit der strategischen Ausrichtung erstellt, den Build-Vorgang in der Cloud zu betreiben. Hier nur eine kleine Liste an vorhandenen Tools:
-
Docker BuildKit [13]
-
Kaniko [14]
-
Buildah [15]
-
Source-to-Image [16]
-
Makisu [17]
Bei dieser Auswahl sollte für jeden Geschmack etwas dabei sein, aber Folgendes gilt es trotzdem zu bedenken: In Bezug auf dieses Thema finden gerade viele Veränderungen statt, deswegen kann es durchaus vorkommen, dass sich die getroffene Toolauswahl morgen vielleicht schon als falsche Entscheidung herausstellt. Deswegen sollte man bei der Auswahl folgenden Gedanken im Hinterkopf behalten: Tools, die mit dem original Dockerfile operieren, behalten zumindest diese Basis bei einem Toolwechsel bei, wodurch sich der Aufwand bei einem Austausch des Tools in Grenzen hält. Diese Tatsache, zusammen mit dem zuvor ausgeführten Argument der Indirektion bei proprietären Formaten, kann hilfreich sein, um die Toolauswahl weiter zu verfeinern.
LUST AUF NOCH MEHR CLOUD?
Entdecken Sie Workshops 14. - 16. Oktober 2024 in Berlin
Deployment der Anwendung in Kubernetes
Nachdem unsere Anwendung jetzt als Docker Image zur Verfügung steht und auch in die zugehörige Docker Registry publiziert worden ist, können wir uns nun um das Deployment der Anwendung in Kubernetes kümmern.
Auf den ersten Blick bekommt man den Eindruck, dass hier dieselbe Situation vorherrscht wie bei Maven oder Gradle. Viele der Tools haben ihr End of Life erreicht, wie beispielsweise Draft [18], ksonnet [19], Metaparticle [20] oder Forge [21]. Aber zum Glück sind auf der anderen Seite auch wieder neue Tools entstanden, wie zum Beispiel kustomize [22], Garden [23], Skaffold [24] oder Helm [25]. Bleibt zu hoffen, dass man bei dieser Auswahl nicht abermals auf ein totes Pferd setzt. Um hier eine Risikominimierung zu betreiben, hilft es, der klassischen Börsenweisheit zu folgen: „The trend is your friend“.
Laut einer Umfragen der CNCF-Community von 2018 verwenden 68 Prozent der befragten Teilnehmer Helm als Cloud-Package-Manager, und mit Helm in Version 3.0 (verfügbar seit November 2019) ist auch ein großer Pferdefuß eliminiert worden: Helm v3 verwendet keinen Tiller mehr, wodurch die gesamten Securityprobleme beim Deployment mit Tiller ad acta gelegt worden sind. Wenn man sich einmal mit der gewöhnungsbedürftigen Syntax der Helm-Templates angefreundet hat, leistet Helm sehr gute Dienste beim Betreuen des Lifecycle von Cloud-Anwendungen. Um den Einstieg in die Welt der Helm-Charts zu erleichtern, werden beim initialen Erstellen eines Helm-Charts passende Kubernetes-Manifest-Dateien erstellt. Mit nur einigen minimalen Anpassungen an diesen Dateien kann ein Deployment der eigenen Anwendung sehr leicht durchgeführt werden. Erst wenn die Anforderungen der Applikation wachsen (Datenbankzugriff, Kubernetes Secrets, Kubernetes ConfigMaps etc.) muss man sich mit weiteren Helm-Dateien auseinandersetzen.
Umgang mit Kubernetes
Der Umgang mit Kubernetes zwingt einen Entwickler, sich mit dem CLI von Kubernetes kubectl zu beschäftigen. Außerdem wird eine Menge an Manifest-Dateien, meist im YAML-Format, benötigt. Für beides gibt es sinnvolle Hilfestellungen.
Zum einen sollte man sich, falls noch nicht passiert, einen passenden YAML-Editor installieren. Dieser hilft einem zumindest dabei, die klassischen Formatierungsfehler zu vermeiden. Die Auswahl ist mannigfaltig, und es sollte für jeden Geschmack etwas dabei sein. Der zweite Teil im einfacheren Umgang mit Kubernetes ist die Installation einer Bash Completion für die Kubernetes-Befehle [26].
Hiermit können beim Arbeiten in der Shell die notwendigen Kubernetes-Befehle und sogar die Befehlsparameter mit einem einfachen Tab vorgeschlagen bzw. ausgewählt werden. Diese Unterstützung geht sogar so weit, dass bei einer vorhandenen Verbindung zu einem Kubernetes-Cluster die aktuellen Kubernetes-Objekte wie beispielsweise Pod-Name, Service-Name oder Deployment-Name zur Auswahl angeboten werden. Das spart lästige Tipparbeit, reduziert Tippfehler und unterstützt sogar bei der Auswahl der möglichen Befehlsparameter.
Zugriff auf Anwendungen in Kubernetes
Jetzt, wo die Anwendung endlich in Kubernetes deployt worden ist, will man sie auch testen. Dazu muss man erst einmal den Zugriff auf den Port des Kubernetes Service und somit auf den entsprechenden Pod ermöglichen. Kubernetes bietet hierfür die Möglichkeit des Port Forwarding an. Mit dem Befehl kubectl port-forward <pod-name> <port extern>:<port intern> wird der interne Port der Anwendung im angegebenen Pod mit dem externen Port verbunden. Zugriffe, die jetzt auf den externen Port erfolgen, werden auf den internen Port im Kubernetes-Cluster weitergeleitet. Dieses Vorgehen ist leider ein wenig holprig bzw. umständlich, da man zuerst den Namen des Pods ermitteln muss, um dann für jeden Pod, auf den man zugreifen will, ein eigenes Forwarding einrichten zu können. Leider vergibt Kubernetes bei einem Neustart eines Pods immer wieder einen neuen Namen, womit das eingerichtete Forwarding nicht mehr funktioniert. In diesem Fall muss man die gesamte Prozedur von Anfang an wiederholen. Für unseren Wunsch, schnell und einfach neue Versionen im Kubernetes Cluster zu testen, ist dies äußerst hinderlich.
Mit kubefwd [27] (Abb. 1) existiert ein Tool, das uns dieses Problem aus dem Weg räumt. Das Tool wird als „Bulk port forwarding Kubernetes services for local development“ bezeichnet. Mit einem einzigen Befehl kann man alle Services innerhalb eines Kubernetes Namespace auf einmal nach außen freigeben. Es werden alle Kubernetes Services im angegebenen Namespace ermittelt, und für jeden Treffer wird ein passender Domain-Name in die lokale Hosts-Datei eingetragen. Dazu muss kubefwd mit lokalen Root-Rechten gestartet werden. Sollte man lokal keine Root-Rechte besitzen, lässt sich kubefwd auch innerhalb eines lokalen Docker-Containers starten. Jeder Pod-Neustart wird erkannt und das Forwarding entsprechend automatisch angepasst. Auf diese Weise können beliebig viele Versionen der Anwendung zu Testzwecken deployt werden, ohne dass der Zugriff ständig neu konfiguriert werden muss.
Noch ein kleiner, aber feiner Benefit am Rande: Der Domain-Name, den kubefwd generiert, entspricht dem DNS-Namen des Kubernetes Service. Auf diese Weise sind die Zugriffe auf die Anwendung mit denselben Host-Namen möglich, wie es auch innerhalb des Kubernetes-Clusters erfolgt. Ein lästiges Anpassen der unterschiedlichen Host-Namen (localhost und Service DNS) wird somit hinfällig.
Logfiles in Kubernetes
Wer auf die Kubernetes-Pods zugreift, will natürlich auch die Logausgaben des jeweiligen Zugriffs sehen und analysieren. Hierfür bietet Kubernetes mit kubectl logs dieselbe Funktionsweise wie beim Port Forwarding an. Leider auch mit denselben Nachteilen bezüglich des Pod-Neustarts.
Auch hier gibt es ein kleines, aber feines Tool, das diese Probleme für uns behebt: stern [28]. Einmal gestartet, bietet es dem Entwickler ein „Multi pod and container log tailing for Kubernetes“. Über einen regulären Ausdruck als Startparameter werden die Pods oder auch Container so weit spezifiziert, wie man es für die Loganalyse benötigt.
Damit kein Chaos in der gemeinsamen Anzeige der Logausgaben aller ausgewählten Pods/Container entsteht, wird jedem Pod/Container eine eigene Farbe zugewiesen, mit der dann die Logzeilen farblich hervorgehoben werden. Über diese bunte Darstellung ist es leicht möglich, die Logzeilen korrekt zuzuordnen – auch dann, wenn sich die Logausgaben der Pods/Container vermischen. Wem das zu unübersichtlich wird, der kann natürlich in einem weiteren Shellfenster ein weiteres Mal stern starten.
Abschließend auch hier wieder der Hinweis: Pod-Neustarts werden von stern erkannt, und jeder neu gestartet Pod wird in das existierende Log-Tailing mit seiner eigenen Farbe integriert.
Redeploy von neuen Versionen der Anwendung
Kommen wir nun zum eigentlichen Kern unseres Entwicklungsprozesses: Wie bekommt man schnell eine neue Version der Anwendung im Kubernetes-Cluster deployt, wenn man mit Trial and Error ausprobieren will, wie der eigentliche Bugfix später einmal aussehen soll? Oder wie kann man schnell verproben, ob die neue Implementierungsidee auch wirklich funktioniert? Zum einen könnte man den kompletten Ablauf von vorhin wiederholen: Build, Docker Image, Kubernetes Deployment. Wenn man diesen Ablauf ein paarmal durchgeführt hat, wünscht man sich einen einfacheren Weg, der schneller die neuen Ergebnisse liefern kann.
Grundsätzlich will man ja nichts anderes erreichen, als ein spezielles File im Pod auszutauschen, um zu sehen, ob das unseren Bug beheben würde. Die erste Möglichkeit hierfür bietet Kubernetes CLI selbst schon an. Mit kubectl cp kann man einzelne Files in einen Pod bzw. Container kopieren. Dazu muss aber eine grundlegende Voraussetzung erfüllt sein: Der Application-Server muss den automatischen Reload dieses Files durchführen. Tomcat als Defaultserver bei Spring-Boot-Anwendungen und OpenLiberty für MicroProfile-basierte Services bieten diese Funktionalität an. Aber auch andere App-Server verfügen über dieses Feature.
Ein Blick in die Dokumentation des entsprechenden Application-Servers verrät, welche Einstellungen hierfür notwendig sind. Sobald diese Einstellung aktiviert worden ist, kann der folgende Befehl das lokal vorhandene WAR File (customer.war) im Container austauschen, das dann vom Application-Server innerhalb von ein paar Millisekunden neu geladen und gestartet wird:
kubectl cp ./target/customer.war default/customer-f4ccd5cdf-qjkms:/config/dropins
Der Application-Server führt den Reload aus, ohne dass Kubernetes den Container bzw. den Pod neu starten muss. Wer hier noch mehr Komfort haben will, kann sich den Kopiervorgang mit ksync [29] auch nochmal vereinfachen. Dieses Tool geht sogar noch einen Schritt weiter, indem es ganze Verzeichnisse zwischen dem lokalen Entwicklungsrechner und dem Kubernetes-Cluster synchronisiert. Sobald sich eine Datei lokal oder im Container verändert hat, schlägt ksync automatisch zu und verteilt die neue Datei entsprechend.
Bei diesem prototypischen Entwicklungsprozess muss Folgendes in Betracht gezogen werden: Sobald der Kubernetes Pod neu gestartet wird, sind diese Änderungen wieder verschwunden, da Kubernetes beim Neustart eines Pods das ursprüngliche Docker Image verwendet und den alten Inhalt eines Pods löscht. Diese Snowflakes, die durch das Kopieren entstanden sind, werden somit gelöscht und sind nicht mehr nachvollziehbar. Aber vielleicht ist ja das genau der Effekt, den man erreichen will, wenn man schnell ein paar Dinge ausprobieren möchte.
Debugging von Cloud-Anwendungen
Leider gibt es immer wieder Situationen in der Fehlersuche, bei denen die Analyse der Logausgaben allein nicht ausreicht, um den Fehler einzugrenzen. In den meisten Fällen liegt es daran, dass an der notwendigen Stelle keine passende Logausgabe im Source Code verfügbar ist. Oder es handelt sich um Codestellen, die zu einem Zeitpunkt ausgeführt werden, zu dem der Logger noch nicht initialisiert worden ist und man einen System.out.println nicht verwenden wollte. Dann hilft nur noch ein klassisches Remote Debugging des Containers.
Auch hier muss wieder eine Voraussetzung erfüllt sein: Die JVM des Application-Servers muss einen Debug Port öffnen, auf den man sich mit seiner IDE verbinden kann. Der folgende Eintrag führt an passender Stelle dazu, dass die JVM den Port 7777 für das Debugging öffnet: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=7777. Der Parameter suspend=n bewirkt das Starten der JVM, ohne dass auf eine aktive Verbindung auf den Debug Port gewartet wird. Mit suspend=y unterbricht die JVM den Startvorgang und wartet, bis der Remote-Debugger die Verbindung aufgebaut hat. In der Regel ist der Debug-Port des Containers nicht für Zugriffe von außerhalb des Kubernetes-Clusters freigegeben, was aber mit einem klassischen Port Forwarding auf den Port 7777 oder mit kubefwd schnell ermöglicht werden kann.
Jetzt steht einem das Remote Debugging, wie man es aus Zeiten des klassischen Application-Server-Betriebs kennt, mit all seinen Vor- und Nachteilen zur Verfügung. Die Nachteile des fehlenden Hot Code Replacement oder eine möglicherweise langsame Debug-Verbindung sind auch in der Kubernetes-Welt gegeben. Aber schließlich ist es manchmal die einzige Möglichkeit, dem Fehler auf die Spur zu kommen.
Software Architecture Summit vom 11. - 13. März in München
Technische und methodische Themen, Kommunikationstrends, Cloudlösungen, MLOps, Design und Psychologie
Lokale Entwicklung im Service Mesh
In der Regel besitzen Cloud-Anwendungen, die in einem Verbund mit oder ohne Service Mesh betrieben werden, mehrere Kommunikationspartner. Bin ich nun als Entwickler für einen dieser Services verantwortlich und möchte das Kommunikationsverhalten meines Service anpassen, bin ich schnell mit folgendem Problem konfrontiert: Soll ich all die anderen Services, mit denen mein Service eine Kommunikationsbeziehung hat, lokal deployen? Welchen Aufwand habe ich, damit die anderen Services lokal auch richtig funktionieren? Oder sollte ich mir dazu Mocks erstellen? All diese Fragen sind nicht ganz einfach zu beantworten, und der damit verbundene Aufwand ist in der Regel auch nicht gerade marginal. Zum Glück gibt es hierfür ein Tool, das uns das Leben um einiges erleichtert: Telepresence [30]. Als Sandbox Project wird es bei der Cloud Native Computing Foundation (CNCF) gelistet und sogar die Kubernetes-Dokumentation [31] verweist beim Thema Debugging auf dieses Werkzeug.
Telepresence bietet einige Einsatzszenarien, aber ein ganz besonderes ist das sogenannte Swap Deployment. Beim Start der Telepresence Shell wird angegeben, welches Kubernetes Deployment mit einem Telepresence Proxy ausgetauscht werden soll. An Stelle des Application Pod tritt nun ein Telepresence Pod, der alle eingehenden Requests auf den Pod an den lokalen Entwicklungsrechner weiterleitet. Die aufrufenden Services merken nichts von der Weiterleitung des Requests. Darüber hinaus stellt die Telepresence Shell das Kubernetes Environment des ursprünglichen Pods und eventuell verbundene Kubernetes Volumes lokal zur Verfügung (Abb. 2).
Wird nun die IDE innerhalb der Telepresence Shell gestartet, läuft der in Entwicklung befindliche Service innerhalb dieser IDE in einer identischen Umgebung wie der ursprüngliche Pod in Kubernetes. Aufrufe von Kommunikationspartnern im Kubernetes-Cluster werden an den Service in der IDE geleitet. Dieser kann nun ganz normal debuggt werden und auch ein Hot Code Replacement ist möglich. Die Entwicklung des Service kann somit ohne großen Aufwand in gewohnter Art und Weise stattfinden.
Eine Beschränkung hierzu sollte noch erwähnt werden: Das Swap Deployment ist nur für einen einzigen Pod möglich – aber wer programmiert schon gerne an zwei Services gleichzeitig? Solange das Swap-Deployment aktiv ist, wird jeder Zugriff auf den Telepresence Pod auf den lokalen Rechner weitergeleitet, also auch die Zugriffe der Entwicklerkollegen. Nachdem die Telepresence Shell beendet wird, stellt das Tool den ursprünglichen Zustand im Cluster wieder her. In Verbindung mit kubefwd können ausgehende Requests vom lokalen Service in den Cluster hinein mit den Kubernetes-DNS-Namen der Services ausgeführt werden. Womit ein weiterer Schritt in Richtung identische Umgebungen erfolgt.
Fazit
Die steile Lernkurve, die jeder Cloud-Entwickler durchlaufen muss, verliert durch den Einsatz passender Tools ihren Schrecken. Die vorgestellten Tools vereinfachen das Entwicklerleben und reduzieren Fehler, wodurch sich die Frustration in Grenzen halten wird. Das Aufgabengebiet bei der Entwicklung für die Cloud ist umfangreicher und der Entwickler hat mehr Verantwortung. Er muss sich um mehr Dinge kümmern als zuvor. Auch hier können die aufgelisteten Tools ihren Beitrag leisten.
Da die Toollandschaft noch starken Veränderungen unterliegt, ist es bei der Toolauswahl wichtig, möglichst flexibel zu bleiben. Man muss immer wieder damit rechnen, morgen schon auf einem toten Pferd zu sitzen. Das ist nicht tragisch, solange man absteigen kann und ein passendes Ersatzpferd findet. Darüber hinaus ist es wichtig, dass die ausgewählten Tools sehr wenig Abhängigkeiten untereinander haben, damit der Austausch leichter fällt.
Last, but not least sollte bei der Auswahl der Tools immer wieder die vorhandene oder geplante CI/CD-Pipeline für die Cloud im Auge behalten werden. Der Einsatz derselben Tools, lokal wie auch in der Pipeline, ist zu bevorzugen, da auch dies die Lernkurve ein wenig abflachen kann.
Links & Literatur
[1] https://minikube.sigs.k8s.io
[2] https://www.docker.com/products/docker-desktop
[3] https://docs.okd.io/latest/minishift/
[4] https://developers.redhat.com/products/codeready-containers/overview
[6] https://start.microprofile.io
[7] https://github.com/moby/buildkit
[8] https://github.com/fabric8io/docker-maven-plugin
[9] https://github.com/GoogleContainerTools/jib/tree/master/jib-maven-plugin
[10] https://github.com/palantir/gradle-docker
[11] https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin
[12] https://github.com/bmuschko/gradle-docker-plugin
[13] https://docs.docker.com/develop/develop-images/build_enhancements/
[14] https://github.com/GoogleContainerTools/kaniko
[15] https://buildah.io
[16] https://github.com/openshift/source-to-image
[17] https://github.com/uber/makisu
[18] https://github.com/Azure/draft
[19] https://github.com/ksonnet/ksonnet
[21] https://forge.sh
[22] https://github.com/kubernetes-sigs/kustomize/
[23] https://github.com/garden-io/garden
[24] https://github.com/GoogleContainerTools/skaffold
[25] https://helm.sh
[27] https://github.com/txn2/kubefwd
[28] https://github.com/wercker/stern
[29] https://github.com/ksync/ksync
[30] https://www.telepresence.io
[31] https://kubernetes.io/docs/tasks/debug-application-cluster/local-debugging/