Microservices im Zusammenspiel mit Continuous Delivery, Teil 2 – die Ablaufumgebung

Architektur/Methoden Eberhard Wolff  –  1 Kommentare

Wenn jede Anwendung aus einer Vielzahl Microservices besteht, bringt das architekturelle Vorteile mit sich. Aber für eine Anwendung eine Vielzahl Prozesse zu starten – möglichst noch in eigenen virtuellen Maschinen –, führt zu erheblichem Aufwand. Oder gibt es vielleicht Alternativen?

Microservices sind ein Komponentenmodell: Jede Komponente ist ein Prozess. Die Prozesse kommunizieren beispielsweise über REST miteinander oder können untereinander verlinkt sein. Wie lassen sich aber solche Systeme ausliefern? Einfache Installationsautomatismen reichen nicht aus – denn idealerweise sollte die Software in einem Format vorliegen, das eine einfache Installation des gesamten Microservice mit seiner vollständigen Infrastruktur ermöglicht. Bislang gesetzte Modelle helfen hier nicht weiter – JAR- oder WAR-Dateien von Java enthalten beispielsweise immer nur eine Anwendung und setzen einen Application Server als Infrastruktur voraus.

Microservices im Zusammenspiel mit Continuous Delivery

Eine Möglichkeit ist es, jeden Microservice als eigene virtuelle Maschine (VM) auszuliefern. In ihr kann ein Prozess installiert sein, der die entsprechenden notwendigen Dienste per HTTP anbietet. Weitere Software wie ein Datenbankserver lässt sich ebenfalls einrichten. Und schließlich besteht mit einer VM die Möglichkeit,
Monitoring-Features oder Software zur zentralen Sammlung und Auswertung von Log-Dateien zu nutzen. Aber dieser Ansatz führt auch schon bei kleinen Anwendungen zu einer Vielzahl von VMs. Und die virtuellen Maschinen sind zu installieren und jeweils bei einem Update mit einer neuen Version der Software zu versorgen. In der Summe ist das zu komplex und aufwendig.

Für diese Herausforderungen kann die Container-Verpackung Docker eine Lösung sein. Sie kombiniert dafür verschiedene Techniken:

  • Statt einer vollständigen virtuellen Maschine nutzt Docker Linux-Container (LXC). Sie nutzen denselben Linux Kernel. Prozesse, Dateisysteme und Netzwerk sind getrennt. Ein LXC hat fast keinen Overhead gegenüber den einfachen Prozessen.
  • Docker nutzt als Dateisystem spezielle Images. Diese können beispielsweise eine einfache Ubuntu-Installation enthalten und lassen sich außerdem "stapeln". Der Container sucht Dateien in allen Images. Wenn also Software wie Java oder eine eigene Anwendung in einem Container betrieben werden soll, sind für diesen Container nur die Änderungen gegenüber einer Ubuntu-Basisinstallation zu speichern – den Rest stellt das Basis-Image bereit. Dadurch benötigt jeder Container kaum Speicherplatz. Natürlich gibt es auch beschreibbare Images, sodass Container Daten speichern können (s. Abb. 1).
Docker-Dateisystem-Stapel: Mehrere Container können sich eine Betriebssystem-Installationen teilen (Abb. 1).

In einem solchen Docker-Container lässt sich ein Microservice unterbringen. Einzige Einschränkung: Dieser muss auf einer Linux-Maschine laufen. Für den Microservice ist nur noch ein Image zu erstellen, das die Software enthält und sie beim Booten des Abbilds startet. Um ein solches Image für einen Microservice zu erzeugen, kann man einen Docker-Container von einem Basis-Image aus starten, dann die Software manuell installieren und den neuen Stand "committen".

Der Unterschied zwischen dem Basis-Image und dem aktuellen Zustand des File-Systems wird dadurch in einem eigenen Image abgelegt. Ein Docker-Container, der von diesem Abbild zusammen mit dem Basis-Image startet, kann auf die Installation der Software zugreifen. Das Docker-Tutorial zeigt, wie dieser Ansatz funktioniert. Ohne Installation irgendwelcher Software kann so jeder in einem Browser mit Docker Erfahrungen sammeln. Aber dieses Vorgehen hat einen entscheidenden Nachteil: Die Installation der Software ist nicht automatisiert. Bei einer neuen Version sind genau dieselben Schritte manuell durchzuführen – das schreit nach Automatisierung.

Eine andere Möglichkeit ist die Nutzung von Docker mit sogenannten Dockerfiles. Sie enthalten Instruktionen für den Aufbau eines Image. Dadurch ist das Erzeugen eines Image automatisiert und lässt sich jederzeit wiederholen.

FROM ubuntu:14.04
RUN apt-get update ; apt-get dist-upgrade -y --force-yes
RUN apt-get install -y openjdk-7-jre-headless

Der Code zeigt ein Beispiel für ein einfaches Dockerfile: Als Basis-Image, das von einem öffentlichen Repository heruntergeladen wird, kommt mit FROM eine Ubuntu-Installation zum Einsatz. Die RUN-Kommandos enthalten die Befehle für den Aufbau des Image: Der erste führt mit dem Ubuntu-Werkzeug apt-get ein Update der Installation aus. Der zweite installiert dann Java. Beim Start des Dockerfile mit docker build entsteht als Ergebnis ein Image mit einer Java-Installation. Es lässt sich nun als Grundlage nutzen, um beliebige Java-Programme zu installieren.

Eine einfache Anwendung zeigt das folgende Codebeispiel. Es basiert auf dem Image aus obigem Code, das es mit dem FROM-Eintrag auswählt. Mit ADD wird eine Datei aus dem Dateisystems des Hosts in den Docker-Container übernommen. Schließlich legt CMD den Befehl fest, der beim Start des Images ausgeführt werden soll. Das führt zum Start der vorher in den Docker-Container kopierten Java-Anwendung.

FROM java
ADD application.jar application.jar
CMD /usr/bin/java -jar /target/application.jar
EXPOSE 8080

Nach der Installation von Docker lassen sich mit dem Befehl docker build die Images erzeugen und mit ihnen via docker run neue Container starten.