Bereits vor über zwei Jahren hat sich mein geschätzter Kollege und Vorgänger in dieser Kolumne mit den Grundlagen von Docker beschäftigt. Und obwohl zwei Jahre in der IT eine Ewigkeit darstellen, sind die dort enthaltenen Informationen immer noch gültig und dienen als guter Einstieg in das Thema Docker.
Zur Erinnerung: Docker wird auf einem sogenannten Docker-Host installiert. Über den Docker-Client kann man Images bauen, deren Inhalte in Dockerfiles beschrieben werden. Ein Image wird dabei später über seinen Namen und gegebenenfalls die Version identifiziert.
Möchte man ein Docker-Image auf einem Docker-Host ausführen, genügt dafür der
Aufruf von docker run image-name
. Ist das Image auf dem Host noch nicht
vorhanden, lädt Docker es automatisch aus einer Registry herunter. Standardmäßig
wird hierfür die öffentliche Registry genutzt. Es lässt sich aber
auch eine eigene verwenden.
Beim Start eines Containers können Ports exportiert werden. Diese sind anschließend über den Docker-Host von außen erreichbar. Damit Container untereinander kommunizieren können, nutzt man sogenannte Links oder virtuelle Docker-Netzwerke.
Szenario
Als Szenario für diesen Artikel dient ein System, das aus zwei Services besteht.
- Der erste Service, die Datenbank, ist für die Persistenz der Daten zuständig. Dafür wurde exemplarisch Redis gewählt. Redis ist primär eine In-Memory-Key/Value-Datenbank.
- Als zweiter Service wurde ein Frontend mit Spring Boot entwickelt. Bei jedem Aufruf antwortet dieses dem Client mit einem JSON-Dokument, das den aktuellen Wert eines Zählers und den Hostnamen des Frontends enthält. Dabei nutzt es die Datenbank, um den aktuellen Wert des Zählers zu persistieren. Das Frontend horcht auf Port 8080 und nutzt den Wert der Umgebungsvariable SPRING_REDIS_HOST, um die Verbindung zur Datenbank aufzubauen (s. Listing 1).
Starten und Stoppen des Systems mit Docker
Der erste Schritt, um das System nun auf einem Docker-Host laufen zu lassen, besteht darin, für beide Services jeweils ein Docker-Image zu bauen und dieses anschließend zu starten.
Für Redis gibt es bereits eine Vielzahl an fertigen Images im Docker-Hub. Um
Aufwand zu sparen, wird deshalb das offizielle Docker-Image
genutzt. Zum Starten eines Containers mit Redis genügt der Aufruf
docker run -d --name redis redis
. Ist das Image noch nicht auf dem Docker-Host
vorhanden, lädt dieser es herunter. Nach kurzer Zeit ist der Container dann
erfolgreich gestartet.
Um das Frontend als Container zu starten, muss zuerst ein Image erzeugt werden. Natürlich gibt es hierfür kein fertiges Image und somit wird ein eigenes Dockerfile (s. Listing 2) benötigt.
Dabei wird als Basis ein Image genutzt, in dem bereits ein JDK installiert wurde – hier airhacks/java. Anschließend fügen wir unser zuvor mit Maven gebautes JAR hinzu und teilen Docker noch mit, dass der Service auf dem Port 8080 horcht. Als Letztes geben wir noch den Befehl an, mit dem das Frontend innerhalb des Docker-Containers gestartet wird.
Aus diesem Dockerfile können wir nun mit den Befehlen docker build -t frontend
.
ein Image erzeugen und dieses anschließend mit docker run -d --name frontend
--link redis:redis --env SPRING_REDIS_HOST=redis -p 8080:8080 frontend
starten.
Anschließend ist das System unter der IP des Docker-Hosts und dem Port 8080 erreichbar. Listing 3 zeigt eine exemplarische Abfragesequenz mit curl.
Um das komplette System nun wieder sauber zu stoppen, reicht der Befehl docker
stop frontend redis
. Zusätzlich müssen diese Container vor dem nächsten Start
noch mit dem Befehl docker rm frontend redis
entfernt werden. Dies ist
notwendig, damit Docker beim nächsten Start die zugewiesenen Namen erneut
verwenden kann.
Bereits hier kann man erkennen, dass schon die Verwaltung eines kleinen Systems über einzelne Docker-Befehle mühsam ist. Es müssen sämtliche Images einzeln gebaut und gestartet werden. Beim Starten muss auf die richtige Reihenfolge und die korrekte Angabe von Links, exportierten Ports und Umgebungsvariablen geachtet werden. Zudem vergisst man auch beim Stoppen des Systems schnell, die Container zusätzlich zu entfernen.
Der erste Reflex eines Entwicklers ist es, an dieser Stelle die notwendigen Befehle zu automatisieren. Glücklicherweise wurde dies auch von Docker erkannt und bietet deshalb mit Docker Compose ein Tool an, das genau für diesen Use-Case entwickelt wurde.
Docker Compose
Um das System mit Docker Compose verwalten zu können, wird die Datei docker-compose.yml angelegt. Innerhalb dieser Datei wird das komplette System in YAML beschrieben (s. Listing 4).
Auf der ersten Ebene werden dabei die vorhandenen Services des Systems aufgelistet. Jeder Service enthält wiederum diverse Eigenschaften, die Docker benötigt, um das Image zu finden oder zu bauen. Und auch Umgebungsvariablen, exportiere Ports und die richtigen Verlinkungen zwischen den Services werden hier definiert. Durch die Angabe der Links kann Docker zudem die richtige Reihenfolge zum Starten herausfinden und erkennt dabei sogar zirkuläre Abhängigkeiten.
Das Starten des gesamten Systems kann nun mit dem einzelnen Befehl
docker-compose up -d
erledigt werden und auch das anschließende Stoppen ist
dank des Befehls docker-compose down
sehr einfach. Docker Compose übernimmt
hierbei zusätzlich zum Stoppen auch direkt das Entfernen der gestoppten
Container. Die Option -d sorgt dabei dafür, dass die Container im Hintergrund
laufen.
Betrachtet man nach dem Start einmal die laufenden Container (s. Listing 5), so stellt man fest, dass die Container und das Frontend-Image ein Präfix und die Container zudem ein Suffix enthalten.
Das Präfix dient dazu, das System auf einem Docker-Host mehrfach starten zu können. Als Standard nutzt Docker Compose den Namen des Ordners, in dem sich die YAML-Datei befindet, als Präfix. Dies lässt sich jedoch durch die Angabe von -p NAME oder über die Umgebungsvariable COMPOSE_PROJECT_NAME ändern. Zusätzlich dürfen zwischen den Systemen keine Kollisionen entstehen. Ein zweites Starten des Systems mit dem Namen dce2 führt zum Beispiel dazu, dass das Frontend aufgrund des exportierten Ports 8080 nicht startet. Die zweite Datenbank läuft jedoch trotzdem. Docker Compose unterstützt somit kein atomares Starten des Systems. Der Versuch, anschließend das nur halb gestartete System wieder zu stoppen, funktioniert jedoch erfolgreich und hinterlässt keine Spuren (s. Listing 6).
Skalieren von Services
Neben dem einfachen Starten und Stoppen bietet Docker Compose die Möglichkeit, einzelne Services innerhalb des Systems zu skalieren, indem mehrere Container für ein Image instanziert werden. Dies ist auch der Grund für das Suffix am Containernamen.
Docker Compose kennt hierzu das Kommando scale. Man könnte daher annehmen,
dass sich eine zweite Instanz des Frontend-Containers einfach per Befehl
docker-compose scale frontend=2
oder ähnlichem starten ließe. Leider
funktioniert das praktisch nicht, da es zu einem Portkonflikt kommt.
Um also das Frontend zu skalieren, wird ein Load-Balancer benötigt. Wie für Redis gibt es auch hierfür verschiedene fertige Lösungen im Docker-Hub. Letztendlich habe ich mich für dockercloud/haproxy entschieden. Nach dem Hinzufügen des Load-Balancers als dritten Service (s. Listing 7) lässt sich nun das Frontend um eine zweite Instanz erweitern, und anschließend werden die Requests abwechselnd von den beiden Instanzen beantwortet (s. Listing 8).
Ausfallsicherheit und Deployments mit Docker Compose
Neben der Skalierung unterstützt Docker Compose auch bei den Themen Ausfallsicherheit und Deployment. Beides funktioniert jedoch nur in Kombination mit Docker Swarm (s. Kasten „Docker Swarm”).
Docker Swarm
Mit Docker Swarm (1) lassen sich mehrere Docker-Hosts zu einem „Swarm“ zusammenschließen. Nach außen hin verhält sich ein solcher Schwarm wie ein einzelner Docker-Host, intern jedoch kann zum Beispiel der Ausfall eines Docker-Hosts durch einen Neustart aller dort vorher laufenden Container auf einem anderen Docker-Host kompensiert werden.
Seit Version 1.13 von Docker Swarm und Version 3 des Docker Compose-Dateiformates ist es nun auch möglich, beides zusammen zu verwenden. Somit kann ein System aus mehreren Services innerhalb eines Schwarms deployt und gemanagt werden.
Für die Ausfallsicherheit sorgt hierbei eine Kombination aus dem mit Docker Version 1.12 eingeführten Healthcheck und der in der Docker Compose-Datei angegebenen Restart Policy. Somit lässt sich zum Beispiel konfigurieren, dass ein Container, dessen Healthcheck fehlschlägt, automatisch neu gestartet wird. Einen guten Einstieg zu Healthchecks bietet der Blog Post „Test-drive Docker Healthcheck in 10 minutes“ von Alex Ellis.
Zusätzlich lässt sich über die Anzahl der Replikas sicherstellen, dass dauerhaft mehr als eine Instanz eines jeden Services läuft. Docker Swarm sorgt zudem dafür, dass nicht alle Replicas auf demselben Docker-Host laufen.
Die Kombination aus Docker Compose und Swarm bietet zudem die Möglichkeit zu definieren, wie Container aktualisiert werden sollen. Somit können Updates ohne Downtime ausgerollt werden. Mit geschickter Wahl der Konfigurationswerte lassen sich sogar sogenannte „Canary Releases“ umsetzen.
Weitere Features von Docker Compose
Neben Services lassen sich mit Docker Compose auch Docker Networks verwalten. Zum Einstieg bieten sich hier die Blog Posts „Understanding Docker Networking Drivers And Their Use Cases“ sowie „Docker Networking and DNS“ an.
Außerdem lassen sich Docker Compose-Dateien auch wiederverwenden oder für die Nutzung in verschiedene Umgebungen konfigurieren. Als Einstiegspunkt bietet sich hierfür die offizielle Dokumentation an.
Fazit
Die manuelle Verwaltung eines größeren Systems aus mehreren Services mit Docker wird schnell unübersichtlich und kompliziert. Docker bietet hierzu mit Docker Compose die Möglichkeit, ein solches System kompakt innerhalb einer Datei zu definieren und anschließend mit wenigen Befehlen zu verwalten.
In Kombination mit Docker Swarm ist es seit Kurzem nun auch möglich, ein mit Compose definiertes System in Produktion zu überführen. An dieser Stelle tritt Docker jedoch sehr stark in Konkurrenz zu Container-Cluster-Managern wie Kubernetes oder Mesos. Die Zeit wird zeigen, welches System sich durchsetzt.
-
https://www.docker.com/products/docker–swarm ↩