AWS lambda Services mit Spring Boot erstellen
Amazon Web Services ermöglicht mit Lambda Services Code-Ausführung ohne (eigenen) Server - serverless. Hier wird gezeigt, wie man Lambda Code in Java mit Spring Boot erstellt bzw. wie man ein RestContoller-basiertes Hello World mit AWS Lambda und Spring Boot erstellt.
Spring Boot Lambda Projekt mit Maven Archetype generieren
Die AWS Labs bieten ein Maven Archetype Template an, mit dem man sich einfach ein neues Spring Boot Projekt generieren kann. Wenn man Maven installiert und in seiner Shell eingerichtet hat, kann man einfach folgenden Befehl (Achtung vorher Zeilenumbrüche entfernen) ausführen:
mvn archetype:generate
-DgroupId=de.demo
-DartifactId=aws-demo
-Dversion=0.0.1
-DarchetypeGroupId=com.amazonaws.serverless.archetypes
-DarchetypeArtifactId=aws-serverless-springboot2-archetype
-DarchetypeVersion=1.5.1
Das neue Projekt wird im selben Verzeichnis erstellt, in dem auch der Befehl ausgeführt wurde. 💣Archetype Version 1.5.1 generiert einen Bug in der pom.xml Datei, weiter unten erkläre ich wie man das fixt. Stand 21.09.2020 habe ich keine neuere, fehlerfreie Version gefunden.
Ohne eigene Maven Installation kann man noch das in der IDE integrierte Maven verwenden. In Eclipse geht das so: Menu => Run => Run As... => Maven build... Im sich danach öffnenden "Edit Configuration" das Ziel-Verzeichnis auswählen (Knopf "File System...") und unter goals den zuvor genannten Maven Befehl eintragen (ohne "mvn" also ab "archetype"), danach nur noch den "Run" Knopf drücken.
Exkurs Maven Archetype
- archetype:generate definiert das mit Maven Archetype ein neues Projekt generiert werden soll.
- DgroupId, DartifactId und Dversion definieren Gruppen Id, Artefakt Id und Version des neu generierten Projektes.
- DarchetypeGroupId, DarchetypeArtifactId und DarchetypeVersion definieren welches Maven Archetype Template in welcher Version zum Generieren verwendet werden soll.
Spring Boot lambda Projekt verstehen und bauen
Aufbau Projekt
Unser neu generiertes Spring Boot Lambda Projekt ist wie folgt aufgebaut:
- PingController.java ist ein klassischer RestController, wie man ihn aus vielen Spring Boot Web Projekten kennt, siehe dazu auch microservices-mit-spring-boot-erstellen.html
- Application.java ist die klassische Start-Datei von Spring Boot. Sie hat daher die @SpringBootApplication Annotation und die "public static void main"-Methode.
- pom.xml ist die Maven-Konfiguration. Sie hat folgende Unterschiede zu anderen Spring Boot Projekt Konfigurationen:
- Es gibt eine Dependency zum Laden der Bibliotheken für AWS Lambda, sie hat die artifactId aws-serverless-java-container-springboot2.
- Die package Phase des Build Prozesses ist durch 2 Profile shaded-jar und assembly-zip so angepasst, dass am Ende eine zip-Datei gebaut wird, die als Code für eine AWS lambda Funktion verwendet werden kann.
- StreamLambdaHandler.java ist eine Implementierung des Interfaces RequestStreamHandler, welches durch eine AWS Lambda Bibliothek bereitgestellt wird. Diese Klasse ist die Schnittstelle zwischen dem Lambda Service in der AWS Cloud und dem Code im Spring Boot Projekt. Hier sieht man, dass die Methode handleRequest einen Proxy verwendet, der alle eingehenden Lambda Code Aufrufe an die Spring Controller in unserem Projekt weiterleitet.
Alternativ könnte man auch das Interface RequestHandler implementieren und dann z.B. andere Spring Beans aufrufen, welche die Business Logik des Lambda Services implementieren. Das kann z.B. sinnvoll sein, wenn man kein Web Projekt mit Spring Boot implementiert haben. - template.yml kann zum automatisierten Deployment in die AWS Cloud verwendet werden. Alternativ kann man das Deployment bzw. das Erstellen des Lambda Services in der AWS Cloud auch über die AWS Management Console im Browser machen.
Bugfix in pom.xml
Wie schon beim Generieren des Spring Boot Projektes erwähnt, gibt es einen Bug in der pom.xml Datei. AWS Labs hat auf GitHub Code mit Samples veröffentlicht:
https://github.com/awslabs/aws-serverless-java-container
Teil dieses GibHub Projektes ist der verwendete Maven Archetype aws-serverless-springboot2-archetype und diverse Beispiel-Projekte. Vergleicht man das Beispiel samples/springboot2/pet-store mit dem generierten Projekt, so fällt auf dass in der pom.xml vom pet-store folgendes XML gelöscht wurde:
<!-- don't build a jar, we'll use the classes dir --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <id>default-jar</id> <phase>none</phase> </execution> </executions> </plugin>
Das muss in der pom.xml des generierten Projektes auch gelöscht werden, ansonsten funktioniert die zip-Datei mit dem Code im AWS Lambda Service nicht und wir bekommen einen "Internal Server Error".
Projekt zum Upload in den AWS Lambda Service bauen
Hat man Maven installiert und in der Shell eingerichtet, kann man ganz ohne IDE das Projekt bauen. Dazu einfach diesen Befehl ausführen: mvn clean package shade:shade
Anschließend befindet sich im target Verzeichnis eine zip-Datei, die alles notwendige zur Ausführung als AWS Lambda Service enthält.
Alternativ kann man ohne eigene Maven Installation die Eclipse IDE nutzen, um das Projekt zu bauen. Dazu einfach das generierte Projekt als Maven Projekt importieren. Danach kann das Projekt bzw. die zip-Datei gebaut werden, indem man mit per Rechts-Klick auf das Projekt => Run As => Maven Build... den Maven Dialog öffnet. Im Dialog müssen nur die Maven "Goals:" auf "clean package shade:shade" gesetzt werden und dann der "Apply"- und "Run"-Knopf gedrückt werden. Wird am Ende des Maven Builds "BUILD SUCCESS" geloggt, so findet man nach einem Refresh des Projektes in Eclipse (Projekt markieren und F5 drücken) die zip-Datei im target Verzeichnis.
Optional: Projekt lokal starten
Falls man das generierte Projekt in der IDE starten möchte, so muss man zuerst die pom.xml anpassen. Folgender Abschnitt muss gelöscht werden, so dass der standardmäßig vorhandene Tomcat doch nicht entfernt wird:
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
In Spring Boot Projekten für AWS lambda Services wird der Tomcat durch die Bibliotheken von AWS ersetzt, daher wurde er in der Maven Konfiguration des spring-boot-starter-web Artefakts exkludiert.
Nach einem Maven-Update des Projektes (Rechts-Klick auf das Projekt => Maven => Update Project...) kann das Projekt als Spring Boot Applikation gestartet werden, dazu Projekt markieren und im Menu => Run => Run As => Spring Boot App.
Dabei fällt auf, dass relativ wenig geloggt wird und der Start ziemlich schnell geht. Das ist aufgrund der Optimierung des Spring Boot Projektes für den AWS Lambda Service. Im Browser kann man die Anwendung testen, indem man diese URL aufruft: http://localhost:8080/ping
AWS Lambda Service & API Gateway erstellen
Für das Erstellen des Lambda Services in der AWS Console sei auf die Doku von Amazon verwiesen:
https://docs.aws.amazon.com/de_de/apigateway/latest/developerguide/api-gateway-create-api-as-simple-proxy-for-lambda.html
Da ich diese Doku aber nicht so gut finde, hier noch ein paar Anmerkungen:
- Nachdem der Lambda Service erstellt wurde, muss die zip-Datei hochgeladen werden - der Inline Code Editor kann nur bei Script-Sprachen (z.B. Python, JavaScript) verwendet werden. Der Knopf zum Upload ist im Screenshot rot markiert.
- Außerdem ist es wichtig, dass man die Klasse konfiguriert, welche das Interface RequestHandler oder RequestStreamHandler implementiert. In diesem Projekt ist das StreamLambdaHandler.java. Auch diese Einstellung ist im Screenshot markiert.
- Die Test-Funktion in der AWS Console funktioniert für unser Projekt nicht, weil als Input das Format des API Gateways erwartet wird. Führt man den Test trotzdem aus, sollte das angezeigt werden "Execution result: succeeded". Schaut man aber die Details an, so sieht man, dass es einen "Internal Server Error" gab. Zumindest wurde mit diesem Test gezeigt, dass der hoch-geladene Code ausgeführt wurde, da man im Stacktrace der geloggten Exception die Klasse StreamLambdaHandler aus dem generierten Projekt sieht.
Würde die Klasse StreamLambdaHandler das Interface RequestHandler implementiert werden, so gäbe es keinen "Internal Server Error" und der Test wäre erfolgreich.
AWS Lambda Console |
Man sollte so vorgehen, wie im Abschnitt zur Proxy-Integration beschrieben.
Danach kann man den Link zum API-Gateway nutzen, um die Lambda Funktion vom Browser aus zu testen.
Mir ist bewusst, dass die Schritte in der AWS Console hier nur sehr grob beschrieben sind. Lasst mir gerne Kommentare da, ob es so ausreichend ist oder ob ihr hier weitere Details benötigt?
Optimierungen im Spring Projekt für Ausführung als AWS Lambda Service
Eine Besonderheit von AWS Lambda Services ist, dass man nur dann bezahlen muss, wenn sie auchg ausgeführt werden. Es gibt als keinen Server, der die ganze Zeit läuft und Ressourcen verbraucht. Daher wird der Code in Lambda Service mit einem sogenannten Kaltstart gestartet. Das bedeutet, wird die Funktion getriggert, so fährt sie alles hoch, was notwendig ist. Erst dann wird die Anfrage verarbeitet, siehe dazu auch: https://docs.aws.amazon.com/de_de/lambda/latest/dg/runtimes-context.html
Normalerweise brauchen Spring Boot Web Applikationen ein paar Sekunden bis der komplette Spring Kontext aufgebaut und der Tomcat gestartet ist. Wenn Spring Boot Applikationen aber als Lambda Service ausgeführt werden, dann empfiehlt es sich Optimierungen vorzunehmen.
Hier sind einige aufgelistet, die wir direkt in unserem Projekt umsetzen können bzw. die schon vom Maven Archetype Template generiert wurden. Wenn ein bestehendes Spring Projekt in eine AWS Lambda Funktion überführt werden soll, sind diese Punkte sicherlich besonders wichtig:
- Der embedded Tomcat wird in der Maven Konfiguration (pom.xml) exkludiert, siehe dazu auch den Abschnitt Projekt lokal starten weiter oben.
- In der Application.java Klasse können mehrere Optimierungen umgesetzt werden:
- Mit der @Import Annotation werden nur die nötigsten Controller und Beans geladen, um die Zeit für den Komponenten Scan von Spring (@ComponentScan) zum Erstellen des Spring Kontextes zu sparen.
- Im Pet-Store Beispiel sieht man noch wie diese beiden Handler-Beans erstellt werden, um zu verhindern, dass Spring diverse Standard Instanzen von HandlerMapping und HandlerAdapter automatisch erstellt:
@Bean |
public HandlerMapping handlerMapping() { |
return new RequestMappingHandlerMapping(); |
} |
@Bean |
public HandlerAdapter handlerAdapter() { |
return new RequestMappingHandlerAdapter(); |
} |
- Das Loggen in die Console sollte bei Lambda Funktionen ebenfalls deaktiviert werden. Das ist ein Unterschied zu Docker Containern, wo das Loggen im Idealfall nur im Standard-Out erfolgen sollte. AWS Lambda bietet einen eigenen LambdaLogger an, siehe dazu auch https://docs.aws.amazon.com/lambda/latest/dg/java-logging.html
Kommentare