Services und Stacks im Cluster

So funktioniert Docker Swarm: Eine Einführung in das Continuous Deployment

Tobias Gesellchen

© Shutterstock / SasinTipchai

Nach unserer grundlegenden Einführung in Docker tauchen wir ab ins Wasser und betrachten den Docker Swarm. Der einzelne Container tritt dabei in den Hintergrund. Das Zusammenspiel mehrerer Instanzen über verschiedene Hosts hinweg rückt in den Mittelpunkt.

Docker Swarm: Der Weg zum Continuous Deployment

Docker als Akteur im DevOps-Umfeld lässt sich schon lange nicht mehr nur auf Container reduzieren. Eine in mehrere Microservices aufgeteilte Applikation sorgt für höheren Orchestrierungsbedarf anstelle einfacher Skripte. Docker hat dazu eine Serviceabstraktion eingeführt, die mittels Docker Swarm über mehrere Hosts hinweg bei der Orchestrierung von Containern hilft.

Docker Swarm gibt es in zwei Versionen, von denen die ältere als eigenständige Lösung ein etwas aufwendigeres Set-up mit eigenem Key-Value Store benötigt. Die neuere Variante, häufig Swarm Mode genannt, ist seit Docker 1.12 Teil der Docker Engine und braucht kein spezielles Set-up mehr. Dieser Artikel behandelt ausschließlich den Swarm Mode, da er von offizieller Seite empfohlen und intensiver weiterentwickelt wird. Bevor wir tiefer in den Swarm eintauchen, betrachten wir zunächst, wo sich Docker Services positionieren und wie sie sich zu den bekannten Docker Images und Containern verhalten.

Docker Swarm: Von Containern zu Tasks

Traditionell nutzen Sie Docker Images als Mittel zum Einpacken und zum Austausch von Artefakten oder Anwendungen. Die anfangs noch übliche Methode, komplette Ubuntu Images als Docker Image zu benutzen, ist bereits von minimalen Binaries in zugeschnittenen Betriebssystemen wie Alpine Linux überholt worden. Die Interpretation eines Containers hat sich vom Virtual-Machine-Ersatz zur Prozesskapsel gewandelt. Der Trend hin zu minimalen Docker Images ermöglicht höhere Flexibilität und bessere Ressourcenschonung: Sowohl Storage als auch Netzwerk werden weniger beansprucht, und nebenbei sorgen kleinere Images mit weniger Features auch für eine kleinere Angriffsfläche. Das Starten von Containern geht somit schneller, und Sie gewinnen eine allgemein bessere Dynamik. Mit dieser Dynamik macht ein Microservice-Stack erst richtig Spaß und macht sogar den Weg frei für Projekte wie Functions as a Service.

Docker-Services machen Container allerdings keinesfalls obsolet, sondern ergänzen Konfigurationsmöglichkeiten wie die gewünschte Anzahl von Replicas, Deployment Constraints (z. B. Proxy nicht auf dem Datenbank-Node einrichten) oder Update-Policies. Container mit ihren servicespezifischen Eigenschaften werden im Kontext von Services „Tasks“ genannt. Tasks sind damit die kleinste Einheit, die innerhalb eines Service läuft. Da Container als Prozesskapsel nichts vom Docker Swarm und dessen Serviceabstraktion wissen, fungiert der Task als Verbindungsstück zwischen Swarm und Container.

Einen Service, beispielsweise basierend auf dem Image nginx:alpine, können Sie mit drei Replicas einrichten, sodass Sie ein ausfallsicheres Set-up erhalten. Die gewünschten drei Replicas äußern sich konkret als drei Tasks und somit als Container, die Ihnen durch Docker Swarm auf der verfügbaren Menge der Swarm Nodes verteilt werden. Die Ausfallsicherheit erreichen Sie natürlich nicht allein durch das Verdreifachen der Container. Vielmehr kennt Docker Swarm nun Ihre gewünschte Zielkonfiguration und greift entsprechend ein, wenn ein Task oder ein Node ausfallen sollte.

Sprung ins Wasser

Um die Theorie etwas greifbarer zu machen, gehen wir die einzelnen Schritte eines Service-Deployments durch. Voraussetzung ist eine aktuelle Docker-Version, ich verwende die derzeit neueste Version 17.07 auf Docker for Mac. Die Beispiele sind übrigens auf einem einzelnen Rechner nachvollziehbar, sind aber in einer produktiven Umgebung nur über verschiedene Nodes hinweg sinnvoll. Alle Aspekte einer produktionsnahen Umgebung lassen sich in der offiziellen Dokumentation einsehen. Dieser Artikel wird nur punktuell Hinweise geben können.

Die Docker Engine startet per Default mit deaktiviertem Swarm Mode. Um ihn zu aktivieren, geben Sie auf der Konsole ein: docker swarm init.

Docker quittiert diesen Befehl mit der Bestätigung, dass der aktuelle Node als Manager eingerichtet wurde. Falls Sie vorher schon die Docker Engine in den Swarm Mode geschaltet haben, wird eine entsprechende Meldung darauf hinweisen.

