Ein mächtiges Werkzeug

Mit Docker leistungsfähige Java-Integrationstests realisieren
Kommentare

Java-Entwickler kennen Container schon zur Genüge. Nichts anderes sind die als WAR- und EAR-Pakete geschnürten Anwendungen, die in Applikationsserver deploytwerden. Das Ziel, Applikationen von der Laufzeitumgebung zu entkoppeln wird jedoch durch die JEE-Standardisierung nur begrenzt erreicht. Dazu unterscheiden sich die einzelnen JEE-Server zu sehr voneinander, denn sie bieten viel Zusatzfunktionalität außerhalb des Standards. Docker kann hingegen Applikationsserver gleich mit in den Docker-Container stecken. Ferner hilft Docker bei der Erstellung gekapselter Integrationstests. Diese beiden wichtigen Aspekte werden in diesem Artikel näher betrachtet.

Um Integrationstests und das Bauen von Docker-Containern erfolgreich zu realisieren, wird Docker idealerweise direkt in den Build-Prozess integriert. Für Maven-basierte Builds werden verschiedene Maven-Plug-ins vorgestellt und verglichen. Ein einfaches Beispiel demonstriert die Verwendung eines Plug-ins und zeigt die jeweiligen Besonderheiten auf. Docker kann den Entwickleralltag in zwei Bereichen entscheidend bereichern: Da ist zum einen die teils ungeliebte Pflege und Betrieb von Integrationstests, die oft einen erheblichen Aufwand bedeuten und für die es mithilfe Dockers ganz neue Ansätze gibt. Außerdem besteht mit Docker zusätzlich die Möglichkeit, produktionsnahe Deployment-Artefakte in Form von Applikationsimages zu bauen, die anstelle der JEE-Artefakte wie WAR- oder EAR-Dateien durch die Continous-Delivery-Pipeline geschoben werden und die letztendlich in der Produktionsumgebung landen. In den folgenden Abschnitten werden diese beiden Aspekte im Detail beleuchtet.

Integrationstests

Eine Disziplin, die notorisch Kopfschmerzen bereitet, ist die Erstellung und Wartung von Integrationstests. Eine typische Eigenschaft von komplexen Tests ist, dass Sie externe Abhängigkeiten besitzen. Dazu gehören Datenbanken und Web Services aber auch LDAP-, FTP-, Messaging- oder SSH-Server. Es gibt verschiedene Möglichkeiten, diese Abhängigkeiten zu berücksichtigen.

Zunächst wollen wir aber definieren, was gute Tests ausmacht. Unsere Kopfschmerzen kommen meist daher, da oft eine oder mehrere der folgenden, nicht funktionalen Eigenschaften fehlen:

  • Robust: Ein Test sollte robust sein. Das heißt er sollte entweder immer erfolgreich sein oder immer fehlschlagen, wenn sich an dem zu testendem Code oder an den Tests selbst nichts geändert hat. Es gibt nichts Schlimmeres als flaky Tests, die eigentlich meistens funktionieren, aber halt nicht immer. Das Fehlschlagen dieser Tests lässt sich auch oft nicht reproduzieren. Die Ursache dafür ist eine oft temporäre Veränderung im Ausführungskontext der Tests. Das kann ein parallel laufender Test sein oder aber auch ein Änderung innerhalb der externen Abhängigkeiten.
  • Autark: Tests sollten idealerweise autark lauffähig sein, sodass die Entwickler die volle und exklusive Kontrolle über alle Testparameter haben. Das schließt auch die Governance für externe Testsysteme ein. Andernfalls ist es oft die dann notwendige, zusätzliche Kommunikation, die einen Mehraufwand bedeutet.
  • Isoliert: Wenn auf einem Continous-Integration-System mehrere gleichartige Tests von z. B. verschiedenen Feature-Branches überlappend gestartet werden, sollten diese sich nicht gegenseitig behindern. Insbesondere sollten sie nicht ihre Ausführungsumgebung beeinflussen und z. B. keine für alle sichtbaren Netzwerkports öffnen.
  • Schnell: Tests sollten zügig durchlaufen, was insbesondere in der Entwicklungsphase für kurze Turnaroundzeiten und damit schnelles Feedback sorgt.

