© Minur/Shutterstock.com, © Vik Y/Shutterstock.com, © Gravvi/Shutterstock.com
Cloud Native Serverless Java mit Quarkus und GraalVM auf AWS Lambda

Ein paar graue Haare, aber …


Wer jetzt noch nicht „Bingo“ gerufen hat, ist selbst schuld. Wie kann es gelingen, nahezu alle oben aufgezählten Bleeding-Edge-Technologien, Frameworks und Plattformen in einem Real-World-Projekt abseits der grünen Wiese und Hello-World-Demos erfolgreich und miteinander zu verwenden? Ein Erfahrungsbericht.

Mit diesem Artikel möchte ich bewusst keine Anleitung oder Beschreibung eines Frameworks geben, die erklärt, wie man es auf welche Weise und für welchen Anwendungszweck einsetzt. Da ich gerne mit neuen Technologien arbeite, stellte sich für mich in den letzten Monaten die folgende Herausforderung: Ist es möglich, viele der neuen und aktuellen Frameworks so miteinander zu verbinden, dass die Anwendung letztendlich betrieben werden kann, sich der programmatische und operative Aufwand aber in zu bewältigenden Grenzen hält? Aufbauend auf dieser Fragestellung möchte ich hier von meinem Vorhaben berichten. Details und Anleitungen zu den verwendeten Komponenten können auf den jeweiligen Webseiten und in Artikeln unterschiedlicher Autoren in vergangenen Ausgaben des Java Magazins nachgelesen werden.

Als Anwendung aus der realen Welt, die sich in Produktion befindet, habe ich die Anmelde- /Registrierungs-App der Java User Group Darmstadt [1] ausgewählt, für die ich in der Organisation tätig bin. Diese Applikation ist bereits vor drei Jahren im Rahmen meines Serverless-Buchprojekts [2] entstanden und läuft seitdem ohne die Hilfe eines Web-Frameworks wie Spring, Java EE o. ä. auf Basis von Java 8 in AWS Lambda [3]. Mittlerweile hat sich die Anwendung wie so viele Projekte weiterentwickelt und der Code ist, wie man so schön sagt, historisch gewachsen.

Die Anwendung ist lokal nur bedingt lauffähig, Änderungen werden aufwendiger und führen teilweise zu recht fragilem Code. Höchste Zeit also, ein komplettes Review der Codebasis durchzuführen und diese zu aktualisieren.

Fachlich ist die Anwendung recht übersichtlich aufgebaut: Zu einer beliebigen Veranstaltung der JUG DA sollen sich interessierte Teilnehmer einfach und unverbindlich über ein Webformular registrieren können, sodass wir im Vorfeld der Veranstaltung bereits einen guten Überblick über die Teilnehmerzahlen bekommen und gegebenenfalls in Raum- und Cateringorganisation eingreifen können. Ist ein Teilnehmer erfolgreich registriert, verschicken wir eine Bestätigungsmail. Dieser Bereich ist öffentlich verfügbar, kommt also ohne eine Anmeldung des Anwenders aus. Auf Benutzerkonten haben wir aus verschiedenen Gründen bewusst verzichtet. Der Zugriff auf die bereits getätigten Anmeldungen für uns Organisatoren erfolgt über einen abgesicherten Bereich der Anwendung. Hierfür ist eine Anmeldung eines Orga-Mitglieds nötig.

Von Ist nach Soll

Die Anwendung besteht also aus einem Browser-Frontend, Authentifizierung/Autorisierung, Datenverarbeitung, Datenspeicherung und E-Mail-Versand. Für das Rendering des Frontends habe ich auf eine serverseitige Lösung mittels der Handlebars Templating Engine zurückgegriffen. Den Einsatz einer JavaScript-basierten Single-Page Application habe ich bewusst nicht gewählt, um möglichst wenig Einzelkomponenten zu haben. Ich wollte letztendlich nur ein oder maximal zwei Deployment-Komponenten verwalten. Das gesamte HTTP(S)-Handling wird vom Amazon API Gateway übernommen, in dem auch konfiguriert wird, welche Pfade der Anwendung mit einem sogenannten Authorizer versehen werden, um diese nur autorisierten Anwendern zugänglich zu machen. Innerhalb der Anwendung habe ich mir meine Service-Klassen über Singletons selbst erzeugt und verwaltet. Singletons klingen erstmal altbacken und anfällig, haben aber in Java-basierten AWS-Lambda-Funktionen durchaus eine Berechtigung, da es zur Laufzeit zu keinerlei konkurrierenden Threads kommen kann: Jeder Thread wird in einer eigenen Lambda-Instanz bearbeitet. Die Daten werden in einer Amazon DynamoDB NoSQL DB gespeichert und die E-Mails über den Amazon Simple E-Mail Service versendet. Für beide Dienste stellt AWS ein Java-API bereit. Letztendlich hatte ich damit vier Lambda-Funktionen (Registrierung, Löschung, Admin und Authorizer), ein API Gateway-Mapping und eine DB-Tabelle, die ich mit Hilfe des Serverless Frameworks [4] deployt habe; mehrere Einzelkomponenten zwar, aber durch das Serverless Framework sehr einfach als eine Einheit in einem Projekt zu verwalten. Der Legacy-Code ist im GitHub Repository [5] unter dem Tag legacy zu finden.

Die Nutzung der Java Runtime in AWS Lambda hat jedoch den Nachteil, dass die Start-up-Zeiten von Java-basierten Funktionen verhältnismäßig hoch sind. In asynchronen, Event-getriebenen Datenverarbeitungs-Pipelines ist das kein Problem, kann in einem Kontext mit Benutzerinteraktion (also beispielsweise einer Website) jedoch schnell zu unerwünscht hohen Latenzen und damit zu unzufriedenen Anwendern führen. Einziger Workaround war bislang, den provisionierten Speicher für eine Lambda-Funktion zu erhöhen, sodass dieser damit auch mehr CPU-Leistung und Netzwerkbandbreite zugewiesen wird. Auch wenn man den eigentlichen Speicher nicht benötigt, kann das zu reduzierten Kosten führen, da die Ausführungszeit sinkt. Zusätzlich kann man eine Instanz der Lambda-Funktion für einige Zeit „warm“ halten, indem man sie mit Cron Events aus AWS CloudWatch regelmäßig aufruft. Klingt schräg, war aber lange Zeit wirklich die einzige Möglichkeit hierfür. Mittlerweile bietet AWS die Option der „Reserved Concurrency“ [6] an, mit der man angeben kann, wie viele Instanzen vorgewärmt (initialisiert) zur Verfügung stehen sollen, und die damit schnellere Antwortzeiten liefern. Die weitere Nutzung von AWS Lambda war gesetzt, da ich zum einen ein Serverless Fanboy bin und zum anderen die Kosten für unseren Anwendungsfall auf einem sehr überschaubaren Level von null Euro gehalten werden können, da das kostenlose AWS-Nutzungskontingent nicht komplett ausgeschöpft wird.

An dieser Stelle kommen nun diverse neue Technologien, Frameworks und Plattformoptionen ins Spiel. Mit der GraalVM [7] ist es zwar möglich, eine Java-Anwendung in OS-nativen Code zu kompilieren und somit performanter und auch speicherschonender auszuführen, jedoch sind der Aufwand und die Einstiegshürde nicht unerheblich, wenn man damit noch nicht gearbeitet hat. AWS Lambda bietet außerdem keine vorkonfigurierte Laufzeitumgebung an, um native Binaries auszuführen. Das wurde erst mit dem sogenannten Custom Runtime API [8] möglich, mit dem man beliebige eigene Runtimes erzeugen, hochladen und nutzen kann.

Im Frühjahr 2019 wurde dann von Red Hat das Quarkus Framework [9] vorgestellt, das durch schnelle Start-up-Zeiten, komfortable Hot-Reload-Möglichkeiten während der Entwicklung und der Option der nativen Kompilierung mittels GraalVM per einfachen Kommandozeilenparametern glänzen möchte. Für die Entwicklung von Microservices konzipiert, die später in Containern ausgeführt werden, spielt es im gleichen Lager wie etwa Micronaut [10] und Helidon [11]. Quarkus unterstützt jedoch auch die Entwicklung von AWS-Lambda-Funktionen [12]. Diese Features machten Quarkus initial für mich interessant und brachten mich dazu, das Framework hinsichtlich der Überarbeitung der JUG-DA-Registrierung zu untersuchen und auch zu verwenden.

Eine Meinung haben

Quarkus gilt als „opinionated“ Framework. Es macht also Dinge auf seine eigene Weise und nach eigener Meinung. Und genau das sollte auch nicht vergessen werden. Das, was Quarkus kann und macht, macht es gut, aber auf seine Weise und unter eigenen Bedingungen. Damit kann das Framework für bestimmte Anwendungsfälle hervorragend geeignet sein, für andere wiederum überhaupt nicht.

Ich habe versucht, mich unvoreingenommen an die Arbeit zu machen, und habe zunächst nach einem Plug-in für meine IntelliJ-Entwicklungsumgebung Ausschau gehalten. Es gibt zwar bereits ein Plug-in, das sich jedoch in einer sehr frühen Phase (Version 0.0.3) befindet und noch nicht viele Features bietet: lediglich einen Wizard, um neue Projekte oder Module auf Basis des Generators von https://code.quarkus.io zu erzeugen, und eine Autovervollständigung auf Basis des Language-Servers für die Quarkus Properties in der application.properties-Datei. Eine Debug-Möglichkeit ist im Plug-in (noch) nicht enthalten. Dafür wird, wenn eine Quarkus-App im Dev Mode gestartet wird, automatisch ein Debug-Port geöffnet, sodass man sich mit einem Remote-Debugger aus der IDE damit verbinden kann. Wenigstens etwas. So ist das halt, wenn man mit jungen Frameworks arbeitet, für die das Ökosystem und Tooling noch im Entstehungsprozess begriffen sind.

Im ersten Schritt habe ich die benötigten Quarkus-Bibliotheken als Abhängigkeiten in die Maven pom.xml eingefügt. Ich habe mich für eine Lösung mit RESTEasy als JAX-RS-Implementierung und der AWS Lambda HTTP Extension [13] für Quarkus entschieden. Die AWS-Lambda-Extensions sind noch im „Preview“-Status, API und Properties können sich also im Laufe der Entwicklung noch ändern. Wenn man „Living on the (tech) edge“ betreibt, ist man das ja gewohnt.

Danach machte ich mich daran, meine Lambda-Funktionsklassen, die das HTTP Event Handling abdeckten, i...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang