Beliebte Suchanfragen
|
//

Ein Microservice mit Kotlin und Ktor – ohne Spring

14.6.2022 | 4 Minuten Lesezeit

Ktor (s. https://ktor.io/ ) ist ein Framework für Kotlin, das sowohl Client- als auch Serverfunktionen bereitstellt und sich vorrangig der Kotlin DSL anstelle von Annotations bedient.

Vor einiger Zeit (2018 war doch erst gestern?…) hat sich Lovis dieses Themas in einem anderen Blogbeitrag angenommen. Darin stellte er Ktor an einem kurzen Beispiel für einen REST-Service vor. Und das ohne das Allheilmittel-Framework Spring! 🙂

Ich möchte diesen Blogpost als Grundlage aufgreifen und demonstrieren, wie ein solcher Microservice aussehen kann, den Lovis in seinem Ausblick beschrieben hat.

Was ist denn eigentlich ein Microservice?

Der Begriff Microservice ist hier nun bereits einige Male gefallen und sicherlich auf verschiedene Weisen zu verstehen und zu interpretieren. Dieser begrifflichen Diskussion möchte ich hier etwas aus dem Weg gehen, da diese den Beitrag sprengt. Ich erzähle lieber mehr darüber, was ich an dieser Stelle darunter verstehe:

In diesem Kontext ist (m)ein Microservice ein möglichst kompakter Server mit einigen von außen erreichbaren REST-Schnittstellen, der durch äußere Systeme konfiguriert oder überwacht werden kann. Ich denke da an Werkzeuge für Containerorchestrierung wie Docker oder Prometheus bzw. Micrometer, wenn es um den Gesundheitszustand geht. Die beschriebene Kompaktheit bezieht sich vorrangig auf den Fokus dieser einen Aufgabe: der Verwaltung von Benutzerinformationen.

Nicht mehr, aber auch nicht weniger. Dadurch lässt sich diese Komponente sehr gezielt automatisiert testen und die Softwarequalität auf einem hohen Niveau halten. Dieser Microservice bleibt gut wartbar.

Benutzerinformationen verwalten, Okay. Das heißt?

Unsere User Application enthält einen Controller und einen Service. Der Controller stellt klassisch verschiedene Endpunkte nach außen und ruft den Service für verschiedene Operationen auf, z. B. um Benutzer abzufragen oder zu speichern.

Implementierung

Seit dem oben genannten Blogpost sind nun mehr als drei Jahre gegangen. Was hat sich denn nun bei der Implementierung gegenüber dem FruitService von Lovis geändert? Die Antwort dürfte sehr angenehm sein: nicht viel! Sowohl die Routenkonfiguration als auch die Ktor-Features sind genau so erhalten geblieben:

1routing {
2    get("/") {
3        call.respondText("Hello World!")
4    }
5}

Diese Features werden in Form von Modulen eingebunden und können z. B. den Umgang mit Exceptions, die in unserem Code geworfen werden, behandeln. Darüber hinaus ist das Feature ‚Metrics‘ sehr interessant. Ich möchte meinen Microservice z. B. durch Prometheus überwachen lassen. Neben den gängigen Informationen, die die JVM bereitstellt, können wir so auch eigene Metriken pflegen und mitliefern:

1install(MicrometerMetrics) {
2    registry = appMicrometerRegistry
3}
4
5Metrics.addRegistry(appMicrometerRegistry)
6//...//
7Metrics.counter("add.user.successfull").increment()
8//...//
9
10get("/metrics") {
11call.respond(appMicrometerRegistry.scrape())
12}

Gängige micrometerfähige Scraper, wie z. B. Prometheus, sollten keine Verständnisprobleme damit haben:

# HELP ktor_http_server_requests_seconds_max
# TYPE ktor_http_server_requests_seconds_max gauge
ktor_http_server_requests_seconds_max{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",} 0.116123034
# HELP ktor_http_server_requests_seconds
# TYPE ktor_http_server_requests_seconds summary
ktor_http_server_requests_seconds{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",quantile="0.5",} 0.113246208
ktor_http_server_requests_seconds{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",quantile="0.9",} 0.113246208
ktor_http_server_requests_seconds{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",quantile="0.95",} 0.113246208
ktor_http_server_requests_seconds{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",quantile="0.99",} 0.113246208
ktor_http_server_requests_seconds_count{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",} 1.0
ktor_http_server_requests_seconds_sum{address="localhost:8082",method="GET",route="/hello/{username}",status="400",throwable="n/a",} 0.116123034
...

Apropos… Konfiguration

In vielen Anwendungsfällen bietet es sich an, den Betrieb unseres Service durch den einen oder anderen äußeren Mechanismus konfigurierbar zu gestalten. Allem voran dürfte die Portkonfiguration als Stellvertreter genannt werden, aber grundsätzlich sind wir natürlich frei, was das Thema Konfiguration angeht. Ktor stellt die Konfiguration auf nachvollziehbarem Weg in unserer Applikation zur Verfügung, sodass wir uns praktisch nur bedienen müssen:

1user {
2    port = 8083
3    basepath = /user
4}

Testing

Damit wir aber auch in Zukunft möglichst fehlerfreie Endpunkte mit unserem Controller nach außen stellen können, müssen wir uns um automatisierte Tests kümmern. Dabei bedienen wir uns withTestApplication und übergeben eine Referenz zu unserer Testumgebung. Schon sind wir in der Lage, unsere Endpunkte über Unit-Tests mit verschiedenen Requests, Payloads und was sonst noch gebraucht wird zu testen.

Praktisch ist außerdem, dass wir nicht nur etwaige Fehler schnell in unserer Anwendung finden, sondern auch, dass wir die Module für die Metriken und Fehlerbehandlung ebenso mit Tests abdecken können.

Im nachstehenden Beispiel testen wir, ob auch die Metriken um 1,0 erhöht werden, sobald ein erfolgreicher Request beantwortet wird:

1@Test
2fun `GET returns 200 and increases success metrics if authenticated`() {
3    runBlocking {
4        withApplication(testEnv) {
5            handleRequest(
6                    HttpMethod.Get,
7                    "/user/test"
8            ) {
9                addHeader(HttpHeaders.Authorization, "Bearer 1234567")
10            }.apply {
11                assertEquals(HttpStatusCode.OK, response.status())
12                assertEquals(response.content, "Hello user test, your id is test")
13                assert(Metrics.counter("user.successfull").count().equals(1.0))
14            }
15        }
16    }
17}

Gegenüber dem Positivbeispiel möchten wir auch sicherstellen, dass wir einen HTTP-401 zurück liefern, wenn kein Authentifizierungstoken enthalten ist:

1@Test
2fun `POST returns 401 if unauthorized`() {
3    withApplication(testEnv) {
4        handleRequest(
5                HttpMethod.Post,
6                "/user/test"
7        ).apply {
8            assertEquals(HttpStatusCode.Unauthorized, response.status())
9        }
10    }
11}

Fazit: Microservices mit Kotlin und Ktor

Kann man denn nun mit Kotlin und Ktor Microservices bauen? Oh ja.

Und zwar nicht nur schnell und intuitiv, sondern auch gleich qualitätsgesichert. Annotations und Namenskonventionen spielen erfreulicherweise keine unmittelbare Rolle, auch wenn sie sicherlich noch den einen oder anderen Mehrwert bieten können. Um Beans und Application-Kontexte müssen wir uns keine Gedanken machen.

Es reicht aus, dass wir uns mit dem beschäftigen, was wir aus Anforderungssicht brauchen und was absolut notwendig ist: einen überwachbaren, konfigurierbaren Restcontroller und seine Schnittstellen. Und ganz nebenbei ist auch das Entwickeln ziemlich angenehm, da wir bei einer Startzeit von rund einer Sekunde sehr schnelles Feedback erhalten können.

Gerade ist die Version 2.0 releast worden und hat sich diese Vorgehensweise auch weiterhin ganz oben auf die Prioritätenliste geschoben. Nach den ersten Bugfix-Versionen wird es  einen weiteren Blogpost geben, der die für dieses Beispiel notwendige Migrationsschritte und die entsprechenden Veränderungen untersucht.

|

Beitrag teilen

Gefällt mir

2

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.