Aufbau eines Docker Swarm Clusters mit Docker Machine auf KVM-basierten virtuellen Maschinen

In den letzten Wochen habe ich mich recht intensiv weiter mit Docker beschäftigt. Das Ergebnis vorweg – grundsätzlich funktioniert es schon recht gut, doch es gibt noch einige – nennen wir sie „Kinderkrankheiten“, die in der Praxis unter Umständen zu Problemen, oder nein, besser „Herausforderungen“ führen können.

Die Idee von Docker Swarm hat etwas für sich – anstatt Docker nur auf einem einzelnen Host zu benutzen, lassen sich mehrere Hosts zu einem Cluster verbinden, wobei die Verwaltung dieses „Swarm“ auf dieselbe Art und Weise geschieht wie bei einem einzelnen Docker-Host. Da die API von Swarm und Host identisch sind, kann man die gewohnten Docker-Kommandos einsetzen, um Container im Cluster zu starten, stoppen etc..

Der einfachste Weg, einen solchen Cluster, wobei ich Swarm hier beinahe synonym benutzen will, einzurichten, ist die Verwendung des Tools namens „docker-machine“. Mit docker-machine lassen sich virtuelle Maschinen bei verschiedenen Cloud-Providern kreieren, darunter AWS EC2, DigitalOcean, Google Compute Engine, Microsoft Azure, um nur die bekanntesten zu nennen. Bei der Einrichtung der VMs können direkt entsprechende Parameter übergeben werden, um den Cluster mittels Docker Swarm zu aktivieren. Dabei übernimmt docker-machine alle notwendigen Arbeiten, d.h. zunächst wird die VM selbst konfiguriert und gestartet, anschließend die Docker Engine installiert, SSH-Keys und TLS-Zertifikate für den Zugang eingerichtet, während der letzte Schritt daraus besteht, die Container für den Swarm Master (swarm-agent-master) sowie die Swarm Nodes (swarm-agent) mit den entsprechenden Parametern einzurichten und zu starten. Zuvor sollte noch ein Key-Value-Store für die Speicherung der cluster-übergreifenden Konfiguratonsdaten eingerichtet werden, praktischerweise läuft dieser auch innerhalb des Clusters, für erste Experimente reicht jedoch auch ein einzelner Container, dazu später mehr.

Mein Anliegen bestand somit darin, einen Swarm-Cluster aufzubauen, um damit experimentieren, lernen und testen zu können. Dieser Swarm-Cluster sollte auf einigen KVM-basierten VMs installiert werden. Zwar hätte ich auch AWS EC2 dazu nutzen können, jedoch hatte ich die VMs bereits auf verschiedenen Servern eingerichtet, einerseits bei einem Provider, andererseits auf den Rechnern zu Hause, daher erschien mir diese kostenneutrale Lösung eher geeignet. Die in zahlreichen Tutorials beschriebene Möglichkeit, Virtualbox auf der lokalen Maschine zu nutzen, erschien mir weniger sinnvoll. Ein Notebook läuft meist nicht permanent, Aussagen über Stabilität der Dienste lassen sich somit kaum treffen, noch dazu wäre es auf eine Ressourcenfrage hinaus gelaufen. Diese Tutorials mit Virtualbox halte ich daher auch persönlich für ungeeignet und praxisfern, um ein Gefühl für den Umgang mit Docker Swarm zu erhalten.

Vorbereitungen

Um beliebige Hosts mit docker-machine einzurichten, dient der „generic“ Driver. Dieser setzt lediglich einen SSH-Zugang voraus, bedarf allerdings einiger Vorbereitungen. Zunächst muss er Host bzw. die virtuelle Maschine bereits existieren, im folgenden Beispiel handelt es sich um bestehende KVM-basierte VMs, die auf zwei Servern eingerichtet worden sind. Das Tool docker-machine loggt sich letztlich auf den (virtuellen) Hosts ein, installiert Docker Engine, sorgt für die Konfiguration und startet die notwendigen Docker Container. Bei Verwendung der Cloud-Lösungen wie AWS, Google Compute Engine etc. sorgt hingegen docker-machine für das Anlegen des Hosts, so dass die genannten Vorarbeiten nicht notwendig sind.

Die folgenden Beispiele beziehen sich auf virtuelle Hosts unter Ubuntu 15.10, insofern mit systemd als Init-Prozess. Damit docker-machine den Zugriff erhält, muss auf den Hosts ein entsprechender User mit SSH-Zugang existieren. Zunächst sollte ein SSH-Key für den Zugang auf die zu verwendenden Hosts generiert werden:


Man kann sich docker-machine als eine Art „Fernbedienung“ vorstellen. Auf dem Client werden docker-machine sowie docker installiert. Der Aufruf von docker-machine konfiguriert nun die zum Cluster gehörenden Docker Hosts. Beim Aufruf von docker mit entsprechenden Parametern oder Umgebungsvariablen wird der Cluster angesprochen, im Gegensatz zum üblichen Betrieb auf der lokalen Maschine. Falls man Windows- oder Mac-Clients nutzt, kann dort die Docker-Toolbox installiert werden, die alle notwendigen Programme bereits mitbringt. Ich nutze als Client eine der VMs unter Linux, daher werden dort docker-machine sowie docker installiert, um die anderen VMs im Cluster anzusprechen. Der SSH-Key wird ebenfalls auf dem Client angelegt, anschließend für den Zugriff zunächst auf alle Hosts kopiert. Der SSH-Key sollte dabei ohne Passwort eingerichtet werden. Theoretisch müsste es zwar auch mit Passwort bzw. Passworteingabe oder ssh-agent funktionieren, jedoch wurde die Passworteingabe bei mir jedes Mal nicht akzeptiert, während der ssh-agent überhaupt nicht genutzt wurde. Es empfiehlt sich daher, den Client gut abzusichern, da von diesem aus der gesamte Cluster bedient wird.

Sobald der SSH-Key vorliegt, wird er auf alle beteiligten Hosts kopiert und dort eingerichtet:


 

Der User muss sudo-Rechte besitzen, die ihm ohne Passwort-Eingabe gewährt werden. Wenn der entsprechende User in der Gruppe „sudo“ ist, kann die „sudoers“-Datei wie folgt geändert werden. Einloggen, dann mittels „visudo“-Kommando als letzte Zeile einfügen:


Alternativ kann auch direkt und ausschließlich dem User die Erlaubnis erteilt werden, etwa dem User „docker“:

Ubuntu-Eigenheiten

Wie weiter oben erwähnt, wird Ubuntu 15.10 auf allen Hosts verwendet. Leider ist das Zusammenspiel von Docker Engine mit Systemd noch etwas suboptimal. Zwar wird die Docker Engine von Systemd gestartet, jedoch werden etwaige Konfigurationsoptionen, die in der Datei /etc/default/docker gespeichert sind, nicht genutzt. Der Grund ist, dass in der Konfigurationsdatei /lib/systemd/system/docker.service schlicht und einfach eine Option fehlt. Ein Ausschnitt aus der docker.service-Datei:


Damit wird docker gestartet, ohne dass die Möglichkeit besteht, weitere Optionen wie das Lesen von Einstellungen aus einer Konfigurationsdatei zu ermöglichen.

Dabei muss erwähnt werden, dass diese Einschränkung auch nur bei der Verwendung des „generic“-Drivers aufgefallen ist, d.h. bei den Cloud-Lösungen wie AWS EC2 etc. richtet docker-machine die Docker Engine wie erwartet korrekt ein.

Jedoch gibt es zur Lösung einen Wortaround, dazu wird eine Datei „/etc/systemd/system/docker.service.d/ubuntu.conf“ angelegt mit folgendem Inhalt:


 

Gewissermaßen besteht hier auch ein Henne-Ei-Problem. Eigentlich sollte docker-machine für die Einrichtung von Docker auf dem Host sorgen. Jedoch führt das zu dem beschriebenen Problem, so dass Docker nach der Installation ohne die notwendigen Parameter für Ports etc. gestartet wird, so dass sich docker-machine wiederum nicht mit dem Host verbinden kann und quasi ins Leere läuft. Um dies zu verhindern, muss der Host bzw. Ubuntu die genannte Konfigurationsdatei kennen. Dem Systemd muss die Datei wiederum kenntlich gemacht werden mit


Danach kann der Docker-Daemon neu gestartet werden (falls er bereits auf dem jeweiligen Host existiert):


Um die Docker-Engine grundsätzlich zum Laufen zu bringen, habe ich daher zunächst Docker einzeln auf den Hosts installiert, dann die o.g. zusätzliche Konfigurationsdatei angelegt und Docker neu gestartet. Im Einzelnen:


Die genannten Schritte sind auf allen Hosts durchzuführen, d.h. Docker Engine sollte laufen, inkl. Änderung der Konfiguration, und der User auf dem Client sollte per SSH und Key Zugriff sowie sudo-Rechte haben.

Erster Host mit docker-machine für Consul (und weitere Verwirrungen)

Der erste Host, der mit docker-machine eingerichtet wird, soll noch nicht im Swarm-Cluster enthalten sein. Wiederum so ein wenig von Henne-Ei-Problem, denn auf der Maschine soll Consul als Key-Value-Store verwendet werden. Daneben kann Consul für Service Discovery eingesetzt werden, hauptsächlich dient Consul jedoch zur Speicherung der Swarm- und späteren Netzwerk-Konfiguration. Um somit die Hosts des Docker Swarms überhaupt einrichten zu können, ist Consul als Voraussetzung notwendig, dabei wäre es suboptimal, wenn Consul danach erst im Swarm laufen würde… Nicht unerwähnt bleiben sollen Alternativen zu Consul, etwa etcd, oder der im Beispiel auf den Docker-Seiten genannte Service Discovery Token, im Beispiel beschränke ich mich jedoch auf Consul.

Für den Test genügt zunächst ein Consul-Node, im Produktivbetrieb sollte hingegen ein Consul-Cluster eingesetzt werden, da es sich um eine durchaus kritische Infrastruktur-Komponente handelt. Denn um etwa einen Host dem Swarm hinzuzufügen, ein Overlay-Netzwerk einzurichten oder auch einen Host aus dem Swarm zu entfernen, ist zwangsläufig ein Update des Key-Value-Stores notwendig, so dass ohne Consul keine Veränderungen beim Docker Swarm stattfinden können. Consul könnte auch als normaler Daemon ohne Zuhilfenahme von Docker auf den jeweiligen dafür vorgesehenen Hosts laufen, dies setzt aber eine etwas aufwändigere und unflexiblere Konfiguration voraus.

Der Cluster soll letztlich aus folgenden Hosts bestehen:

  • lausen  – Consul, nicht im Swarm
  • miltitz – Docker-Swarm-Master
  • lindenau, kaditz, tolkewitz als Docker-Swarm-Nodes
  • connewitz als Client, d.h. von hier aus werden die anderen VMs bedient

Sicherlich wäre es besser „sprechende“ Namen zu verwenden, aber die Guests, d.h. VMs auf meinen Servern werden allesamt nach Stadtteilen von Städten (Hosts) benannt. Und nur für den Docker-Swarm-Test wollte ich daran auch nichts ändern.

Um eine Maschine mit docker-machine mittels generic-Driver einzurichten, sieht das Kommando wie folgt aus:


Die Optionen sollten selbsterklärend sein, als Driver „generic“, mein Username, private SSH-Key, die IP-Adresse sowie der Name der VM. Ok, im besten Fall erscheint nach einiger Zeit die Meldung „…up and running…“. Jedoch war genau dies nicht der Fall, sondern vielmehr endete der Versuch in Timeouts mit der Meldung „Daemon not responding yet: dial tcp 192.168.10.64:2376: connection refused“.

Was war geschehen? Das Tool docker-daemon hat sich mit dem Host verbunden, dort zunächst einige Zeit benötigt, um per apt-get auszuführen, die Docker-Engine einzurichten und anschließend mitsamt neuer Konfiguration (in /etc/default/docker) den Docker Daemon neu zu starten. Dabei sollte der Daemon so eingerichtet werden, dass er auf Port 2376 (TLS-verschlüsselt) erreichbar gewesen wäre. Als ich mir daraufhin die Konfigurationsdatei angesehen habe, sah diese wie folgt aus:


Auf den ersten Blick schien dies ok, aber beim Restart von Docker wurden die Einstellungen einfach nicht berücksichtigt. Dies lag nicht an dem Start selbst, siehe oben, denn die Datei wurde eingelesen durch die neu hinzugefügte systemd-Konfigurationsdatei.

Das Problem lag vielmehr darin, dass der Docker-Daemon die so generierte Datei nicht verstanden hat, und zwar wegen der Zeilenenden. Wenn man nun sämtliche Optionen in einer Zeile platzierte, ließ sich der Daemon problemlos neu starten und konnte die Einstellungen berücksichtigen. Nun sah die Datei wie folgt aus:


In einem weiteren Versuch war eine weitere Option hinzu gekommen, und zwar „–storage-diver aufs“. Woher docker-machine das genommen hat, war mir nicht klar, denn laut docker info wurde der Storage Driver btrfs aufgrund des darunter liegenden Dateisystems genutzt. Nach Entfernen der Option lief somit auch Docker wieder.

Immerhin – nach Änderung der Datei und Neustart von Docker auf der Maschine wurde von docker-machine der neue Host akzeptiert:


An dieser Stelle möchte ich den Hinweis geben, dass es sich um die Version 0.5 von docker-machine handelte. Dank der steten Weiterentwicklung mögen die beschriebenen Probleme aktuell evtl. bereits behoben sein. Mit der jüngsten Version 0.6 von docker-machine ist glücklicherweise auch das Kommando „provision“ hinzu gekommen, mit dem es möglich ist, die Provisionierung neu zu starten, ohne dass eine komplette Maschine neu erstellt werden muss.

Zunächst konnte Consul jedoch erfolgreich auf der nun erreichbaren Maschine eingerichtet werden:


Consul bietet dabei eine rudimentäre Web-Oberfläche auf Port 8500, diese sollte nach Start von Consul erreichbar sein. Auch hier gilt, dass beim Produktivbetrieb ein entsprechender Schutz per Firewall o.ä. vorgesehen werden muss. Bei AWS lassen sich z.b. die Security Groups entsprechend konfigurieren, so dass der Port 8500 zwar im eigenen VPN verfügbar, nach außen hin jedoch nicht erreichbar ist.

Damit waren auch die Vorbereitungen für den ersten Swarm-Node, d.h. den Swarm-Master abgeschlossen.

Docker Swarm Master

Als nächstes soll der Swarm Master eingerichtet werden. Das ist nichts anderes als eine Maschine im Cluster, auf dem die Verwaltungsinstanz des Docker Swarm läuft, natürlich innerhalb eines Containers. Über den Master werden alle Aktionen im Swarm gesteuert, insbesondere Starten und Stoppen der Container.

Vorausgesetzt, Consul läuft wie beschrieben, lautet das Kommando zur Einrichtung des Swarm Masters wie folgt:


Die ersten Optionen sind bereits bekannt, neu sind die Parameter für den Swarm selbst sowie alle weiteren. Letztlich sorgen sie dafür, dass auf der VM, auf der dann der Docker-Daemon eingerichtet wurde, zwei zusätzliche Container, und zwar für den Swarm Master und Swarm Node gestartet werden. Dabei wird der Consul-Server als Parameter für Service Discovery genannt, des Weiteren werden zusätzliche Optionen (–engine-opt) dem Docker-Konfigurationsfile /etc/default/docker hinzugefügt. Zuletzt wird der Name des Swarms angegeben – hier analog zum Server-Namen „miltitz“ genannt.

Das hätte funktionieren können, wenn, ja, wenn nicht das bereits beschriebene Problem bzgl. der Zeilenumbrüche in der Konfiguratonsdatei /etc/default/docker das Einlesen der neuen Optionen beim Restart des Docker-Daemons verhindert hätte. Der Aufruf von docker-machine endete letztlich in einem Timeout nach etlichen Verbindungsversuchen zum Docker-Daemon. Daraufhin hätte man die Datei korrigieren und Docker neu starten können. Leider bestand der letzte Schritte von docker-machine jedoch darin, die Swarm-Container inkl. der notwendigen Optionen zu starten, was danach manuell hätte erfolgen müssen, wodurch die einfache Bedienung mittels docker-machine wieder ad absurdum geführt worden wäre.

Die Lösung bestand nun darin, sich während docker-machine noch lief und sich langsam dem Timeout näherte, auf dem Host einzuloggen, die Optionen in /etc/default/docker in einer Zeile zu platzieren und den Docker-Daemon neu zu starten. Daraufhin konnte sich docker-machine auf dem Client mit dem Host verbinden und alles Weitere einrichten, d.h. damit wurden auch die Swarm-Container gestartet.

Diese etwas kritische Aktion war letztlich erfolgreich:


Auch hierbei gilt natürlich, dass durch die Weiterentwicklung der Tools derartige Hürden möglicherweise bereits längst erledigt sind. Zudem dürfte das Kommando „–provision“ hilfreich sein und zur Lösung beitragen, momentan habe ich dies aber noch nicht mit dem „generic“ Driver getestet, sondern nur auf AWS angewendet.

Hinzufügen der Docker Swarm Nodes

Das Hinzufügen der Nodes geschieht analog – nur ohne die Option „–swarm-master“. Im Beispiel:


Auch hier habe ich mich während docker-machine auf den Docker-Damon des Hosts wartete, eingeloggt, Konfigurationsdatei geändert und Docker neu gestartet. Falls das nicht möglich gewesen wäre, hätte der jeweilige Swarm-Agent-Container auf dem Host auch manuell gestartet werden können:


Für den Swarm-Master-Container:


Dabei ist zu beachten, dass auf dem Swarm-Master sowohl Swarm-Master-Container als auch Swarm-Node-Container laufen müssen, das Ergebnis wäre dann:


 

Alle weiteren Nodes wurden analog mit in den Swarm aufgenommen. Der Aufruf von docker-machine ls zeigt nun folgende Liste:


Hier wurde docker-machine in der Version 0.6 verwendet, was zusätzlich die auf den Hosts vorliegende Docker-Version anzeigt.

Bei der Einrichtung wurden zudem noch Labels übergeben:


Damit ist es möglich, einzelne Nodes zu „taggen“. Diese Tags können von Docker berücksichtigt werden, somit lassen sich Container explizit auf einzelne Hosts verteilen, etwa wenn für Datenbank-Hosts andere Instanzen genutzt werden sollen als für Web-Anwendungen, beides jedoch im selben Swarm-Cluster laufen soll.

Das Ergebnis all jener Kommandos ist jedenfalls ein funktionierender Docker Swarm:


Somit können Docker-Container im gesamten Swarm gestartet werden, dazu genügt es, den Swarm-Master mit dem –swarm Parameter anzusprechen, wie etwa:


Die Methode per Environment-Variablen empfand ich als etwas unübersichtlich. Dabei werden zunächst Environment-Variablen erzeugt und der laufenden Shell zugewiesen:


Danach bezieht sich der docker-Client immer auf den gesamten Swarm, bzw. auf den im Environment befindlichen Host.

Fazit

Die Einrichtung eines Docker Swarms mit docker-machine über den „generic“ Driver fühlte sich noch sehr nach Beta-Version an. Das ist auch völlig in Ordnung, die Versionsnummer von docker-machine liegt noch weit unter der 1.0, und wie anhand der neuen Option –provision zu sehen ist, kommen immer mehr sinnvolle Features hinzu. Es bleibt dennoch zu hoffen, dass nicht nur die bekannten und neuen Cloud-Provider besser integriert werden, sondern auch der generic-Driver nicht auf dem aktuellen Stand stehen bleibt.

Sobald der Cluster einmal lief, war er bisher auch stabil, einzig ein längerer Ausfall von Consul blieb nicht ohne Spuren (dazu mehr in einem weiteren Blog-Artikel). Wie schon erwähnt, ist Consul eine kritische Infrastruktur und sollte unbedingt in geclustert werden.

Und richtig sinnvoll wird der Swarm-Cluster auch erst mit den inzwischen nicht mehr ganz so neuen Netzwerk-Features, z.B. dem Overlay-Network. Sofern der Swarm-Cluster einmal läuft, ist die Einrichtung sehr einfach, und damit macht es erst richtig Spaß. Den hebe ich mir heute jedoch für einen weiteren Artikel auf…

Tags:
Kategorie:

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.