Richtig interessant wird der Einsatz von Containern typischerweise erst, wenn mehrere Hosts zur Verfügung stehen, auf denen ein sogenannter Orchestrator Container starten, stoppen und migrieren kann. Dies ermöglicht es beispielsweise, Funktionen wie Loadbalancing und Hochverfügbarkeit von Diensten auf der Basis von Containern zu realisieren. Für Test-Setups lässt sich dieses Szenario mit virtuellen Maschinen aufbauen, die als Host-Systeme für die Container dienen. Eine preisgünstige Alternative besteht darin, Raspberry-Pi-Rechner zu verwenden, um einen entsprechenden realen Cluster aufzubauen. Diesen Ansatz stellen wir im Folgenden näher vor.
Da alle Orchestrierungslösungen wenigstens aus einem Master- und einem Worker-Knoten bestehen, brauchen Sie mindestens zwei Minirechner. Wer etwa den Ausfall eines Nodes oder die Migration von Containern von einem Node auf einen anderen simulieren möchte, muss drei oder mehr Raspberries in seinen Einkaufskorb legen.
Zur Vernetzung der Maschinen gibt es mehrere Optionen. So bietet der Raspberry Pi 3 neben der schon bei den älteren Modellen vorhandenen Ethernet-Buchse auch WLAN. Eine Alternative besteht darin, die Rechner über USB-over-Ethernet zu vernetzen (mit USB/OTG-Port, den nur der Raspberry Zero besitzt). Wir haben uns für die einfachste und üblicherweise am wenigsten fehlerträchtige Methode entschieden, die Boards über Ethernet und einen Mini-Switch zu vernetzen. So funktioniert beispielsweise der virtuelle Netzwerk-Interface-Typ "macvlan" des Linux-Kernels nicht mit Wireless Devices.
Je nach Netzwerktopologie kann der Switch zur Verbindung der Rechner schon genügen. Wir verwenden zusätzlich einen günstigen WLAN-Router/Switch, der den Uplink des Clusters ins Internet bereitstellt und es darüber hinaus ermöglicht, sich über ein WLAN mit dem Cluster-Netzwerk zu verbinden, zum Beispiel von einem Laptop aus. Solche Geräte sind im Handel schon ab etwa 20 Euro zu haben. Die Stromversorgung übernimmt ein passender USB-Hub mit sechs Ports (Bild 1).
Da die Raspberry-Rechner keine SATA-Schnittstelle besitzen, bleiben als Massenspeicher nur die typischen Mini-SD-Karten. Wir haben hier 32 GByte große SD-Karten von Samsung verwendet, die eine vergleichsweise gute Performance bieten. Kleinere Karten mit acht oder 16 GByte genügen in der Regel auch, aber letztlich ist der preisliche Unterschied nicht so groß, dass sich hier das Sparen lohnt. Alles in allem belaufen sich die Kosten für unseren Cluster damit auf etwa 280 Euro.
Wer im Internet nach Anleitungen für die Installation von Docker auf Raspberry sucht, stößt schnell auf die "Docker Pirates ARMed with explosive stuff", die dazu eine eigene Linux-Distribution namens Hypriot verwenden. Seit geraumer Zeit gibt es aber auch Docker-Support für die offizielle Raspberry-Debian-Distribution namens Raspbian [1], die neuer ist als Hypriot, und die wir deshalb bevorzugen.
Wie beim Raspberry üblich, wird das Betriebssystem von einem PC oder Mac aus installiert, indem man das heruntergeladene Image von dort auf die Micro-SD-Karte schreibt. Dies kann entweder auf der Commandline mit dem Tool "dd" passieren oder mit einem grafischen Frontend wie Etcher [2], das für Windows, Linux und macOS zur Verfügung steht. Für unsere Zwecke genügt das Image von Raspbian Stretch Lite, das keine grafische Oberfläche mitbringt.
Da wir außerdem möglichst darauf verzichten wollen, jeden Node unseres Clusters an einen Bildschirm anzuschließen und mit Keyboard und Maus auszustatten, um sie manuell einzurichten, legen wir die Netzwerkeinstellungen vor der Installation fest. Dazu ist es zunächst nötig, die Zip-Datei des Raspbian-Images zu entpacken. Anschließend mounten wir die darauf enthaltenen Partitionen als Loopback-Devices auf einem Linux-System. Die Loop-Devices für die Partitionen legt das Tool "losetup" mit dem entsprechenden Parameter automatisch an:
# losetup -P /dev/loop0 2017-09-07-raspbian-stretch-lite.img
In dem Image sind zwei Partitionen vorhanden: eine Boot-Partition und die Root-Partition des Debian-Systems. Die Boot-Partition mounten Sie mit dem folgenden Befehl:
# mount /dev/loop0p1 /mnt/
Nun finden Sie die Dateien der Boot-Partition unter "/mnt". Aus Sicherheitsgründen ist der Secure-Shell-Daemon per Default deaktiviert. Automatisch gestartet wird er, wenn auf der Boot-Partition eine Datei namens "ssh" mit beliebigem Inhalt existiert. Der folgende Befehl legt sie an und bindet die Boot-Partition wieder aus:
# touch /mnt/ssh # umount /mnt
Nun fehlt noch die Netzwerkkonfiguration der Raspberry-Rechner. Dabei verwendet Raspbian nicht die von Debian gewohnten Konfigurationsdateien "/etc/network/interfaces", sondern startet automatisch den DHCP-Daemon, in dessen Konfiguration die statische IP-Adresse eingetragen werden muss. Wer einen Router mit DHCP betreibt und auf diesem die MAC-Adressen der Pi-Rechner einträgt, kann auf diesen Schritt gegebenenfalls verzichten.
Wir verwenden jedenfalls statische IP-Adressen und mounten dazu erst einmal die Root-Partition des Raspbian-Images auf dem Linux-Rechner, bevor wir die Konfigurationsdatei des DHCP-Servers editieren:
# mount /dev/loop0p2 /mnt/ # vi /mnt/etc/dhcpcd.conf
Suchen Sie in der Konfiguration nach dem Abschnitt für die statische IP-Adresse und setzen Sie dort etwa die folgenden Zeilen ein:
interface eth0 static ip_address=10.0.0.1/24 static routers=10.0.0.254 static domain_name_servers=8.8.8.8
Jetzt binden Sie die Partition wieder aus und entfernen die Loop-Devices, bevor Sie das geänderte Image auf die SD-Karte schreiben:
# umount /mnt # losetup -d /dev/loop0 # sudo dd if=2017-09-07-raspbian-stretch-lite.img of=/dev/mmcblk0 bs=4M status=progress
Wenn Sie nun den Raspberry mit der Karte booten und an den Switch angeschlossen haben, sollten Sie sich über die Adresse 10.0.0.1 per SSH einloggen können (Login "pi", Passwort "raspberry"). Wiederholen Sie nun den zweiten Schritt (mit der Root-Partition) für die restlichen Raspberry-Rechner und passen Sie jeweils die IP-Adresse an. In unserem Fall sind anschließend alle Nodes unter den Adressen 10.0.0.1-4 erreichbar.
Statt jeden Rechner einzeln als Docker-Host einzurichten, verwenden wir im Weiteren das Konfigurationsmanagement-Tool Ansible, da bis zu einem bestimmten Punkt das Setup der Nodes identisch ist. Als Erstes legen wir eine "Inventory"-Datei an, die die von Ansible gemanagten Nodes enthält, etwa unter dem Namen "hosts", mit folgendem Inhalt:
[cluster] 10.0.0.1 10.0.0.2 10.0.0.3 10.0.0.4
Jetzt können Sie die grundlegende Funktion von Ansible testen:
$ ansible -i hosts cluster -m ping -u pi -k
Hinter den Kulissen loggt sich Ansible dabei als User "pi" auf dem Node ein, wozu es beim Aufruf das Passwort abfragt ("-k"). Funktioniert alles, bekommen Sie für jeden Host eine Meldung ähnlich der folgenden zu sehen:
10.0.0.2 | SUCCESS => { "changed": false, "ping": "pong" }
Um die Verwendung von Ansible und die Installation von Software zu vereinfachen, legen wir einen Benutzer mit SSH-Key an, der sich ohne Passwort einloggen darf. Außerdem wird er der Gruppe "docker" hinzugefügt, damit er später Docker ohne Root-Rechte bedienen darf. Diese Änderungen nehmen wir über das Playbook in Listing 1 vor. Um das Passwort direkt im Playbook angeben zu können (nicht unbedingt eine Security-Best-Practice), wird der Filter "password_hash" verwendet, der die Installation des Python-Moduls "passlib" voraussetzt (»sudo -H pip install passlib
«
). Abgespielt wird das Playbook mit dem Kommando "ansible-playbook", das über den Schalter "-b" (become) per sudo Root-Rechte erlangt. Ein Passwort ist für sudo nicht nötig, da die Default-Konfiguration von Raspbian dafür kein Passwort verlangt.
$ ansible-playbook -i hosts -bk -u pi setup-users.yml
Listing 1: setup-users.yml
--- - hosts: cluster remote_user: pi become: yes become_method: sudo tasks: - name: create new user user: name: oliver shell: /bin/bash groups: docker,sudo update_password: always password: "{{ 'pi' | password_hash('sha512') }}" - name: add key to authorized_keys authorized_key: user: oliver state: present key: "{{ lookup('file', '/home/oliver/.ssh/id_rsa.pub') }}"
Wer einen Cluster betreibt, muss sicherstellen, dass die Nodes zeitlich synchronisiert sind. Dazu verwenden wir NTP, das wir per Ansible und mit einer Community-Rolle konfigurieren, die Ansible Galaxy im lokalen Verzeichnis installiert:
$ ansible-galaxy install -p roles geerlingguy.ntp
Das entsprechende Playbook, das auch die Timezone auf "Europe/Berlin" setzt, ist in Listing 2 zu sehen.
Listing 2: setup-time-ntp.yml
--- - hosts: cluster remote_user: oliver become: true tasks: - name: set timezone timezone name: Europe/Berlin roles - geerlingguy.ntp
Viele Softwarepakete setzen außerdem eine funktionierende Zuordnung von Hostnamen zu IP-Adressen voraus. Da wir aber keinen ausgewachsenen Nameserver betreiben wollen, pflegen wir per Ansible ein Hosts-File, das wir auf alle beteiligten Knoten verteilen.
Listing 3: hosts
[cluster] 10.0.0.1 hostname=pi1 10.0.0.2 hostname=pi2 10.0.0.3 hostname=pi3 10.0.0.4 hostname=pi4
Auch dies lässt sich per Ansible einigermaßen einfach umsetzen. Wir erweitern dazu das Inventory-File um eine Variable, in der der Hostname steht, den der Node bekommen soll (Listing 3). Eine Schleife im Ansible-Playbook stellt sicher, dass alle am Cluster beteiligten Knoten auch im Hosts-File landen. Nützlich ist darüber hinaus die Playbook-Anweisung "linein", die sicherstellt, dass die entsprechende Zeile in einer Datei vorhanden ist, und nur dann eine Änderung vornimmt, wenn sie fehlt. Das Playbook in Listing 4 spielen sie so ab:
$ ansible-playbook -i hosts -bK set-hostnames.yml
Wenn Sie sich jetzt auf einem Node einloggen, sollten Sie in der Datei "/etc/hosts" alle beteiligten Rechner sehen und von jedem Knoten per Hostnamen alle anderen erreichen können.