Docker Swarm unterscheidet zwischen Managern und Workern. Worker stehen rein für das Deployment von Tasks zur Verfügung, während Manager darüber hinaus auch den Swarm pflegen. Dazu gehört, kontinuierlich die Services zu beobachten, mit dem gewünschten Zielzustand abzugleichen und unter Umständen auf Abweichungen zu reagieren. Damit beim Ausfall eines Managers der Swarm seine Entscheidungsfähigkeit behält, werden in einer Produktionsumgebung drei oder gar fünf Nodes als Manager eingerichtet. Diese pflegen den globalen Cluster-Zustand per Raft-Log, sodass bei Ausfall des Leader-Managers einer der übrigen Manager die Rolle des Leaders übernimmt. Fallen mehr als die Hälfte der Manager aus, lässt sich ein fehlerhafter Cluster-Zustand nicht mehr korrigieren. Bereits laufende Tasks auf intakten Nodes bleiben allerdings bestehen.

DevOpsCon Whitepaper 2018

Free: 40+ pages of DevOps expert knowledge

Learn about Containers,Continuous Delivery, DevOps Culture, Cloud Platforms & Security with articles by experts like Kai Tödter (Siemens), Nicki Watt (OpenCredo), Tobias Gesellchen (Europace AG) and many more.

Der oben eingegebene Befehl zeigt neben der Erfolgsmeldung auch eine Kopiervorlage für das Hinzufügen von Worker Nodes. Die Worker müssen den Manager unter der IP-Adresse erreichen, die ganz am Ende des Befehls steht. Das kann unter Docker for Mac oder Docker for Windows für externe Worker schwierig werden, weil unter Mac und Windows die Engine in einer Virtual Machine läuft, die interne IP-Adressen nutzt. Für unser Beispiel ist das aber kein Problem.

Die Beispiele werden etwas realistischer, wenn wir lokal neben dem Manager weitere Worker Nodes starten. Das geht mit Docker sehr einfach, indem pro Worker ein Container gestartet wird, in dem eine Docker Engine läuft. Diese Methode erlaubt es sogar, verschiedene Versionen der Docker Engine auszuprobieren, ohne notwendigerweise eine Virtual Machine oder einen dedizierten Server aufsetzen zu müssen. In unserem Kontext wird beim Starten von Services auf den einzelnen Workern auch relevant, dass jeder Worker sich die benötigten Images vom Docker Hub oder einer anderen Registry ziehen muss. Mithilfe eines lokalen Registry Mirrors lassen sich diese Downloads etwas optimieren. Das war noch nicht alles: Für lokal gebaute Images richten wir eine lokale Registry ein, sodass wir für das Deployment nicht gezwungen sind, diese privaten Images auf eine externe Registry wie den Docker Hub zu pushen. Wie sich das komplette Set-up mithilfe von Scripts einrichten lässt, wurde schon beschrieben. Um das Set-up noch weiter zu vereinfachen, bietet sich Docker Compose an. Sie finden auf GitHub daher ein passendes docker-compose.yml, das drei Worker, eine Registry und einen Registry Mirror startet. Die folgenden Befehle richten Ihnen die notwendige Umgebung ein, um die im Artikel beschriebenen Beispiele nachvollziehen zu können.

git clone https://github.com/gesellix/swarm-examples.git
cd swarm-examples
swarm/01-init-swarm.sh
swarm/02-init-worker.sh

Alle weiteren Beispiele finden Sie ebenfalls im genannten Repository wieder. Falls nicht anders beschrieben, werden die Kommandos im Wurzelverzeichnis ausgeführt.

Der erste Service

Nachdem die lokale Umgebung vorbereitet ist, können Sie einen Service deployen. Der nginx als dreifaches Replica lässt sich wie folgt einrichten:

docker service create \
  --detach=false \
  --name proxy \
  --constraint node.role==worker \
  --replicas 3 \
  --publish 8080:80 \
  nginx:alpine

Die meisten Optionen wie –name oder –publish sollten keine Überraschung sein, sie definieren nur einen individuellen Namen und konfigurieren das Portmapping. Abweichend zum gewohnten docker run wird per –replicas 3 direkt definiert, wie viele Instanzen des nginx gestartet werden sollen, und per –constraint=… wird gefordert, dass Servicetasks nur auf Worker Nodes und nicht auf den Managern gestartet werden dürfen. Darüber hinaus erlaubt –detach=false das Beobachten des Service-Deployments. Ohne diesen Parameter, bzw. bei –detach=true, können Sie auf der Konsole direkt weiterarbeiten und der Service wird im Hintergrund deployt.

Der Befehl weist letztlich die Docker Engine an, das gewünschte Image auf den einzelnen Workern herunterzuladen, Tasks mit der individuellen Konfiguration zu erzeugen und die Container zu starten. Je nach Netzwerkbandbreite dauert der Download der Images initial am längsten, die Startdauer der Container hängt von den konkreten Images bzw. dem im Container laufenden Prozess ab.

Falls Sie anstelle einer konkreten Anzahl von Replicas erreichen wollen, dass ein Service auf jedem aktiven Node laufen soll, dann kann der Service mit –mode global gestartet werden. Wenn Sie dem Swarm nachträglich neue Node Worker hinzufügen, wird Docker selbstständig den global-Service auf die neuen Nodes erweitern. Dank einer solchen Konfiguration müssen Sie die Anzahl der Replicas nicht mehr manuell um die Anzahl der neuen Nodes vergrößern.

Lesen Sie auch: Von Linux zu Docker: Die Grundlagen der Container-Technologie

Befehle wie docker service ls und docker service ps proxy zeigen Ihnen auch nach dem Deploy den aktuellen Status des Service bzw. dessen Tasks. Doch auch mit konventionellen Befehlen wie docker exec swarm_worker2_1 docker ps werden Sie die Instanzen des nginx als normale Container wiederfinden. Per Browser oder curl können Sie jetzt unter http://localhost:8080 die Standardseite von nginx laden.

Bevor wir uns der Frage zuwenden, wie drei Container jeweils unter demselben Port erreichbar sein können, schauen wir uns an, wie Docker Swarm einen fehlgeschlagenen Task wiederherstellt. Dazu reicht ein einfaches docker kill swarm_worker2_1, das einen der drei Container entfernt, sodass der Swarm einen neuen Task erstellen muss. Im konkreten Fall passiert das sogar so schnell, dass Sie im nächsten docker service ps proxy schon den neuen Container sehen sollten. Das Kommando zeigt Ihnen die Taskhistorie, also auch den fehlgeschlagenen Task. Dieses automatische Self-Healing von fehlschlagenden Tasks kann wohl als eines der Kernfeatures von Containermanagern betrachtet werden. Mit swarm/02-init-worker.sh können Sie den eben gestoppten Worker wieder starten.

Docker Swarm erlaubt Ihnen eine freie Konfiguration, wie auf fehlschlagende Tasks reagiert werden soll. Beispielsweise kann im Rahmen eines Serviceupdates der Vorgang gestoppt werden, oder Sie wünschen ein Rollback auf die Vorgängerversion. Je nach Kontext erweist es sich als sinnvoll, sporadische Probleme zu ignorieren, sodass das Serviceupdate bei den restlichen Replicas versucht wird.

Load Balancing via Ingress Network

Doch zurück zu der Frage, wie der gleiche Port auf drei verschiedenen Containern in einem Service gebündelt wird. Tatsächlich wird der Service-Port nicht mit konventionellen Mitteln pro Container auf dem physischen Netzwerkinterface gebunden, sondern die Docker Engine richtet einige Indirektionen ein, die eingehenden Traffic über virtuelle Netzwerke bzw. Bridges routen. Konkret wurde oben beim Request auf http://localhost:8080 das Ingress Network benutzt, das als Node-übergreifendes Overlay-Netzwerk Pakete zu einer beliebigen Service-IP routen kann. Letztlich wird auf diese Weise Load Balancing über die verschiedenen Nodes ermöglicht. Dieses Netzwerk können Sie auch mit docker network ls sehen und mit docker network inspect ingress detailliert untersuchen.

Das Load Balancing ist auf einer Ebene implementiert, die ganz nebenbei einen unterbrechungsfreien Betrieb von Frontend Proxies ermöglicht. Typischerweise werden Webapplikationen hinter solchen Proxies versteckt, um die Services nicht direkt dem Internet aussetzen zu müssen. Das bietet neben einer größeren Hürde für potenzielle Angreifer auch andere Vorteile, beispielsweise um unterbrechungsfreies Continuous Deployment zu implementieren. Proxies bilden die notwendige Zwischenschicht, um die jeweils gewünschte und verfügbare Version Ihrer Applikation bereitzustellen. Doch sollte nicht nur die Applikation, sondern auch der Proxy stets mit Sicherheitskorrekturen und Bugfixes versorgt werden. Es gibt verschiedene Mechanismen, auch auf dieser Ebene für möglichst geringe Unterbrechungen zu sorgen. Bei Verwendung von Docker-Services benötigen Sie allerdings keine speziellen Vorrichtungen mehr. Sollten Sie wie oben gezeigt eine Instanz der drei nginx-Tasks abschießen, so werden die anderen beiden weiterhin erreichbar sein. Das passiert nicht nur lokal, sondern auch in einem Multi Node Swarm. Einzige Voraussetzung ist ein entsprechender Swarm aus Docker Engines und ein intaktes Ingress Network.

Deployment via Serviceupdate

Ähnlich wie das zufällige oder manuell herbeigeführte Beenden eines Tasks können Sie sich auch ein Serviceupdate vorstellen. Im Rahmen des Serviceupdates können Sie verschiedene Eigenschaften des Service anpassen. Dazu gehören das Image oder dessen Tag, Sie können das Container-Environment ändern oder Sie passen die extern erreichbaren Ports an. Darüber hinaus können im Swarm verfügbare Secrets oder Configs einem Service zur Verfügung gestellt oder wieder entzogen werden. Alle Optionen hier zu beschreiben, würde den Rahmen des Artikels sprengen, die offizielle Dokumentation hilft Ihnen im Detail weiter. Das folgende Beispiel zeigt Ihnen, wie sich eine Environment-Variable FOO hinzufügen und sich der Ablauf eines konkreten Deployments beeinflussen lässt:

docker service update \
  --detach=false \
  --env-add FOO=bar \
  --update-parallelism=1 \
  --update-order=start-first \
  --update-delay=10s \
  --update-failure-action=rollback \
  proxy

