27.05.2022, von Christian Glahn
Dieser Beitrag erklärt die die manuelle Installation eines einfachen Edge-Computing-Clusters mit Ubuntu Linux, LXD/LXC zur Systemvirtualisierung, OVS für die Netzwerkvirtualisierung und Docker Swarm für die eigentlichen Edge-Anwendungen. Die Installation hat viele Stolperfallen, für die im Internet keine Lösungen beschrieben wurden. Stattdessen finden sich viele Beiträge, die behaupteten, dass sich ein solches Setup nicht umsetzen lässt. Dieser Beitrag zeigt schrittweise wie die Installation trotzdem gelingt.
Dieser Blog-Beitrag ist der Erste in einer Serie, die die verschiedenen Konzepte unseres MultiMICO Edge Computing Clusters beschreibt. Mit den weiterführenden Konzepten zur Einrichtung eines Edge Clusters befassen sich andere (zukünftige) Beiträge in dieser Serie.
Motivation
Eine unserer Herausforderungen beim Entwickeln von Prototypen von Micro-Services für das Edge-Computing ist eine flexible Arbeitsumgebung. Wir haben uns dazu ein kleines Experimental-Cluster eingerichtet, damit wir verschiedene Konzepte und Technologien ausprobieren und weiterentwickeln können.
Unser kleines Edge-Computing-Cluster besteht aus 3 Intel NUC 10 Computern. Jeder dieser Computer hat einen Intel i7 Prozessor mit 6 Prozessorkernen, 64GB Ram und eine 2TB SSD. Das ist genug Leistung, um verschiedene Virtualisierungsexperimente durchzuführen. Normalerweise würden wir solche Hardware direkt für einen Docker Swarm oder ein Kubernetes Cluster verwenden. Für eine Experimentalumgebung wollen wir aber nicht für jedes Experiment das ganze Cluster neu aufsetzen. Stattdessen wollen wir unsere Experimentalumgebungen als virtuelle Infrastruktur laufen lassen und diese bei Bedarf starten, unterbrechen oder löschen können.
Eine klassische Virtualisierung im Sinne von VMWare VSphere oder OpenStack ist für unsere Computer viel zu anspruchsvoll. Bei diesen Virtualisierungsumgebungen wird jeder Server vollständig virtualisiert und/oder es werden zusätzliche Computer für die Cluster-Verwaltung benötigt. Solche Systeme sind sehr komplex und nicht leicht zu installieren. Es lässt sich leicht nachvollziehen, dass solche komplexen Ansätze sehr viel Leistung für die Virtualisierungsinfrastruktur verbrauchen, die dann der eigentlichen Experimentalumgebung nicht mehr zur Verfügung steht. Wir haben deshalb nach einer weniger anspruchsvollen Lösung gesucht und mit LXD/LXC gefunden.
LXC steht für Linux Containers und wird Lexi ausgesprochen. LXC stellt nutzt Sicherheits- und Virtualisierungsfunktionen des Linux-Kernels aus, um “virtuelle Systeme” bereitzustellen, bei denen allerdings keine Hardware simuliert wird. Anstatt Hardware zu simulieren, isoliert der Linux-Kernel einzelne Bereiche so, dass sie sich wie eigene Systeme verhalten. Diese Systeme laufen nur “zufällig” auf der gleichen Hardware. Diese Technik bezeichnet man als Containerisierung. LXC-Container werden als System-Container bezeichnet, weil diese Container ein ganzes Betriebssystem virtualisieren.
Im Gegensatz zur konventionellen Virtualisierung gibt es bei LXC-System-Containern jedoch eine Einschränkung: Alle LXC-Container müssen Linux-Systeme sein. Es ist deshalb nicht möglich, mit LXC Windows oder MacOS zu virtualisieren. Praktisch hat diese Einschränkung keine Bedeutung für uns, weil Windows und MacOS sich als Edge-Computing-Systeme nur sehr bedingt eignen.
Die zweite Komponente von LXD/LXC ist LXD und wird Lex-Di ausgesprochen. Mit LXD können verschiedene Virtualisierungstechniken auf dem gleichen Computer kombiniert und über eine einheitliche Schnittstelle verwaltet werden. LXD stellt dafür Befehle zur Verfügung, die die Komplexität der unterschiedlichen Virtualisierungsformen vereinheitlicht und vereinfacht. Damit liessen sich auch andere Betriebssysteme auf dem gleichen Rechner betreiben.
Für das MultiMICO Edge-Computing-Cluster stellt LXD/LXC die Plattform für die Virtualisierung von Anwendungs-Container-Systemen. Zu diesen Systemen gehören Docker (Swarm) und Kubernetes.
Bei der Vorbereitung unseres Clusters ist uns aufgefallen, dass die Verbindung der verschiedenen Container-Technologien auf der gleichen Hardware nicht ganz einfach ist und dass die Dokumentation im Internet erhebliche Lücken hat bzw. hatte. Mit diesem Blog-Post fasse ich unsere recht umständliche Reise zu einem funktionsfähigen Docker-Swarm-Cluster auf Basis von LXC-Containern zusammen.
Begriffe
- Host-System: Rechner mit Anschlüssen und Ein-/Ausschalter und Linux-Betriebssystem.
- System-Container: Virtuelles System auf einem Host-System, dass sich ansonsten wie ein Host-System verhält.
- Cluster: Verbund von Host-Systemen und/oder System-Containern, die die Arbeit untereinander aufteilen.
- Docker-Container oder Anwendungs-Container: Virtuelles System zum Ausführen einer einzelnen Anwendung.
- OVS (Open Virtual Switch): Virtueller Netzwerk-Switch, der alle virtualisierten Komponenten mit dem echten Netzwerk verbindet.
Anmerkung
Diese Anleitung ist beim Installieren komplexer Infrastrukturen ineffizient. Sie zeigt die minimalen manuellen Schritte, zur Bereitstellung des Basissystems. Mein Team verwendet diese Anleitung nicht, sondern hat weite Teile automatisiert. Das ist aber ein Thema für einen anderen Blog-Beitrag.
Diese Anleitung fokussiert auf Docker Swarm, weil es sich um einen leicht erlernbaren Orchestrator für Docker-Container handelt. Das ist nicht als Wertung für Docker Swarm und gegen Kubernetes zu verstehen.
Voraussetzungen
Die Anleitung setzt voraus, dass Netzwerkkonfigurationen, Firewall-Einstellungen angepasst, die wichtigen SSH-Schlüssel bei GitHub registriert sowie gegebenenfalls DNS- und DHCP-Einträge vorgenommen wurden.
Ebenfalls wird vorausgesetzt, dass die Public-Key-Anmeldung an einen SSH-Server und die Bedienung von Linux-Systemen auf der Kommandozeile bekannt sind.
Die Arbeitsschritte
- Ubuntu (22.04) downloaden
- Installationsmedium erstellen
- Installation von Ubuntu Server auf allen Rechnern
- Installation zusätzlicher Pakete auf allen Rechnern
- Konfiguration von OVS
- Konfiguration von LXD/LXC
- Vorbereiten der System-Container für den Docker Swarm
- Bereitstellen der System-Container für den Docker Swarm
- System-Container zu einem Docker Swarm verbinden
1. Ubuntu downloaden
In diesem Beispiel verwenden wir Ubuntu 22.04 als Betriebssystem für alle Systeme. Weil unsere NUCs einen Intel i7 Prozessor haben, verwenden wir die amd64
Architektur.
wget https://releases.ubuntu.com/22.04/ubuntu-22.04-live-server-amd64.iso
2. Installationsmedium erstellen
Für die Installation wird ein USB-Stick erstellt, wie es in der Ubuntu Installationsanleitung beschrieben wird.
3. Installation von Ubuntu auf allen Rechnern
Vom Installationsmedium wird ein minimales Ubuntu mit OpenSSH-Server
auf allen Rechnern installiert. Die Installationen sind unabhängig voneinander und können theoretisch gleichzeitig durchgeführt werden.
- Es hilft, wenn auf allen Rechnern der gleiche Nutzer mit dem gleichen Passwort erstellt wird.
- Es hilft, wenn die eigenen offentlichen SSH-Schlüssel via GitHub auf dem neuen Rechner installiert werden (Punkt
Import SSH Keys
im Installationsprogramm) - Es ist wichtig, dass ausser dem OpenSSH-Server keine zusätzlichen Pakete vom Installationsprogramm installiert werden. Solche Pakete laufen später ausschliesslich in unseren System-Containern.
Nach der Installation werden alle Host-Systeme neu gestartet.
4. Installation zusätzlicher Pakete auf allen Rechnern
Die folgende Software sollte anschliessend auf allen Rechnern installiert werden.
sudo apt update && sudo apt upgrade
sudo apt install -y openvswitch-switch-dpdk libosinfo-bin
Optional kann noch das Programm petname
installiert werden. Damit lassen sich schnell neue Rechnernamen erzeugen.
sudo apt install -y petname
5. Konfiguration von OVS
OVS ist für unser LXD/LXC-Cluster wichtig, weil unsere virtuellen Server sonst nicht richtig ins Netz kommen und sich nicht über die Hardware-Hosts hinweg verbinden können. OVS ersetzt das standardmässig von LXD verwendete Linux Bridged Network, mit dem Docker Swarm leider nicht funktioniert.
Wichtig Dieser Schritt muss auf allen Host-Systemen wiederholt werden.
Die OVS-Konfiguration kann nicht über eine SSH-Verbindung eingerichtet werden, weil wir die Netzwerkschnittstelle austauschen. Die Konfiguration muss deshalb direkt auf dem jeweiligen Host erfolgen.
# neuen virtuellen Switch erstellen
ovs-vsctl add-br ovs0
# das Host-System mit dem Switch verbinden
# Erstellt ein virtuelles mgnt0 Interface für das Host-System
ovs-vsctl add-port ovs0 mgnt0 -- set interface mgnt0 type=internal
# Den virtuellen Switch mit dem physischen Netzwerk verbinden.
# Achtung! Falls man mit SSH verbunden ist, bricht die Verbindung ab.
ovs-vsctl add-port ovs0 eno1
# Ubuntu's netplan Konfiguration anpassen und unser neues mgnt0 Interface einfügen
sed -i -e "s/dhcp4: true/dhcp4: no/" \
-e "/^ version: 2/i \\ \\ \\ \\ mgnt0:\\n\\ \\ \\ \\ \\ \\ dhcp4: true\\n\\ \\ \\ \\ \\ \\ macaddress: 00:16:3e:f0:06:42" \
/etc/netplan/00-installer-config.yaml
# Netplan-Konfiguration übernehmen
netplan apply
WICHTIG Die MAC-Adresse 00:16:3e:f0:06:42
in der netplan
-Konfiguration muss für jedes Host-System angepasst werden und eindeutig sein. Das Tool create-hwmac.sh
aus unserem MultiMICO Cluster Repository erzeugt beliebig viele “eindeutige” MAC-Adressen im Adressraum des XEN-Hypervisors. Das ist in unserem Fall in Ordnung, weil wir diesen Hypervisor in unserem Netzwerk nicht verwenden.
Nach dem Befehl netplan apply
steht der Rechner wieder über das Netz bereit. Alle weiteren Schritte können deshalb über eine SSH-Verbindung abgewickelt werden. Das macht das Kopieren und Einfügen der Befehle leichter.
Achtung In Unternehmensnetzwerken kann es sein, dass (unbekannte) virtuelle MAC-Adressen nicht akzeptiert werden. Ein Host bekommt dann vom DHCP-Server keine IP-Adresse zugewiesen. Hier helfen die Netzwerkadministratoren weiter.
6. Konfiguration von LXD/LXC
LXD/LXC ist auf Ubuntu 22.04 immer installiert. Wir müssen es nur noch konfigurieren. Dazu erstellen wir auf allen Host-Systemen eine sog. preseed.yaml
-Datei mit dem folgenden Inhalt. Dazu kann ein beliebiger Editor verwendet werden (z.B. vi
oder nano
).
# preseed.yaml
config: {}
cluster: null
networks: []
storage_pools:
- config:
source: /var/snap/lxd/common/lxd/storage-pools/default
description: ""
name: default
driver: btrfs
- config:
source: /var/snap/lxd/common/lxd/storage-pools/docker
description: docker-storage
name: docker
driver: btrfs
profiles:
- name: default
config: {}
description: "Standard Konfiguration"
devices:
eth0:
nictype: bridged
name: eth0
parent: ovs0
type: nic
root:
path: /
pool: default
type: disk
- name: docker
config:
security.nesting: true
security.syscalls.intercept.mknod: true
security.syscalls.intercept.setxattr: true
linux.kernel_modules: ip_tables,ip6_tables,iptable_nat,iptable_mangle,bridge,nf_nat,br_netfilter,xfrm_user,xt_conntrack,xt_MASQUERADE,bonding,overlay,netlink_diag,ip_vs,ip_vs_dh,ip_vs_ftp,ip_vs_lblc,ip_vs_lblcr,ip_vs_lc,ip_vs_nq,ip_vs_rr,ip_vs_sed,ip_vs_sh,ip_vs_wlc,ip_vs_wrr
limits.memory.swap: false
limits.memory: 16GB
limits.cpu: 2
description: "Universelle Docker Swarm Konfiguration"
devices:
eth0:
nictype: bridged
name: eth0
parent: ovs0
type: nic
root:
path: /
pool: docker
type: disk
Die Werte in den Zeilen limits.memory
und limits.cpu
sollte für die eigene Host-Ausstattung und Anwendungsszenarien angepasst werden. Die hier vorgeschlagenen Limite folgen der Logik von e2-highmem-2
-Instanzen der Google Compute Engine (GCE) und reichen auch für anspruchsvollere Container-Dienste.
Anschliessend initialisieren wir LXD mit dieser Konfiguration.
cat preseed.yaml | lxd init --preseed
7. Vorbereiten der System-Container für den Docker Swarm
Wir erstellen auf allen Host-Systemen eine Datei mit dem Namen cloud-init.cfg
. Diese Datei sollte den folgenden Inhalt haben:
#cloud-config
hostname: "${HOSTNAME}"
users:
- name: multimico
passwd: "${CRYPTPASSWD}"
groups:
- sudo
- adm
- plugdev
- cdrom
- docker
ssh_import_id:
- "gh:${GITHUBNAME}"
lock_passwd: false
shell: /bin/bash
network:
version: 2
ethernets:
eth0:
dhcp: yes
apt:
preserve_sources_list: true
sources:
docker:
source: "deb [arch=amd64] https://download.docker.com/linux/ubuntu jammy stable"
keyid: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88
package_update: true
package_upgrade: true
package_reboot_if_required: true
packages:
- docker-ce
- docker-ce-cli
- containerd.io
Die Datei cloud-init.cfg
wird von Ubuntu dazu verwendet, um die wichtigsten Konfigurationen des System-Containers automatisch vorzunehmen. In unserem Beispiel weist diese Konfiguration einen neuen System-Container an, den Systemnutzer multimico
einzurichten und Docker zu installieren. Unser Systemnutzer wird ausserdem den Gruppen sudo
und docker
zugewiesen, damit wir das System später leichter administrieren können. Ausserdem weisen wir die Installation mit ssh_import_id
an, SSH-Schlüssel für die Passwort-freie Anmeldung von GitHub zu laden.
Mit dieser cloud-init
-Datei stellen wir sicher, dass alle System-Container identisch konfiguriert sind.
8. Bereitstellen der System-Container für den Docker Swarm
Mit der Konfiguration können wir die System-Container bereitstellen.
Damit wir feste IP-Adressen vergeben können, legen wir die MAC-Adressen vorher fest. Wir können später einen System-Container vollständig ersetzen und sind sicher, dass der Ersatz-Container die gleiche IP-Adresse wie das alte System erhält.
Wir benennen unsere Systemcontainer wie folgt. In Klammern stehen die verwendeten MAC-Adressen für die virtuellen Netzwerk-Interfaces der System-Container.
docker1
(00:16:3e:4B:49:53
)docker2
(00:16:3e:C1:87:51
)docker3
(00:16:3e:EE:A0:46
)docker4
(00:16:3e:E2:95:2C
)docker5
(00:16:3e:78:C2:C7
)
Achtung Falls die MAC-Adressen für die Hosts in Schritt 5 zentral registriert werden mussten, dann müssen auch die MAC-Adressen für die System-Container vorher registriert werden.
Die Container docker1
und docker4
laufen auf Host 1. Die Container docker2
und docker5
laufen auf Host 2. Der Container docker3
läuft auf Host 3.
Unser Passwort ist in diesem Beispiel f00b4rista
. Selstverständlich muss es auf einem Produktivsystem angepasst werden.
Jeweils der erste Befehl (export GITHUBNAME=phish108
) muss so angepasst werden, dass anstatt von phish108
der gewünschte GitHub-Nutzer eingetragen ist, von welchem die Anmeldeschlüssel für die passwortfreie Anmeldung importiert weden sollen. Nur so gelingt die Anmeldung als Systemnutzer über das Netzwerk.
Auf Host 1 führen wir die folgenden Befehle aus:
export GITHUBNAME=phish108
export HOSTNAME=docker1 CRYPTPASSWD=$( echo -n "f00b4rista" | openssl passwd -6 -stdin )
lxc init -p docker ubuntu:22.04 $HOSTNAME
echo "$(cat cloud-init.cfg | envsubst )" | lxc config set $HOSTNAME user.user-data -
lxc config set $HOSTNAME volatile.eth0.hwaddr '00:16:3e:4B:49:53'
lxc start $HOSTNAME
export HOSTNAME=docker4
lxc init -p docker ubuntu:22.04 $HOSTNAME
echo "$(cat cloud-init.cfg | envsubst )" | lxc config set $HOSTNAME user.user-data -
lxc config set $HOSTNAME volatile.eth0.hwaddr '00:16:3e:E2:95:2C'
lxc start $HOSTNAME
Auf Host 2 führen wir die folgenden Befehle aus:
export GITHUBNAME=phish108
export HOSTNAME=docker2 CRYPTPASSWD=$( echo -n "f00b4rista" | openssl passwd -6 -stdin )
lxc init -p docker ubuntu:22.04 $HOSTNAME
echo "$(cat cloud-init.cfg | envsubst )" | lxc config set $HOSTNAME user.user-data -
lxc config set $HOSTNAME volatile.eth0.hwaddr '00:16:3e:C1:87:51'
lxc start $HOSTNAME
export HOSTNAME=docker5
lxc init -p docker ubuntu:22.04 $HOSTNAME
echo "$(cat cloud-init.cfg | envsubst )" | lxc config set $HOSTNAME user.user-data -
lxc config set $HOSTNAME volatile.eth0.hwaddr '00:16:3e:78:C2:C7'
lxc start $HOSTNAME
Auf Host 3 führen wir die folgenden Befehle aus:
export GITHUBNAME=phish108
export HOSTNAME=docker3 CRYPTPASSWD=$( echo -n "f00b4rista" | openssl passwd -6 -stdin )
lxc init -p docker ubuntu:22.04 $HOSTNAME
echo "$(cat cloud-init.cfg | envsubst )" | lxc config set $HOSTNAME user.user-data -
lxc config set $HOSTNAME volatile.eth0.hwaddr '00:16:3e:EE:A0:46'
lxc start $HOSTNAME
Je nach Netzwerkbandbreite sind die System-Container nach 1-5 Minuten startklar und auf jedem dieser Systeme läuft automatisch ein eigener Docker-Dienst.
Wir können uns nun über SSH mit den System-Containern verbinden.
9. System-Container zu einem Docker Swarm verbinden
Abschliessend müssen wir nur noch die System-Container zu einem Docker Swarm verbinden. Dazu müssen wir mindestens einen Manager festlegen. Diese Rolle kann jeder unserer fünf System-Container übernehmen. Um es einfach zu machen, wird docker1
unser Manager-Knoten.
Auf dem System docker1
führen wir deshalb den folgenden Befehl aus:
docker swarm init
Das Ergebnis sieht dann wie folgt aus.
Swarm initialized: current node (jxq17wttcxceai04mktfofzwj) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-XXX-ZENSIERT-XXX 192.168.1.128:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
Die eingerückte Zeile mit docker swarm
am Anfang kopieren wir. Es ist möglich, dass diese Zeile im Terminal auf zwei Zeilen umgebrochen wurde. Es muss unbedingt die ganze Zeile kopiert werden! Diesen Befehl führen wir anschliessend in den anderen System-Containern aus.
Wir prüfen den Docker Swarm abschliessend mit:
docker node ls
Das Ergebnis wird unsere fünf virtuellen Docker-Knoten zeigen.
Jetzt haben wir einen funktionierenden Docker Swarm in LXC-Containern. 🥳