CloudEvents Hands-On: Was taugt die neuste Graduation der CNCF?

24.04.2024Tom Trapp
Cloud Cloud Native Computing Foundation cloudevents Cloud Computing Event-Driven open-source Microservices

Banner

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

img_1.png

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.

img_4.png

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import {CloudEvent, emitterFor, httpTransport} from "cloudevents";

const emit = emitterFor(httpTransport("http://localhost:8080"));

interface ButtonEvent {
    clicked: boolean;
}
//...

app.post('/button-clicked', (req, res) => {
    console.log('Button was clicked!');
    const ce = new CloudEvent<ButtonEvent>({
        type: 'com.bnova.techhub.button.clicked',
        source: 'cloud-events-example-frontend',
        data: {clicked: true},
    });
    emit(ce);

    res.json({message: 'Button click handled by server!'});
});
  • 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 Typ ButtonEvent
  • 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:

1
npm run build && npm start

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    ctx := context.Background()
    p, err := cloudevents.NewHTTP()
    if err != nil {
        log.Fatalf("failed to create protocol: %s", err.Error())
    }
    
    c, err := cloudevents.NewClient(p)
    if err != nil {
        log.Fatalf("failed to create client, %v", err)
    }
    
    log.Printf("will listen on :8080\n")
    log.Fatalf("failed to start receiver: %s", c.StartReceiver(ctx, receive))
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func receive(ctx context.Context, event cloudevents.Event) (*event.Event, protocol.Result) {
    if event.Type() == "com.bnova.techhub.button.clicked" {
        log.Printf("Received event, %s", event)
        data := &ButtonEvent{}
        err := event.DataAs(data)
        if err != nil {
            log.Printf("failed to get data as ButtonEvent: %s", err)
        }
        log.Printf("Button clicked: %t", data.Clicked)
    
        sendCloudEvent(event)
	} else {
		log.Printf("Unknown type, %s", event)

		return nil, cloudevents.NewHTTPResult(500, "Blöd gelaufen")
	}
	return nil, nil
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func sendCloudEvent(event cloudevents.Event) *event.Event {
    c, err := cloudevents.NewClientHTTP()
    if err != nil {
        log.Fatalf("failed to create client, %v", err)
    }

	event.SetSource("cloud-events-example-go")

	ctx := cloudevents.ContextWithTarget(context.Background(), "http://localhost:8081/")

	resp, result := c.Request(ctx, event)
	if cloudevents.IsUndelivered(result) {
		log.Printf("Failed to deliver request: %v", result)
	} else {
		log.Printf("Event delivered at %s, Acknowledged==%t ", time.Now(), cloudevents.IsACK(result))
		var httpResult *cehttp.Result
		if cloudevents.ResultAs(result, &httpResult) {
			log.Printf("Response status code %d", httpResult.StatusCode)
		}

		if resp != nil {
			log.Printf("Response, %s", resp)
			return resp
		} else {
			log.Printf("No response")
		}
	}
	return nil
}

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:

1
go run main.go

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Path("/")
@Consumes({ JsonFormat.CONTENT_TYPE })
@Produces({ JsonFormat.CONTENT_TYPE })
public class ActivityResource
{
	private static final Logger LOGGER = LoggerFactory.getLogger(ActivityResource.class);
	@Inject
	ObjectMapper mapper;
	@Inject
	@RestClient
	BoredApiService boredApiService;

	@SneakyThrows @POST
	public Response create(CloudEvent event)
	{
		LOGGER.info("Received event: {}", event);
		if (event == null || event.getData() == null)
		{
			throw new BadRequestException("Invalid data received. Null or empty event");
		}

		switch (event.getType())
		{
			case "com.bnova.techhub.button.clicked" ->
			{
				var buttenEvent = PojoCloudEventDataMapper
						.from(new ObjectMapper(), ButtonEvent.class)
						.map(event.getData())
						.getValue();
				LOGGER.info("Received ButtonEvent: {}", buttenEvent);
				Thread.sleep(Duration.ofSeconds(2).toMillis());

				return Response
						.ok()
						.build();
			}
			default ->
			{
				LOGGER.info("Received event: {}", event);
				return Response
						.status(Response.Status.BAD_REQUEST)
						.entity("Invalid event type")
						.build();
			}
		}
	}
}

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.

1
2
2024-04-02 13:36:10,754 ERROR [io.qua.res.rea.jac.run.map.NativeInvalidDefinitionExceptionMapper] (executor-thread-1) com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `io.cloudevents.CloudEvent` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 1]

Ok doch noch hinbekommen, scheinbar sind bestimmte Abhängigkeiten untereinander nicht kompatibel und man benötigt eine Custom Config für den ObjectMapper.

1
2
3
4
5
6
7
@Singleton
public class ObjectMapperConfig implements ObjectMapperCustomizer {
	@Override
	public void customize(ObjectMapper objectMapper) {
		objectMapper.registerModule(JsonFormat.getCloudEventJacksonModule());
	}
}

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:

1
mvn quarkus:dev

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.

1
2
3
4
5
6
7
curl -X POST http://localhost:8081 -H "Content-Type: application/json" -d '{
    "specversion" : "1.0",
    "type" : "com.bnova.techhub.button.clicked",
    "source" : "/mycontext",
    "id" : "1234-1234-1234",
    "data" : {"clicked": true}
  }'

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.

1
2
3
4
5
2024-04-02 14:05:15,660 DEBUG [org.jbo.res.rea.ser.han.RequestDeserializeHandler] (executor-thread-1) Error occurred during deserialization of input: com.fasterxml.jackson.databind.exc.MismatchedInputException: Missing mandatory specversion attribute
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 16]
	at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
	at io.cloudevents.jackson.CloudEventDeserializer$JsonMessage.getStringNode(CloudEventDeserializer.java:211)
	at io.cloudevents.jackson.CloudEventDeserializer$JsonMessage.read(CloudEventDeserializer.java:87)

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.

1
curl -X POST http://localhost:8081 -H "ce-specversion: 1.0" -H "ce-type: com.bnova.techhub.button.clicked" -H "ce-source: /mycontext" -H "ce-id: 1234-1234-1234" -H "Content-Type: application/json" -d '{"clicked": true}'

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.

1
2
-	ctx := cloudevents.ContextWithTarget(context.Background(), "http://localhost:8081/")
+	ctx := cloudevents.ContextWithTarget(cloudevents.WithEncodingStructured(context.Background()), "http://localhost:8081/")

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.

img_2.png

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
app.post('/get-activity', (req, res) => {
    console.log('Get Activity Button was clicked!');
    const ce = new CloudEvent<ButtonEvent>({
        type: 'com.bnova.techhub.get.activity',
        source: 'cloud-events-example-frontend',
        data: {clicked: true},
    });

    emit(ce).then((result) => {
        console.log('Result:', result);
        let ceResultStr = (result as { body: string }).body;
        let ceResult = JSON.parse(ceResultStr) as CloudEvent<Activity>;
        res.json(ceResult.data);
    }).catch(console.error);
});

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    else if event.Type() == "com.bnova.techhub.get.activity" {
		log.Printf("Received event, %s", event)
		data := &ButtonEvent{}
		err := event.DataAs(data)
		if err != nil {
			log.Printf("failed to get data as ButtonEvent: %s", err)
		}

		if data.Clicked {
			log.Printf("Querying activity")
			result := sendCloudEvent(event)
			log.Printf("Result: %s", result)

			event.SetSource("cloud-events-example-go")
			if err := event.SetData(cloudevents.ApplicationJSON, result); err != nil {
				log.Fatalf("failed to set data, %v", err)
			}
			return &event, nil
		}
    }

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
case "com.bnova.techhub.get.activity" ->
{
    var buttenEvent = PojoCloudEventDataMapper
            .from(mapper, ButtonEvent.class)
            .map(event.getData())
            .getValue();
    LOGGER.info("Received ButtonEvent: {}", buttenEvent);

    Activity activity = boredApiService.getActivity();

    CloudEvent cloudEvent = CloudEventBuilder.v1(event)
            .withSource(URI.create("cloud-events-example-java"))
            .withData(PojoCloudEventData.wrap(activity, mapper::writeValueAsBytes))
            .build();

    LOGGER.info("Prepare Sending activity: {}", cloudEvent);
    Thread.sleep(Duration.ofSeconds(2).toMillis());
    LOGGER.info("Now Sending activity: {}", cloudEvent);

    return Response.ok(cloudEvent).build();
}

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.

img.png

  • 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

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.

img_3.png

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!

Tom Trapp

Tom Trapp – Problemlöser, Innovator, Sportler. Am liebsten feilt Tom den ganzen Tag an der moderner Software und legt viel Wert auf objektiv sauberen, leanen Code.