Im zweiten TechUp zu CloudEvents wollen die neuste Graduation der CNCF genauer und Hands-On unter die Lupe nehmen. Selbstverständlich findet ihr den genutzten Code auf GitHub.
Folgende Fragen wollen wir in diesem TechUp beantworten:
- 🌩️ Was ist CloudEvents?
- 🏆 Warum ist die Graduation von CloudEvents ein Meilenstein?
- 🛠️ Wie kann CloudEvents in der Praxis eingesetzt werden?
- 🔍 Wie kann CloudEvents getestet werden?
- 💡 Lohnt sich der Einsatz von CloudEvents?
CloudEvents?
CloudEvents ist eine Open-Source-Initiative, die von der Serverless Working Group der CNCF ins Leben gerufen wurde. Die Arbeit an CloudEvents begann im Jahr 2017, und seitdem hat sich die Initiative zu einem bedeutenden Standard für den Austausch von Ereignissen zwischen Cloud-Anwendungen und Diensten entwickelt.
Mehr dazu in Ricky’s CloudEvents TechUp.
Graduation
Im Januar 2024 gab die Cloud Native Computing Foundation (CNCF) bekannt, dass CloudEvents den Graduation-Status erreicht hat. Dieser Status wird an Projekte vergeben, die die Reife und Akzeptanz erreicht haben, um als vollwertige Mitglieder der CNCF-Community zu gelten. Die Graduierung von CloudEvents markiert einen wichtigen Meilenstein in der Entwicklung der Initiative und unterstreicht ihre weitreichende Akzeptanz und Reife. Somit hat CloudEvents das höchste Gütesiegel der CNCF erhalten, und steht somit auf Augenhöhe mit anderen bekannten Projekten wie Kubernetes, Argo, Cilium, Prometheus und Envoy.
Hands-On
In diesem TechUp wollen wir uns CloudEvents komplett hands-on anschauen und eine kleine aber dennoch vollwertige Anwendung bauen, welche aus mehreren Microservices besteht und CloudEvents nutzt, um miteinander zu kommunizieren.
Schauen wir uns zuerst an, welche SDKs es gibt:
Ganz schön umfangreich, oder? Wir wollen uns in diesem TechUp auf Go, Java und JavaScript konzentrieren.
Das Ziel
Wir haben ein einfache FrontEnd, mit einem Button. Dieser Button soll uns eine Aktivität vorschlagen, wenn uns langweilig ist.
Technisch soll das Frontend einen Go-Service aufrufen, welcher eine Anfrage an einen weiteren Java Microservice stellt. Dieser Java Microservice frägt dann die Aktivitäten-API von BoredAPI an und gibt uns eine Aktivität zurück.
Zu Beginn wollen wir uns aber erstmal die Kommunikation zwischen den Services anschauen ohne Responses.
Big Picture
Hier ist schön zu sehen, dass die Kommunikation zwischen den unterschiedlichen MicroServices via CloudEvents stattfindet. Selbstverständlich könnte die Kommunikation zwischen Browser und Node.js-Server auch via CloudEvents stattfinden, der Einfachheit halber nutzen wir hier aber “plain” HTTP.
Wir haben hier einen reinen synchronen Use-Case, wir senden ein Event und warten auf eine Antwort.
One Way Communication
Die Kommunikation zwischen den Services soll über HTTP und CloudEvents erfolgen. Dazu müssen wir in jedem Service ein CloudEvents-SDK einbinden und die Events entsprechend verarbeiten.
Zuerst wollen wir uns anschauen, wie wir Daten in eine Richtung senden können. Damit bauen wir gleichzeitig auch die komplette Architektur unsere Anwendung auf und können danach unseren Use-Case implementieren.
Frontend
Im Repository cloud-events-example-frontend findet ihr ein Node.js Projekt, welches Express nutzt, um einen einfachen Webserver zu starten. Der Webserver liefert eine einfache HTML-Seite aus, welche zwei Buttons enthält. Wenn einer der Buttons gedrückt wird, soll ein Event an den Go-Service gesendet werden.
Das Frontend-Projekt ist über http://localhost:3000
erreichbar.
Auf den generellen Aufbau des Frontend-Projektes wollen wir an dieser Stelle nicht weiter eingehen, schauen wir uns aber direkt den CloudEvents-Teil an:
|
|
- Auf Zeile 1 sehen wir, dass wir unterschiedliche Teile von cloudevents importieren müssen
- Auf der nächsten Zeile legen wir uns einen Emitter an, welcher die Events via HTTP
localhost:8080
an den Go-Service sendet - Nun definieren wir einen Endpunkt, welcher ein CloudEvent an den Go-Service sendet
- Darin legen wir ein neues CloudEvent an. Dieses hat einen Typen, eine Quelle und Daten. In diesem Fall ist der Typ
com.example.button.clicked
, die Quelle/button-clicked
und die Daten{ clicked: true }
vom TypButtonEvent
- Und zu guter Letzt senden wir das Event mit
emit(ce)
ab
Schön zu sehen ist, dass das CloudEvent vollständig in TypeScript definiert ist und wir somit die Typensicherheit von TypeScript nutzen können.
Recht einfach, oder? So haben wir nun ein CloudEvent an unseren Go-Service gesendet. Schauen wir uns jetzt an, wie wir dieses Event in unserem Go-Service verarbeiten können.
Gebaut und gestartet wird das FE mit folgendem Befehl:
|
|
Go Service
Das Repository cloud-events-example-go beinhaltet unser Go-Projekt, welches das CloudEvent empfängt und verarbeitet. Dazu nutzen wir das Go-SDK von CloudEvents.
Schauen wir uns auch hier den CloudEvent spezifischen Code genauer an. Wir nutzen hierfür direkt CloudEvent als HTTP Handler, damit wir nicht erst HTTP-Requests parsen müssen.
Unsere main Fuction sieht wie folgt aus.
|
|
Wir definieren einen HTTP Client und anschliessend darauf einen CloudEvent Client.
Mittels StartReceiver
beginnen wir, auf Events zu lauschen und rufen die Funktion receive
auf, wenn ein Event empfangen wird.
Recht praktisch, da wir uns nicht um die HTTP-Verarbeitung kümmern müssen. Sämtliche CloudEvents werden uns direkt als cloudevents.Event
übergeben, es gibt in diesem Setup nur einen Endpunkt.
Die Methode receive
nimmt den CloudEvent Context sowie das eigentliche Event entgegen.
|
|
Wir sehen, dass wir zuerst schauen, ob es sich um den erwarteten Type handelt, falls ja casten wir unsere Daten in ein ButtonEvent und loggen den Wert des Feldes Clicked
.
Sollte es sich nicht um einen bekannten Typ handeln, liefern wir einen 500er zurück.
Die Funktion sendCloudEvent
sendet ein neues CloudEvent an den Java-Service.
|
|
Schön zu sehen ist, dass wir uns einen neuen HTTP Client aufbauen, welchen wir zur Weiterleitung des Events nutzen. Wir setzen die Quelle auf cloud-events-example-go
und senden das Event an http://localhost:8081/
.
Technisch definieren wir kein neues Event, sondern reichern das bestehende Event mit einer neuen Quelle an.
Sollte das Event nicht zugestellt werden können, loggen wir dies und geben den Fehler aus. Ansonsten loggen wir die Antwort und geben diese zurück.
Nun ist auch unser Go-Service bereit, CloudEvents zu empfangen und zu verarbeiten.
Gebaut und gestartet wird das Go-Service mit folgendem Befehl:
|
|
Java Service
Das Repository cloud-events-example-java beinhaltet unser Java-Projekt, welches das CloudEvent empfängt und verarbeitet. Dazu nutzen wir das Java-SDK von CloudEvents. Hier hatte ich ein paar Schwierigkeiten, das Git Repo enthält natürlich den finalen Stand.
Glücklicherweise bietet uns die Java-SDK ein, leider recht veraltetes, Quarkus Beispiel an. Da ich auf der neusten Quarkus Version aufsetzen will, initialisiere ich mir ein neues Projekt und füge die CloudEvent Dependencies hinzu.
Schauen wir uns den relevanten Teil des Codes an.
|
|
In der ActivityResource registrieren wir einen neuen Endpoint, welcher CloudEvents entgegennimmt. Wir schauen, ob es sich um den erwarteten Typ handelt und loggen die Daten.
Leider nutzt CloudEvents keine Generics, daher müssten wir mit dem PojoCloudEventDataMapper
unsere Data selbst in das gewünschte Objekt, hier unser ButtonEvent
Model, mappen.
Bedauerlicherweise habe ich nach zahlreichen Versuchen und unterschiedlichen Lösungsansätzen den Quarkus nicht sauber zum Laufen bekommen, entweder er kam 415 Unsupported Media Type zurück oder eine Exception flog beim Deserialize von CloudEvent
.
|
|
Ok doch noch hinbekommen, scheinbar sind bestimmte Abhängigkeiten untereinander nicht kompatibel und man benötigt eine Custom Config für den ObjectMapper.
|
|
Leider war dies nicht sofort ersichtlich, ich musste mich durch zahlreiche, teils veraltete, Dokumentationen und Beispiele kämpfen. Die eigentliche Lösung habe ich dann bei einem Spring Boot Example gefunden.
Nun ist auch unser Java-Service bereit, CloudEvents zu empfangen und zu verarbeiten.
Gebaut und gestartet wird das Java-Service mit folgendem Befehl:
|
|
Testing & Troubleshooting
Freudig klicke ich den Button und sehe, dass der Go-Service das Event empfangen hat und loggt. Die Weiterleitung klappt leider nicht, es fliegt ein Fehler im Java Service.
Mit curl
schaffe ich es, ein CloudEvent im Structured Mode an meinem Java-Service zu senden.
|
|
Unser Go-Service liefert aber weiterhin einen 400er, wieso?
Leider loggt uns der Quarkus keinerlei Informationen, wieso er den Request ablehnt. Wir müssen uns also selber um die Fehlerbehebung kümmern.
Setzen wir zuerst doch mal das LogLevel via quarkus.log.level=DEBUG
und schauen uns die Logs an.
|
|
Das klingt doch hilfreich, scheinbar fehlt das specversion
Feld in unserem Go-Service.
In den Payloads der fehlerhaften Requests sehen wir aber, dass eine SpecVersion von 1.0 mitgeschickt wird. Auch das explizite Setzen hat leider nichts gebracht.
Haben wir hier ein Problem zwecks der unterschiedlichen Arten, wie man mit CloudEvents Daten übertragen kann? Mit dem Structured Mode haben wir ein JSON-Object, welches die Daten sowie die CloudEvents Metadata enthält. Mit dem Binary Mode haben wir die Daten weiterhin im Body als JSON, die CloudEvents Metadata aber in den HTTP-Headern.
Mittels des Debugger sehen wir schnell, dass in der CloudEventDeserializer
Klasse der Body durchsucht wird, dort sind aber, da wir Binary Mode nutzen, keine CloudEvents Metadata vorhanden.
Versuchen wir doch den curl mach im Binary Mode zu machen.
|
|
Immerhin etwas, wir haben den gleichen Fehler. 🤣 Bedeutet, entweder müssen wir Quarkus den Binary Mode noch beibringen, oder Quarkus unterstützt diesen nicht. In diesem Fall müssten wir unseren Go-Service auf Structured Mode umstellen.
Versuchen wir doch, den Structured Mode in unserem Go-Service zu nutzen. Hierfür müssen wir, nach tiefem Eingraben in die Dokumentation und die SDK Samples, lediglich eine Zeile verändern.
In der Methode sendCloudEvent
müssen wir den Context anpassen. Im nachfolgenden Patch ist zu sehen, wie wir den Context anpassen, um den Structured Mode zu nutzen.
|
|
Und Tada, es funktioniert. Wir können nun Events von unserem FrontEnd an den Go-Service und dann an unseren Java-Service senden.
Wir sehen, das unter Frontend loggt, dass der Button geklickt wurde. In der Mitte sehen wir dann, das unser Go-Service das CloudEvent empfangen hat und entsprechend weiterleitet. Rechts sehen wir dann, dass der Java Service das Event empfangen hat und die Daten ausgibt.
Was das für ein cooles Terminal ist, erfährst du in Stefan’s TechUp zu Warp.
Somit haben wir unsere Architektur aufgebaut und können Daten in eine Richtung senden und loggen. 🚀
Two Way Communication
Nun aber zum eigentlichen UseCase, wir wollen ja die Activity entsprechend zurückgeben. Dafür legen wollen wir uns einen eigenen Typ anlegen.
Frontend
Nun kommt der zweite, Generate Activity
Button ins Spiel!
In unserem TypeScript Code duplizieren wir die Methode, binden diese auf einen neuen Endpunkt und passen das Event entsprechend an. Ausserdem implementieren wir noch ein Response Handling, damit wir die Daten auch anzeigen können.
|
|
Die Methode sieht sehr ähnlich aus, wir legen wieder ein CloudEvent mit dem Inhalt ButtonEvent (innerhalb von data) an und senden dieses an den Go-Service. Nun warten wir aber auf die Antwort, welche vom Typ Activity ist und geben diese ans HTML zur Anzeige zurück.
Go Service
In unserem Go-Service definieren wir einen neuen If-Else Bedingung für den neuen CloudEvent Type.
|
|
Gleich wie beim ersten Beispiel, schauen wir, ob es sich um den erwarteten Typ handelt und arbeiten dann weiter mit dem ButtonEvent.
Wenn nun data.Clicked
true ist, senden wir das CloudEvent an den Java-Service und warten auf die Antwort.
Sollte eine Antwort zurückkommen, so setzen wir diese Daten in das Event und geben dieses zurück.
Hier ist schön zu sehen, dass wir die Source explizit für die Rückgabe setzen, unser Frontend weiss somit, dass die Daten vom Go-Service kommen.
Java Service
Die Erweiterung im Java-Service ist konzeptionell gleich, wir definieren einen neuen Block, welcher unser CloudEvent handelt.
|
|
Hier ist wieder schön zu sehen, dass wir unser Pojo erstmal von Hand casten müssen.
Anschliessend holen wir uns die eigentlichen Daten vom Type Activity
von der BoredAPI und reichern diese dem aktuellen CloudEvent an.
Für Demo Zwecke warten wir noch 2 Sekunden, bevor wir das Event zurücksenden. So haben wir genug Zeit, um zu sehen, dass die Daten wirklich vom Java-Service kommen.
Anschliessend geben wir das cloudEvent wieder an den Go-Service zurück.
Hier sei anzumerken, dass diese Kommunikation synchron ist.
Solltest du nun Probleme haben, dass der Go-Service die Daten nicht entgegennehmen kann, achte auf deine Consumes
& Produces
Annotationen in deinem Java-Service.
Ich hatte zuerst Probleme, da der normale JSON MediaType noch in Produces
drin war, der Go-Service konnte die Daten dann nicht parsen.
Man muss explizit den hauseigenen CloudEvents MediaType setzen.
Und nun steht unsere komplette Implementation für die synchrone, bidirektionale Kommunikation. 💡
Testing
Testen wir nun unsere komplette Applikation und drücken den Generate Activity
Button.
Wir sehen in unserem Warp Terminal, dass alle Services entsprechende Logs und sich geben und nach kurzer Warterei die Daten auch im FrontEnd angezeigt werden.
-
Im linken Terminal, im FrontEnd, sehen wir, dass
- der Button geklickt wurde
- ein Event empfangen wurde
- das Event die Activity Daten von der BordeAPI enthält
-
Im mittleren Terminal, im Go-Service, sehen wir, dass
- das Event empfangen wurde
- das Event die ButtonEvent Informationen enthält
- das Event weitergeleitet wurde
- eine Antwort empfangen wurde, mit Status Code 200
- die Activity Daten in das Event gesetzt wurden
-
Im rechten Terminal, im Java-Service, sehen wir, dass
- ein Event vom Typ
com.bnova.techhub.get.activity
empfangen wurde - es sich um ein ButtonEvent mit
clicked: true
handelt - ein Resultat mit Activity Daten von der BoredAPI zurückgegeben wurde
- ein Event vom Typ
Somit haben wir unsere komplette Architektur aufgebaut und getestet. 🎉
Diese Daten werden uns dann auch im FrontEnd angezeigt. Nach ein paar Versuchen, haben wir eine passende, sommerliche Aktivität gefunden und unsere Langeweile besieht.
Somit ist unser CloudEvents Hands-On Projekt fürs Erste fertig! 🍦
Fazit
Wir haben nun CloudEvents gemeinsam angeschaut, mit einem Hands-On, HTTP UseCase. Ich sehe klar die Vorteile von CloudEvents, es bietet einen standardisierten Weg, um gemeinsame Schematypen für Events zu definieren und zu nutzen.
Mir stellt sich die Frage, ob HTTP da der richtige Use-Case ist. Eine Kommunikation wie im Beispiel hätten man auch super, ich behaupte auch einfacher mit normalen HTTP Requests umsetzen können. Hier wäre sicherlich der Einsatz von OpenAPI Specs sinnvoll.
Aus meiner Sicht könnte CloudEvents sein volles Potenzial in einer Microservices Architektur ausspielen, wo Events von unterschiedlichen Services ausgelöst und verarbeitet werden. Vorausgesetzt, es handelt sich hier auch um technische unterschiedliche Transport Typen, wie z.B. Protobuf oder JSON, Kafka oder RabbitMQ.
Was meinst du? Sollen wir ein drittes, Deep-Dive TechUp über CloudEvents machen? Uns schwebt beispielsweise ein UseCase mit Rest & Kafka vor, um die synchrone und asynchrone Welt miteinander zu verbinden. Ausserdem wäre es spannend zu sehen, ob und wie man eine Schema Registry nutzt, um die Schemata zu verwalten.
Des Weiteren würde mich brennend interessieren, wie einfach eine Anpassung des Transport-Layers ist. Wie umfangreich ist der Aufwand, um von HTTP auf Kafka zu wechseln?
Wäre das interessant? Lasst es uns wissen!