Kolumne

Docker rockt Java: Wie Sie Docker-Images selbst gestalten

Peter Roßbach
docker-fuer-java

Wer auf dem Docker Hub nach vorgefertigten Images sucht, wird schnell fündig. Häufig passen diese aber nicht zu der eigenen Projektsituation. JAXenter-Kolumnist und W-JAX Sprecher Peter Roßbach zeigt in dieser Kolumne, wie man Docker-Images selbst anfertigt.

Docker-Images selbst gestalten

Auf dem Docker Hub gibt es jede Menge Images, aber nicht alle passen auf die eigenen Bedürfnisse. In vielen Unternehmen gibt es Regeln für die Sicherheit, welches Betriebssystem und welche Basis-Images genutzt werden müssen, Standards für die Konfiguration oder Beschränkungen der Herkunft von Packages und Artefakten.

In meiner Praxis hat sich bewährt, dass ein Image möglichst klein ist, es einen Zweck des Images gibt, ein Container zur Laufzeit genau einen Prozess startet, die Herkunft des Images bekannt ist und eine Lizenz existiert (Kasten: „Best Practice zum Bauen eines Docker-Images“). Bevor ein Image für die Produktion zum Einsatz kommt, sollten die Quellen des Projekts eingesehen werden und auf Mängel sowie Änderungsbedarf überprüft werden. Für Produktionsserver sollten die verwendeten Images besser auf einer eigenen Build-Umgebung erzeugt werden.

Wenn eine Wiederholbarkeit des Builds erforderlich ist, müssen die Packages und Artefakte außerdem von einer kontrollierten Quelle stammen. Der Einsatz eigener Repositories erfordert meist einen erheblichen zusätzlichen Aufwand, der aber einen Gewinn an Sicherheit und Kontrollierbarkeit bringt.

Best Practice zum Bauen eines Docker-Images

  • Zu jedem Image existiert ein Sourcecode-Projekt mit Dockerfile (siehe: offizielles Java Repository)
  • Abhängigkeiten kontrollieren und Vertraulichkeit der Quellen herstellen
  • Eigene Package Repositories nutzen, am besten mit kontrollierten Snapshots
  • Signaturen und Checksums der Downloads prüfen
  • Fixierte Versionen für die zusätzlichen Artefakte nutzen
  • Minimalen Inhalt mit möglichst wenigen Layern erzeugen
  • Schlankes Base OS (Alpine) verwenden
  • Shellbefehl in einem Layer kombinieren
  • Nicht den Cache beim Bau der Images für die Produktion benutzen
  • Download, Extraktion, Modifikation und evtl. Löschung von Artefakten und Package im selben Layer
  • Nach der Nutzung eines Package Managers die Caches und evtl. ungenutzte Packages für die Laufzeit löschen
  • Nur Daten zur Docker Engine übertragen, die auch ins Image gehören (.dockerignore)
  • Nur einen Prozess pro Container starten
  • Containerprozess nicht als Nutzer ROOT laufen lassen
  • Entrypoint-Script nutzen und den eigentlichen Prozess mit einem anderen Nutzer mittels gosu starten
  • In der Produktion versuchen, nur mit Read-only-Container zu arbeiten. Alle Daten und Konfigurationen sollten in Volumes bereitgestellt und gespeichert werden können
  • Optionen zur Konfiguration mit Beispielen dokumentieren
  • Hinzufügen einer Lizenz ist notwendig

Klein und fein

In den meisten Fällen sind die zusätzlichen Anforderungen der eigenen Infrastruktur nachvollziehbar, aber nicht immer einfach umzusetzen. Ein paar Muster zur Umsetzung kristallisieren sich in der Docker-Community langsam heraus. Die Anforderungen, ein minimales und schlankes Image zu nutzen, wird von den meisten geteilt. Im letzten Jahr sind die Trusted Images auf einer gemeinsamen Debian-Basis erstellt worden. Seit diesem Jahr wird zunehmend das wesentlich kleinere Alpine Linux genutzt. Die aktuellen Java Images (7u92) sind auf 145 MB für das JDK und 108 MB für das JRE geschrumpft. Wer möchte, kann natürlich auch schon Java 9 nutzen. Der Apache Tomcat wird allerdings noch auf der Basis von Debian bereitgestellt. Ein Alpine-basierter Tomcat befindet sich in meinem InfraBricks-Tomcat-Projekt und ist nur 128 MB klein. Ohne lästiges Auschecken und Modifizieren lässt sich in dem Projekt eine abweichende Version des Tomcats verwenden oder die Veränderung der Default-Parameter herstellen (Listing 1). Die Anweisung docker build lässt sich direkt auf ein Docker-Git-Projekt anwenden, und mit der Option build-arg lassen sich die vorgesehenen Parameter überschreiben. Um die Anzahl der Layer zu reduzieren, werden die Shellbefehle meist in einer RUN-Anweisung formuliert. Die Gruppierung der Anweisungen ENV, EXPOSE, LABEL und VOLUME ist ebenfalls sinnvoll.

$ docker build \
  --build-arg TOMCAT_MINOR_VERSION=8.0.35 \
  --build-arg JAVA_MAXMEMORY=1024 \
  -t infrabricks/tomcat:8.0.35-alpine \
  -f Dockerfile.alpine \
  git://github.com/infrabricks/tomcat8
$ docker run -ti --rm infrabricks/tomcat:8.0.35-alpine bin/version.sh
Using CATALINA_BASE:   /opt/tomcat
Using CATALINA_HOME:   /opt/tomcat
Using CATALINA_TMPDIR: /opt/tomcat/temp
Using JRE_HOME:        /usr/lib/jvm/java-1.8-openjdk/jre
Using CLASSPATH:       /opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/tomcat-juli.jar
Server version: Apache Tomcat/8.0.35
Server built:   May 11 2016 21:57:08 UTC
Server number:  8.0.35.0
OS Name:        Linux
OS Version:     4.4.8-boot2docker
Architecture:   amd64
JVM Version:    1.8.0_92-internal-alpine-r0-b14
JVM Vendor:     Oracle Corporation

Der Wechsel des Nutzers für den Containerprozess lässt sich durch eine USER-Anweisung im Dockerfile formulieren. Der Nutzer muss dafür im Image mit einer korrekten User-ID und Group-ID angelegt werden (Listing 2). Allerdings ist das resultierende Image merkwürdigerweise 23 MB größer geworden. Die Anweisung chown bewirkt, dass alle Dateien im Verzeichnis $CATALINA_HOME in einen neuen Layer kopiert werden. Wenn mit Nutzern gearbeitet wird, ist also darauf zu achten, dass die Rechte und Nutzereigenschaften im Layer der Entstehung korrekt sind. Es ist zu empfehlen, Artefakte direkt von einem HTTP(S)-Server in eine RUN-Anweisung zu laden, statt sie mit einer ADD– oder COPY-Anweisung dem Image hinzuzufügen, insbesondere wenn später die Extraktion oder eine Modifikation erforderlich ist. Ein kleiner weiterer Mangel ist, dass im Standard-Tomcat:8-Image Java 7 statt Java 8 verwendet wird. Wer Java 8 nutzen möchte, muss das Basis-Image tomcat:8-jre nutzen. Aber das steht aktuell nur in der Version 8u72 in der Debian-Variante bereit. Das ist ein echtes Dilemma, und es zeigt mal wieder, dass das Wiederverwenden von bestehenden Images nicht problemlos ist. Wer am Puls der Zeit bleiben will und eigene Ansprüche hat, ist gut damit beraten, eigene Docker-Projekte aufzubauen.

$ cat >Dockerfile <<EOF
FROM tomcat:8
RUN groupadd -r tomcat --gid=999 \
  && useradd -r -g tomcat --uid=999 tomcat \
  && chown -R tomcat:tomcat $CATALINA_HOME
USER tomcat
EOF
$ docker build -t tomcat:8-user .
$ docker run --name tomcat —d -P tomcat:8-user
$ docker exec -ti tomcat ps -o uname,pid,ppid,pcpu,pmem,args 1
$ docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}" \
  |grep tomcat
tomcat:8-user 370.3 MB
tomcat:8 357.2 MB

Eigene Docker-Images gestalten

Weitere Anforderungen an ein Docker-Image betreffen die Konfigurierbarkeit. Bei der Wahl der Optionen, Parameter, Konfigurationsdateien, Zertifikate, Zugangsdaten und Anmeldeinformationen muss der richtige Mix an Flexibilität und Zweckgebundenheit gefunden werden. Entstehen persistente Daten im Container, muss ein Sicherungsverfahren beschrieben werden. Wenn ein Container gestoppt werden soll, muss darauf geachtet werden, dass der Prozess auch kontrolliert herunter- und wieder heraufgefahren werden kann. So manches Image ist auf diesen Fall nicht gut vorbereitet. Einige Services werden beispielsweise im Hintergrund gestartet, und im Vordergrund läuft ein Tail auf einer Logdatei. Dies führt beim Herunterfahren eines Docker-Containers dazu, dass der Tail-Befehl das OS-Signal anstelle des Service bekommt – ein sehr unsanfter Seiteneffekt. Das Projekt gosu hat weitere solcher unerwünschten Seiteneffekte beim Nutzerwechsel mit sudo aufgedeckt. Bei der Verwendung von sudo bleibt die Shell erhalten, die den Service startet. Nicht alle Signale werden an den Serviceprozess weitergegeben. Bei Services, die persistente Daten speichern, droht da schnell eine Dateninkonsistenz. Ein gutes Beispiel für die Anwendung von gosu ist das offizielle Postgres-Image. Es zeigt, wie verschiedene Tools genutzt werden können. Die Initialisierung der Datenbank beim ersten Start wird von weiteren Starts unterschieden, und die Signale werden korrekt an den Postgres-Prozess weitergegeben.

Das Thema Konfigurierbarkeit von Docker-Images ist nicht ganz einfach. Im Standard werden die Konfigurationsparameter mit ENV-Optionen beim Erzeugen des Containers übergeben. Wenn einfache Parameter nicht reichen, werden meist Konfigurationsdateien als Volume übergeben. Große Probleme bereitet nur die Übergaben von Anmelde- und Zugangsinformationen. In Images oder als Übergabeparameter bei docker run haben solche ENV-Parameter sicherlich nichts verloren, da diese über die zentrale Docker Engine beauskunftet werden können. Oft werden die sensiblen ENV-Parameter deshalb erst beim Start des Containers aus einer Datei gelesen. Während der Entwicklung werden Default-Parameter genutzt, die oftmals ausreichend sind. In einigen Fällen werden die ENV-Parameter dazu genutzt, um die Konfigurationsdateien aus Templates heraus zu erstellen. Oftmals reicht hier das Linux-Kommando envsubst. Die Werkzeuge confd und Consul Template versprechen mehr Komfort und nutzen erweiterte Go-Templates für die Formatierungsaufgabe. Beide können die Parameter aus einem externen Key-Value-Store holen. Confd unterstützt verschiedene Quellen, während sich Consul Template auf Consul und Vault beschränkt. Beide Projekte unterstützen auch Laufzeitänderungen. Sobald sich ein Wert ändert, wird aus dem Template eine neue Datei generiert. Dann kann ein beliebiger Prozess im Container gestartet werden. Oftmals wird der laufende Service durch ein Signal zum Neuladen der Konfiguration aufgefordert.

Beim Starten eines Service in einem Container gibt es allerdings weitere Aufgaben. Sehr praktisch ist es beispielsweise, dass Docker-Container alle Ausgaben auf stdout/stderr verarbeiten können. Nicht jede Software kann alle Ausgaben zu dieser Konsolenausgabe lenken. Eigentlich sollte moderne Software so geschrieben sein, dass ein Backend-Ausfall kompensiert werden kann und die Reihenfolge des Starts keine Rolle spielt. In der Realität ist das nicht immer einfach herzustellen. Viele Java-Frameworks für das Anbinden von Datenbanken oder Caches wünschen geradezu, dass bei der Initialisierung des Service bestimmte Backends vorhanden sind und reagieren sehr unpässlich darauf, wenn dies nicht der Fall ist. Abhilfe schafft hier das Projekt dockerize. Neben einer Template-Engine für ENV-Parameter lassen sich Dateien auf stdout umlenken. Die Verfügbarkeit bestimmter Ports verzögert den Start des eigentlichen Service (Listing 3).

#!/bin/bash
set -e

if [ "$1" = 'catalina.sh' ]; then
  exec gosu tomcat dockerize \
    -wait tcp://mysql:3306 \
    -template /usr/local/tomcat/conf/server.xml.template:/usr/local/tomcat/conf/server.xml \
    -stdout /usr/local/tomcat/logs/access.log "$@"
fi
exec "$@"

Fazit

Sicherlich gibt es im Detail bei Ihrer Software noch jede Menge weiterer Anforderungen und clevere Lösungen, um die Konfigurierbarkeit von Docker-Containern zu verbessern. In dieser Kolumne sind nur einige der häufigsten Probleme angesprochen. Die wichtigste Quelle für Ideen und Lösungen sind die zahlreichen Docker-Projekte auf dem Docker Hub. In den eigenen Projekten ist eine Menge Sorgfalt notwendig, die brauchbaren Lösungen zu kopieren. Ein paar strikte Konventionen und regelmäßige Reviews helfen sicherlich, die Qualität und Sicherheit der eigenen Docker-Projekte noch zu steigern.

 

Geschrieben von
Peter Roßbach
Peter Roßbach
Peter Roßbach ist ein Infracoder, Systemarchitekt und Berater vieler Websysteme. Sein besonderes Interesse gilt dem Design und der Entwicklung von komplexen Infrastrukturen. Er ist Apache Tomcat Committer und Apache Member. Mit der bee42 solutions gmbh realisiert er Infrastrukturprodukte und bietet Schulungen auf der Grundlage des Docker-Ökosystems, aktuellen Webtechnologien, NoSQL-Datenbanken und Cloud-Plattformen an.
Kommentare

Hinterlasse einen Kommentar

1 Kommentar auf "Docker rockt Java: Wie Sie Docker-Images selbst gestalten"

avatar
400
  Subscribe  
Benachrichtige mich zu:
K.P.
Gast

Setzt der Mensch in einem echten Projekt Docker ein? Hat er jemals ein grosses System mal mit Docker produktiv genommen? Hat er Projekt-Erfahrung?
In diesem Artikel finden sich die gesammelten Blogs der letzten 2 Jahre von Leuten die keine Ahnung habe und ihre ich habe mal Docker ausprobiert Erfahrungen niederschreibe. Viel Text, viel Mist.