Das Kommando sieht auf den ersten Blick sehr komplex aus. Letztlich dient es aber nur als Beispiel für einige Optionen, die Sie hinsichtlich des Updates auf Ihre Bedürfnisse zuschneiden können. In diesem Beispiel wird per –env-add die Variable in den Containern ergänzt. Das geschieht schrittweise über die Replicas hinweg (–update-parallelism=1), wobei temporär eine vierte Instanz gestartet wird, bevor eine alte Version gestoppt wird (–update-order=start-first). Zwischen jedem Taskupdate wird zehn Sekunden gewartet (–update-delay=10s) und im Fall eines Fehlers wird auf die vorherige Version zurückgerollt (–update-failure-action=rollback).

In einem Cluster mit Swarm-Managern und Workern sollten Sie vermeiden, dass ressourcenhungrige Tasks auf den Manager-Nodes laufen. Vermutlich wollen Sie auch nicht den Proxy auf demselben Node laufen lassen wie die Datenbank. Um solche Regeln abzubilden, erlaubt Docker Swarm das Konfigurieren von _Service constraints_. Diese Constraints formuliert der Entwickler mithilfe von Labels. Einige Labels vergibt Docker Swarm, darüber hinaus können Sie selbst an den Docker Engines, jedem Node, pro Service oder gar pro Task Labels setzen. Labels lassen sich sowohl beim docker service create als auch per docker service update für einen Service ergänzen oder entfernen. Labels an Services und Nodes lassen sich sogar ohne Unterbrechung des Tasks ändern. Ein Beispiel haben Sie oben schon als node.role==worker kennen gelernt, weitere Beispiele finden Sie in der offiziellen Dokumentation.

Stellen Sie sich nun vor, dass Sie nicht nur ein oder zwei Services pflegen müssen, sondern möglicherweise zehn oder zwanzig verschiedene Microservices. Jeder dieser Services müsste nun mittels der oben genannten Befehle deployt werden. Die Serviceabstraktion nimmt Ihnen das Verteilen der konkreten Replicas auf verschiedene Nodes ab. Einzelne Ausfälle werden automatisch korrigiert, und Sie können sich immer noch mit den gewohnten Befehlen einen Überblick über die Gesundheit Ihrer Container verschaffen. Wie Sie sehen, nehmen die Kommandozeilen dennoch eine unangenehme Länge an. Ich bin noch nicht darauf eingegangen, wie verschiedene Services miteinander zur Laufzeit kommunizieren können und wie Sie den Überblick über die Gesamtheit Ihrer Services behalten.

Inter-Service-Kommunikation

Zur Verknüpfung von Services stehen Ihnen wieder verschiedene Wege offen, von denen ich einen oben schon angedeutet habe: Docker bietet Ihnen sogenannte Overlay Networks, die wieder Node-übergreifend bzw. Node-ignorierend erlauben, Services anstelle von konkreten Containern oder Tasks anzusprechen. Soll der oben beispielhaft konfigurierte Proxy als Reverse Proxy für einen anderen Service fungieren, können Sie das mit den Kommandos aus Listing 1 erreichen.

docker network create \
  --driver overlay \
  app

docker service create \
  --detach=false \
  --name whoami \
  --constraint node.role==worker \
  --replicas 3 \
  --network app \
  emilevauge/whoami

docker service update \
  --detach=false \
  --network-add app \
  proxy

Nach der Erzeugung eines Overlay-Networks app wird ein neuer Service whoami in genau diesem Netzwerk angelegt. Danach wird der proxy aus obigem Beispiel ebenfalls in das Netzwerk aufgenommen. Die beiden Services können nun den jeweils anderen Service über den Servicenamen erreichen. Ports müssen für whoami nicht explizit publisht werden, sondern Docker macht die im Image per EXPOSE deklarierten Ports innerhalb des Netzwerks erreichbar. Im konkreten Fall lauscht der whoami-Service innerhalb des gemeinsamen Netzwerks auf Port 80.

Jetzt fehlt nur noch, den proxy so zu konfigurieren, dass eingehende Requests an den whoami-Service weitergeleitet werden. Der nginx lässt sich mit der Konfiguration aus Listing 2 als Reverse Proxy für den whoami-Service einrichten.

upstream backend {
  server whoami;
}

server {
  listen 80;

  location / {
    proxy_pass http://backend;
    proxy_connect_timeout 5s;
    proxy_read_timeout 5s;
  }
}

Das passende Dockerfile ist sehr einfach gehalten, denn es muss nur die individuelle Konfiguration zum Standard-Image ergänzen:

FROM nginx:alpine
RUN rm /etc/nginx/conf.d/*
COPY backend.conf /etc/nginx/conf.d/

Den Code finden Sie in dem anfangs schon erwähnten GitHub Repository wieder. Die folgenden Befehle bauen das individuelle nginx Image und laden es in die lokale Registry. Danach wird der bereits laufende nginx per Serviceupdate mit dem eben erzeugten Image ausgestattet:

docker build -t 127.0.0.1:5000/nginx -f nginx-basic/Dockerfile nginx-basic
docker push 127.0.0.1:5000/nginx

docker service update \
  --detach=false \
  --image registry:5000/nginx \
  proxy

Beim Serviceupdate fällt auf, dass der Image-Name anstelle von 127.0.0.1 nun registry als Repository-Host lautet. Das ist notwendig, weil das Image aus der Perspektive der Worker geladen werden soll und diese die lokale Registry nur unter dem Namen registry kennen. Der Manager kann allerdings den Registry-Hostnamen nicht auflösen, dadurch das Image nicht verifizieren und warnt deshalb auch im Rahmen des Serviceupdates vor potenziell zwischen den Workern abweichenden Images.

Nach erfolgreichem Update können Sie wieder per curl http://localhost:8080 prüfen, ob der Proxy erreichbar ist. Anstelle der nginx-Standardseite sollte nun aber die Response aus dem whoami-Service erscheinen. Diese Response sieht bei aufeinander folgenden Requests stets etwas anders aus, da Sie durch das Round-Robin-Verfahren von Docker immer auf den nächsten Task weitergeleitet werden. Am besten erkennen Sie das am geänderten Hostnamen oder der IP. Per docker service update –replicas 1 whoami oder docker service update –replicas 5 whoami können Sie bequem den Service hoch- oder herunterskalieren, während der Proxy im Zusammenspiel mit Docker stets eine der noch verfügbaren Instanzen benutzen wird.

Mehr zum Thema: Docker versus VM: Wie Container die heutige IT verändern

In Abbildung 1 sehen Sie eine Übersicht über den aktuellen Swarm mit drei Worker-Nodes und einem Manager. Die gestrichelten Pfeile folgen dabei dem Request auf http://localhost:8080 durch die beiden Overlay-Netzwerke ingress und app. Dabei landet der Request zunächst auf dem nginx-Task proxy.2, der dann als Reverse Proxy den Request an sein Upstream-Backend weiterreicht. Dieses steht genau wie der Proxy in mehreren Replicas zur Verfügung, sodass für den konkreten Request der Task whoami.3 auf Worker Nummer 3 angesprochen wird.

Docker Swarm: Abb. 1: Ein Request auf dem Weg durch Overlay-Netzwerke

Abb. 1: Ein Request auf dem Weg durch Overlay-Netzwerke

Sie haben nun erfahren, wie für bestehende Services ein unterbrechungsfreies Update möglich ist, auf sich ändernde Last per Einzeiler reagiert werden kann und wie mittels Overlay Network die Notwendigkeit wegfällt, interne Ports nach außen zu öffnen. Operative Details, wenn beispielsweise die Docker Engines, der Worker oder der Manager aktualisiert oder einzelne Nodes ausgetauscht werden sollen, sind ebenso bequem zu handhaben. Für diese Use Cases finden Sie in der Dokumentation die entsprechenden Hinweise. Ein Node kann beispielsweise per docker node update –availability=drain angewiesen werden, alle Tasks abzugeben. Docker kümmert sich dann darum, den Node praktisch leer zu räumen, sodass Sie ungestört und risikofrei Wartungsarbeiten durchführen können. Mit docker swarm leave und docker swarm join können Sie stets Worker und Manager entfernen bzw. hinzufügen. Die notwendigen Join-Tokens erhalten Sie von einem der Manager, indem Sie dort docker swarm join-token worker oder docker swarm join-token manager aufrufen.

Docker Stack

Wie bereits erwähnt, fällt es in einer wachsenden Servicelandschaft schwer, den Überblick zu behalten. In der Regel eignen sich Consul oder ähnliche Werkzeuge, um eine Art Registry zu pflegen, die Ihnen sogar mehr als nur eine Übersicht bietet. Werkzeuge wie Portainer bringen neben der Unterstützung für Docker Swarm passende Dashboards mit, die Ihnen einen grafischen Überblick über Ihre Nodes und Services geben.

Docker bietet Ihnen eine schlanke Alternative in Form von Docker Stack. Wie der Name schon suggeriert, geht diese Abstraktion über die einzelnen Services hinaus und beschäftigt sich mit der Menge Ihrer Services, die eng miteinander verzahnt oder abhängig voneinander sind. Die technologische Grundlage ist gar nicht so neu, denn sie verwendet viele Elemente von Docker Compose wieder. Ganz allgemein formuliert: Docker Stack verwendet das YAML-Format von Compose und ergänzt die für Swarm relevanten Eigenschaften für ein Service-Deployment. Als Beispiel finden Sie den Stack für die eben manuell eingerichteten Services unter nginx-basic/docker-stack.yml. Wenn Sie ihn anstelle der manuell eingerichteten Services ausprobieren wollen, müssen Sie vorher den proxy stoppen, um Port 8080 freizugeben. Die folgenden Befehle sorgen für einen sauberen Stand und starten den vollständigen Stack:

docker service rm proxy whoami
docker network rm app

docker stack deploy --compose-file nginx-basic/docker-stack.yml example

Der docker stack deploy-Befehl erhält per –compose-file die gewünschte Beschreibung des Stacks. Die Bezeichnung example dient einerseits als leicht merkbarer Verweis auf den Stack und intern auch als Mittel zum Namespacing der verschiedenen Services. Docker benutzt nun die Angaben in der docker-stack.yml, um praktisch das Äquivalent zu den docker service create …-Kommandos intern zu erzeugen und an die Docker Engine zu senden. Wirklich neu in der Konfigurationsdatei sind im Vergleich zu Compose nur die Blöcke unter deploy:, die wie schon erwähnt die Swarm-spezifischen Eigenschaften definieren. Constraints, Replicas und Updateverhalten sind dort symmetrisch zu den Kommandozeilenparametern hinterlegt. Die Dokumentation enthält Details und weitere Optionen, die je nach Anwendungsfall bei Ihnen relevant sein könnten.

Der praktische Nutzen der Stacks besteht nun darin, dass Sie die Konfiguration in Ihrem VCS einchecken können und somit auch eine vollständige und aktuelle Dokumentation zum Set-up aller zusammenhängenden Services haben. Änderungen reduzieren sich dann auf ein Editieren dieser Datei und dem wiederholten docker stack deploy –compose-file nginx-basic/docker-stack.yml example. Docker prüft bei der wiederholten Ausführung des Befehls, ob sich zwischen den YAML-Inhalten und den tatsächlich deployten Services Abweichungen ergeben haben und korrigiert sie entsprechend per internem docker service update. Sie erhalten dadurch eine gute Übersicht über Ihren Stack. Diese liegt versioniert direkt in der Nähe des Sourcecodes Ihrer Services und Sie müssen wesentlich weniger fehleranfällige Skripte pflegen. Da die Stackabstraktion eine rein clientseitige Implementierung darstellt, behalten Sie dennoch die volle Freiheit, per manuellem oder gescriptetem docker service … eigene Aktionen auszuführen.

Falls im Rahmen häufiger Serviceupdates das ständige Editieren der docker-stack.yml übertrieben erscheint, sollten Sie Variablenauflösung per Environment in Erwägung ziehen. Dazu ist im Beispielstack schon der Platzhalter NGINX_IMAGE vorgesehen. Hier der relevante Ausschnitt:

...
services:
  proxy:
    image: "${NGINX_IMAGE:-registry:5000/nginx:latest}"
...

Mit einer entsprechend vorbereiteten Umgebung können Sie ohne vorheriges Editieren der YAML-Datei ein anderes nginx Image deployen. Das folgende Beispiel ändert das Image für den Proxy wieder zurück auf das Standard-Image und aktualisiert den Stack:

export NGINX_IMAGE=nginx:alpine
docker stack deploy --compose-file nginx-basic/docker-stack.yml example

Der Deploy dauert nun wieder so lange, bis die einzelnen Instanzen aktualisiert sind. Danach sollte ein curl http://localhost:8080 wieder die nginx-Standardseite liefern. Die YAML-Konfiguration des Stacks bleibt damit stabil und wird nur mittels Environment-Variablen angepasst.

Die Auflösung der Platzhalter kann an beliebiger Stelle erfolgen. In der Praxis wäre es deshalb besser, anstelle des kompletten Images nur das Image-Tag variabel zu halten.

...
services:
  proxy:
    image: "nginx:${NGINX_VERSION:-alpine}"
...

Das Aufräumen eines Stacks ist sehr einfach mit docker stack rm example möglich. Achtung: dabei werden ohne weitere Rückfrage alle Services entfernt. Auf einem Produktionssystem ist der Befehl wohl eher als gefährlich einzustufen, macht aber den Umgang mit Services für lokale Set-ups und auf Test-Stages sehr bequem.

Wie erwähnt, sorgt der Stack nur mittels Label-Namespacing für einen Zusammenhalt verschiedener Services, arbeitet aber letztlich mit den gleichen Mechanismen wie die gewohnten docker service …-Befehle. Es steht Ihnen deshalb frei, einen initial per docker stack deploy eingerichteten Stack im Laufe des Betriebs per docker service update zu ergänzen.

Secrets und Service-Configs

Docker Services und Stack bieten Ihnen mehr als nur das Management von Tasks über verschiedene Nodes hinweg. Auch Secrets und Configs lassen sich über Docker Swarm einfacher verteilen und sind im Vergleich zu den unter https://12factor.net/ empfohlenen Environment-Variablen sicherer in genau den Containerdateisystemen hinterlegt, die von Ihnen autorisiert wurden. Prinzipiell benutzen Docker Secrets und Configs das gleiche Konzept: Sie legen zunächst zentral im Swarm via docker secret create … bzw. docker config create … entsprechende Objekte bzw. Dateien an, die von Docker intern abgelegt werden – Secrets werden dabei vorher verschlüsselt. Diese Objekte erhalten von Ihnen einen Namen, den Sie dann bei der Verknüpfung mit Services verwenden.

Aufbauend auf dem bisherigen Beispiel mit nginx und mit Auszügen aus der offiziellen Docker-Dokumentation können wir HTTPS-Unterstützung hinzufügen. Docker Swarm mountet die dazu notwendigen SSL-Zertifikate und Keys als Dateien in die Container. Secrets landen dabei aus Sicherheitsgründen nur in einer RAM-Disk. Zunächst benötigen Sie passende Zertifikate, die im Repository unter nginx-secrets/cert vorbereitet sind. Falls Sie die Zertifikate aktualisieren möchten, liegt ein geeignetes Script nginx-secrets/gen-certs.sh bereit.

Docker Swarm erlaubt pro Secret bis zu 500 KB Inhalt, der dann unter /run/secrets/ als Datei abgelegt wird. Secrets werden wie folgt angelegt:

docker secret create site.key nginx-secrets/cert/site.key 
docker secret create site.crt nginx-secrets/cert/site.crt

Ähnlich wie Secrets können auch Configs gepflegt werden. Am Beispiel der individuellen nginx-Konfiguration vom Anfang des Artikels werden Sie gleich sehen, dass das eigens gebaute Image nicht mehr notwendig sein wird. Für die Konfiguration des nginx verwenden wir die Konfiguration unter nginx-secrets/https-only.conf und legen sie mithilfe von Docker Config an:

docker config create https.conf nginx-secrets/https-only.conf

Zuerst definiert der Entwickler den gewünschten Namen der Config. Danach gibt er den Pfad bzw. Dateinamen an, dessen Inhalt Docker dann im Swarm ablegt. Mit docker secret ls und docker config ls können Sie die eben angelegten Objekte wiederfinden. Jetzt fehlt nur noch die Verknüpfung des Service mit den jetzt im Swarm bekannten Secrets und der Config. Beispielhaft können Sie einen neuen Service wie folgt starten. Beachten Sie dabei, dass das offizielle nginx Image ausreicht:

docker service create \
  --detach=false \
  --name nginx \
  --secret site.key \
  --secret site.crt \
  --config source=https.conf,target=/etc/nginx/conf.d/https.conf \
  --publish 8443:443 \
  nginx:alpine

Im Browser können Sie das Ergebnis unter https://localhost:8443 bewundern, allerdings müssen Sie wegen der selbst ausgestellten Certification Authority des Serverzertifikats einige Warnungen überspringen. Etwas einfacher geht es in diesem Fall per Kommandozeile:

curl --cacert nginx-secrets/cert/root-ca.crt https://localhost:8443

Secrets und Configs werden auch in Docker Stack unterstützt. Passend zu den manuellen Befehlen wird auch innerhalb der YAML-Datei auf oberster Ebene das Secret oder die Config deklariert und ggf. angelegt, während die Verknüpfung mit den gewünschten Services dann genau dort definiert wird. Unser vollständiges Beispiel sieht aus wie in Listing 3 und kann wie folgt deployt werden:

cd nginx-secrets
docker stack deploy --compose-file docker-stack.yml https-example
version: "3.4"

services:
  proxy:
    image: "${NGINX_IMAGE:-nginx:alpine}"
    networks:
      - app
    ports:
      - "8080:80"
      - "8443:443"
    deploy:
      placement:
        constraints:
          - node.role==worker
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: any
    configs:
      - source: https.conf
        target: /etc/nginx/conf.d/https.conf
    secrets:
      - site.key
      - site.crt
  whoami:
    image: emilevauge/whoami:latest
    networks:
      - app
    deploy:
      placement:
        constraints:
          - node.role==worker
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure

networks:
  app:
    driver: overlay

configs:
  https.conf:
    file: ./https-backend.conf

secrets:
  site.key:
    file: ./cert/site.key
  site.crt:
    file: ./cert/site.crt

Spannend wird es, wenn Sie Secrets oder Configs aktualisieren möchten. Docker kann keine generische Lösung zur Aktualisierung der Containerdateisysteme anbieten: Einige Prozesse erwarten bei der Aktualisierung der Konfiguration ein Signal wie SIGHUP, andere erlauben keinen Reload, sondern müssen neu gestartet werden. Docker schlägt daher vor, bei geänderten Secrets oder Configs diese unter einem neuen Namen anzulegen und dann per docker service update –config-rm –config-add … atomar gegen die alten Versionen auszutauschen.

Stateful Services und Volumes

Wenn Sie Datenbanken per Docker Service einrichten wollen, werden Sie unweigerlich zu der Frage gelangen, wie die Daten einen Container-Restart überleben. Volumes sind Ihnen wahrscheinlich schon ein Begriff, um genau diese Herausforderung zu adressieren. In der Regel werden Volumes sehr eng mit einem konkreten Container verbunden, sodass beide praktisch eine Einheit darstellen. In einem Swarm mit potenziell wandernden Containern lässt sich diese enge Bindung nicht mehr voraussetzen – im Zweifel wird ein Container also auf einem anderen Node gestartet, auf dem entweder das benötigte Volume gänzlich fehlt, der leer ist oder gar veraltete Daten enthält. Ab Datenmengen in einer Größenordnung von mehreren Gigabytes ist das ständige Kopieren oder Verschieben von Volumes an andere Nodes nicht mehr sinnvoll. Natürlich haben Sie auch hier je nach Umgebung mehrere Lösungsoptionen.

Die Grundidee besteht darin, einen passenden Volume-Driver auszuwählen, der sich dann um das Verteilen der Daten auf verschiedene Nodes oder auch an einen zentralen Ort kümmert. Docker erlaubt daher, beim Anlegen von Volumes den gewünschten Driver auszuwählen und ggf. zu konfigurieren. Es gibt bereits eine Fülle von Volume-Plug-ins, die der Docker Engine neue Volume Driver bekannt machen. Die Dokumentation zeigt eine umfangreiche Auswahl dieser Plug-ins. Unter Umständen halten Sie die spezifischen Plug-ins für NetApp oder vSphere in Ihrer Umgebung für geeignet. Alternativ kann ich Ihnen das REX-Ray-Plug-in für eine nähere Betrachtung empfehlen, da es einen guten Ruf in der Community genießt und recht plattformneutral konzipiert ist.

Lesen Sie auch: 12 Docker Tipps: So schaffen Sie den Einstieg in die Container-Welt

Da die Konfiguration und Verwendung der verschiedenen Volume-Plug-ins und -Driver zu spezifisch für Ihr konkretes Umfeld ist, werde ich hier auf eine detaillierte Beschreibung verzichten. Bitte beachten Sie, dass Sie mindestens Docker 1.13 oder in manchen Fällen sogar Version 17.03 einsetzen müssen. Die notwendigen Docker-spezifischen Befehle lassen sich üblicherweise auf zwei Zeilen reduzieren, die beispielhaft für vSphere in Listing 4 aufgeführt sind.

docker plugin install \
  --grant-all-permissions \
  --alias \
  vsphere vmware/docker-volume-vsphere:latest
docker volume create \
  --driver=vsphere \
  --name=MyVolume \
  -o size=10gb \
  -o vsan-policy-name=allflash

Neben der Installation des Plug-ins unter einem Alias vsphere wird im zweiten Schritt das gewünschte Volume MyVolume angelegt. Ein Teil der Konfiguration wird im Dateisystem abgelegt, während Sie zum Zeitpunkt der Volume-Erzeugung per -o individuelle Parameter konfigurieren können.

Proxy mit echter Docker Swarm Integration

Am Beispiel von nginx war es sehr einfach, statisch die bekannten Upstream-Services zu definieren. Je nach Anwendungsfall und Umgebung benötigen Sie vielleicht mehr Dynamik und wollen die Varianz der Services häufiger verändern. Das bequeme Hinzufügen neuer Services ist gerade in der heutigen Microservices-Umgebung ein häufiger Anwendungsfall. Die statische Konfiguration eines nginx oder HAProxies fühlt sich dann leider etwas unbequem an. Zum Glück gibt es schon bequeme Alternativen, von denen Træfik wohl herausragend und mit hervorragender Docker-Integration ausgestattet ist.

Äquivalent zum ersten Stack mit nginx finden Sie unter den gleichen Stack mit Træfik anstelle des nginx. Træfik benötigt Zugriff auf das Docker-Engine-API eines Swarm-Managers, um seine Konfiguration auf neue oder geänderte Services entsprechend dynamisch anpassen zu können. Træfik wird daher per Deployment Constraints auf den Manager-Nodes platziert. Da Træfik manche servicespezifischen Einstellungen nicht erraten kann, wird mittels Labels an den jeweiligen Services die relevante Konfiguration hinterlegt. Sie sehen dazu im Beispiel, wie die Netzwerkkonfiguration (Port und Netzwerk) festgelegt wird, sodass das Routing auch dann noch den Service erreicht, wenn er in mehreren Netzwerken stecken sollte. Außerdem wird durch traefik.frontend.rule definiert, für welche eingehenden Requests die Pakete an den whoami-Service weitergereicht werden sollen. Neben dem Routing basierend auf Request Headern lassen sich auch Pfade und andere Elemente eines Requests als Kriterien heranziehen. Welche Möglichkeiten es noch gibt, sehen Sie in der Træfik-Dokumentation. Darüber hinaus gibt es weitere Details zur Integration mit Docker Swarm im Swarm User-Guide. Dem Beispielstack fehlt noch die Konfiguration für HTTPS-Unterstützung. Da Træfik native Integration mit Let’s Encrypt mitbringt, verweise ich dazu nur auf die passenden Beispiele.

Fazit

Docker Swarm bietet im Detail noch wesentlich mehr Facetten, die je nach Kontext mehr oder weniger relevant werden können. Funktionen wie Scheduled Tasks bzw. Pendants zu Cronjobs als Services werden zwar oft verlangt, sind aber mit Bordmitteln aktuell nur schwer abzubilden. Dennoch ist Docker Swarm im Vergleich zu anderen Containerorchestrierern noch übersichtlich und schlank gehalten, sodass Sie nur eine geringe Einstiegshürde überwinden müssen und schnell zu sinnvollen Ergebnissen gelangen.

Gerade für Continuous Deployment übernimmt Docker Swarm viele Details und die konfigurierbare Fehlerbehandlung, sodass Sie weniger eigenen Deployment-Code pflegen müssen und nebenbei auch noch rudimentäres Load Balancing geschenkt bekommen. Einige Aspekte wie Autoscaling lassen sich mittels Orbiter ergänzen und an eigene Bedürfnisse anpassen. Dadurch, dass Docker Swarm wenig invasiv auf eine bestehende Infrastruktur wirkt, bleibt das Risiko in Sachen Experimente mit Docker Swarm relativ gering. Es macht auf jeden Fall Spaß, im Swarm mitzuschwimmen – ob per Kommandozeile, YAML-Datei oder auch direkt per Engine-API.

Geschrieben von
Tobias Gesellchen
Tobias Gesellchen
Tobias Gesellchen ist als Softwareentwickler bei der Hypoport AG in Berlin tätig. Seine Schwerpunkte sind Java und JavaScript, während er sich aktuell dem Aufbau einer Continuous-Deployment-Pipeline auf Basis von Gradle und Docker widmet.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: