Docker Orchestrierung mit Kubernetes

Docker bietet uns eine leichtgewichtige, containerbasierte Form der Virtualisierung, welche sich gut dafür eignet, Anwendungen zu paketieren und auszuliefern. Dabei wird das Verwalten der Images mit Hilfe von Dockerfiles und Registries schon gut von Docker direkt unterstützt und gestaltet sich sehr einfach. Etwas mehr Aufwand ist bei der Verwaltung der Container selber zu Laufzeit notwendig. Zwar bietet Docker hier natürlich entsprechende Möglichkeiten für das mitgeben von Parametern beim Containerstart, was so direkt aber nicht die Wiederverwendbarkeit der Laufzeitkonfiguration unterstützt und sehr viel Eigeninitiative bei Themen wie Networking etc. erfordert. Docker-Compose kann uns hier bereits einiges an Arbeit abnehmen, wobei auch hier noch mehr Unterstützung für die alltäglichen Aufgaben bei Entwicklung und Betrieb der Anwendungen wünschenswert wäre.

Es gibt eine Reihe von Plattformen die uns bei der sogenannten Orchestrierung von Dockercontainern unterstützen. Diese Technologien ermöglichen uns unter anderem die Container in einer geclusterten Umgebung auszuführen, wobei die Anzahl Instanzen der Container ganz einfach zur Laufzeit beliebig erhöht werden kann. Die Plattform übernimmt dabei das Hochfahren und Verteilen der Container in dem Cluster. Darüber hinaus werden viele weitere Features geboten, wie z.B. wiederverwendbare Konfiguration, Loadbalancing, Rolling Updates, Mechanismen für Austausch von Passwörten/Zertifikaten und Servicediscovery. Im folgendem wird nun Kubernetes als eine Plattform zur Docker Orchestrierung vorgestellt unter dem Hauptgesichtspunkt, wie uns diese Vorgehensweise bei dem Entwickeln und Betreiben von eigenen Anwendungen unterstützen kann.

Kubernetes

Kubernetes ist eine ursprünglich von Google entwickelte opensource Plattform für das Clustermanagement und Betreiben dockerbasierter Applikationen. Es zeichnet sich durch seine deklarativ gehaltene Art der Konfiguration aus und erfreut sich mittlerweile auch schon einer recht großen Verbreitung. Als grundlegende Technologie wird Kubernetes von einigen Cloud-Anbietern genutzt, wie z.B. Openshift und Google Containerengine.

Kubernetes ist im Grunde gemäß dem Master-Slave-Prinzip aufgebaut. Ein Cluster setzt sich also zusammen aus einem Master und einer Reihe von Nodes. Der Master bietet die API Schnittstelle an und ist für das Verwalten der Nodes und das Hochfahren und Aufteilen der Container auf diese zuständig (scheduling). Die Nodes benötigen jeweils einen Docker-Runtime und sind für das tatsächliche Starten der Dockercontainer zuständig. Auf diese Weise ist ein elastisches Clustering möglich, dass es ermöglicht, bei Bedarf neue Nodes hinzuzufügen, welche dann vom Master genutzt werden können um Containerinstanzen zuzuweisen.

API

Die Schnittstellen für die Kommunikation mit dem Kubernetes Cluster werden per REST bereitgestellt. Das Kommandozeilenprogramm kubectl kann hierfür als komfortabler Client verwendet werden. Es gibt in Kubernetes verschiedene Arten von Objekten, die angelegt und Konfiguriert werden können. Alle diese Objekte können in Form von YAML Files definiert und über die Schnittstellen eingespielt werden. Dadurch, dass alle Konfigurationsobjekte durch deskriptive YML Strukturen beschrieben werden können, ist es einfach diese Konfigurationen wiederzuverwenden und in einem Versionsverwaltungssystem wie Git zu unterzubringen.

Pods

Ein Pod ist das grundlegendste Objekt in Kubernetes und kapselt einen (oder mehrere) Docker Container. Als Einheit für das Scheduling innerhalb von Kubernetes werden enstprechend Pods verwendet und nicht Dockercontainer direkt. Man kann sich einen Pod als Wrapper um einen Container vorstellen. Zwar ist es möglich mehrere Container in einem Pod unterzubringen, allerdings wird nach der am meist verbreiteten Vorgehensweise immer ein Pod für einen Container genutzt. Es handelt sich bei einem Pod um eine nicht trennbare Einheit, kann also immer nur komplett auf einem Clusterknoten laufen. An Konfiguration enthält ein Pod einen Verweis auf das damit Verknüpfte Dockerimage und als weitere Parameter Hauptsächlich Angaben zu Volumes, Umgebungsvariablen und Ports. Also im Großen und Ganzen das, was ansonsten als Information beim Docker run Befehl mitgegeben wird.

Beispiel für einen Pod:

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: demoapp
spec:
  containers:
  - image~: demoapp:1
    name: demoapp
    ports:
      - containerPort: 3306
        protocol: TCP

Dieses Konfigurationsobjekt würden wir jetzt im Falle einer eigenen Anwendung einfach im Git-Repository des Projektes mit einchecken. Über einen Befehl kann es eingespielt werden:

kubectl create –f mypod.yml

Metadata: Labels + Selectors und Annotations

Den Objekten wie Pods können in Kubernetes Metadaten angehängt werden, sogenannte Labels und Annotations. Dabei Handelt es sich um Key-Value-Paare. Objekte können mit Hilfe dieser Labels durch Selector-Queries gefunden werden. Dies ist wichtig, da im Cluster ja viele verschiedene oder auch mehrere Instanzen einer Podkonfiguration laufen können. Über diese Selektor-Queries ist es möglich bestimmte Elemente im Cluster nach bestimmten Kriterien zu finden. Zum Beispiel könnte dies für Discovery genutzt werden, ein Pod könnte ein bestimmtes Label haben um zu kennzeichnen, dass metrische Daten per REST bereitgestellt werden. Der Value von einer Annotation könnte dann die URL sein. Ein Werkzeug für das Sammeln der Daten könnte sich so jetzt alle Anwendungen im Cluster heraus suchen, die metrische Daten anbieten.

Services

Wenn nun mehrere Anwendungen miteinander kommunizieren sollen, z.B. unsere Anwendung mit einer MySQL Datenbank, so brauchen wir dafür eine Schnittstelle, über welche diese Applikation zentral angesprochen werden kann. Direkt mit dem Pod Verbinden geht nicht, da wir in einer geclusterten Umgebung sind und nicht wissen, wo der Pod läuft und außerdem soll es ja später mal möglich sein, mehrere Instanzen eines Pods zu betreiben. Diese zentralen Schnittstellen werden über sogenannte Services abgebildet. Ein Service besteht aus einem Namen und ein oder mehreren Ports unter welchem der Zugriff auf die Anwendung dahinter Clusterweit möglich ist. Der Service-Endpoint ist dabei auch in der Lage ein Loadbalancing durchzuführen, es könnten also mehrere Instanzen der Anwendung dahinter laufen. Die Pods die mit diesem Service verknüpft sind werden mit Hilfe einer Selector-Query festgelegt, benötigen also entsprechende Metadaten in der Podkonfiguration.

apiVersion: v1
  kind: Service
  metadata:
    name: demoapp
  spec:
    selector:
      name: demoapp
    ports:
    - port: 8080
      protocol: TCP

Der Service wird auf einer IP im gesamten Cluster bereitgestellt, die mit dem Namen des Service aufgelöst werden kann. Die Adresse des Service kann in anderen Pods aus Umgebungsvariablen, die automatisch von Kubernetes gesetzt werden, ausgelesen werden:

DEMOAPP_SERVICE_HOST=10.0.0.11

DEMOAPP_SERVICE_PORT=8080

Services sind zunächst nur innerhalb des Cluster erreichbar, können aber auch nach außen hin sichtbar gemacht werden. Dies wird über den Service Typ festgelegt. Auf einer Cloud-Plattform wie Openshift besteht hier die einfache Möglichkeit über Type:LoadBalancer eine IP über einen externen LoadBalancer der Plattform zu bekommen. Services können auch genutzt werden, um Schnittstellen zu externe Anwendungen für Applikationen innerhalb des Clusters anzubieten.

Dies bedeutet, wenn wir eine eigene Anwendung betreiben, welche auf eine Mysql Datenbank angewiesen ist, müssten wir zwei Services bereitstellen. Die Datenbank mit einem normalen Service Clusterintern, die eigentliche Anwendung mit einem Service der eine Adresse extern bereitstellt. Die Anwendung kann jetzt über die Umgebungsvariablen an die Adresse für die Datenbank gelangen.

ReplicationControllers / ReplicaSets

ReplicationControllers erlauben es auf einfach Art und Weise mehrere Instanzen einer Podkonfiguration zu betreiben. Der ReplicationController stellt dabei sicher, dass immer eine gewünschte Anzahl der Pods im Cluster läuft. Erzeugt werden die Pods vom Controller dabei aus einem Pod-Template. Es kann eine gewünschte Anzahl Replicas direkt in der Konfiguration angegeben werden, diese kann dann später aber auch zur Laufzeit geändert werden.

apiVersion: v1
  kind: ReplicationController
  metadata:
    name: mysql-1
  spec:
    replicas: 1
    selector:
      name: mysql
    template:
      apiVersion: v1
      kind: Pod
      metadata:
        labels:
          name: mysql
      spec:
        containers:
        - image~: mysql
          name: mysql
          ports:
            - containerPort: 3306
              protocol: TCP

Wenn es nun einen Service gibt, der per Selector die Pods einsammelt, so sind alle Replicas die der Controller hochfährt mit diesem Service verknüpft.

ReplicaSets sind eine neuere Variante der ReplicationController in Kubernetes, die im Großen und Ganzen die gleichen Möglichkeiten bieten, aber zusätzlich neuere Selector-Features nutzen können.

Deployments

Die Funktionalität eines ReplicationControllers/ReplicaSets ist für das einfache Betreiben und Ausrollen einer Anwendung noch ein wenig zu low-level. Für diese Aufgabe gibt es daher spezielle Objekte, die sogenannten Deployments. Von der grundlegenden Konfiguration her sieht das Objekt so aus wie ein ReplicationController, aber ermöglicht uns darüber hinaus weitere Funktionalität.

Wenn wir eigene Anwendungen betreiben brauchen wir eine Möglichkeit, einfach eine neue Version auszurollen. Dies bedeutet im Dockerumfeld in der Regel, dass wir eine neue Version des Images erstellt haben. Nun müssen die alten Container gestoppt und die neuen gestartet werden. Ein Deployment ermöglicht genau dies über einen einfachen Befehl:

kubectl set image deployment/demoapp-deployment demoapp=demoapp:1.1

Nicht nur das Umstellen der Imageversion, auch andere Änderungen an der Deploymentconfig (außer Replica Scaling) bewirken einen Rollout. Dabei wird eine Änderungshistorie geführt, sodass es immer möglich ist, im Problemfall auf eine frühere Version des Deployments zurückzurollen.

Für das Ausrollen werden mehrere Verfahren angeboten, hier ist vor allem die Strategie RollingUpdate interessant, die uns ermöglicht ein Update ohne Ausfallzeit durchzuführen. Dabei würden zunächst die Pods mit dem neuen Image gestartet und erst anschließend wenn diese erfolgreich hochgefahren wurden, werden die alten Pods angehalten. Dabei bleibt immer die gewünschte Mindestanzahl Replicas verfügbar.

Ob ein Pod erfolgreich gestartet wurde wird zunächst davon abhängig gemacht, ob der Container ohne Fehler gestartet wurde und noch läuft. Interessant ist hier aber natürlich auch, ob die Anwendung schon läuft, denn erst dann sollen ja die alten Instanzen gestoppt werden. Dies ist über Angabe einer entsprechenden livenessProbe möglich. Hierfür kann entweder ein Command oder eine URL angegeben werden, mit welchem die Verfügbarkeit der Anwendung getestet werden kann.

Für unsere Anwendung bedeutet das nun, dass wir für diese ein Deployment definieren und immer wenn wir eine neue Version gebaut haben und das Dockerimage in die Registry gepushed ist, ein Rollout mit dem neuen Imagetag auf dem Deployment antriggern.

Persistent Volumes

Daten werden wie in Docker gewohnt in Volumes abgelegt, welche in den Pod Konfigurationen den Containern zugeordnet werden können. Genutzt werden können hier viele verschiedene Arten von Volumes, angefangen von einem Pfad auf dem Node, einem Netzwerklaufwerk über verschiedene Cloudspeicherlösungen bis hin speziellen Lösungen wie Git-Repositories.

Zusätzlich bietet Kubernetes ein eigenes Konzept bestehend aus speziellen Objekten, den PersistenVolumes und PersistenVolumeClaims. PersistentVolumes sind dabei Objekte die vom Administrator eingetragen werden und einen Zugriff auf einen Speicher (auch hier verschiedene Typen möglich) mit einer bestimmten Größe definieren.

apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: mysqlvolume
  spec:
    capacity:
      storage: 2Gib
    accessModes:
      - ReadWriteOnce
    hostPath:
      path: /data/volumes/mysql
    persistentVolumeReclaimPolicy: Retain

Auf der anderen Seite legen die Nutzer dieses Speichers ein PersistentVolumeClaim Objekt an, welches dann über Selektoren mit einem PersistentVolume Verknüpft wird. Diese Claims können dann wie normale Volumes in den Pods eingebunden werden.

 apiVersion: v1
  kind: PersistentVolumeClaim
  metadata:
    name: mysql
  spec:
    accessModes:
      - ReadWriteOnce
    resources:
      requests:
        storage: 2Gib
    volumeName: mysqlvolume
kind: Pod
metadata:
  labels:
    name: mysql
spec:
  containers:
  - image~: mysql
    name: mysql
    ports:
      - containerPort: 3306
        protocol: TCP
    volumeMounts:
      - mountPath: /var/lib/mysql
        name: storage
  restartPolicy: Always
  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: mysqlvolume

Die Idee hier ist also, dass das tatsächliche bereitstellen des Speichers eine Aufgabe der Administration ist und die Anwender sich über die Claims diese bereitgestellten Volumes anfordern, ohne über die technischen Hintergründe des dahinterliegenden Speichermediums informiert sein zu müssen.

Secrets

Falls mehrere Anwendungen miteinander kommunizieren sollen, müssen in der Regel Passwörter, Tokens und Zertifikate ausgetauscht werden. Kubernetes bietet hierfür die sogenannten Secrets als spezielle Objekte an. In einem Secret werden die Daten in Key-Value Form gespeichert. Diese Secrets können nun entweder als Volume in die Pods eingebunden werden, oder Werte aus ihnen können dem Pods als Umgebungsvariable übergeben werden. Kubernetes selbst hängt in alle Pods Secrets für den Zugriff auf die Cluster API als Volume ein, sodass aus den Containern heraus Anfragen an die Kubernetes API möglich sind.

Definieren eines Secrets:

apiVersion: v1
kind: Secret
metadata:
  name: mysqlsecret
type: Opaque
data:
  password: ZGVtb3Bhc3N3b3Jk
  username: ZGVtb3VzZXI=

Einbinden als Umgebungsvariable:

containers:
          - name: demoapp
            image~: "demoapp:1"
            ports:
              - containerPort: 8080
                protocol: "TCP"
            env:
              - name: DB_SCHEMA
                valueFrom:
                  secretKeyRef:
                    name: mysqlsecret
                    key: database

Entsprechend könnten Secrets eingesetzt werden um z.B. die Zugriffsdaten für die Datenbank bereitzustellen. So müssen sie nicht in der Anwendung hart kodiert sein, sie müssen noch nicht einmal bekannt sein. Die Anwendung bindet sich einfach das Secret ein und holt sich die Werte daraus für die Anmeldung an der Datenbank. Verschiedene Passwörter für Produktions- und Testdatenbank sind somit einfach zu handhaben.

ConfigMaps

Ähnlich wie die Secrets aber etwas allgemeiner, für nicht sensible Daten gehalten, sind die Configmaps. Diese bieten die Möglichkeit zentrale Konfiguration anzubieten, welche dann von den Pods konsumiert werden kann. Dadurch, dass die Konfiguration so in die Plattform ausgelagert wird und nicht direkt in den Container-Images verdrahtet ist, sind die Anwendungsimages besser portabel/wiederverwendbar.

Namespaces

Kubernetes bietet uns zu Zwecken der Isolation an, sogenannte Namespaces zu definieren. Die können dafür genutzt werden, um mehrere voneinander getrennte Umgebungen innerhalb des Clusters zu definieren. Objekte werden immer in einem Namespace angelegt wo sie zunächst Zugang zu den anderen Objekten dieses Namespaces haben (Zugriffe auf andere Namespaces sind auch möglich). Wenn eine Anwendung einen Service über seinen Namen anspricht, so wird der Service aus demselben Namespace angezogen. Man kann sich das auch bei sonstigen Selector-Queries oder Zuordnungen zunutze machen, z.B. PersistentVolumes, Secrets, Configmaps usw.

Wofür die Namespaces sich daher gut einsetzen lassen ist also das Bereitstellen mehrerer Umgebungen wie z.B. test, qualitätsicherung und produktion. Wenn die zu dem Projekt gehörenden Objekte alle entsprechend portabel geschrieben sind, kann also eine neue Umgebung aufgesetzt werden in dem ein neuer Namespace erstellt wird und alle Objekte in diesem angelegt werden. Wodurch auch das duplizieren sehr komplexer, aus mehreren Anwendungen bestehenden, Umgebungen mit einigen Befehlen umzusetzen ist.

Templates

Wenn es darum geht, eine Umgebung, die aus vielen verschiedenen Objekten besteht, in einem Schritt einzuspielen, können Templates von großer Hilfe sein. Ein Template wird definiert in einem YAML File, welches beliebig viele Kubernetes Objekte enthalten darf. Es ist möglich Parameter für das Template zu definieren und diese Parameter in der Konfiguration der Objekte zu verwenden. Das Template kann nun entweder direkt aus dem File heraus angewendet, oder im Cluster für die einfache Wiederverwendung eingespielt werden. Damit ist es dann möglich über anwenden des Template alle darin enthaltenen Objekte auf einen Schlag in einem Namespace einzuspielen (Unter eventueller Angabe der benötigten Parameter).

Weiteres

Es gibt darüber hinaus noch viele weitere Features in Kubernetes. Zugriff auf Ressourcen, Namespaces, oder Aktionen im Cluster kann beschränkt werden. Anwendungen können sich über ServiceAccounts am API Server anmelden so wir normale Benutzer und haben dann Aktionen gemäß der ihnen zugeteilten Rollen zur Verfügung.

Automatisches horizontales anpassen der Replicas anhand von metrischen Daten, wie CPU Nutzung, ist möglich

Cron Jobs können definiert werden, die um eine bestimmte Zeit einen Prozess anstoßen, der die für die Durchführung notwendigen Pods hochfährt, welche nach abarbeiten der Aufgabe wieder gestoppt werden.

Fazit

Kubernetes bietet uns viele Funktionen, die für den Betrieb containerbasierter Anwendungen hilfreich sind. Alle Objekte können zusammen in einem Template mit der Anwendung als YAML Datei mit in das Repository eingecheckt werden. Mit dem Template können mehrere gleichartige Umgebungen aufgesetzt werden. Neue Images können einfach ausgerollt werden mit Hilfe der Deployments. Viele Instanzen der Anwendung können parallel gestartet werden und ein elastisches Clustering wird ermöglicht. Es gibt außerdem Mechanismen für Service-Discovery und das Verwalten von zentraler Konfiguration und Secrets.

Für das einfache Ausprobieren von Kubernetes auf einem lokalen Rechner empfiehlt sich Minikube, welches einen 1Knoten-Cluster(Master+Node) in einer virtuellen Maschine wie VirtualBox startet.

Als Alternative zur direkten Nutzung von Kubernetes kann auch Openshift empfohlen werden. Dies ist eine opensource Cloudplattform welche auf Kubernetes aufbaut und dieses um einige Features erweitert. Zum Beispiel ist es hier einfach Möglich Services über Routen nach außen hin sichtbar zu machen, dies wird von dem enthaltenen externen Loadbalancer übernommen.

Zusammenfassung Vorgehensweise für Anwendungsentwicklung

Als Beispiel gehen wir von eine einfachen Anwendung bestehend aus einer Java-Webanwendung und einer Mysql Datenbank aus.

Wir benötigen ein Docker-Image für die Datenbank und eines welches als Basis für unsere Anwendung dient (eventuell mit ApplicationServer).
Desweiteren benötigen wir nun :

Einen ReplicationController für die Datenbank und einen Service der die Datenbank innerhalb des Clusters zur Verfügung stellt. Für die eigene Anwendung nutzen wir ein Deployment mit der RollingUpdate-Strategie. Diese Anwendung wird über einen Service nach draußen zur Verfügung gestellt. Auf die Datenbank greift die Anwendung über die entsprechenden Umgebungsvariablen für den Service zu. Zugangsdaten für die Datenbank werden als Secret bereitgestellt und in den Pod der Anwendung eingebunden. Als Volume wird ein PersistentVolumeClaim in dem Pod für die Datenbank eingebunden. Alle diese Objekte werden zusammen in ein Template verpackt und mit der Anwendung eingecheckt. Nun können mit Hilfe von Namespaces verschiedene Stages für Test und Produktion erzeugt werden. In diesen müssen jeweils die PersistentVolumes angelget werden, sowie eventuell die Secrets für die Datenbank (Sofern die unterschiedlich sein müssen.). Anschließend kann das Template genutzt werden um die Umgebung in den Namespace einzuspielen.

Für das Bereitstellen einer neuen Version in einer der Umgebung muss nun das Dockerimage der Anwendung gebaut und getaggt (und in eine Registry gepushed) werden. Danach wird über einen Aufruf der Kubernetes API ein erneutes Ausrollen des Deployments mit dem neuen Imagetag angestoßen.

WebSocket mit Java EE und JavaScript

Die Spezifikation (rfc6455) zu Websockets hat schon einige Jahre auf den Buckel, ist damit aber in allen bekannten Browsern verfügbar. Die Javascript API ist übersichtlich und mit nur wenigen Schritten kann eine bidirektionale Verbindung zwischen Browser und Server hergestellt werden, um Benachrichtigungen vom Server zu erhalten ohne ein alt modisches Polling zu implementieren. Auf Java-Seite steht mit Java EE 7 ebenfalls ein Standard bereit (jsr356) der die Server-Seite für Websocket Szenarien bereit stellt. Werfen wir einen kurzen Blick auf das Zusammenspiele im Beispiel JavaScript + Java EE 7

Die Initialisierung einer Websocket Verbindung in JavaScript ist mit einigen wenigen Zeilen Code bereitgestellt und stellt  Listener-Methoden bereit um auf Ereignisse zu reagieren

var wsConnection = new WebSocket('ws://example.ws.service/ws');
wsConnection.onopen = function () {...};
wsConnection.onerror = function (error) {...};
wsConnection.onmessage = function (e) {...};

Auch das Pendant auf der Serverseite ist nicht wesentlich umfangreicher, das Vorgehen ist simple: wir erstellen eine Klasse und registrieren sie als WebSocket Endpoint mittels der Annotation „ServerEndpoint“. Die entsprechenden Nachrichten um auf Events zu reagieren werden mit zusätzlichen Annotation versehen. In unserem Beispiel führen wir zusätzlich eine Liste aller verbundenen Clients um diese mit Push-Nachrichten zu versorgen.

EchoEndpoint.java

@ServerEndpoint("/echo")
public class EchoEndPoint {

    private static Set<Session> userSessions = Collections.newSetFromMap(new ConcurrentHashMap<Session, Boolean>());

    @OnOpen
    public void onOpen(Session userSession) {
	System.out.println("Neue Verbindung aufgebaut...");
	userSessions.add(userSession);

    }

    @OnClose
    public void onClose(Session userSession) {
	System.out.println("Verbindung getrennt...");
	userSessions.remove(userSession);

    }

    @OnMessage
    public void onMessage(String message, Session userSession) {
	broadcast(message);

    }

    public static void broadcast(String msg) {
	System.out.println("Broadcast Nachricht an alle:" + msg);
	for (Session session : userSessions) {
	    session.getAsyncRemote().sendText("Re: " + msg);
	}

    }
}

 

websocket_demo

 

Das war es schon, mehr ist nicht nötig um JavaScript und Java EE 7 über einen WebSocket zusammen zu bringen.

Ein vollständiges Beispiel findet sich wie immer bei uns im Github:

websocket-demo @ GITHUB

 

 

Swagger – REST APIs im Griff

Wer kennt das nicht? Die Liste der Rest APIs die eine Anwendung bereit stellt wächst mit jedem Release. Dokumentationen sind rar gesät und befinden sich verstreut im Code oder externen Tools und werden nur rudimentär (oder überhaupt nicht?) auf dem aktuellen Stand gehalten. Darüber hinaus sind die Schritte um eine neue Schnittstelle ein zu binden erschreckend ähnlich und die Implementierung nimmt doch immer wieder Zeit in Anspruch. Eine Lösung? Swagger.

Swagger definiert erst einmal ein einheitliches Format (yaml oder json) in dem Schnittstellen beschrieben werden. Diese Dokumentationen kann entweder per Hand über einen entsprechenden Online Editor erstellt werden (http://editor.swagger.io/#/) oder aber aus bestehenden Rest-Schnittstellen generiert werden. Schauen wir uns einfaches Beispiel (top-down) einer solchen Definition einmal an.

Dieses Beispiel ist im YAML-Format über den Online Editor verfasst:

swagger: '2.0'
info:
  version: 1.0.0
  title: Simple Swagger Demo
paths:
  '/echo/{name}':
    get:
      description: Simple Echo-Service
      parameters:
        - name: name
          in: path
          description: name of the caller
          required: true
          type: string
        - name: message
          in: query
          description: message to be echoed
          required: true
          type: string
      produces:
        - application/json
      responses:
        '200':
          description: Successful
          schema:
            title: Echo
            type: string
        '400':
          description: Parameter not fulfill the requirements
  /time:
    get:
      description: get the current time as object
      produces:
        - application/json
      responses:
        '200':
          description: Successful
          schema:
            title: current time
            type: object
            properties:
              year:
                type: number
              month:
                type: number
              day:
                type: number
              hour:
                type: number
              minute:
                type: number
              second:
                type: number

apiex

Ohne auf die einzelnen Elemente ein zu gehen wird ersichtlich das wir mithilfe dieser Beschreibung die vorhandenen Rest-Schnittstellen in einem einheitlichen Format beschrieben und dokumentieren werden können. Am Ende kann Swagger daraus eine nette Web UI erzeugen, die auch mit rudimentären Test-Aufruf-Möglichkeiten versehen ist. Das alleine wird aber wohl noch niemandem zur großen Begeisterungsstürmen veranlassen. Seine wahre Stärke spiele Swagger im nächsten Schritt aus:

swagger-gen

Vorbei die Zeiten in denen eine Dokumentation das Dasein als simples Dokument fristen muss. Swagger bietet uns die Möglichkeit auf Basis unserer Dokumentation sowohl die Server-Komponente, als auch Client-Stubs zu generieren, die sich im eigenen Projekt einbinden lassen. Aus der oben gezeigten Beschreibung entstehen so auf Knopfdruck Server und Client die im Fall von „JaxRS Resteasy“ und „Angular 2“ so aussehen wie in den folgenden beiden Beispiel Projekten:

GITHUB – swagger-server-demo

GITHUB – swagger-angular2-demo

Button-Up

Zugegeben auf Seiten des Servers mag das Schreiben einer Swagger Definition für komplexe Objekte eher mühsam sein. Die Alternative zum Schreiben der Definition besteht darin Swagger im eigenen Projekt als Bibliothek ein zu binden und die Definition erstellen zu lassen. Die entsprechenden Dokumentation werden dann innerhalb der Klassen per Swagger-Annotationen durchgeführt. Nach dem Deployment bietet Swagger dann die übliche Web-UI mit Test-Funktionalität und Dokumentation. Zusätzlich kann die yaml/json Beschreibung herunter geladen werden um daraus dann wieder beliebige Client-Stubs zu generieren. Um dies in bestehende Projekte ein zu binden Bedarf es einer zusätzlichen Maven-Abhänigkeit

        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-jaxrs</artifactId>
            <version>1.5.10</version>
        </dependency>

Anschließend konfigurieren wir Swagger noch über eine entsprechende Application-Klassen-Implementierung:

de.gedoplan.swagger.server.bu.demo.SwaggerApplication.java

import de.gedoplan.swagger.server.bu.demo.resource.CustomerAPI;
import io.swagger.jaxrs.config.BeanConfig;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("rest")
public class SwaggerApplication extends Application {

  public SwaggerApplication()
  {
    BeanConfig beanConfig = new BeanConfig();
    beanConfig.setVersion("1.0.0");
    beanConfig.setSchemes(new String[]{"http"});
    beanConfig.setHost("localhost:8080/swagger-server-bu-demo-1.0-SNAPSHOT");
    beanConfig.setBasePath("/rest");
    beanConfig.setResourcePackage("de.gedoplan.swagger.server.bu.demo.resource");
    beanConfig.setScan(true);
  }

  @Override
  public Set<Class<?>> getClasses()
  {
    HashSet<Class<?>> set = new HashSet<>();

    set.add(CustomerAPI.class);

    set.add(io.swagger.jaxrs.listing.ApiListingResource.class);
    set.add(io.swagger.jaxrs.listing.SwaggerSerializers.class);

    return set;
  }
}

Nun können unsere Rest-Schnittstellen mit entsprechenden Annotationen versehen werden um die Dokumentation auf zu bauen (s. Annotations)

de.gedoplan.swagger.server.bu.demo.resource.CustomerAPI.java

  @GET
  @ApiOperation(value = "Returns one random customer", notes = "Mock Implementation", response = Customer.class)
  public Customer getCustomer()
  {
...
  }

Anschließend kann die json/yaml Datei über den Webserver bezogen werden (http://localhost:8080/swagger-server-bu-demo-1.0-SNAPSHOT/rest/swagger.json) um dann über den Editor entsprechende Clients zu generieren. Will man gleich auf dem lokalen Webserver eine Test/Doku Funktionalität bieten kann die Swagger-UI auch im eigenen Projekt eingebunden werden. Dazu einfach den „dist“ Ordner des Github Projektes im eigenen Web-Folder unterbringen und in der index.html die URL auf die eigene swagger.json ändern.

swagger-ui-mozilla-firefox_038

GITHUB – Button Up

Dokumentationen zu schreiben ist mühsam und zeitintensiv und allzu oft werden diese nicht aktuell gehalten. Swagger bietet neben der Dokumentation-Funktion einen echten Mehrwert für die Entwicklung und bietet mit der automatischen Generierung von Code, sowohl für den Server, als auch für den Client, einen echten WOW-Effekt.

Also: Schluss mit Word-Dokumente und Excel Sheets. Check Swagger!

Angular 2 + vis.js – 3D Charts

Es gibt gleich ein dutzend JavaScript Bibliotheken die sich um die Generierung von Charts kümmern. In diesem Beispiel soll es exemplarisch um die Verwendung und Einbindung von vis.js gehen. Analog dazu lässt sich natürlich auch jede andere Bibliothek auf die gezeigte Art und Weise in ein Angular 2 Projekt integrieren

vis-js

Wir initialisieren unser Projekt in diesem Beispiel mittels angular-cli (https://github.com/angular/angular-cli) welches uns viele alltägliche Aufgaben bei der Erstellung und Weiterentwicklung von Angular 2 Projekten erleichtert Zusätzlich fügen wir die Abhängigkeiten zu Bootstrap und vis.js unsrem Projekt hinzu

ng start ng-vis-demo
 cd ng-vis-demo
 npm install bootstrap –save
 npm install vis –save

Sowohl bootstrap (bootstrap.css) als auch vis (vis.js) stellen Dateien zur Verfügung die normalerweise in der index.html direkt über ein script/css Tag referenziert werden und somit global in der Anwendung zur Verfügung stehen. Bei der Verwendung von angular-cli kümmert sich der Buildprozess um derlei Aufgaben, wir müssen lediglich unsere zentralen Scripte und CSS-Files konfigurieren:

angular-cli.json

...
"styles": [
"../node_modules/bootstrap/dist/css/bootstrap.min.css",
"../node_modules/vis/dist/vis.min.css",
"styles.css"
],
"scripts": [
"../node_modules/vis/dist/vis.min.js"
],
…

Für die Einbindung von nativen Bibliotheken wie vis.js dient nun eine entsprechende Direktive die sich um die Initialisierung und Verarbeitung des Graphen kümmert. Diese kann entweder manuell angelegt werden oder mittels „ng“ generiert und automatisch dem Modul bekannt gemacht werden (ng g directive vis3dgraph)

src/app/directives/vis3dgraph.directive.ts

import { Directive, ElementRef, Input } from '@angular/core';

declare var vis: any;

const DEFAULT_OPTIONS = {
    width: '100%',
    height: '700px',
    style: 'surface',
    showPerspective: true,
    showGrid: true,
    showShadow: false,
    keepAspectRatio: true,
    verticalRatio: 0.5,
    xCenter: '50%',
    yCenter: '35%',
    cameraPosition: { horizontal: 1.0, vertical: 0.5, distance: 2.0 }
};

@Directive({
    selector: '[appVis3DGraph]'
})

export class Vis3dGraphDirective {

    @Input()
    public data: any;

    private _graph; any;

    constructor(private _element: ElementRef) { }

    public createGraph() {
        let container = this._element.nativeElement;

        if (!this._graph) {
            this._graph = new vis.Graph3d(container, this._generateData(), DEFAULT_OPTIONS);
        } else {
            this._graph.setData(this._generateData());
            this._graph.redraw();
        }
    }

    private _generateData() {
        let data = [];
        this.data.forEach((xValue, x) => data.push(...xValue.map((yValue, y) => {
            return { x, y, z: yValue.value };
        })));

        return data;
    }
}

Hier erstellen wir eine Direktive die wir später mit dem Selektor “appVis3DGraph“ in unserem Template verwenden werden. Die Daten für das zu generierende Chart erhält die Direktive über einen entsprechenden Input Parameter (@Input() data) Für die eigentliche Erzeugung des Graphen lassen wir uns das Container-Element der Direktive im Konstruktor injecten, dessen Attribut „nativeElement“ das konkrete HTML Element referenziert, welches wir vis.js als Container zur Verfügung stellen. Zusätzlich bereiten wir die übergebenen Daten noch im benötigten Format auf und setzen einige Optionen bevor wir mittels vis.Graph3d einen den Graphen durch vis.js rendern lassen.

Die Verwendung der Direktive erfolgt dann im Template einer unserer Komponenten über den oben gewählten Selektor und die Übergabe der Daten in den Input Parameter der Direktive:

src/app/app.component.html

<div appVis3DGraph [data]="data"></div>

Der Vollständigkeit halber hier noch der entsprechende Controller, der die Daten für den Graphen bereit stellt:

src/app/app.component.ts

import { Component, OnInit, ViewChild} from '@angular/core';
import {Vis3dGraphDirective} from './directives/vis3dgraph.directive'

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})

export class AppComponent implements OnInit {

    public data:any[][];

    @ViewChild(Vis3dGraphDirective)

    private _graph:Vis3dGraphDirective;

    public ngOnInit(){
        this.data=new Array(6).fill(0).map(_ => new Array(6).fill(0).map(_ => {return {value: Math.floor(Math.random()*100)}}));
    }

    public updateData(){
        this._graph.createGraph();
    }

}

Neben der Generierung einiger Zufallszahlen für das Beispiel lassen wir uns mittels „ViewChild“ die verwendete Instanz der Direktive injizieren um bei entsprechender User Interaktion ein neu Laden des Graphen zu veranlassen.

Das war es schon. Dieses Beisiel zeigt eindrucksvoll wie einfach es ist native Script Bibliotheken in einem Angular 2 Projekt zu integrieren. Vis.js diente hier nur als ein Beispiel und lässt im Bereich der Datenvisualisierung leider einige Funktionen vermissen: so besteht in LineCharts und 3DGraphen z.B. leider nicht die Möglichkeit einzelne Punkte zu selektieren. Bestehen solche Anforderungen lohnt möglicherweise ein Blick auf plotly.js, welches seit geraumer Zeit ebenfalls als OpenSource Variante zur Verfügung steht. Die Einbindung würde dann analog zu oben über eine Direktive geschehen.

 

Github Beispielprojekt:

https://github.com/GEDOPLAN/ng2-vis-demo

Rightsize your Services mit WildFly Swarm

WildFly Swarm

Überblick

Klassische Application Server wie JBoss/WildFly oder Glassfish/Payara sind etablierte und ausgereifte Ablaufumgebungen für Enterprise-Applikationen. Dass mehrere Anwendungen bis zu einem gewissen Grade voneinander isoliert in einem Server berieben werden können, wird in der Praxis wegen der unvermeidlichen Gemeinsamnutzung von Java VM, CPU, Memory etc. selten ausgenutzt. Man trifft also meist Server an, auf denen genau eine Anwendung deployt ist. Durch den Trend zu Microservices kommt es zu einer deutlichen Vergrößerung der Serveranzahl mit den daraus resultierenden Anforderungen im Bereich Installation, Konfiguration und Betrieb. Populäre Alternativen zu Java EE – z. B. Spring und Dropwizard – nutzen ein anderes Betriebsmodell: Statt Anwendungen in jeweils einen Server zu deployen, wird der Server in die Anwendungen integriert. Für die Java-EE-Welt bietet das Open-Source-Projekt WildFly Swarm (wildfly-swarm.io>) einen solchen Ansatz.

Komposition der Runtime

WildFly Swarm ist vergleichbar mit einem in Stücke zerteilten WildFly Application Server. Diese Stücke – sog. Fractions – werden als Maven Dependencies zur Anwendung hinzugefügt:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.wildfly.swarm</groupId>
      <artifactId>bom-all</artifactId>
      <version>${version.wildfly.swarm}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>org.wildfly.swarm</groupId>
    <artifactId>jaxrs-cdi</artifactId>
  </dependency>
  <dependency>
    <groupId>org.wildfly.swarm</groupId>
    <artifactId>jpa</artifactId>
  </dependency>
  ...

Die derzeit (Dez. 2016) aktuelle Version ist 2016.12.0.

Aus den so gewählten Fractions und dem eigentlichen Anwendungscode wird dann ein Uber Jar gebaut. Dies geschieht mit Hilfe eines Plugins in der Maven-Konfiguration des Projektes:

<build>
  <plugins>
    <plugin>
      <groupId>org.wildfly.swarm</groupId>
      <artifactId>wildfly-swarm-plugin</artifactId>
      <version>${version.wildfly.swarm}</version>
      <configuration>
        <mainClass>de.gedoplan.micro.bootstrap.SwarmBootstrap</mainClass>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>package</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Die o. a. Main Class dient der programmatischen Konfiguration der Anwendung (s. u.). In einfachen Fällen wird sie nicht benötigt. Das entsprechende Element der Plugin-Konfiguration entfällt dann.

Durch den Build-Lauf (z. B. per mvn package) entsteht dann im target-Verzeichnis neben dem normalen Zielartefakt xyz.war eine Datei namens xyz-swarm.jar, die die Anwendung inkl. Server Runtime enthält.

Konfiguration der Anwendung

Wenn die Default-Konfiguration nicht ausreicht, weil bspw. DB-Verbindungen eingerichtet werden müssen, kann eine individuell prgrammierte Main-Klasse genutzt werden. Darin können die einzelnen Fractions mit Hilfe eines Fluent API parametrisiert werden. Alternativ kann eine externe Konfigurationsdatei referenziert werden:

public class SwarmBootstrap {

  public static void main(String[] args) throws Exception {
    Swarm swarm = new Swarm();

    System.setProperty("swarm.context.path", "micro-ohne-server");

    URL standaloneFullXml = SwarmBootstrap.class.getClassLoader().getResource("configuration/standalone-full.xml");

    swarm
        .withXmlConfig(standaloneFullXml)
        .start()
        .deploy();
  }

Die im Beispiel genutzte externe Konfigurationsdatei entspricht der vom WildFly Application Server bekannten standalone-full.xml, muss aber nur die für die zu konfigurierenden Fractions benötigten Einstelldaten enthalten.

Anwendungsstart

Die Anwendung wird durch Ausführung des durch das o. a. Plugin erzeugte Uber Jar gestartet: java -jar target/xyz-swarm.jar.

Weitere Infos

Anwendungskonfiguration mit Apache Tamaya

Nahezu jede Anwendung muss irgendwie konfiguriert werden – seien es Namen, die statt im Code fest eingebaut zu werden, aus einem File gelesen werden sollen, seien es Berechnungsparameter oder URLs. Java (EE) bietet dazu bislang keinen Standard an, so dass jeder sich „sein eigenes Süppchen kochen“ muss.

Nun zeigt sich ein Silberstreif am Horizont in Form eines API zur Konfiguration von Anwendungen, das auf der diesjährigen Java One als Bestandteil von Java EE 8 angekündigt wurde und das Ideen aus dem entsprechenden Teil von Apache DeltaSpike und von Apache Tamaya übernehmen soll. Letzteres soll im Folgenden kurz beschrieben werden.

Apache Tamaya (tamaya.incubator.apache.org) befindet sich derzeit noch in einem sehr frühen Stadium, bedient sich aber bewährter Ideen aus Apache DeltaSpike Configuration und ist somit durchaus in Grenzen bereits einsetzbar.

Apache Tamaya ist modular aufgebaut. Der Core-Anteil wird immer benötigt. Darüber hinaus gibt es diverse Extensions wie bspw. zur Bereitstellung von Konfigurationswerten per CDI Injection. Für Maven-Projekte sind dies die folgenden Dependencies:

<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-core</artifactId>
  <version>${version.tamaya}</version>
</dependency>
<dependency>
  <groupId>org.apache.tamaya</groupId>
  <artifactId>tamaya-cdi</artifactId>
  <version>${version.tamaya}</version>
</dependency>

Derzeit (Dez. 2016) ist die aktuelle Version 0.2-incubating.

Konfigurationswerte können mit einem einfachen API abgeholt werden:

String javaVendor 
  = ConfigurationProvider.getConfiguration()
                         .getOrDefault("java.vendor", "unknown");

ConfigurationProvider.getConfiguration() liefert dabei das Einstiegsobjekt vom Typ Configuration, das neben der oben genutzten Methode getOrDefault auch solche anbietet, die den Wert in einen anderen Typ als String gewandelt liefern.

In CDI Beans kann zudem per Injektion auf die Werte zugegriffen werden:

@Inject
@Config(defaultValue = "unknown")
String companyName;

@Inject
@Config
int answerToLifeUniverseAndEverything;

@Inject
@Config("java.version")
String javaVersion;

Apache Tamaya holt die Werte per Default aus System Properties, Environment Entries und der Datei META-INF/javaconfiguration.properties im Classpath. Es können weitere Property Sources registriert werden und auch die Suchreihenfolge manipuliert werden.

Weitergehende Informationen finden sich auf der Homepage des Projektes (tamaya.incubator.apache.org). Aufgrund des aktuellen Projektstands ist aber noch viel im Fluss.

Ein Demo-Projekt zum Anschauen und Ausprobieren gibt es hier: github.com/GEDOPLAN/config-demo.

Microservices und das Uberjar

Ich habe gerade auf der W-JAX Thilo Frotschers Vortrag über Java EE Microservices verfolgt. Er stellte darin dar, dass bspw. mit Payara Micro sog. Uberjars gebaut werden können, die neben der Microservice-Anwendung auch die notwendige Server-Implementierzung enthalten. Motivation und Ziel: Start der Anwendung inkl. Umgebung als ausführbares Jar auf der Kommandozeile.

Ein Teilnehmer stellte dann die Frage „Muss ich denn für jede Instanz meines Microservices immer die komplette Runtime haben – mit dem entsprechnden Platzbedarf, Startzeiten etc.? Oder könnte ich auch eine Shared Runtime nutzen?“.

Das zeigt – wie ich meine – die derzeitige etwas unglückliche Vermischung der Diskussion über das Programmmodell für Anwendungen und ihr Betriebsmodell. Das was der Fragende nutzen könnte, wäre ein klassischer Application Server!

Wichtig ist aus meiner Sicht, dass die innere Struktur der Anwendungen nicht durch das Betriebskonzept eingeschränkt werden sollte. Das ist mit Java EE der Fall: Ich kann klassische War Files bauen und auf einem klassischen Application Server deployen – gerne auch in Umgebungen wie Docker, wo ebendieses Deployment auch nichts anderes ist als eine Datei an einen bestimmten Platz zu legen. Ich kann aber auch die gleiche Anwendung mit einer Server Runtime zu einem Uberjar kombinieren und dann als eine Kommandozeilen-Anwendung betreiben.

Zu dem Thema gibt es am 08.12.16 in unserem Hause einen kostenlosen Vortrag: http://gedoplan-it-consulting.de/expertenkreis-java/aktuelles/.