Ab in die Cloud!

Ein Cloud-Native-Starter-Projekt: Java EE und MicroProfile treffen auf Kubernetes und Istio

Harald Uebele

© Shutterstock / Studio_G

Nicht jede neue Anwendung muss als Microservices-Architektur „cloud-native“ entwickelt werden, aber das Thema Cloud Native hat mittlerweile sicherlich den Hypestatus überwunden und sich als feste Größe in der Anwendungsentwicklung etabliert. Allerdings bedeutet der Umstieg auf Cloud Native, dass man viel Neues lernen und alte Gewohnheiten aufgeben muss. Das kann schon viel auf einmal sein, wie wir selbst erfahren haben…

Anfang des Jahres haben wir in unserem Team entschieden, dass wir uns mit dem Thema Java EE und Container beschäftigen wollen. Java EE, weil es immer noch die meistverwendete Sprache im Enterprise-Umfeld ist, und Container, da sie praktisch unumgänglich sind, wenn es um Cloud-Native-Anwendungen geht. Cloudtechnologie ist ja auch ein wichtiger Baustein für die Themen Digitalisierung und Anwendungsmodernisierung im genannten Enterprise-Umfeld. Unser Ziel war es, herauszufinden, ob man für Anforderungen an Ausfallsicherheit, Testing, Monitoring usw. besser die Funktionen einer Plattform wie Istio verwendet oder die Funktionen eines Frameworks wie MicroProfile. Dabei stellten wir fest, dass es im Internet dafür zwar viele Beispiele gibt, diese aber entweder nur einzelne Aspekte betrachten, auf proprietären Produkten basieren oder so komplex waren, dass sie auf einem typischen Notebook nicht installierbar wären. Deshalb entstand unser Cloud-Native-Starter-Projekt, das komplett auf Open Source basiert, selbst Open Source und mit Hilfe von Skripten für jeden einfach installierbar ist, und von uns auf GitHub veröffentlicht wurde. Wir wollten dabei alle (oder zumindest die meisten) Aspekte abdecken, die einen Anwendungsentwickler im Enterprise-Umfeld zum Thema Cloud Native interessieren könnten:

  • Java und REST APIs
  • Traffic Routing (Dark Launches, Canary Deployments)
  • Resiliency (Verhalten der Anwendung im Fehlerfall)
  • Authentication, Authorization
  • Verteiltes Logging und Monitoring
  • Health Checks
  • Konfiguration
  • Persistenz mit Java Persistence API

Zusätzlich wollten wir es auf möglichst vielen Umgebungen realisieren – im Moment haben wir Skripte für:

  • Minikube
  • IBM Cloud Kubernetes Service
  • Minishift
  • OpenShift auf der IBM Cloud

Im GitHub-Projekt sind momentan auch weit über 20 Blogartikel verlinkt, die wir zu einzelnen Themenbereichen geschrieben haben. Einige davon zitiere ich auch in diesem Artikel, und es ist zusätzlich ein kompletter Hands-on-Workshop zum Thema Cloud Native Applications enthalten.

DevOpsCon Istio Cheat Sheet

Free: BRAND NEW DevOps Istio Cheat Sheet

Ever felt like service mesh chaos is taking over? As a follow-up to our previous cheat sheet featuring the most important commands and functions, DevOpsCon speaker Michael Hofmann has put together the 8 best practices you should keep in mind when using Istio.

Grundlagen

Nachfolgend die Definition für Cloud Native auf der Seite der Cloud Native Computing Foundation (in meiner Übersetzung):

„Cloud-native Computing verwendet einen Open-Source-Softwarestack, um Anwendungen als Sammlung von Microservices zu deployen, jeden einzelnen Microservice in einen eigenen Container zu packen und diese Container dynamisch zu orchestrieren, um die Ressourcenauslastung zu optimieren. Die nativen Cloudtechnologien ermöglichen es Softwareentwicklern, tolle Produkte schneller zu erstellen.“

In anderen Worten: Statt als einen einzigen Monolithen entwickeln wir unsere Cloud-Native-Anwendung aus einzelnen Microservices. Diese werden als Container (Docker) Images in Containern deployt. Um das Orchestrieren der Container kümmert sich Kubernetes, da es mittlerweile der De-facto-Standard für Containerorchestrierung und außerdem Open Source ist.

Basis für die Microservices sind besagte Docker Images, die mit Hilfe von Dockerfiles gebaut werden. Alle Java-basierten Microservices von Cloud Native Starter verwenden diesen Open-Source-Stack:

Im Dockerfile sieht das dann einfach so aus:

FROM open-liberty:microProfile2-java11

Hier stellt sich vielleicht die Frage: Java und Microservices? Ernsthaft? Ja, der Anspruch von MicroProfile ist es, Enterprise Java für eine Microservices-Architektur zu optimieren, und das funktioniert auch.

Die Cloud-Native-Starter-Anwendung

Unsere Beispielanwendung ist eine einfache Microservices-Architektur. Sie besteht aus einem Frontend, das im Browser aufgerufen wird und in Node.js mit dem Vue.js Framework geschrieben ist. Sie zeigt eine Liste unserer Blogeinträge an, für jeden Eintrag den Titel, den Autor und als Details das Twitter Handle und den Link zum Blog (Abb. 1).

Abb. 1: Cloud Native Starter: Web-App als Frontend

Abb. 1: Cloud Native Starter: Web-App als Frontend

Die komplette Anwendung besteht neben diesem Frontend aus drei Microservices (Abb. 2):

  1. Web-API: Backend-for-Frontend-Pattern, REST API für die Artikelliste
  2. Articles: REST API für Titel und Autor
  3. Authors: REST API für Twitter-Handle und Link zum Blog für einen Autor
Abb. 2: Cloud-Native-Starter-Architektur

Abb. 2: Cloud-Native-Starter-Architektur

Damit die Anwendung richtig funktioniert, müssen diese drei Microservices deployt sein und natürlich auch laufen, dafür ist Kubernetes zuständig. Kubernetes sorgt außerdem durch Namensauflösung (Name Resolution) dafür, dass der Web-API-Service den Articles und Authors Service ansprechen kann, ohne Details wie IP-Adresse und dergleichen von ihnen zu kennen. MicroProfile spielt in diesem Zusammenhang mehrere Rollen:

  • Das REST API wird über JAX-RS bereitgestellt
  • MicroProfile Open API erlaubt die Dokumentation des REST API direkt im Code (Swagger)
  • Web-API benutzt den MicroProfile REST Client für den Zugriff auf das Articles und Authors API

Plattform

Zu Kubernetes als Werkzeug zur Containerorchestrierung muss man sicherlich nicht mehr viel sagen. Es ist inzwischen bei jedem großen Cloudanbieter verfügbar und wird auch oft für Cloudanwendungen im eigenen Rechenzentrum eingesetzt, z. B. als Red Hat OpenShift.

Ein anderer wichtiger Bestandteil ist Istio als Service Mesh, es ist wie Kubernetes Open Source. Istio bekommt viel Unterstützung, es sind mittlerweile viele und auch große Firmen beteiligt, neben den Projektgründern Google, IBM und Lyft z. B. Pivotal und Red Hat. Es kann nicht nur Kubernetes als Plattform nutzen, sondern auch Nomad und Mesos. Der Begriff Service Mesh beschreibt ein Netzwerk aus Microservices, die zusammen eine Applikation darstellen, und ihre Interaktionen. Es ist eine Art transparente Schicht, die es erlaubt, die Microservices miteinander zu verbinden, diese Verbindungen abzusichern und die Services zu überwachen (Abb. 3).

Abb. 3: Istio-Architektur

Abb. 3: Istio-Architektur

Dafür installiert Istio in den Pod eines Microservice ein Proxy namens Envoy, das manchmal auch als Sidecar, also Beiwagen, bezeichnet wird. Wie ein Beiwagen neben dem Motorrad, so sitzt der Envoy neben dem Microservices-Container. Die Verbindung und damit der Datenfluss zwischen den Services läuft nur über den Envoy, die Services der Istio-ControlPlane kommunizieren mit den Envoys und konfigurieren das Routing, installieren Zertifikate, sammeln Telemetriedaten usw. Der Vorteil des Sidecars liegt darin, dass kein Code und keine zusätzlichen Bibliotheken in die eigentlichen Microservices eingepflegt werden müssen, das macht die ganze Sache verlinkt verlinkt sprachunabhängig, d. h., es funktioniert in Java genau wie in jeder anderen Programmiersprache.

Verwende ich besser Plattform oder Framework, Istio oder MicroProfile?

Kubernetes und Istio sind also die Plattform, MicroProfile das Framework für unsere Java-basierten Services. Istio und MicroProfile haben zum Teil überlappende Funktionen, beide bieten z. B. Funktionen für Fehlertoleranz, Metriken und Tracing. Welche Version soll man einsetzen? Es folgen zwei Beispiele:

Beispiel 1: Nehmen wir das Thema Fehlertoleranz und besonders Fallback: Nur ich als Entwickler der Businesslogik weiß, wie ich mit einem Verbindungsfehler umgehen kann, ohne dass meine Anwendung abstürzt oder der Benutzer mit unverständlichen Fehlermeldungen irritiert wird, d. h. für Fallback sollte ich auf jeden Fall die Fallback-Methode von MicroProfile einsetzen.

Abbildung 4 zeigt einen Codeausschnitt aus dem Web-API von Cloud Native Starter. An der markierten Stelle wird versucht, fünf Artikel vom Articles Service zu bekommen. Wenn das nicht funktioniert, weil der Service nicht verfügbar ist, wird die Methode aufgerufen, die in der @Fallback-Annotation angeben ist. lastReadArticles liefert die zuletzt gelesenen Artikel, die gecacht wurden. Damit bekommt der Anwender keinen Fehler 500, sondern eine Liste von Artikeln, die vielleicht nicht ganz aktuell ist.

Abb. 4: MicroProfile Fallback

Abb. 4: MicroProfile Fallback

Beispiel 2: Eine der Stärken einer Microservices-Architektur ist die Möglichkeit, einen Service unabhängig von den anderen zu verändern (natürlich nur, solange ich das REST API nicht verändere), z. B. um einen Fehler zu beheben oder eine neue Funktion einzubauen. Bei den großen Internetplattformen testet man neue Funktionen oder Fixe gerne live, indem man sie ohne Ankündigung einer kleinen Testgruppe „unterschiebt“, also einen Dark Launch oder Canary Test macht. Das ist eine meiner Lieblingsdemos für Istio und eine der ganz starken Istio- und damit Plattformfunktionen, sie heißt Traffic Routing.

Wenn ich eine neue Version eines Microservice in Kubernetes ausrolle (kubectl apply …), dann passiert ein Rolling Update, d. h. Kubernetes startet die neue Version und beendet die alte. Wenn danach etwas schiefgeht, mache ich ein Rollback, d. h. die neue Version wird beendet und die alte wieder gestartet. Das ist ein eher grober Test. Natürlich gibt es Möglichkeiten, Canary Testing oder Dark Launches in Kubernetes auszuführen, aber in Istio ist es ganz einfach.

Ich deploye beide Versionen parallel, mit eigenen Namen, und versehe sie aber mit Labels, z. B. app: web-api, version: v1 für Version 1 (Listing 1).

kind: Deployment
...
metadata:
  name: web-api-v1
spec:
  template:
    metadata:
      labels:
        app: web-api
        version: v1
...

Und Version 2 findet sich in Listing 2.

kind: Deployment
...
metadata:
  name: web-api-v2
spec:
  template:
    metadata:
      labels:
        app: web-api
        version: v2
...

Die Kubernetes-Service-Beschreibung sieht wie in Listing 3 aus, mit ihr werden beide Versionen des Web-API (v1 und v2) gleichermaßen angesprochen (50/50, selector: app: web-api).

kind: Service
...
metadata:
  name: web-api
  labels:
    app: web-api
spec:
  selector:
    app: web-api
...

Das ist alles ganz normales Kubernetes-Geschäft. Mit Istio kommen weitere Objekte hinzu, zuerst eine DestinationRule, über die man fürs Traffic Routing sogenannte Subsets definiert (Listing 4).

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: web-api
spec:
  host: web-api
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

Und als weiteres Objekt ein VirtualService, in dem die eigentlichen „Verkehrsregeln“ stehen, diese sind in unserem Fall etwas komplexer, da wir das Traffic Routing für einen Microservice verwenden, der direkt mit dem Frontend kommuniziert. Das heißt, wir verwenden ein Istio Ingress Gateway (default-gateway-ingress-http), um von der Web-App im Browser auf den Web-API-Service zuzugreifen. Dieses Istio Ingress Gateway enthält selbst wieder einen Envoy-Proxy, den wir für das Traffic Routing konfigurieren können (Listing 5).

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: virtualservice-ingress-web-api-web-app
spec:
  hosts:
  - "*"
  gateways:
  - default-gateway-ingress-http
  http:
  - match:
    - uri:
        prefix: /web-api/v1/getmultiple
    route:
      - destination:
          host: web-api
          subset: v1
        weight: 80
      - destination:
          host: web-api
          subset: v2
        weight: 20

Der VirtualService sorgt jetzt dafür, dass jeder Zugriff (hosts: „*“) auf den Istio Ingress über den URI /web-api/v1/getmultiple zu 80 Prozent (weight: 80) über die Version 1 des Web-API geleitet wird, zu 20 Prozent über die Version 2. Man kann das auch schön in Kiali, dem Istio Dashboard, sehen (Abb. 5).

Abb. 5: Traffic Routing sichtbar gemacht in Kiali

Abb. 5: Traffic Routing sichtbar gemacht in Kiali

Was gibt es noch?

Istio und MicroProfile haben noch wesentlich mehr Funktionen, die ich hier nur noch anreißen möchte, wir haben alles in Blogartikeln dokumentiert.

Authorization und Authentication sorgen dafür, dass nur bestimmte und bekannte Anwender meine Anwendung oder sensitive Teile meiner Anwendung verwenden dürfen. Beides wird von Istio und von MicroProfile über Standards wie OpenID und JWT (JSON Web Token) in unterschiedlichem Umfang abgedeckt.

In unserem Beispiel verwenden wir den Cloud Service IBM App ID, dieser ist entweder selbst Identity Provider oder aber Proxy für Social Identity Provider wie Google und Facebook oder auch Enterprise Provider im eigenen Rechenzentrum wie Active Directory über SAML. Diese Identity Provider haben eigene Log-in-Seiten, d. h., als Entwickler muss ich mich darum nicht kümmern, und bei Enterprise Providern funktioniert dann womöglich auch unternehmensweites Single Sign-on. Zu diesem Themenbereich hat mein Kollege Niklas Heidloff Grundlagenforschung betrieben und in zwei Artikeln dokumentiert (Authorization in Cloud-Native Apps in Istio via OpenID, Authorization in Microservices with MicroProfile).

Ein weiteres wichtiges Thema für Microservices-Architekturen ist Observability. Eine Microservices-Architektur ist eine sehr lebendige und flüchtige Umgebung: Objekte (wie z. B. Pods) kommen und gehen; das kommt von der Skalierung, weil Fehler auftreten, oder als Nebeneffekt der Lastverteilung. Im Gegensatz zu einer monolithischen Anwendung haben wir keine einzelne Logdatei, sondern ganz viele verschiedene Stellen, an denen Logs weggeschrieben werden. Wir müssen uns also mit neuen Werkzeugen beschäftigen:

  • Distributed Tracing: Ein Aufruf des Web-API in unserem Beispiel resultiert in nachgeordneten Aufrufen des Articles Services und des Authors Service. Mit Werkzeugen wie Jaeger kann man diese „Trace Spans“ gemeinsam betrachten. Sowohl Istio als auch MicroProfile unterstützen Jaeger.
  • Monitoring und Metriken: Istio enthält und MicroProfile unterstützt Prometheus als Monitoring- und Metrics-Werkzeug. Anstatt sich selbst um den Betrieb eines Monitoring-Service zu kümmern, kann man aber auch externe Werkzeuge wie Sysdig benutzen, die beispielsweise auf der IBM Cloud als Service angeboten werden.
  • Central Logging: In unserem Beispiel haben wir drei Microservices, die an drei verschiedenen Stellen ihre Logs schreiben. Im Fehlerfall möchte ich nicht an drei Stellen suchen müssen. Und das ist ja noch ein ganz simples Beispiel. In Microservices-Architekturen sind Distributed- oder Central-Logging-Systeme ganz essenziell. Ich kann in meinem Kubernetes Cluster z. B. Fluentd installieren und so ein System selbst betreiben – oder, wie eben für Monitoring erwähnt, einen Logging-Service meines Cloudanbieters benutzen, z. B. LogDNA auf der IBM Cloud.
  • Health Check: MicroProfile stellt über eine einfache Annotation ein Health Check Interface zur Verfügung. Damit kann Kubernetes ganz einfach feststellen, wann mein Service bereit ist, Requests zu verarbeiten. Das kann beim Start einer Java-Anwendung ja durchaus einen Moment länger dauern.

Fazit

Wir haben mit unserem Projekt Cloud Native Starter ein einfaches Beispiel für eine moderne Cloud-Native-Anwendung entwickelt, anhand dessen man die wesentlichen Aspekte dieser Art Anwendung lernen kann. Uns kam es darauf an, dass jeder dieses Beispiel benutzen kann, daher haben wir es auf Open-Source-Technologien aufgebaut und so schlank gehalten, dass es auf einem einigermaßen aktuellen Notebook lokal in Minikube betrieben werden kann. Nicht zuletzt haben wir das Projekt selbst zu Open Source erklärt. Um den Start in diese Welt so einfach wie möglich zu gestalten, haben wir jede Menge Skripte geschrieben, die so viel wie möglich automatisieren. Wir haben mittlerweile doppelt so viel Shell Code wie Java in GitHub. Aber damit kann man Cloud Native Starter in etwa einer Stunde komplett selbst aufsetzen. Es gibt also keinen Grund, es nicht selbst einmal auszuprobieren! Ich habe bei diesem Projekt auf jeden Fall viel gelernt.

Geschrieben von
Harald Uebele
Harald Uebele
Harald Uebele ist Developer Advocate bei IBM mit Standort Stuttgart. Er beschäftigt sich seit sechs Jahren mit Cloudtechnologien und ist ein großer Fan von Open Source und Linux.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: