Das Thema Orchestrieren von (Micro-)Services beschäftigt uns nun seit einigen Jahren. Der Gedanke, einen Monolithen in kleine Services zu zerlegen, ist ein Architekturstil, der zwar die Wartbarkeit einzelner Anwendungsteile erhöht, jedoch andere Problemstellungen im Betrieb mit sich bringt.

Die Open Workflow Specification (CNCF) ist ein Standard, der ein Ökosystem zur Definition und Ausführung von DSL-basierten Workflows festlegt. Die Quarkus Extension Flow setzt diesen Standard in Form einer Workflow-Engine um. Neben der Möglichkeit, mithilfe von YAML-Dateien den Workflow zu beschreiben, steht auch eine Java DSL zur Verfügung, die wir uns in diesem Artikel genauer anschauen werden.

Wie passen jetzt die beiden Themen Orchestrieren von Services und Workflows zusammen? Mithilfe einer Workflow-Engine haben wir die Möglichkeit, in einzelnen Schritten (Tasks) Services miteinander zu verbinden.

Einordnung

Das Ökosystem von Quarkus hat als Grundlage die Standards aus Jakarta EE und Microprofile um Enterprise-Anwendungen zu entwickeln. Quarkus Flow baut auf genau diesen Standards auf und nutzt als Basis CDI. Darüber hinaus integriert es REST, Messaging, Scheduling, Authentication und viele weitere Technologien.

Ein Workflow lässt sich sehr einfach mithilfe der Java-DSL aufbauen. Dazu muss die CDI-Bean die Klasse Flow erweitern. In der Methode descriptor wird der Workflow beschrieben. Über den Workflow-Builder lassen sich nacheinander die einzelnen Aufgaben (Tasks) definieren.

@ApplicationScoped
public class WeatherFlow extends Flow {
  @Override
  public Workflow descriptor() {
    return FlowWorkflowBuilder.workflow("weather")
      .tasks(...)
      .build();
  }
}

Abruf von Wetterinformationen

Schauen wir uns zunächst einmal ein einfaches Szenario für eine Orchestrierung an. Es sollen zunächst die Koordinaten für einen bestimmten Ort über eine REST-API abgerufen werden. Im zweiten Schritt soll die aktuelle Temperatur an diesem Ort von einer weiteren REST-API abgerufen werden.

Im ersten Schritt nutzen wir eine freie öffentliche REST-API zum Abruf von Geo-Informationen:

curl https://geocoding-api.open-meteo.com/v1/search?name=Bielefeld&count=1&language=de

Als Ergebnis bekommen wir eine Json-Struktur unter anderem mit den Informationen zum Breiten- und Längengrad. Diese Abfrage können wir jetzt in den Workflow wie folgt einbauen:

get("geoLocation", "https://geocoding-api.open-meteo.com/v1/search")
    .query("name", "${ .location }")
    .query("count", "1")
    .query("language", "de")

Wir können hier also in einer einfachen Art den REST-Aufruf definieren. Im Hintergrund wird für uns automatisch ein REST-Client erstellt. Um die Abfrage des Ortes dynamisch zu halten, können wir in den Workflow beliebige Daten hineingeben. In unserem Fall ist es einfach die location. Mithilfe von jq können wir aus unseren Workflow-Daten die gewünschte Information herausfiltern ("${ .location }") und in unserer Abfrage nutzen.

Als Nächstes müssen wir uns überlegen, welche Daten aus der Antwort wir im weiteren Verlauf des Workflows benötigen. Für die Abfrage der aktuellen Temperatur und Windgeschwindigkeit brauchen wir den Längen- und Breitengrad. Da es sich bei der Bestimmung der Geo-Informationen um eine Suche handelt, können wir auch einen anderen Ort als den vorgegebenen bekommen. Daher speichern wir uns auch den Namen des gefundenen Ortes ab.

.outputAs(
    "${ $input + {geoLocation: {name: .results[0].name, latitude: .results[0].latitude, longitude: .results[0].longitude}} }")

Damit wir den ursprünglichen Inhalt des Workflows nicht verlieren, nutzen wir zunächst die Variable $input und erweitern diese einfach um ein weiteres Objekt.

Zum Schluss rufen wir die aktuellen Wetterinformationen für den ermittelten Ort ab. Diese REST-API wird uns ebenfalls kostenlos von Open Meteo zur Verfügung gestellt.

get("weatherForecast", "https://api.open-meteo.com/v1/forecast")
    .query("latitude", "${ .geoLocation.latitude }")
    .query("longitude", "${ .geoLocation.longitude }")
    .query("current_weather", "true")
    .outputAs("${ $input + {temperature: .current_weather.temperature, windspeed: .current_weather.windspeed} }"))

Der Workflow liefert uns zum Abschluss nun die notwendigen Informationen im Json-Format:

{
    "location": "Hiddenhausen",
    "geoLocation": {
        "name": "Hiddenhausen",
        "latitude": 52.16667,
        "longitude": 8.61667
    },
    "temperature": 19.8,
    "windspeed": 7.2
}

Fazit

Das beschriebene Beispiel ist sicherlich sehr einfach gehalten, zeigt aber die Möglichkeiten zum strukturierten orchestrieren von unterschiedlichen Services auf. Quarkus Flow bietet neben der Integration der Enterprise Technologien außerdem Fehlertoleranz und Metriken. Im Standard werden Retry und Circuit Breaker für den Aufruf über HTTP eingesetzt. Metriken können über Prometheus mithilfe von Micrometer eingesammelt werden.

Die Frage, die sich meist eingefleischte Entwickler an diesem Punkt stellen, ist: Warum sollte ich eine DSL nutzen um die Orchestrierung meiner Services zu erreichen? Ich kann es doch auch mit Java und vor allem Quarkus Dingen erreichen. Das ist sicherlich richtig, der Open Workflow Standard wurde geschaffen, um Hersteller- und Sprach-neutral die Ausführung von Workflows zu ermöglichen. Der Workflow kann visuell dargestellt und analysiert werden. Bei einer Realisierung über Java werden sicherlich Klassenstrukturen geschaffen, die für einen Java-Entwickler eindeutig sind, jedoch für nicht Entwickler nur schwer nachzuvollziehen.

Den Quellcode für das Beispiel findet ihr in unserem Quarkus Demo Projekt auf Github.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert