Docker Swarm Cluster auf Openmediavault

Als Grundlage für mein Homelab habe ich ein Proxmox Cluster installiert, der Hochverfügbar eingerichtet ist. Dazu sind drei Cluster-Nodes installiert, die einen gemeinsamen Speicher über Ceph für die Virtuellen Maschinen (VMs) haben. Damit können VMs im Falle eines Neustarts zwischen den Proxmox Nodes ohne Verzögerung hin und her wandern.

Da ich Docker Container nutzen möchte, benötige ich zumindest eine VM. Wird diese VM jedoch im Rahmen von regelmäßigen Updates neugestartet, sind alle Docker Container im Rahmen dieses Updates nicht erreichbar. Ein Docker Swarm Cluster kann hierfür eine Lösung sein. Weiterhin ist das Loadbalancing auf die drei physikalischen Knoten mit nur einer Docker VM nicht möglich. Auch dafür kann der Docker Swarm Cluster eine Lösung darstellen.

Table of Contents

    Ich schreibe diese Anleitung in erster Linie für mich als „Installationsdokumentation“, damit ich bei eventuellen späteren Neuinstallationen eine Gedächtnisstütze habe. Es kann deswegen gut sein, dass manche Teile durch Updates in Zukunft nicht mehr korrekt sind.

    Meine Quellen waren für diese Installation im Wesentlichen:

    Openmediavault Installation

    Openmediavault basiert auf Debian, hat aber zusätzlich noch eine schöne Weboberfläche, über die die Administration vereinfacht wird. Insbesondere wenn bestehende persistente Docker Verzeichnisse verschoben werden sollen, ist es einfach, einen SMB/CIFS Share zu erstellen und dort einfach alles drauf zu kopieren. Zusätzlich ist es einfacher neue Festplatten einzuhängen oder bestehende zu vergrößern.

    Für die VM habe ich folgende Einstellungen gewählt:

    • 4 Cores, 1 Socket
    • 16G RAM
    • OS-Festplatte: 30G
    • Zusätzliche Festplatte für microceph: 100G
    • Eine Netzwerkkarte

    Die Installation geht recht einfach vonstatten, nachdem die VM mit der Openmediavault ISO gebootet ist:

    • Sprachen-Einstellungen: Deutsch, Deutschland, Deutsch
    • Rechnername: docker-swarm-01 (und natürlich 02 und 03 für die anderen zwei Nodes)
    • Domainnamen vergeben
    • Root Passwort vergeben
    • Spiegelserver in der Nähe auswählen
    • Gerät für Bootloader-Installation auswählen
    • Reboot & CD auswerfen

    Jetzt kann man sich im Webinterface auf dem Server anmelden mit den Default Login-Daten. User: admin – Password: openmediavault. Das wird als erstes geändert im Menü. Rechts oben auf das User-Icon klicken und „Passwort ändern“ auswählen. Neues Passwort eintippen und fertig.

    Updates werden einfach übers Webinterface gestartet:

    System -> Aktualisierungsverwaltung -> Aktualisierungen -> Pfeil runter mit der „Nummer“ klicken -> Bestätigen. Warten. Konfigurationsänderungen bestätigen. Reboot.

    Jetzt noch eine statische IP vergeben für das System:

    Netzwerk -> Schnittstellen -> Interface bearbeiten:

    • IPv4
    • Methode: Statisch
    • Adresse: Freie Adresse im Heimnetzwerk, beispielsweise 10.0.1.11 und für die anderen zwei Nodes hinten die 12 und die 13
    • Netzmaske: 255.255.255.0
    • Gateway: Sollte bekannt sein, üblicherweise der Router. Beispielsweise: 10.0.1.1
    • DNS Server: Sollte bekannt sein, üblicherweise ebenfalls der Router. Beispielsweise 10.0.1.1
    • Ausstehende Konfigurationsänderungen anwenden
    • Es scheint einzufrieren, einfach die neu konfigurierte IP im Browser aufrufen. Die IP wurde schon umgestellt.

    Installation von Docker und Docker Compose

    Zuerst installiere ich kleinere Helferlein:

    apt install qemu-guest-agent htop snap snapd -y
    

    Nun geht es nach der offiziellen Docker Dokumentation weiter:

    # Add Docker's official GPG key:
    apt update
    apt install ca-certificates curl
    install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
    chmod a+r /etc/apt/keyrings/docker.asc
    
    # Add the repository to Apt sources:
    echo \
      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
      $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
      tee /etc/apt/sources.list.d/docker.list > /dev/null
    apt update
    

    Den Erfolg des Befehls kann man sich mit dem nachfolgenden Befehl anschauen. Hier sollte der Eintrag zum Repository von Docker zu sehen sein:

    nano /etc/apt/sources.list.d/docker.list
    

    Dann wird Docker installiert:

    apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    

    Den Erfolg der Installation von Docker kann man auch wieder überprüfen. Hierzu wird einfach ein „hello-world“ image heruntergeladen und gestartet. Als Ergebnis gibt es eine Bestätigungsmeldung und der Container beendet sich wieder.

    docker run hello-world
    

    Wiederholen für Node 2 und Node 3

    Den bisherigen Teil der Anleitung für die zwei weiteren Nodes durchführen, damit alle auf dem gleichen Zustand sind. Danach kann man mit Swarm loslegen.

    Docker Swarm installation

    Auf Node 1 starten wir die Initialisierung vom Swarm cluster mit folgendem Befehl:

    docker swarm init --advertise-addr 10.0.1.10
    

    Als Ergebnis wird der Join-Command angezeigt mit dem dafür notwendigen Token. Den Join-Command kopiert man und tippt ihn in die Node 2 und 3 ein.

    docker swarm join --token <<<YOURTOKEN>>> 10.0.1.11:2377
    

    Damit werden die zwei hinzugefügten Nodes „worker“ im Swarm. Das wird nun dem Node 1 bekannt gemacht, indem man im Node 1 eintippt:

    docker node promote docker-swarm-02
    docker node promote docker-swarm-03
    docker node ls
    

    Der letzte Befel listet alle aktuellen Hosts auf.

    Virtuelle IP-Adressen

    Damit die Docker Container nach außen mit einer IP Adresse sprechen können und zwar unabhängig vom jeweiligen Docker Host auf dem sie laufen, wird nun eine Virtuelle IP Adresse erstellt. Dazu gibt es da Paket keepalived, das installiert wird:

    apt install keepalived -y
    

    Auf Node 1 wird keepalived konfiguriert:

    nano /etc/keepalived/keepalived.conf
    

    Und folgender Inhalt eingefügt (Achtung, der Interface Name kann abweichen!). Dabei werden unter „unicast_peer“ die IP-Adressen der anderen Hosts eingetragen, unter „virtual_ipadress“ die zusätzliche virtuelle IP. Das auth_pass ändern:

    vrrp_instance VI_1 {
      state MASTER
      interface ens192
      virtual_router_id 51
      priority 120
      advert_int 1
      authentication {
        auth_type PASS
        auth_pass 123456789Password
      }
      unicast_peer {
        10.0.1.12
        10.0.1.13
      }
      virtual_ipaddress {
        10.0.1.15
      }
    }
    

    Für die Nodes 2 und 3 wird ebenfalls die Konfigurationsdatei erstellt:

    nano /etc/keepalived/keepalived.conf
    

    Und ein ähnlicher Inhalt eingetragen. Geädert ist „state“ – hier nur als BACKUP. Die Priority muss niedriger sein als die von Node 1. Die unicast peers sind jeweils die zwei anderen Nodes, also nicht die Node, auf der gerade die Datei angelegt wird. Bswp. für Node 2:

    vrrp_instance VI_1 {
      state BACKUP
      interface ens192
      virtual_router_id 51
      priority 110
      advert_int 1
      authentication {
        auth_type PASS
        auth_pass 123456789Password
      }
      unicast_peer {
        10.0.1.11
        10.0.1.13
      }
      virtual_ipaddress {
        10.0.1.15
      }
    }
    

    Und für Node 3 beispielsweise (Priority nochmals niedriger als bei Node 2):

    vrrp_instance VI_1 {
      state BACKUP
      interface ens192
      virtual_router_id 51
      priority 100
      advert_int 1
      authentication {
        auth_type PASS
        auth_pass 123456789Password
      }
      unicast_peer {
        10.0.1.11
        10.0.1.12
      }
      virtual_ipaddress {
        10.0.1.15
      }
    }
    

    Dann wird der Service keepalived auf allen drei nodes gestartet und für Neustarts aktiviert:

    systemctl start keepalived
    systemctl enable keepalived
    systemctl status keepalived
    

    Jetzt kann man die neue IP Adresse pingen und erhält eine Antwort.

    ping 10.0.1.15
    

    Gemeinsamer Speicher mit Ceph

    Gemeinsamer Speicher kann hier recht einfach über Ceph – genauer microceph realisiert werden. Microceph ist deutlich einfacher zu installieren als Ceph selbst. Es wird auf allen drei Nodes installiert:

    export PATH=$PATH:/snap/bin
    snap install microceph
    snap refresh --hold microceph
    

    Mit dem hold Befehl wird die aktuelle Version gepinnt, sodass Updates nur dann ausgeführt werden, wenn das aktiv angestoßen wird. Auf Node 1 wird Ceph initialisiert und die Nodes 2 und 3 hinzugefügt:

    microceph cluster bootstrap
    microceph status
    microceph cluster add docker-swarm-02
    microceph cluster add docker-swarm-03
    

    Auf Node 2 und Node 3 verwendet man die über den add erzeugten Token, um dem Cluster beizutreten:

    microceph cluster join <<<YOURTOKEN>>>
    

    Jetzt können Festplatten zum Cluster hinzugefügt werden. Über lsblk sieht man die noch nicht verwedendete Festplatte. Bei mir lautet der Name: /dev/sdb. Aber dieser Name kann je nach Node abweichend sein! Den folgenden Befehl (angepasst) auf allen drei Nodes ausführen.

    microceph status
    microceph disk add /dev/sdb --wipe
    

    Als Ergebnis wird jeweils success angezeigt. Auf Node 1 geht es dann mit den eingebundenen Festplatten weiter:

    microceph status
    ceph osd pool create cephfs_data 64
    ceph osd pool create cephfs_metadata 64
    ceph fs new cephfs cephfs_metadata cephfs_data
    ceph fs ls
    

    Jetzt wird auf jedem Node ein Verzeichnis angelegt, in das das Ceph Dateisystem hineingemountet werden kann.

    mkdir /mnt/cephfs
    

    Als nächstes wird ein cefh admin token benötigt, mit dem der Mount durchgeführt werden kann. Dafür auf Node 1 ausführen:

    ceph auth get-key client.admin
    

    Mit diesem Token wird die fstab für alle drei Nodes angepasst:

    nano /etc/fstab
    

    und folgende Zeile am Ende eingefügt. Dabei wird hinter „secret=“ das Token eingesetzt und die IP-Adressen vorne entsprechen allen IPs der Nodes:

    10.0.1.11:6789,10.0.1.12:6789,10.0.1.13:6789:/ /mnt/cephfs ceph name=admin,secret=<<<TOKEN>>>,_netdev 0 0
    

    Mit “ mount -a “ wird das neu Laden der fstab erzwungen. Danach wird der Deamon neu geladen. Per df wird dann das neue Dateisystem angezeigt.

    mount -a
    systemctl daemon-reload
    df -h
    

    Damit ist in /mnt/cephfs ein persistenter Speicher hinterlegt.

    Portainer installation

    Bei Portainer gibt es die Möglichkeit für drei Nodes eine kostenlose Lizenz für die Enterprise Edition zu holen. Dazu einfach kurz googlen, anmelden und recht zügig erhält man die Lizenzschlüssel per Mail.

    mkdir /mnt/cephfs/containers/container-portainer/data
    cd /mnt/cephfs/containers/container-portainer
    nano compose.yaml
    

    Mit folgendem Inhalt:

    version: '3.2'
    services:
      agent:
        image: portainer/agent:latest
        environment:
          # REQUIRED: Should be equal to the service name prefixed by "tasks." when
          # deployed inside an overlay network
          AGENT_CLUSTER_ADDR: tasks.agent
          # AGENT_PORT: 9001
          # LOG_LEVEL: debug
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
          - /var/lib/docker/volumes:/var/lib/docker/volumes
        networks:
          - agent_network
        deploy:
          mode: global
          placement:
            constraints: [node.platform.os == linux]
    
      portainer:
        image: portainer/portainer:latest
        command: -H tcp://tasks.agent:9001 --tlsskipverify
        ports:
          #- "9000:9000"
          - "9443:9443"
        volumes:
          - /mnt/cephfs/containers/portainer/data:/data
        networks:
          - agent_network
        deploy:
          mode: replicated
          replicas: 1
          placement:
            constraints: [node.role == manager]
    
    networks:
      agent_network:
        driver: overlay
        attachable: true
    

    Mit den folgenden Befehlen startet Portainer hoch und ist per https auf dem Port 9443 zu erreichen.

    docker node ls
    docker stack deploy --compose-file compose.yaml portainer --detach=false
    docker service ls
    docker stack services portainer
    

    Beim ersten Start erstellt man für Portainer den Benutzer admin und vergibt selbst ein sicheres Passwort.

    Portainer zu Enterprise Edition migrieren

    Für den privaten Gebrauch kann man die „free for three nodes“-Lizenz verwenden. Dazu wird der Container einfach aktualisiert auf einem Node:

    docker service update --image portainer/portainer-ee:latest --force portainer_portainer --detach=false
    

    Nach dem Neustart des Portainer Containers muss im Webinterface der per Mail erhaltene Code eingegeben werden. Dazu muss man sich erneut anmelden. Danach alles neu starten:

    docker service update --force portainer_portainer
    docker service update --force portainer_agent
    

    Einloggen und die Environment ist auch wieder da. Oben links steht nun auch „Business Edition“. Die Änderungen sollte man auch in der yaml nachziehen. Ob alles funktioniert, kann man prüfen, indem man den Stack herunterfährt und neu startet:

    cd /mnt/cephfs/containers/container-portainer
    docker stack deploy portainer --compose-file docker.yaml --detach=false
    

    Quelle: https://docs.portainer.io/start/upgrade/tobe/swarm

    Wer sich über die Services wundert, die in der Swarm übersicht immer dazukommen, wenn ein Service migiert wird:

    https://www.reddit.com/r/portainer/comments/xp0jfb/portainer_spawning_multiples_of_its_own_containers/

    Kurz: Das ist normal. Man kann aufräumen mit folgendem Befehl auf einem Manager Node:

    docker swarm update --task-history-limit=1
    

    Quelle: https://stackoverflow.com/questions/42364695/how-to-clear-docker-task-history

    Aufräumen und Speicherplatz freigeben

    docker system prune --all
    

    Das gibt dann folgende Meldung, die mit „y“ zu bestätigen ist:

    WARNING! This will remove:
      - all stopped containers
      - all networks not used by at least one container
      - all images without at least one container associated to them
      - all build cache
    
    Are you sure you want to continue? [y/N]
    

    Cluster Node in den Wartungsmodus schicken und wieder aktivieren

    Will man einen Node mit Updates versorgen, kann man einzelne Nodes deaktivieren, die Updates durchführen und danach den Node wieder aktiv schalten. Den Node deaktivieren und damit auch alle Services, die auf dem Node laufen:

    docker node update --availability drain docker-swarm-03
    

    Prüfen, ob der Befehl erfolgreich war:

    docker node ls
    

    Den Node wieder aktiv schalten geht mit:

    docker node update --availability active docker-swarm-03
    

    Quellen:
    https://forums.docker.com/t/graceful-restart-of-swarm-manager-leader/114510
    https://docs.docker.com/engine/swarm/swarm-tutorial/drain-node/

    YAML als Stack deployen

    Da nun alle Services auf Docker Swarm laufen, muss definiert werden, wie viele Instanzen (in Docker Swarm sind das „tasks“) eines jeden Services (auch innerhalb eines Stacks) gleichzeitig laufen dürfen.

    Als Faustregel gilt: Es sind nur die Services skalierbar in der Anzahl (Parallelität), die stateless sind. Damit ergibt sich folgende einfache Ergänzung der yaml Dateien:

        # Docker Swarm Single
        deploy:
          mode: replicated
          replicas: 1
          placement:
            constraints: [node.role == manager]
    

    Bei einem Update kann unter „Options“ der Eintrag „Prune Services“ aktiviert werden. Damit wird der Stack aktualisiert und alte Überbleibsel (bspw. von einem früheren migrate) werden entfernt.

    Rolling Updates für einen Stack

    Möchte man das rolling update ausprobieren geht das grundsätzlich ganz einfach.

    Ein Hinweis dabei: Es funktioniert nicht bei Services, die den „mode: global“ im deployment haben, sonsdern nur bei „replicated“ services.

    Ich habe mir für den Anwendungsfall einen watchtower Container aufgesetzt – einfach als neuen Stack mit folgender Konfiguration:

    version: "3"
    services:
      watchtower:
        restart: unless-stopped
        image: containrrr/watchtower
        volumes:
          - /var/run/docker.sock:/var/run/docker.sock
        environment:
          - WATCHTOWER_CLEANUP=true
          - WATCHTOWER_DEBUG=true
          - WATCHTOWER_INCLUDE_RESTARTING=true
          # Entry                  | Description                                | Equivalent To
          # -----                  | -----------                                | -------------
          # @yearly (or @annually) | Run once a year, midnight, Jan. 1st        | 0 0 0 1 1 *
          # @monthly               | Run once a month, midnight, first of month | 0 0 0 1 * *
          # @weekly                | Run once a week, midnight between Sat/Sun  | 0 0 0 * * 0
          # @daily (or @midnight)  | Run once a day, midnight                   | 0 0 0 * * *
          # @hourly                | Run once an hour, beginning of hour        | 0 0 * * * *
          # I want watchtower to check for new updates every night at 4am.
          - WATCHTOWER_SCHEDULE=0 */10 * * * *
          - TZ=Europe/Berlin
        # Docker Swarm 3 replicas
        deploy:
          mode: replicated
          replicas: 3
          placement:
            constraints: [node.role == manager]
          update_config:
            parallelism: 1
            delay: 120s
    

    Es ist also ein Container, der drei mal parallel läuft, aber im Falle des Updates nur eine Instanz parallel aktualisiert. Möchte man den Updatevorgang beobachten geht das unter „Services“ im portainer – da muss man jedoch die Seite immer wieder neu laden. Einfacher geht das Beobachten per Konsole:

    docker service update --force watchtower_watchtower
    

    Ein task fährt sauber herunter und nach einer Wartezeit startet der neue task erst. Damit ist immer nur ein task nicht zu erreichen / nicht am arbeiten. Die verbleibenden tasks erledigen weiter die Arbeit. Damit ergbit sich für mich folgende Konfiguration, die ich einfach bei jedem Service einfüge, damit dieser Service auf dem Swarm cluster läuft und ich damit die automatische Failover-Funktion verwenden kann.

        # Docker Swarm Single
        deploy:
          mode: replicated
          replicas: 1
          placement:
            constraints: [node.role == manager]
          update_config:
            parallelism: 1
            delay: 120s
    

    Anmerkung: Der automatische Failover ist aktiv – der Docker Service läuft also auch dann noch, wenn ein Server zwecks Updates neustartet. Und dennoch gibt es bei einer Migration von einem Swarm Host zum anderen eine Unterbrechung. Denn der Failover dauert in Summe schlicht zu lang. Zusätzlich wird man ausgeloggt.

    Der Vorteil dieser Art der Installation hält sich also in Grenzen. Für aufwändigere Updates oder für längeres Debugging hilft dieses Setup.

    Quellen:
    https://docs.docker.com/engine/swarm/swarm-tutorial/deploy-service/
    https://docs.docker.com/reference/compose-file/deploy/#update_config
    https://www.youtube.com/live/dLBGoaMz7dQ
    https://www.geeksforgeeks.org/docker-swarm-rolling-updates-rollbacks/

    Ordner für volumes werden nicht erstellt

    Hier scheint es einen signifikanten Unterschied zum standalone Docker zu geben. Beim Swarm mode müssen wohl die Ordner und Dateien (volumes) auf dem gemeinsamen Speicher manuell angelegt werden.

    Quellen:
    https://www.reddit.com/r/docker/comments/1768qe7/docker_named_volumes_not_creating_folders_on_host/

    Nach meinem Verständnis die logischer Konsequenz: Docker Swarm kennt den darunterliegenden replizierenden Speicher nicht. Wird also ein Container erstellt, ist unklar auf welchem Node das zu erfolgen hat, damit Arbeits- und Speicherressource zusammen funktionieren.

    Schreiben Sie einen Kommentar

    Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahren Sie mehr darüber, wie Ihre Kommentardaten verarbeitet werden .