Docker-Windows-Container mit Ansible managen (2/2)

Nachdem im ersten Teil der Fokus auf dem automatisierten Erstellen einer Windows-Vagrant-Box mit Packer samt Installation von Docker lag, soll es nun um die Provisionierung von Spring-Boot-Anwendungen per Ansible gehen.

In Pocket speichern vorlesen Druckansicht
Docker-Windows-Container mit Ansible managen (2/2)
Lesezeit: 14 Min.
Von
  • Jonas Hecht
Inhaltsverzeichnis

Um Spring-Boot-Anwendungen in Docker-Windows-Containern laufen zu lassen, sollte Docker auf dem System bereitstehen und der Zugriff für Ansible entsprechend eingerichtet sein. Durch das Verwenden der Windows-Vagrant-Box des Vorartikels, lässt sich sofort mit dem Erstellen eines eigenen Basis-Images für Spring-Boot-Anwendungen starten.

Damit sich Java- oder Spring-Boot-Anwendungen innerhalb von Docker-Containern ausführen lassen, ist eine Java-Laufzeitumgebung sowie ein Dockerfile nötig. Hier zeigt sich in der Welt der Docker-Windows-Container ein ähnliches Bild wie in den Vagrant-Boxes des Vorartikels: es gibt schlicht keine passenden Basis-Images für Spring-Boot-Anwendungen. Dazu ist die Technologie wahrscheinlich noch zu jung und man muss selbst aktiv werden.

Mit Ansible stellt das jedoch kein großes Problem dar. Der letzte Schritt in der Datei prepare-docker-windows.yml zeigt den Aufruf im zentralen Playbook:

  - name: Build the springboot-oraclejre-nanoserver Docker image
include: build-springboot-oraclejre-nanoserver-image.yml
vars:
image_name: springboot-oraclejre-nanoserver
java8_update_version: 144
java_build_version: b01
server_jre_name:
server-jre-8u{{java8_update_version}}-windows-x64.tar.gz

Er enthält das Playbook build-springboot-oraclejre-nanoserver-image.yml, das hauptsächlich für zwei Dinge zuständig ist. Zuerst lädt es das Java Runtime Environment (JRE) 8 in der Server-Variante herunter. Damit das automatisch und ohne Benutzerinteraktion klappt, sind ein paar HTTP-Header-Tricks anzuwenden, die sich gut mit dem Tool wget abbilden lassen:

wget.exe --no-cookies --no-check-certificate --header \"Cookie:
oraclelicense=accept-securebackup-cookie\" \"
http://download.oracle.com/otn-pub/java/jdk/8u144-↵
b01/090f390dda5b47b9b721c7dfaa008135/server-jre-8u144-windows-x64.tar.gz\"
-O C:\\springboot-oraclejre-nanoserver\\server-jre-8u131-windows-x64.tar.gz

Im zweiten Schritt baut das System das Basis-Image für Spring-Boot-Anwendungen. Dazu liegt ein Dockerfile in Form eines Templates im gleichnamigen Verzeichnis. Die entsprechende Datei Dockerfile-SpringBoot-OracleJRE-Nanoserver.j2 stellt den Bauplan des Basis-Images dar:

#jinja2: newline_sequence:'\r\n'
FROM microsoft/nanoserver:latest

# This is a base-Image for running Spring Boot Apps on Docker Windows Containers
MAINTAINER Jonas Hecht

# Extract Server-JRE into C:\\jdk1.8.0_xyz in the Container
ADD {{server_jre_name}} /

# Configure Path for easy Java usage
ENV JAVA_HOME=C:\\jdk1.8.0_{{java8_update_version}}
RUN setx /M PATH %PATH%;%JAVA_HOME%\bin

# Create logging default path for Spring Boot
VOLUME C:\\tmp

Als Templating Engine kommt in Ansible Jinja2 zum Einsatz. Die erste Zeile hält das Tool davon ab, alle Zeilen des Dockerfiles in eine einzige zu konvertieren, da am Ende ein Standard-konformes Dockerfile auf der Windows-Vagrant-Box liegen soll. Wie angesprochen nutzt das Basis-Image den Nanoserver als Grundlage. Soll der ungleich größere windowsservercore zum Einsatz kommen, lässt sich die Zeile einfach anpassen.

Interessant ist das darauf folgende Schlüsselwort ADD, da es Docker nicht nur anweist, das vorher mit Ansible heruntergeladene JRE im Docker-Container zu verstauen. Es entpackt es zudem gleich unter C:\jdk1.8.0_{{java8_update_version}}. Für solche Feinheiten der Befehle lohnt es sich, die Dockerfile-Referenzdokumentation genauer zu studieren. Die folgenden Befehle mit den Schlüsselwörtern ENV und RUN setzen für Java notwendige Umgebungsvariablen. Danach wird noch per VOLUME ein Verzeichnis des Windows Docker Host in den Container gemountet. Das ist vor allem für durch Spring Boot bereitgestellte Webanwendungen relevant, bei denen der integrierte Tomcat-Server persistent ins Dateisystem schreiben können muss.

Nun ist alles bereit, um Spring-Boot-Anwendungen in Docker-Windows-Containern laufen lassen zu können. Für eigene Experimente kann jedwede Spring-Boot-Anwendung zum Einsatz kommen. Sie lassen sich zum Beispiel schnell mit dem Spring Initializr erzeugen und durch das Befolgen der Hinweise in den empfehlenswerten "Getting Started Guides" mit Leben füllen. Allerdings ist auch hierfür ein einfaches Beispiel auf GitHub zu finden, das nach einem auf der Kommandozeile ausgeführten mvn clean package eine fertige .jar-Datei ausspuckt und im target-Verzeichnis ablegt.

Darüber hinaus ist dort ein Repository mit Ansible-Skripten zu finden. Nach einem Wechsel in das entsprechende Verzeichnis lässt sich das Playbook direkt ausführen. Sollte eine eigene Spring-Boot-Anwendung zum Einsatz kommen, sind nur die beiden Parameter app_name und jar_input_path entsprechend anzupassen:

ansible-playbook -i hostsfile ansible-windows-docker-springboot.yml ↵
--extra-vars "host=ansible-windows-docker-springboot-dev app_name=restexamples ↵
jar_input_path=../../restexamples/target/restexamples-0.0.1-SNAPSHOT.jar"

Das Playbook ansible-windows-docker-springboot.yml sorgt dabei in vier Abschnitten für alles Notwendige, damit die Spring-Boot-Anwendung sauber innerhalb eines Windows-Containers läuft.

# Prepare for the Docker build...
- name: Create directory C:\spring-boot\app_name, if not there
win_file: path={{target_path}} state=directory

- name: Template and copy Spring Boot app´s Dockerfile to directory ↵
C:\spring-boot\app_name
win_template:
src: "templates/Dockerfile-SpringBoot-App.j2"
dest: "{{target_path}}\\Dockerfile"

- name: Copy Spring Boot app´s jar-File to directory C:\spring-boot\app_name
win_copy:
src: "{{jar_input_path}}"
dest: "{{target_path}}\\{{app_name}}.jar"

Das Skript legt zunächst mit dem Modul win_file das Verzeichnis für den Docker-Build an. Danach kommt das win_template-Modul zum Einsatz, um das Dockerfile-SpringBoot-App.j2 als Standard-konformes Dockerfile im Build-Verzeichnis abzulegen. Es macht von dem in den vorangegangenen Schritten erzeugte Basis-Image Gebrauch und ist durchaus einen Blick wert:

#jinja2: newline_sequence:'\r\n'
FROM springboot-oraclejre-nanoserver:latest

MAINTAINER Jonas Hecht

# Expose the apps Port
EXPOSE {{app_port}}

# Add Spring Boot app.jar to Container
ADD {{app_name}}.jar app.jar

# Fire up our Spring Boot app by default
CMD ["java.exe", "-jar app.jar --server.port={{app_port}}"]

Über das Schlüsselwort FROM lässt sich das Basis-Image als Grundlage definieren. Dadurch steht Java vollständig nutzbar zur Verfügung. An der Stelle ist folglich nur noch an die konkrete Anwendung zu denken. Dazu wird per EXPOSE eine Port als von außen nutzbar deklariert und im Anschluss die Spring-Boot-.jar-Datei mit ADD in den Container unter C:\app.jar kopiert. Die letzte Anweisung im Dockerfile startet die Spring-Boot-Anwendung mit dem gewohnten Befehl java -jar app.jar, wobei das CMD-Schlüsselwort die Syntax vielleicht etwas gewöhnungsbedürftig erscheinen lässt.

Nach dem erfolgreichen Erstellen des Dockerfiles ist zuletzt noch die .jar-Datei der Spring-Boot-Anwendung per win_copy-Modul im selben Verzeichnis abzulegen. Damit ist alles bereit für den Docker-Build-Schritt.

Bevor mit dem Docker-Build begonnen werden kann, sollte noch etwas aufgeräumt werden:

- name: Stop the Service Docker container
win_shell: docker stop {{app_name}}
ignore_errors: yes

- name: Remove the Service Docker container
win_shell: docker rm {{app_name}} --force
ignore_errors: yes

- name: Remove the Service Docker image
win_shell: docker rmi {{app_name}}:latest --force
ignore_errors: yes

Mithilfe des Moduls win_shell und den Kommandozeilen-Befehlen docker stop appName, docker rm appName und docker rmi imageName werden alte Versionen der Container und Images bereinigt. Da die Schritte nicht beim ersten Ausführen des Ansible-Playbooks relevant sind, kommt ignore_errors: yes zum Einsatz, das entsprechende Fehler bei der Befehlsausführung einfach ignoriert.

An diesem Beispiel ist vor allem im Vergleich mit der Fülle an Modulen für Docker unter Linux zu erkennen, dass Ansible unter Windows trotz allem noch am Anfang seiner Entwicklung steht. Unter Linux ist es längst nicht mehr nötig, native Shell-Module für Docker zu bemühen. Es ist zu hoffen, dass Red Hat als Treiber hinter Ansible in zukünftigen Versionen noch einige Erweiterungen nachliefert.

Die Spring-Boot-Anwendung ausführen

- name: Build the Service´ Docker image
win_shell: docker build . --tag {{app_name}}:latest
args:
chdir: "{{target_path}}"

- name: Run the Service´ Docker container
win_shell: "docker run -d --publish {{app_port}}:{{app_port}} --name= ↵
{{app_name}} --restart=unless-stopped {{app_name}}:latest"

Mit dem Befehl docker build lässt sich der finale Docker-Container für die Spring-Boot-Anwendung bauen. Wichtig ist dabei, dem win_shell-Modul per args: chdir: das korrekte Verzeichnis mitzuteilen, da das Dockerfile sonst nicht gefunden würde und es zu einem Fehler käme. Danach kann der Container gestartet werden. Sinnvoll ist die Verwendung von -d, was den Container "detached" startet und Ansible die Möglichkeit gibt, sich nicht auf ewig an die Ausführung des Containers zu binden.

Durch die Option --publish {{app_port}}:{{app_port}} wird der Port des Docker-Containers direkt auf dem Host zur Verfügung gestellt. Eine Restart Policy wie unless-stopped sorgt dafür, dass der Container auch nach einem Neustart des Windows Docker Host beziehungsweise der Windows Vagrant Box wieder zusammen mit dem Docker Windows Service gestartet wird. Die letzte Option veranlasst Docker, das zuvor gebaute Image zu verwenden.

Mit dem Befehl docker logs containerId kann man sich auf einer PowerShell innerhalb der Windows-Vagrant-Box die typischen Logausgaben der Spring-Boot-Anwendung ausgeben lassen:

Die PowerShell offenbart die typischen Logausgaben beim Start einer Spring-Boot-Anwendung.


Bevor die bisherigen Befehle dazu führen, dass Ansible und damit oft die ganze Continuous-Delivery-Pipeline ein vollständiges und erfolgreiches Deployment der Spring-Boot-Anwendung meldet, sollte geprüft werden, ob die Anwendung ordentlich läuft. Der Vorgang ist auch als "Healthcheck" bekannt.

Jede Spring-Boot-Anwendung lässt sich über localhost:port/health fragen, in welchem Zustand sie sich befindet. Im einfachsten Fall kann man davon ausgehen, dass ein erfolgreicher Aufruf der URL auf eine laufende Spring-Boot-Anwendung schließen lässt – zumindest, wenn der Vorgang einen HTTP-Statuscode 200 zurückliefert.

Nun gibt es allerdings noch eine kleine Einschränkung in der aktuellen Docker-Windows-Container-Implementierung: Der sogenannte localhost-Loopback, der normalerweise im Falle von per --publish an den Host gebundenen Ports genau diesen Zugriff auf die Anwendung im Docker-Container zulässt, funktioniert (noch) nicht. Mit ein paar Tricks lassen sich trotzdem Healthchecks durchführen, wie der letzte Abschnitt des Playbooks verdeutlicht:

- name: Obtain the Docker Container´s internal IP address (because localhost ↵
doesn´t work for now https://github.com/docker/for-win/issues/458)
win_shell: "docker inspect -f {% raw %}'{{ ↵
.NetworkSettings.Networks.nat.IPAddress }}' {% endraw %} {{app_name}}↵
{{ '>' }} container_ip.txt"

- name: Get the Docker Container´s internal IP address from the temporary txt-file
win_shell: cat container_ip.txt
register: win_shell_txt_return

- name: Define the IP as variable
set_fact:
docker_container_ip: "{{ win_shell_txt_return.stdout.splitlines()[0] }}"

- name: Wait until our Spring Boot app is up & running
win_uri:
url: "http://{{ docker_container_ip }}:{{port}}/health"
method: GET
register: health_result
until: health_result.status_code == 200
retries: 10
delay: 5
ignore_errors: yes

Mit dem PowerShell-Befehl docker inspect -f {{.NetworkSettings.Networks.nat.IPAddress }} containerName kommt man an die IP des Docker-Containers, auf der sich der Healthcheck erfolgreich ausführen lässt. Leider sieht der Befehl innerhalb von Ansible nicht mehr ganz so elegant aus, da er die im Tool reservierten spitzen Klammern verwendet, die normalerweise auf ein Template hindeuten und vom Programm ersetzt werden wollen. Das ist an der Stelle zu unterbinden, damit der PowerShell-eigene Templating-Mechanismus greifen kann.

Zusätzlich wird eine temporäre Datei für die IP-Adresse benötigt. Das liegt daran, dass die Benutzung einer registrierten Variable Ansible nochmals dazu veranlassen würde, in den abgesetzten Befehl zu schauen, was aktuell leider zu einem Fehler führt und die Ausführung des Skripts abbrechen lässt. Durch den Umweg über die temporäre Testdatei lässt sich das umgehen.

Das Modul set_fact hilft, die ausgelesene Container-IP für die weitere Nutzung in Ansible zu definieren. Danach kann endlich der Healthcheck erfolgen. Hierfür kommt das win_uri-Modul zum Einsatz. Es wird angewiesen, eine HTTP GET-Operation auf die genannte URL auszuführen. Das Programm registriert das Ergebnis als Variable und wartet mit dem Schlüsselwort until auf das Eintreten des korrekten Statuscodes. Mit den darauf folgenden Optionen retries und delay wird der GET-Befehl mehrfach hintereinander in einem bestimmten Zeitintervall ausgeführt. Sobald der letzte Teil des Ansible-Playbooks erfolgreich durchlaufen wurde, lässt sich von einer funktionsfähigen, aktiven Spring-Boot-Anwendung innerhalb eines Docker-Windows-Containers ausgehen.

Das Microsoft-Universum verschließt sich dem Einsatz moderner Prinzipien der Softwareentwicklung nicht. Mit Ansible lassen sich Windows-Maschinen provisionieren und teilweise komplizierte Installationsroutinen im Griff behalten. Eine mit Packer erzeugte Vagrant-Box bietet durch einen vollständig automatisierten Build-Vorgang eine solide Basis für erste Gehversuche mit Docker-Windows-Containern und hilft später bei der sonst nur aufwendig durchführbaren Fehlersuche.

Die Definitionen der Rahmenbedingungen für den Betrieb der Spring-Boot-Anwendungen liegen durch solch ein Vorgehen nachvollziehbar im Versionskontrollsystem. Zuvor durch komplexe manuelle Arbeit geprägte Aufgaben sind nun leichter zu erledigen, etwa das Aufsetzen zusätzlicher Server für Lastverteilung oder Ausfallsicherheit. Zusätzlich werden die Skripte immer wieder auf Windows-Maschinen ausgeführt, was dafür sorgt, dass die Anwendungen immer in einer Infrastruktur laufen, die im Versionskontrollsystem definiert sind. Das ermöglicht es Softwareentwicklungsteams, sich viel stärker auf das Erarbeiten und Implementieren fachlicher Features zu konzentrieren.

Die Entwicklung bei Microsoft steht derweil nicht still: Der vereinfachte Parallelbetrieb von Docker-Windows- und Linux-Containern ist ein wichtiges Ziel auf der aktuellen Roadmap. Darüber hinaus sollen Container-Orchestrierungssysteme wie Kubernetes und Docker Swarm demnächst Windows-Container vollständig unterstützen können.

Jonas Hecht
führten die Überzeugung, dass Softwarearchitektur und Hands-on-Entwicklung zusammengehören, zu codecentric. Tiefgreifende Erfahrungen in allen Bereichen der Softwareentwicklung großer Unternehmen treffen bei ihm auf eine Leidenschaft für neue Technologien.
(jul)