Blog

Der Cloud-Native-Ansatz in Java unter der Lupe

Okt 10, 2023

Der Begriff Cloud-Native ist nicht mehr wegzudenken. Was sich genau dahinter verbirgt und vor allem, wie Java und Cloud-Native zusammenhängen, ist aber nicht so ganz klar. Wir sehen uns daher die Bestandteile von Cloud Native einmal genauer an, schauen, was uns der Ansatz an Vorteilen bringen soll, und wie weit wir diese in der Java-Welt nutzen können.

Wer seine Kollegen fragt, was sie unter Cloud-Native verstehen, wird sicher eine Menge verschiedener Antworten erhalten. Für den einen ist es einfach Cloud-Computing im neuen Gewand, für den anderen Kubernetes und Co. und für manche einfach ein weiteres neues Buzzword. So ganz klar umrissen wirkt der Begriff Cloud-Native also nicht – zumindest nicht in den Köpfen der meisten Entwickler.

Was war noch gleich Cloud-Native?

Wenn man sich etwas Zeit nimmt und einige der Big Player der Cloud-Welt anschaut, stellt man fest, dass inzwischen alle eine eigene „What is Cloud-Native?“-Website anbieten, seien es Amazon, GitLab oder Oracle. Bei einem Versuch, diese Informationen sinnvoll zusammenzuführen, kommt man im Schnitt zu folgender Erkenntnis: Cloud-Native ist eine Denkweise, die einem hilft, Anwendungen so zu bauen, dass sie das volle Potenzial der Cloud ausschöpfen können.

Man kann Cloud-Native auch als logische Weiterentwicklung von DevOps und der 12 Factor App betrachten. Ersteres ist ebenfalls ein Denkansatz, während das Zweite vor allem eine Sammlung von Best Practices zur Entwicklung von Software-as-Service-Lösungen darstellt. Viele (aber längst nicht alle) Prinzipien aus DevOps und der 12 Factor App haben bereits Einzug in unsere tägliche Arbeit gefunden. Cloud-Native führt einige neue Prinzipien ein, die wir in unser Vorgehen integrieren können.

Cloud-Native als Denkweise ist indessen weitestgehend frei von technologischen Vorgaben. Selbst das Wort „Cloud“ bezieht sich nicht notwendigerweise auf tatsächliche Cloud-Anbieter, ebenso steht es für die „eigene“ Cloud im eigenen Rechenzentrum. In der Praxis ist das oft Kubernetes, aber prinzipiell sind technologisch keine Grenzen gesetzt, solange man sich an den Cloud-Native-Prinzipien orientiert. Daher ist Cloud-Native ein Begriff, der für nahezu alle Entwickler eine gewisse Relevanz oder zumindest Berührungspunkte hat.

Für uns lohnt sich ein Blick auf Cloud-Native aus der Sicht eines Java-Entwicklers. Insbesondere darauf, inwieweit uns Java bei der Umsetzung von Cloud-Native hilft und wo unser Ökosystem vielleicht noch Schwächen aufweist. Dazu werden wir zunächst die Grundpfeiler von Cloud-Native betrachten. Danach drehen wir den Spieß um und schauen uns die versprochenen Vorteile von Cloud-Native an und ob die Java-Welt diese überhaupt ausspielen kann. Neben dem Status quo wollen wir einen Blick in die Zukunft wagen und über das mögliche Potenzial sprechen.

Was macht Cloud-Native aus?

Cloud-Native ist zwar kein streng definiertes Rahmenwerk, aber dennoch gibt es eine Reihe von Grundpfeilern, auf die sich die meisten Verfechter sicher ohne große Diskussion einigen können. Dazu gehören Modularisierung durch Microservices, die Verwendung und Orchestration von Containern – meist mit Hilfe von Kubernetes und Docker –, die Anwendung der DevOps-Prinzipien sowie die Verwendung von Cloud-Diensten. Für viele zählt auch die Verwendung eines Service Mesh explizit zu den Grundpfeilern von Cloud-Native; wir werden später noch klären, warum das so ist. Speziell Microsoft listet noch Modern Design als wichtigen Baustein bei der Entwicklung von Cloud-Native-Anwendungen auf, also im Prinzip die Verwendung heute gültiger Best Practices.

Aus Sicht von Java-Entwicklern spielen manche dieser Grundpfeiler eine größere Rolle als andere. Während wir uns als Java-Entwickler sicher auch mit DevOps und Themen wie Orchestration und Containern auseinandersetzen müssen, sind diese weitestgehend losgelöst von Java. Anders sieht es in den Bereichen Microservices, Cloud-Native Services und Modern Design aus. Hier können Java, das gesamte Ökosystem und wir als Java-Entwickler großen Einfluss auf die sinnvolle Verwendung der Cloud-Native-Prinzipien nehmen.

Im Bereich Microservices können wir uns fragen, inwieweit uns die Java-Welt dabei unterstützt, unsere Anwendung in dieser Form zu modularisieren. Was hilft uns? Was steht uns im Weg? Aber auch: Wohin geht die Reise?

Beim Blick auf Cloud-Services hingegen ist relevant, wie einfach wir diese Technologien in unsere Anwendung integrieren können, wie sehr wir uns an sie binden müssen und wie sie sich in das restliche Java-Ökosystem einbetten.

Im Kontext von Modern Design interessiert uns, wie sehr bestimmte Best Practices in unserer Welt angekommen sind und wirklich gelebt werden; hier geht es weniger um technische Aspekte, sondern mehr um uns als Community und unsere Einstellung zur Entwicklung von Cloud-Native-Anwendungen.

 

LUST AUF NOCH MEHR DEVOPS-TRENDS?

Entdecke Workshops vom 10. – 12. Juni

Die Cloud-Native-Versprechen

Um zu bewerten, wo wir in der Java-Welt stehen, ist es sinnvoll, diese Grundpfeiler nicht nur im Vakuum zu betrachten, sondern sie mit den erhofften Vorteilen von Cloud-Native zu verknüpfen. Es lassen sich eine Reihe von Eigenschaften nennen, die in der Regel als Verkaufsargument für Cloud-Native verwendet werden. Auch wenn keine Einigkeit herrscht, was die exakte Liste der Vorteile angeht, gibt es eine große Schnittmenge, wenn wir erneut einen Blick auf die zahlreichen „What is Cloud-Native?“-Websites werfen.

Eine wirklich gute Übersicht der Vorteile von Cloud-Native hat Google zusammengestellt [2]. Daher werden wir uns die dort genannten Punkte kurz anschauen und dann in Beziehung zur Java-Welt setzen. So wollen wir versuchen, zu beurteilen, wie gut Java eigentlich Cloud-Native kann, aber auch, wo noch Potenzial wartet und ob wir damit rechnen können, in Kürze davon zu profitieren.

An der einen oder anderen Stelle werden wir sicher auch ein wenig Kritik an Java, dem Java-Ökosystem und natürlich auch an uns selbst äußern müssen. In unserer Betrachtung gehen wir aber zunächst blauäugig davon aus, dass Cloud-Native uns wirklich die versprochenen Vorteile bietet.

Faster Innovation

Cloud-Native ermöglicht es, schnell Neues auszuprobieren und auch auf den Markt zu bringen. Gerade in unserer schnelllebigen Welt ist es wichtig, noch vor potenziellen Konkurrenten das Produkt oder auch nur ein spezielles Feature mit geringer Time-to-Market zu veröffentlichen.

In der Java-Welt erfreuen sich in den letzten Jahren Projekte wie Spring Boot oder neuerdings auch Quarkus großer Beliebtheit. Ohne viel Konfiguration können wir neue Anwendungen von jetzt auf gleich quasi aus dem Nichts erzeugen – und das mitunter in durchaus produktionsreifer Qualität. Auch Jakarta EE mit dem Microprofile bietet ein ähnliches Paket aus typischen Funktionalitäten, die für die Entwicklung von Microservices benötigt werden.

Mit Framework-Erweiterungen wie Spring Cloud oder Quarkus Extensions im Bereich Cloud können zudem auf einfache Weise Cloud-Services angebunden werden, die sich nahtlos in das Framework integrieren. So lassen sich viele Cloud-Services etwa annotationsgetrieben steuern, ohne dass dabei explizit das SDK des Cloud-Anbieters verwendet werden muss.

Insgesamt können wir uns als Entwickler auf diese Weise auf die eigentliche Funktionalität unserer Anwendung konzentrieren, ohne dass uns dabei die Technik allzu sehr im Weg steht. Die Java-Welt hat sich diesbezüglich in Richtung Cloud-Native entwickelt und sorgt mit dem vermehrten Einsatz von Convention over Configuration durch gute Voreinstellungen dafür, dass wir direkt Ideen in Code gießen können. Gleichzeitig können dank der zahlreich verfügbaren Cloud-Integrationen passende Cloud-Services ohne große Mühe im Zusammenspiel mit gängigen Frameworks verwendet werden.

Dennoch ist auch hier noch Raum für Verbesserungen. Sobald der goldene Pfad verlassen wird und die Anwendung die ein oder andere Besonderheit besitzt, kommt einem die Technik recht schnell in die Quere. In solchen Fällen ist händische Konfiguration oder die direkte Verwendung der Cloud-SDKs notwendig.

Reliable Releases

Schnell neue Releases zu veröffentlichen, ohne Angst haben zu müssen, dass das Deployment fehlschlägt, ist ein weiterer Vorteil von Cloud-Native. Um diesen Vorteil auszuspielen, bedarf es deutlich weniger technischer Hilfsmittel als erwartet. Hier spielen in erster Linie die Denkweise und die dahinter liegenden Methoden eine große Rolle. Als Java-Entwickler müssen wir uns in diesem Bereich zum Glück nicht verstecken.

Gängige Prinzipien wie Statelessness haben in der Java-Welt längst Einzug gehalten. Auch Microservices oder auch nur die Trennung zwischen Frontend und Backend sorgen für kleinere Deployment-Pakete und somit auch deutlich weniger Risiko beim Deployment. Während vor Jahren das Release einer Anwendung noch eine wohlüberlegte Handlung war, können moderne Java-Anwendungen oft ohne Bedenken in kurzen zeitlichen Abständen bereitgestellt werden.

Ein weiterer Umstand, der zuverlässige Releases ermöglicht, ist die exzellente Testing-Disziplin der Java-Community. Nicht nur sind Unit-Tests von jeher treue Begleiter, sondern in jüngster Zeit auch der vermehrte Einsatz von Integrationstests, bei denen versucht wird, so produktionsnah wie möglich zu sein (Stichwort: DEV/PROD Parity aus der 12 Factor App). Gerade Bibliotheken wie Testcontainers [3], mit denen Docker-Container für einen Test schnell hochgefahren werden können, haben Testing in der Java-Welt auf ein qualitativ neues Level gehoben.

In ähnlicher Weise ist das Java-Ökosystem auch Vorreiter im Bereich Consumer-driven Contract Testing (CDC), speziell durch das Pact-Projekt. Pact stellt in der von Cloud-Native präferierten Microservices-Welt sicher, dass Releases von Services auf Schnittstellenebene keinen negativen Einfluss auf andere Services haben. Mit Pact können Consumer einem API auf maschinenlesbarem Weg mitteilen, welche Erwartungen sie an die Schnittstelle eines anderen Service haben. Sobald diese Erwartungen nicht mehr erfüllt sind, wird Alarm geschlagen und ein möglicherweise problematisches Deployment verhindert.

Auch wenn speziell mit Quarkus schon einiges in der Richtung getan wird, gibt es doch ein paar Minuspunkte für Java, was das Abfangen vermeidbarer Fehler zur Compile Time angeht. Zu viel wird immer noch zur Laufzeit gemacht (z. B. Dependency Injection). Nicht immer können Tests alle Eventualitäten abdecken, was bei fehlenden Überprüfungen zur Compile Time jedoch schlussendlich zu Laufzeitproblemen führt. Diese Art von Problemen ist besonders ärgerlich, da in Konsequenz das Deployment zurückgerollt werden muss.

Scalability

Cloud-Native verspricht die Möglichkeit der bedarfsgerechten (horizontalen) Skalierung sowohl im Compute- als auch im Data-Bereich.

Naturgemäß geschieht die Skalierung im Sinne von Cloud-Native außerhalb der Anwendung. Etwa so, dass mehrere Instanzen der Anwendung hochgefahren werden, um die Anzahl steigender Anfragen besser verarbeiten zu können. Dennoch können auch Java-Anwendungen gewissermaßen ihren Dienst tun, damit eine Skalierung mindestens möglich und idealerweise sogar erleichtert wird.

Wie bereits erwähnt, bauen wir unsere Java-Anwendung inzwischen weitestgehend stateless. Insbesondere die Verwendung von Sessions weicht zunehmend Technologien wie JSON Web Tokens. Das ermöglicht es, auf Mechanismen wie Sticky Session zu verzichten, und erlaubt eine deutlich flexiblere Skalierung, ohne Rücksicht auf einzelne Anwendungsinstanzen nehmen zu müssen. Gleichzeitig kommt auch der Gedanke „Cattle, not Pets“ immer mehr bei uns Java-Entwicklern an: Eine Instanz unserer Anwendung ist eben nichts Besonderes, und wir müssen damit rechnen, dass sie jederzeit terminiert wird. Einer automatischen Skalierung steht also rein technisch nichts mehr im Weg.

Andere Dimensionen, die gute Skalierbarkeit unterstützen, sind der Speicherverbrauch und die Start-up-Zeit. Je geringer beides ist, desto schneller kann man etwa dynamisch auf Last reagieren. Historisch gesehen sind Java und die JVM in diesen Bereichen aber nicht immer ganz so gut weggekommen. Dennoch kann man sagen, dass sich hierfür inzwischen ein gewisses Bewusstsein bei uns Entwicklern, aber auch bei den Frameworks entwickelt hat. Auf technischer Ebene unterstützt vor allem GraalVM dabei, den Speicherverbrauch und die Start-up-Zeit zu verringern, sodass Quarkus- und jüngst auch Spring-Boot-Anwendungen im Sinne von Cloud-Native profitieren. Aber auch minimalistische Frameworks wie Javalin [3] zahlen hierauf ein und ermöglichen die Entwicklung hochskalierbarer Services mit einem kleinen Fußabdruck.

Auch wenn der Start von Java-Anwendungen im Schnitt vielleicht immer noch langsamer als der anderer Sprachen und Umgebungen ist, ist Java inzwischen doch ein guter Kandidat für skalierbare Cloud-Native-Anwendungen.

 

Higher Availability

Anwendungen sind mit Hilfe von Cloud-Native hochverfügbar und haben eine geringe Downtime; auch fehlerhafte Konfiguration kann aufgefangen werden.

Ähnlich wie bei der Skalierbarkeit von Anwendungen ist das Thema Hochverfügbarkeit durchaus mehrschichtig und liegt nicht allein in der Hoheit der Anwendung selbst, sondern zu großen Teilen auch bei der Infrastruktur. In der Vergangenheit erfreuten sich vor allem Resilience-Pattern wie Circuit Breaker, aber auch simple Strategien wie Retries (oft mit Exponential Back-off) großer Beliebtheit. Damit können nicht nur möglicherweise überlastete Services entlastet, sondern es kann auch die Kommunikation zwischen Microservices robuster gestaltet werden. Aspekte, die letztendlich auf eine hohe Verfügbarkeit von Services einzahlen.

Im Kontext von Cloud-Native hat sich gezeigt, dass diese Patterns aus der Anwendung herausgehalten werden können. Stattdessen kann die Verantwortlichkeit gewissermaßen in die Plattform verlagert werden. In der Praxis bedeutet das die Verwendung eines Service Mesh. Es ermöglicht, Funktionalitäten wie Circuit Breaker oder Retries außerhalb der Anwendung und völlig transparent für diese umzusetzen.

Auch das Thema Observability ist entscheidend, wenn es darum geht, eine hohe Verfügbarkeit von Services sicherzustellen. Hier schläft die Java-Welt zum Glück ebenfalls nicht. Im Microprofile für Jakarta EE sind sowohl Health-Checks als auch Metriken ein fester Bestandteil. Genauso bietet Spring Boot mit Actuator einen eingebauten Weg, um wichtige Informationen über die laufende Anwendung zu liefern. All das stellt eine wichtige Grundlage für die Realisierung hoher Verfügbarkeit dar.

Das Einzige, das wir als Java-Community sicherlich noch zu lernen haben, ist, dass wir uns mehr auf die Plattform verlassen sollten (meist Kubernetes mit Erweiterungen wie einem Service Mesh). Als Entwickler tendieren wir oft dazu, Funktionalität direkt in unsere Anwendungen einzubauen, obwohl diese zentralisierter in der Plattform umgesetzt werden kann. Der Grund ist häufig, dass eine gewisse Distanz zwischen Dev und Ops existiert. Nicht jeder Entwickler ist ein Kubernetes-Experte und weiß, was Kubernetes einem als Plattform abnehmen kann.

Portability

Cloud-Native-Anwendungen funktionieren in verschiedenen (Cloud-)Umgebungen, ohne dass größere Änderungen notwendig sind.

„Write once, run anywhere“ war schon immer das Motto von Java. Gerade die in Cloud-Native übliche Verwendung von Containern hat das nicht nur für Java zum täglichen Brot gemacht. Für eine containerbasierte Anwendung ist daher auch fast unerheblich, in welcher Cloud sie am Ende läuft, zumindest wenn es um den reinen Compute-Aspekt geht.

Für die Portabilität bezüglich der Cloud spielen jedoch vor allem die Infrastrukturkomponenten wie Datenbanken, Caches usw. eine große Rolle. Von Bedeutung ist vor allem, wie genau wir diese aus unserer Java-Anwendung heraus aufrufen.

Auch hier dürfen wir uns als Java-Entwickler auf die Schulter klopfen. Mit JPA haben wir von jeher bereits eine funktionstüchtige Abstraktion für verschiedene SQL-basierte Datenbanksysteme. Aber gerade Standards wie Java Messaging Service (JMS) erleben im Kontext von Cloud-Native aufgrund von nachrichtenbasierter Kommunikation eine gewisse Renaissance. Zahlreiche Messaging-Services bieten eine JMS-Implementierung für ihren ansonsten proprietären Dienst an; Vertreter sind unter anderem Amazon SQS bei AWS oder auch Azure Service Bus. Einem Wechsel zwischen AWS und Azure steht aus Sicht des Messaging also kaum etwas im Weg.

Leider gibt es auch in Java nicht für jede Art von Infrastrukturkomponente einen Standard, der als Abstraktion dienen kann. Dennoch versuchen gerade Frameworks wie Spring, unter anderem mit Erweiterungen wie Spring Cloud Vault (Teil von Spring Cloud) oder Spring Data, Abstraktion über typische Java-Standards hinaus zu schaffen, die auch über die Grenzen eines einzelnen Cloud-Anbieters hinweg funktionieren.

Natürlich lassen sich nicht für alle Infrastrukturkomponenten passende Abstraktionen finden. Dafür ist die Cloud-Welt sicher auch zu schnelllebig. In der Praxis stellt sich die Frage, wie viel Portabilität wirklich wert ist, wenn dafür die Verwendung spezieller Features der gegebenen Technologie nicht möglich ist. Gerade Cloud-Dienste kommen ja häufig mit einer besonderen Eigenschaft daher, die es nirgendwo anders gibt. Wenn diese Eigenschaft durch die Abstraktion verloren geht, stellt sich schnell die Frage, wozu der Dienst überhaupt genutzt werden soll.

Lower Costs

Sowohl die Kosten für die Entwicklung neuer Features als auch der Betrieb einer Cloud-Native-Anwendung versprechen, eher niedrig zu sein.

Auch hier können wir natürlich neben schlanken Anwendungen, die weniger Speicher verbrauchen und insgesamt schneller sind, in der Anwendung selbst nur wenig tun. Potenzielle Kostenersparnisse können vor allem durch den Einsatz von Cloud-Diensten, die ein bedarfsgerechtes Kostenmodell verfolgen, erreicht werden. Weder glänzt Java hier mit besonderer Unterstützung für solche Dienste, noch stellt es sich besonders schlecht dar.

Hier steht uns als Community hauptsächlich im Weg, dass wir gerade Technologien wie Serverless oder auch NoSQL-Datenbanken immer noch sehr skeptisch gegenüberstehen. Nicht ganz verwunderlich, wenn wir doch alle mit Application-Servern und Standards wie JPA groß geworden sind. Um aber wirklich von den geringeren Kosten zu profitieren, muss hier mehr Offenheit für solche zugegeben oft proprietären Services vorhanden sein.

Better Security/Improved Compliance

Aufgrund standardisierter Vorgehen durch Cloud-Native weisen Anwendungen mehr Sicherheit auf; mögliche Schwachstellen werden deutlich schneller gepatcht oder zumindest erkannt. Richtlinien und unternehmensweite Vorgaben lassen sich in der Cloud-Native-Welt besser zentralisiert verwalten und durchsetzen.

In Cloud-Native-Anwendungen wird vermehrt auf Standardprotokolle gesetzt – insbesondere beim Thema Authentication und Authorization. Sowohl OAuth/Open ID Connect als auch allgemein JSON Web Token sind inzwischen oft die erste Wahl. Auch Eigenentwicklungen im Bereich der Benutzerverwaltung nehmen dank etablierter Projekte wie etwa KeyCloak [5] deutlich ab.

Zudem profitiert auch die Java-Welt vom anhaltenden Trend, mehr Funktionalität in die Plattform zu bringen. Wir müssen uns lediglich darauf einlassen und der Plattform vertrauen. Geschenkt bekommen wir dadurch Funktionalitäten wie etwa Mutual TLS, voneinander isolierte Services oder automatische Audit-Trails. Alles Dinge, die historisch schwer umzusetzen waren und oft auch Einfluss auf die eigentliche Anwendung genommen haben.

Gleichzeitig bieten Technologien wie Kubernetes, aber auch die verschiedenen Clouds die Möglichkeit, zentralisierte und deklarative Richtlinien und Policies zu definieren. Als Beispiele seien RBAC von Kubernetes und IAM von Amazon genannt, mit denen auf einfache Weise Berechtigungen in textueller Form verwaltet werden können.

Mehr auf die Plattform zu setzen, sollte für uns als Java-Entwickler aber auch bedeuten, mehr über das Java-Ökosystem nachzudenken. Je mehr Funktionalität außerhalb der Anwendung angesiedelt wird, desto weniger davon muss in entsprechenden Java-Bibliotheken liegen. Wie wichtig ist es, dass ein Logger zahlreiche mögliche Ausgabekanäle anbietet, wenn im Cloud-Native-Umfeld die Ausgabe auf stdout völlig ausreichend ist? Gerade im Bereich Security ist weniger eben häufig mehr. Hier ist ein Umdenken in der Java-Welt durchaus ratsam.

 

Fazit

Cloud-Native ist ein Ansatz mit vielen Facetten. Nicht alle Aspekte davon betreffen uns als Java-Entwickler direkt. Dennoch lohnt es sich, immer auch einen Blick hinter den vermeintlichen Hype zu werfen und zu überlegen, wo wir als Java-Community eigentlich stehen. Einfach zu sagen „Wir machen Cloud-Native“, ohne überhaupt zu verstehen, was die meisten damit meinen, führt dazu, dass der Begriff weiter verwässert. Deshalb hilft es auch, zu schauen, welche Vorteile denn wirklich bei uns ankommen können und wo noch Nachholbedarf besteht.

Zusammenfassend können wir aber durchaus stolz auf unser Ökosystem sein. In vielen Bereichen ist es keineswegs vermessen zu behaupten, dass wir Cloud-Native auch wirklich leben und nicht nur Verpackungsschwindel betreiben. Raum für Verbesserung gibt es natürlich immer. Ich habe aber keinen Zweifel daran, dass wir diesen Raum in Zukunft noch weiter füllen werden.