Tatsächlich sind diese Eigenschaften nicht getrennt betrachtbar und überlappen sich erheblich. Beispielsweise sind autarke und isolierte Tests oft auch schon automatisch sehr robust. Auch ist diese Liste von Anforderungen sicher nicht vollständig, es fehlt beispielsweise die Forderung nach einer ausreichenden Testabdeckung. Sollten jedoch diese vier Eigenschaften nicht erfüllt sein, ist dies auf jeden Fall mit einem erhöhten Wartungsaufwand der Tests verbunden. Die daraus entstehenden Kosten sind dann auch oft der Grund für einen gänzlichen Verzicht auf Integrationstests mit allen Konsequenzen.

Nachdem wir nun wissen, welche Eigenschaften gute Tests haben sollten, wollen wir betrachten, wie die anfangs erwähnten externen Abhängigkeiten technisch umgesetzt werden können und wie verschiedene Varianten in Bezug auf die postulierten Kriterien abschneiden.


Inside Docker: Wenn Sie mehr über Docker wissen möchten, empfehlen wir Ihnen das Entwickler Magazin Spezial Vol. 2: Docker zum leichten Einstieg in die Container-Virtualisierung.

docker-coverMit Docker feiern Linux-Container momentan ein eindrucksvolles Comeback. Während der Einsatz von virtuellen Maschinen viele Vor-, aber auch zahlreiche Nachteile mit sich bringt, ist Docker eine leichtgewichtige, containerbasierte Alternative, die die System-Level-Virtualisierung auf ein neues Level hebt. Dabei ergänzt Docker das Deployment von Betriebssystemen und Webanwendungen um die Lösungen, die man beim Original schmerzlich vermisst. In diesem Jahr hat Docker eine hohe Dynamik entwickelt und wird in allen aktuellen Linux-Distributionen wie Redhat, SUSE oder Ubuntu ausgeliefert. Firmen wie Spotify, Google, BBC, eBay und seit kurzem auch Zalando setzen Docker bereits produktiv ein. Das Entwickler Magazin Spezial „Docker“ informiert kompetent über diese revolutionäre Technologie, von der viele meinen, dass sie eine neue Ära in der IT einläuten wird. Wir erklären technische Hintergründe, demonstrieren Best Practices und zeigen, wie Docker effektiv eingesetzt werden kann. Das Sonderheft vereint umfangreiches Wissen über die wichtigsten Aspekte von Docker, spannende Ideen für eigene Docker-Projekte und wertvolle Impulse für ihre strategische Planung.

Externe Testsysteme

Externe Testsysteme sind dedizierte Installationen der realen externen Abhängigkeiten, die exklusiv als Testsysteme betrieben werden. Diese können beispielsweise spezielle Web Services für Testdaten sein, die von einem externen Dienstleister bereitgestellt werden. Auf die Verfügbarkeit dieser Testsysteme hat man oft keinen Einfluss, sodass man nur abhängig und in Abstimmung mit dem Betreiber Tests durchführen sollte. Auch existiert nur meist ein Testsystem für mehrere Nutzer, sodass parallele Testläufe nicht isoliert sind. Die Folge ist, dass Tests mit externen Testsystemen oft auch nicht robust sind.

Der Vorteil externer Testsysteme ist aber natürlich, dass diese der Produktionsumgebung sehr nahe kommen und damit realistische Testergebnisse erzielen.

Mock-Testsysteme

Mit Mock-Testsystemen werden die externen Abhängigkeiten komplett simuliert. Diese können speziell für die aktuellen Testfälle selbst erstellt werden, oder speziell dafür ausgerichtete Software kann dafür genutzt werden. Für Letzteres eignet sich ganz hervorragend das Testframework Citrus, das es auf sehr flexible Art und Weise ermöglicht, Mock-Systeme für die verschiedensten Netzwerkprotokolle deklarativ zu erstellen. Dabei werden die vorpräparierten Antworten über eine Konfiguration bestimmt.

Der Vorteil von Mock-Systemen in Tests ist, dass man diese vollständig in den Build integrieren kann, sodass der Test autark ist. Diese werden während des Testlaufs gestartet und können mit einer individuellen Konfiguration, z. B. der Ports, auch abgeschottet und isoliert werden. Auch deshalb sind diese Art von Tests robust und werden gerne für Continuous Integration genutzt.

Der große Nachteil ist jedoch, dass Mock-Systeme nicht zwangsläufig dem Produktionssetup nahe kommen. Es hängt ganz davon ab, wie gut die Simulation umgesetzt ist und das reale System abbildet. Zudem muss die Mock-Konfiguration gepflegt und bei jeder Änderung der zu simulierenden externen Systeme angepasst werden. Während sich beim Modell „Externe Testsysteme“ unbemerkte API-Änderungen sofort in Testfehlern niederschlagen, fällt dies bei Tests mit Mock-Systemen erst viel später, vielleicht zu spät, auf.

Docker-Images

Die externen Abhängigkeiten einer Anwendung können natürlich auch über Docker-Container erfüllt werden, die die entsprechenden externen Dienste enthalten. Dies ist ein Spezialfall der externen Testsysteme, nur dass in diesem Fall der Lebenszyklus und die Governance eine andere ist. Idealerweise werden Docker-Images vom Hersteller der externen Dienste bereitgestellt, so wie sie auch in Produktion genutzt werden. Auch wenn dies nicht der Fall sein sollte, hat die Nutzung von Docker-Images mit abhängigen Services wie Webdevices oder Datenbanken entscheidende Vorteile:

  • Per se sind Docker-Container isoliert, sodass mehrere Tests ohne Probleme parallel laufen können, ohne sich gegenseitig zu stören (sofern dynamisch zugewiesene Host-Ports benutzt werden).
  • Docker-Images werden über eine Registry verwaltet und können dynamisch installiert werden. Somit benötigt der Build-Prozess außer einer Docker-Installation nichts weiter und ist damit weitgehend autark in sich geschlossen.
  • Da der Lebenszyklus der Docker-Container vollständig vom Build selbst kontrolliert werden kann, sind diese Arten von Test sehr robust und liefern reproduzierbare Ergebnisse.
  • Diese Kapselung ließe sich natürlich auch via kompletter VMs, z. B. mit Vagrant, realisieren. Der Vorteil der Docker-Variante mit seiner System-Level-Virtualisierung ist der wesentlich geringere Ressourcenverbrauch und auch die Geschwindigkeit, mit der Docker-Container gestartet werden können.

Zu beachten ist, dass Docker aktuell nur genutzt werden kann, wenn die externen Systeme unter Linux lauffähig sind. Das kann sich in Zukunft aber noch ändern. Die Docker-Variante vereint die Vorteile externer und Mock-Testsysteme: Sie erlaubt isolierte, autarke und robuste Test wie bei Mock-Systemen und kommt dem realen Kontext so nahe wie die Variante mit externen Testsystemen.

Stellen Sie Ihre Fragen zu diesen oder anderen Themen unseren entwickler.de-Lesern oder beantworten Sie Fragen der anderen Leser.

Deployment

Der zweite Bereich im Entwicklungsprozess, in dem Docker eine große Rolle spielen kann, ist das Deployment von Anwendungen. Die zentrale Entität, die die Anwendung beinhaltet, ist typischerweise ein Web- oder Enterprise-Archiv (WAR oder EAR). Dieses wird lokal in der Entwicklungsumgebung getestet, bevor ein CI-Server wie Jenkins die Artefakte baut und in einem Repository speichert. Von dort aus beginnt das Artefakt seine Reise durch die Continous-Delivery-Pipeline (Abb. 1) mit verschiedenen Arten von weiteren Tests, bevor es dann letztendlich in der Produktionsumgebung landet (siehe: Humble, Jez; Farley, David: „Continous Delivery“, Addison Wesley). Hier ist zu erwähnen, dass das Artefakt auf diesem Weg zwar identisch bleibt, jedoch nicht die Umgebung, in der es getestet und letztendlich produktiv installiert wird. Diese Umgebungen sind typischerweise natürlich sehr ähnlich, aber nicht identisch, was trotz aller Tests am Ende zu unerwarteten Effekten führen kann.

huss_java_1

Mit Docker kann diese Grenze nun verschoben werden: Statt der Applikationsarchive werden nun ganze Images durch die Continuous-Delivery-Pipeline geschleust. Diese Images enthalten die Anwendung und die Laufzeitumgebung, inklusive der Applikationsserver. Dadurch kann aus Anwendungssicht sichergestellt werden, dass die Umgebung in der Entwicklungsumgebung identisch zu der in Produktion ist.

Auch ist der Wechsel der Produktionssysteme aufgrund des standardisierten Docker-Image-Formats kein Problem: Ob man es zunächst auf eigenem „Blech“ laufen lassen möchte, um dann später gegebenenfalls in die Cloud zu wechseln – für den Entwicklungsprozess bedeutet das keinerlei Einschränkungen, ganz getreu dem Docker-Motto „Build, Ship and Run Any App, Anywhere“.

Es gibt mehrere Möglichkeiten, die Applikations-Images zu gestalten: Entweder erzeugt man reine „Daten-Images“, die nur die Anwendungsartefakte wie WARs enthalten und verknüpft diese später mit Containern, die die Applikationsserver enthalten. Oder aber man erzeugt gleich ein merged Image, das sowohl den Server als auch die Artefakte enthält. Der letztere Ansatz bietet sich insbesondere für Microservices an, die zusammen mit einem fat-JAR ausgeliefert werden.

Docker-Build-Integration

Es gibt verschiedene Wege, Docker in den Java-Build-Prozess zu integrieren. Im einfachsten Fall ruft man den lokal installierten Docker-Client auf. Jedes Build-Tool bietet die Möglichkeit, externe Prozesse zu starten. Mit Ant gelingt das über die exec-Task, Maven bietet das exec-maven-Plug-in und Gradle hat ebenfalls eine exec-Task, bzw. kann auch direkt aus Groovy heraus externe Prozesse starten.

Eleganter ist natürlich eine direkte Integration in den Build, die das Docker-REST-API nutzt. Dadurch entfällt die Notwendigkeit einer lokalen Docker-Client-Installation. Für Gradle und Maven gibt es dedizierte Plug-ins, die eine nahtlose Integration in den Build versprechen und den Installationsaufwand minimieren.

Im Folgenden werden wir uns auf Maven konzentrieren und uns die angebotenen Plug-ins etwas genauer ansehen.

Der neue Sport

Im Frühjahr suchte ich eine Lösung für ein lang schwelendes Problem. Die Integrationstests, die auf mehr als zwanzig Applikationsservern ausgeführt werden, sollten für meine Open Source JMX-HTTP-Bridge Jolokia automatisiert werden. Docker ist hierfür natürlich das perfekte Werkzeug. Anfang März 2014 fehlte aber noch eine nahtlose Docker-Integration in den vorhandenen Maven-Build. Nachdem weder auf Maven Central noch auf GitHub ein entsprechendes docker-maven-Plug-in vorhanden war, entschloss ich mich, selbst eins zu schreiben.

Jedoch war ich scheinbar nicht der einzige, der auf diese Idee kam. In kürzester Zeit waren auf GitHub bereits fünf docker-maven-Plug-ins zu finden. Inzwischen sind es sage und schreibe elf solcher Plug-ins, die sich auf GitHub tummeln, alle mit anderem Fokus und Konfigurationssyntax. Auch für Gradle existieren mehrere Plug-ins, sodass das scheinbar ein prinzipielles Phänomen ist.

Einerseits ist diese Vielfalt ein Indiz für den realen Bedarf nach einer Docker-Maven-Integration bzw. einer Docker-Integration in den Build-Prozess, aber natürlich erschwert sie dem Nutzer die Entscheidung. Zumal auch davon auszugehen ist, dass sich der Staub bald legen wird und nur ein paar Plug-ins überleben werden.

Die vier Überlebenden

Bei genauerer Betrachtung der GitHub-Commit-Timeline, der Forks und Stars, der Anzahl der Issues und der Lines-of-Code der verschiedenen docker-maven-Plug-ins kristallisieren sich aktuell vier aktive Plug-ins heraus:

  • wouterd/docker-maven-plugin: Dieses Plug-in von Danes Wouter unterstützt sowohl das Erstellen und Pushen von Docker-Images als auch das Starten und Stoppen von Containern.
  • alexec/docker-maven-plugin: Alex Collins hat ein docker-maven-Plug-in geschrieben, das es ermöglicht, Images zu bauen, zu starten und zu pushen. Es nutzt dazu eine eigene Konfigurationsdatei mit eigenem Format, die außerhalb des POMs gepflegt wird.
  • spotify/docker-maven-plugin: Spotify setzt intern sehr intensiv Docker ein. Dabei entstanden einige Supporttools, die Spotify als Open Source frei gibt. Dieses Plug-in unterstützt ausschließlich die Erzeugung und das Deployment von Docker-Images.
  • rhuss/docker-maven-plugin: Die Motivation für mein Plug-in wurde ja bereits im vorhergehenden Abschnitt geliefert. Dieses Plug-in unterstützt das Lifecycle-Management von Docker-Containern (erzeugen/starten/stoppen) sowie das Erstellen und Pushen von Images.

Wie man sieht, haben alle Plug-ins einen unterschiedlichen Fokus. Um die Entscheidung etwas zu erleichtern, bieten die nächsten Abschnitte einen Überblick der Gemeinsamkeiten und Unterschiede dieser Plug-ins im Hinblick auf ihre Eigenschaften. Natürlich ist der Vergleich nicht vollständig. Da ich selbst der Autor eines dieser Plug-ins bin, ist er wahrscheinlich auch noch subjektiv eingefärbt. Die Entwicklung der Plug-ins, wie das gesamt Docker-Umfeld, schreitet sehr dynamisch voran, sodass der Vergleich zum jetzigen Zeitpunkt auch schon nicht mehr aktuell sein kann und mehr als Momentaufnahme zu betrachten ist. Dennoch gibt der Vergleich einen guten Einstiegspunkt für die eigene Auswahl eines Plug-ins.

Plug-in-Konfiguration

Die Konfiguration des wouterd-Plug-ins orientiert sich an den verschiedenen Zielen des Plug-ins und wird typischerweise für jedes Ziel in separaten <execution>– Konfigurationen gepflegt. Das führt zu recht aufgeblähten POMs und erschwert die Navigation etwas, da die Build- und Runtime-Konfiguration auf unterschiedliche Sektionen verteilt ist.

Das alexec-Plug-in führt eine eigene, YAML-basierte Konfiguration für das Erstellen von Docker-Images ein. Diese wird in externen Dateien gepflegt und vom POM aus referenziert. Die eigentliche Plug-in-Konfiguration innerhalb von pom.xml gestaltet sich schlicht und einfach.

Mit dem spotify-Plug-in lassen sich Docker-Images flexibel aus dem Build-Prozess heraus bauen. Dabei kann entweder das zu bauende Image innerhalb der Plug-in-Konfiguration spezifiziert werden oder aber auch über einen Verweis auf ein Verzeichnis, das ein Dockerfile enthält.

Das rhuss-Plug-in nutzt einen einfachen, geschachtelten Abschnitt in der Plug-in-Konfiguration, um diese so kompakt wie möglich zu halten. Dabei wird zwischen den einzelnen Images unterschieden, deren Konfiguration wiederum in Laufzeit- und Build-Konfiguration aufgeteilt ist.

Container starten und stoppen

Mit dem alexec-Plug-in können nur Images, die man selbst gebaut hat, auch gestartet werden. Es ist aktuell nicht möglich, fremde Images direkt von einer Docker-Registry zu pullen und für Integrationstests zu starten. Ein Pull ist nur indirekt in Form eines Basis-Images für ein eigenes Image möglich. Healthchecks (Zeit- oder URL-basiert) ermöglichen eine Start-up-Synchronisation der Integrationstests.

Das Plug-in von wouterd erlaubt es, ein oder mehrere Docker-Container zu erzeugen und zu starten. Dabei bietet es umfassende Konfigurationsmöglichkeiten. Als weiteres Schmankerl bietet das Plug-in unter anderem die Möglichkeit, Images zu pullen, die nicht selbst gebaut wurden. Ferner kann es die Ausführung des Builds so lange blockieren, bis in der Konsole des laufenden Containers ein auf ein Muster passender Text erscheint. Damit lassen sich recht elegant Race Conditions beim Starten von Integrationstests vermeiden.

Auch mein rhuss-Plug-in bietet einen Autopull-Mechanismus analog zu dem von wouterd. Neben dem Warten auf Logausgaben bietet es auch die Möglichkeit, einen URL zu pollen und erst dann weiterzugehen, wenn der URL erfolgreich abgefragt werden konnte. Dies entspricht den Healthchecks des alexec-Plug-ins.

Die Verknüpfung verschiedener Container („Linking“) zur Laufzeit ermöglicht sowohl wouterd, alexec und rhuss. Das dynamische Portmapping mit Zuweisung der (von Docker zufällig gewählten) Ports zu Maven-Variablen beherrschen wouterd und rhuss.

Das spotify-Plug-in unterstützt wie erwähnt gar kein Laufzeitmanagement.

Images bauen und pushen

Das alexec-Plug-in erwartet im Unterverzeichnis src/main/docker jeweils ein Verzeichnis. Diese Verzeichnisse müssen obligatorisch ein Dockerfile sowie eine eigene Konfigurationsdatei conf.yml enthalten. conf.yml kann Metadaten wie den Namen des zu erstellenden Images als auch weitere Dateien referenzieren, die dem Dockerfile via ADD-Direktiven hinzugefügt werden. Die Auslagerung der Konfiguration entschlackt einerseits das POM, andererseits muss jedoch eine weitere Konfigurationssyntax erlernt und mindestens eine weitere Datei gepflegt werden.

wouterd dagegen nutzt die Plug-in-Konfiguration exklusiv für das Bauen von Images. Die Möglichkeiten sind recht einfach gestaltet: Es können Dateien explizit deklariert werden, u. a. auch das Dockerfile. Auch alle anderen Dateien, die eingebunden werden sollen, müssen hier referenziert werden.

Bei spotify kann man wählen, ob das Image klassisch über ein Verzeichnis mit einem enthaltenen Dockerfile gebaut werden soll oder aber direkt über die Maven-Konfiguration. Als Besonderheit erlaubt es dieses Plug-in, die Git-Commit-ID als Tag für ein Image zu nutzen.

Das rhuss-Plug-in bietet wie das spotify-Plug-in die Möglichkeit, sowohl das Image direkt über die Maven-Konfiguration zu definieren oder aber auch ein Dockerfile zu nutzen. Es hebt sich dadurch ab, dass es erlaubt, über einen vom maven-assembly-Plug-in bekannten Deskriptor die einzubindenden Dateien zu spezifizieren. Dabei ist es einfach möglich, abhängige Artefakte mit in das Image zu packen. Alle Möglichkeiten des maven-assembly-Plug-ins stehen hier zur Verfügung.

Sicherheit

Alle Plug-ins erlauben das Pushen der erstellten Images zu einer Registry. Bei der Kommunikation mit der Registry kann eine Authentifizierung mit Username und Password genutzt werden. Während alle Plug-ins die Konfiguration von Username und Password direkt im pom.xml und als System-Properties (im Klartext) unterstützen, bietet rhuss darüber hinaus die Möglichkeit, das Passwort im Klartext oder mit einem Master-Key verschlüsselt in der ~/.settings.xml zu speichern. Dabei können dort, analog zum Maven-Repository-Server, Docker-Registries in einer <servers>-Sektion registriert werden.

Sonstiges

Des Weiteren gibt es eine ganze Reihe weiterer Unterschiede auf nicht funktionaler Ebene. Zum besseren Vergleich finden sich online jeweils die aktuellen Metriken der vier Plug-ins. Ein Zwischenstand vom Mitte Oktober 2014 sieht grob wie folgt aus:

Tabelle 1: GitHub-Metriken der „docker-maven“-Plug-ins (Stand: Oktober 2014)

Tabelle 1: GitHub-Metriken der „docker-maven“-Plug-ins (Stand: Oktober 2014)

Wer gewinnt?

Die Basisfunktionalität des Bauens und Deployens von Docker-Images beherrschen alle Plug-ins. Die für Tests wichtige Funktionalität des Startens und Stoppens von Containern bieten bis auf spotify auch alle Plug-ins. Jedes der vorgestellten Plug-ins legt einen leicht unterschiedlichen Fokus auf die angebotenen Funktionen und Konfigurationsmöglichkeiten. Wie es in der Zukunft und dem Support dieser Plug-ins aussieht, kann man schwer prognostizieren, da die meisten (bis auf spotify) privat gepflegt werden. Da der Funktionsumfang überschaubar ist, sollte ein Wechsel zwischen den Plug-ins mit etwas Anpassungsaufwand möglich sein.

Beispiel

Ein einfaches Beispiel soll verdeutlichen, wie Docker in den Build-Prozess eingebunden werden kann. Es wird ein Microservice auf Basis eines Embedded Apache Tomcat gebaut, der auf eine PostgreSQL zugreift, um HTTP-Aufrufe zu protokollieren. Dieser Service liefert die Liste der letzten zehn Aufrufe als JSON-Objekt zurück. Dieses Beispiel ist bewusst einfach gehalten und fokussiert sich auf das Bauen von Image-Artefakten und die Ausführung eines Integrationstests. Hierbei kommt ein Testcase zum Zuge, der via REST-assured auf den Mircoservice zugreift und einfach nur überprüft, ob der aktuelle HTTP-Request mitgeloggt wurde. Der komplette Beispielcode dazu befindet sich auf GitHub.

Das Beispiel nutzt zwei Docker-Images: Ein Standard-PostgreSQL-Image von Docker-Hub und ein Image mit dem Microservice. Die Aufgabe der Plug-ins ist es, das Microservice-Image zu bauen und vor dem Integrationstest sowohl einen PostgreSQL-Container als auch einen Container mit dem Microservice zu starten, wobei der DB-Container in den Microservice-Container gelinkt wird. Der Microservice erzeugt dann beim Start automatisch mit Flyway das Schema (falls noch nicht vorhanden). Es ist somit das einfachste mögliche Setup mit mehreren verlinkten Containern, das man sich vorstellen kann. Ein paar Besonderheiten besitzt dieses Beispiel dennoch:

  • Der Container mit dem Microservice greift bei der Initialisierung bereits auf die Datenbank zu, sodass Postgres zu diesem Zeitpunkt bereits zur Verfügung stehen muss.
  • Der Integrationstest sollte auch erst loslaufen, wenn alle Container gestartet und initialisiert sind.
  • Das erzeugte JAR-File mit dem Microservice sollte möglichst einfach in das Image gelangen.

Neben dem eigentlichen Service und dem Integrationstest sind vor allem die im pom.xml enthaltenen Maven-Profile von Bedeutung. Für jedes Plug-in existiert ein individuelles Profil (wouterd„, „alexec„, „spotify und rhuss) mit der jeweiligen Konfiguration, sodass man diese gut vergleichen kann. Für die Verwendung dieses Beispielprojekts ist eine lokale Docker-Installation notwendig, und die Umgebungsvariable DOCKER_HOST muss gesetzt sein. Da alle Plug-ins nur den Zugriff über das Remote-API erlauben, muss es in der Docker-Konfiguration eingeschaltet sein.

Anhand des wouterdPlug-ins soll exemplarisch gezeigt werden, wie man das Image mit dem Microservice baut und den Integrationstest ausführt. Es genügt hier ein mvn -Pwouterd install, wobei das PostgreSQL-Image von Docker-Hub automatisch geholt wird, falls es noch nicht lokal vorhanden ist. Daher dauert auch der erste Aufruf des Builds einige Zeit. Im Anschluss steht das gebaute Image unter jolokia/shootout-docker-maven-wouterd im Docker-Host zur Verfügung (und natürlich auch das Postgres-Image). Listing 1 zeigt einen Ausschnitt eines Build-Laufs.

Listing 1
$ mvn -Pwouterd install
[INFO] ————————————————————————
[INFO] Building shootout-docker-maven 0.0.1-SNAPSHOT
[INFO] ————————————————————————
....
[INFO][INFO] — docker-maven-plugin:2.0:build-images (build) @ shootout-docker-maven —
[INFO] Using docker provider: remote
[INFO] Building image 'log-service', with name and tag 'jolokia/shootout-docker-maven-wouterd:0.0.1-SNAPSHOT'..
[INFO] Image 'log-service' has Id '1cecde69829f'
[INFO][INFO] — docker-maven-plugin:2.0:start-containers (start) @ shootout-docker-maven —
[INFO] Using docker provider: remote
[INFO] Starting container 'postgres'..
[INFO] Started container with id '69571d79175b53bae0a5d390c1ac6b9631510a1806938786304ad6e164a65f8e'
[INFO] Starting container 'log-service'..
[INFO] Started container with id '2183762e288970b97002e4820a233fe433e37d8665ebf615bb702e215457aca3'
[INFO] Waiting for container 'postgres' to finish startup (max 30 sec.)
[INFO] Container 'postgres' has completed startup
[INFO] Waiting for container 'log-service' to finish startup (max 30 sec.)
[INFO] Container 'log-service' has completed startup
...
[INFO] — docker-maven-plugin:2.0:stop-containers (stop) @ shootout-docker-maven —
[INFO] Using docker provider: remote
[INFO] Stopping container '2183762e288970b97002e4820a233fe433e37d8665ebf615bb702e215457aca3'..
[INFO] Deleting container '2183762e288970b97002e4820a233fe433e37d8665ebf615bb702e215457aca3'..
[INFO] Keeping image 1cecde69829f
...

Bei der Ausarbeitung dieses Beispiels mussten ein paar Besonderheiten der Plug-ins beachtet werden:

wouterd:

  • Es gibt keine Variablenersetzung beim Kopieren des Dockerfiles, sodass man den Artefaktnamen direkt im Dockerfile pflegen muss.
  • Bei verlinkten Containern werden die Container parallel gestartet. Die waitForHttp wartet nicht solange, bis ein Container oben ist, um dann den nächsten zu starten. Daher wurde ein sleep in das Docker-Kommando zum Starten des Microservice eingebaut. Das sollte in einer neueren Version behoben werden.

alexec:

  • Man hat keinen Einfluss auf den Alias- bzw. Image-Namen des erzeugten Microservice-Images. Das wiederum bedeutet, dass die Umgebungsvariablen des dazu gelinkten Postgres-Containers fix sind (SHOOTOUT_DOCKER_MAVEN_DB_PORT) und der Code entsprechend angepasst werden muss.
  • Es existiert kein dynamisches Portmapping mit einer Zuweisung an Maven-Variablen. Es wird immer fix auf den entsprechenden Port gemappt. Das ist insbesondere ein Problem mit boot2docker oder anderen Techniken, die eine weitere VM-Indirektion benötigen. Somit laufen die Integrationstest hier nur unter Linux ohne Fehler durch.

spotify:

  • Wie bereits beschrieben, kann man mit diesem Plug-in nur Images bauen. Das Plug-in erlaubt die Verwendung von Dockerfiles, jedoch werden auch hier keine Variablen ersetzt. Um das Image zu bauen, wird einfach mvn -Pspotify docker:build aufgerufen. Das Image steht dann beim lokalen Docker-Host zur Verfügung.

Insgesamt lässt sich sagen, dass wouterd und rhuss die beiden reifsten Plug-ins sind. alexec fällt aufgrund der beschriebenen Einschränkungen (kein dynamisches Portmapping, fixer Container-Name) hier etwas ab, und spotify ist nur für das Bauen von Images geeignet.

Fazit

Wie wir gesehen haben, lassen sich zwei Aspekte bei der Entwicklung von Anwendungen mit Docker vereinfachen: In Integrationstests lassen sich externe Abhängigkeiten durch verlinkte Docker-Container realisieren, und an die Stelle der gewohnten Artefakte wie WARs oder EARs treten ganze Docker-Images, die unverändert von der Entwicklungsumgebung bis in den Produktivbetrieb wandern. Die Integration in den Build-Prozess kann auf verschiedene Weise realisiert werden, je nachdem welches Build-System genutzt wird. Für Maven wurden vier Plug-ins vorgestellt und verglichen, die alle ihre Stärken und Schwächen haben. Das gezeigte Beispiel verdeutlicht, wie einfach die Integration von Docker in einen Maven-basierten Build-Prozess möglich ist.

Aber egal wie Docker nun in den Build-Prozess eingebunden wird, sei es direkt über die Docker-CLI oder über eine Ant-, Maven- oder Gradle-Integration, Docker bereichert den Entwicklerwerkzeugkasten auf jeden Fall um ein mächtiges Werkzeug, das bisher offene technische Probleme in Bezug auf Integrationstests und Produktions-Deployment lösen kann.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Aufmacherbild: forklift handling the container box via Shutterstock.com / Urheberrecht: SOMKKU

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -