Flatcar Container Linux und Docker Swarm (Teil 1)

In diesem Artikel möchte ich euch Flatcar Container Linux vorstellen. Für alle, die bisher noch nichts davon gehört haben, folgt zunächst eine kurze Einführung.

Flatcar Container Linux

Flatcar Container Linux ist ein minimalistisches, unveränderliches (immutable) Linux-Betriebssystem, das speziell für den sicheren und automatisierten Betrieb von Containern entwickelt wurde.

Flatcar ist als Fork des mittlerweile eingestellten CoreOS Container Linux entstanden und eignet sich besonders für den Einsatz in Kubernetes-Clustern sowie in Umgebungen mit hohen Sicherheitsanforderungen.

Durch seine minimale Angriffsfläche und die automatisierten Sicherheitsupdates lässt sich der administrative Aufwand im laufenden Betrieb erheblich reduzieren. Gleichzeitig sorgt das unveränderliche Systemdesign dafür, dass die Systeme konsistent und reproduzierbar bleiben.

Features

  • Ignition - zur Provisionierung
  • Immutable OS - unveränderliche Systembasis
  • Kompakter Fußabdruck - kompakter Ressourcenverbrauch
  • Atomare Updates - über Dual-Partitionen
  • Automatisches Rollback - bei fehlgeschlagenen Updates


Ein weiterer Vorteil ist die standardisierte Installation. Die Server werden über eine Ignition-Konfiguration eingerichtet und erhalten dadurch eine einheitliche Grundkonfiguration. Dadurch gibt es nur wenige individuelle Abweichungen zwischen den einzelnen Systemen.

Das erleichtert sowohl die Wartung als auch den Austausch einzelner Server erheblich. Ein defekter Host kann beispielsweise ersetzt und mit derselben Konfiguration neu bereitgestellt werden, ohne das System manuell konfigurieren zu müssen.

Meine Testlabor-Umgebung

Das Ziel meines Testlabors ist es, einen funktionierenden Docker-Swarm-Cluster aufzubauen. Der Cluster besteht aus drei Servern, die jeweils die Manager-Rolle übernehmen.

Damit die im Cluster benötigten Daten redundant zur Verfügung stehen, verwende ich GlusterFS. Dafür erstelle ich auf den Hosts jeweils einen sogenannten Brick und binde diesen anschließend als gemeinsamen Daten-Pool ein.

Da ich meine Services möglichst hochverfügbar betreiben möchte, benötige ich zusätzlich eine virtuelle Cluster-IP. Diese IP-Adresse soll im Fehlerfall von einem der verbleibenden Hosts übernommen werden können.

Die Umsetzung dieser Hochverfügbarkeit erfolgt mit keepalived.

Die komplette Konfiguration meiner Testumgebung habe ich in einer Butane-Datei zusammengestellt. Butane wird dabei verwendet, um aus der deklarativen Konfiguration eine Ignition-Datei zu erzeugen.

Diese Ignition-Datei kann anschließend bei der Installation von Flatcar verwendet werden. Dadurch lässt sich die komplette Grundkonfiguration der Server automatisiert und reproduzierbar durchführen – ohne dass jeder Server nach der Installation manuell eingerichtet werden muss.

Anbei die von mir verwendete Datei:

variant: flatcar
version: 1.0.0
passwd:
  users:
    - name: core
      ssh_authorized_keys:
        - "DEIN SSHKEY"         #SELBST-ANPASSEN
storage:
  links:
    - path: /etc/localtime
      target: ../usr/share/zoneinfo/Europe/Vienna
  disks:
    - device: /dev/sda
      wipe_table: false
      partitions:
        # ROOT-Partition (Flatcar System) auf 25 GB (25600 MiB) festlegen
        - number: 9
          label: ROOT
          size_mib: 25600
          resize: true
        # Daten-Partition für den GlusterFS-Brick (Restlicher Speicherplatz)
        - number: 10
          label: data
  filesystems:
    - device: /dev/disk/by-partlabel/data
      format: ext4
      wipe_filesystem: false
      label: DATA-BRICK
  files:
    - path: /etc/hostname
      mode: 0644
      contents:
        inline: FLATCAR01           #SELBST-ANPASSEN
    # Wartungsfenster für automatische Updates
    - path: /etc/flatcar/update.conf
      overwrite: true
      mode: 0644
      contents:
        inline: |
          REBOOT_STRATEGY=reboot
          LOCKSMITHD_REBOOT_WINDOW_START=02:00
          LOCKSMITHD_REBOOT_WINDOW_LENGTH=1h

    # Statische IP-Adresse (ACHTUNG: Pro Server anpassen!)
    - path: /etc/systemd/network/10-static.network
      mode: 0644
      contents:
        inline: |
          [Match]
          Name=en* eth*

          [Network]
          Address=172.16.1.1/24         #SELBST-ANPASSEN
          Gateway=172.16.1.254          #SELBST-ANPASSEN
          DNS=172.16.1.254          #SELBST-ANPASSEN

    # FUSE Kernel-Modul für GlusterFS Mounts beim Booten laden
    - path: /etc/modules-load.d/fuse.conf
      mode: 0644
      contents:
        inline: fuse

    - path: /etc/keepalived/keepalived.conf
      mode: 0644
      contents:
        inline: |
          vrrp_instance VI_1 {
              state MASTER
              interface eth0
              virtual_router_id 51
              priority 100             #SELBST-ANPASSEN: 100 für Master, 90/80 für Backups
              advert_int 1

              authentication {
                  auth_type PASS
                  auth_pass DEINGEHEIMESPASSWORT            #SELBST-ANPASSEN
              }

              virtual_ipaddress {
                  172.16.1.100                  #SELBST-ANPASSEN: Deine Cluster-IP
              }
          }
systemd:
  units:
    # 1. ROOT-Dateisystem explizit auf die 25GB Partitionsgröße ausdehnen
    - name: rootfs-resize.service
      enabled: true
      contents: |
        [Unit]
        Description=Resize ROOT Filesystem to match 25GB partition
        After=local-fs.target

        [Service]
        Type=oneshot
        RemainAfterExit=yes
        ExecStart=/usr/sbin/resize2fs /dev/disk/by-partlabel/ROOT

        [Install]
        WantedBy=multi-user.target

    # 2. Docker aktivieren
    - name: docker.service
      enabled: true

    # 3. Die ext4-Partition als lokalen "Brick" für GlusterFS mounten
    - name: mnt-brick.mount
      enabled: true
      contents: |
        [Unit]
        Description=Mount Local Ext4 Brick for GlusterFS
        Before=docker.service

        [Mount]
        What=/dev/disk/by-partlabel/data
        Where=/mnt/brick
        Type=ext4

        [Install]
        WantedBy=multi-user.target

    # 4. Den GlusterFS Server-Daemon im Container starten
    - name: glusterd.service
      enabled: true
      contents: |
        [Unit]
        Description=GlusterFS Server
        Requires=docker.service mnt-brick.mount
        After=docker.service mnt-brick.mount

        [Service]
        Restart=always
        RestartSec=10
        # Lokalen Brick-Ordner erstellen
        ExecStartPre=/usr/bin/mkdir -p /mnt/brick/gv0
        ExecStartPre=-/usr/bin/docker rm -f glusterd
        # Start mit Named Volumes, ohne /dev Bind und im Vordergrund (-N)
        ExecStart=/usr/bin/docker run --name glusterd \
            --net=host \
            --privileged \
            -v glusterfs_lib:/var/lib/glusterd \
            -v glusterfs_etc:/etc/glusterfs \
            -v glusterfs_log:/var/log/glusterfs \
            -v /mnt/brick:/mnt/brick \
            gluster/gluster-centos:latest \
            /usr/sbin/glusterd -N

        [Install]
        WantedBy=multi-user.target

    # 5. Den verteilten Gluster-Speicher nach /media/DATA mounten
    - name: mount-gluster-volume.service
      enabled: true
      contents: |
        [Unit]
        Description=Mount GlusterFS Volume to /media/DATA
        Requires=glusterd.service
        After=glusterd.service

        [Service]
        Restart=always
        RestartSec=5
        ExecStartPre=-/usr/bin/umount -l /media/DATA
        ExecStartPre=/usr/bin/mkdir -p /media/DATA
        # Container bleibt dauerhaft am Leben, damit der FUSE-Prozess nicht stirbt
        ExecStart=/usr/bin/docker run --rm --name gluster-client --net=host --privileged \
            -v /media:/media:shared \
            gluster/gluster-centos:latest \
            /bin/bash -c "mount -t glusterfs localhost:/dockervol /media/DATA && sleep infinity"
        # Mount sauber trennen, wenn der Dienst beendet wird
        ExecStop=-/usr/bin/umount -l /media/DATA

        [Install]
        WantedBy=multi-user.target

    # 6. Installation für Cluster IP
    - name: keepalived.service
      enabled: true
      contents: |
        [Unit]
        Description=Keepalived Cluster VIP
        After=docker.service
        Requires=docker.service

        [Service]
        TimeoutStartSec=0
        Restart=always
        ExecStartPre=-/usr/bin/docker stop keepalived
        ExecStartPre=-/usr/bin/docker rm keepalived
        ExecStartPre=/usr/bin/docker pull osixia/keepalived:latest

        ExecStart=/usr/bin/docker run --name keepalived --rm \
          --net=host \
          --cap-add=NET_ADMIN \
          --cap-add=NET_BROADCAST \
          --cap-add=NET_RAW \
          -v /etc/keepalived/keepalived.conf:/etc/keepalived/keepalived.conf:ro \
          osixia/keepalived:latest

        ExecStop=/usr/bin/docker stop keepalived

        [Install]
        WantedBy=multi-user.target

    # 7. Docker Swarm Verfügbarkeit beim Booten und Rebooten steuern
    - name: docker-swarm-availability.service
      enabled: true
      contents: |
        [Unit]
        Description=Docker Swarm Node Availability Manager
        Requires=docker.service
        After=docker.service

        [Service]
        Type=oneshot
        RemainAfterExit=true
        # Beim HOCHFAHREN: Node wieder aktiv schalten
        ExecStart=/usr/bin/docker node update --availability active %%H
        # Beim HERUNTERFAHREN: Node evakuieren (Draining)
        ExecStop=/usr/bin/docker node update --availability drain %%H

        [Install]
        WantedBy=multi-user.target

Die Datei kann anschließend mit folgendem Befehl umgewandelt werden.

docker run --rm -i quay.io/coreos/butane:release --pretty --strict < FILENAME.bu > FILENAME.ign

Die Datei dann z.B. auf einem Webserver ablegen und anschließend wie folgt in Flatcar aufrufen

wget https://pfadzudeinemfile.ign
sudo install-flatcar -d /dev/sda -i filename.ign

GlusterFS in Betrieb nehmen

Nachdem meine drei Hosts in Betrieb genommen waren habe ich GlusterFS mit den folgenden Befehlen auf dem Primären Host aktiviert.

Prüfen ob die anderen Server erreichbar sind und GlusterFS läuft:

docker exec -it glusterd gluster peer probe 172.16.1.2
docker exec -it glusterd gluster peer probe 172.16.1.3

GlusterFS Volume erstellen und starten

docker exec -it glusterd gluster volume create dockervol replica 3 \
172.16.1.1:/mnt/brick/gv0 \
172.16.1.2:/mnt/brick/gv0 \
172.16.1.3:/mnt/brick/gv0 force
docker exec -it glusterd gluster volume start dockervol

GlusterFS Status prüfen

docker exec -it glusterd gluster peer status

Auf allen Hosts ausführen

sudo systemctl restart mount-gluster-volume.service

Anschließend alle Hosts neu starten. Wer möchte kann dann nach dem Neustart optional noch die Berechtigungen für core setzen.

sudo chown core:core /media/DATA -R

Fazit

Nach all diesen Konfigurationen und Befehlen verfüge ich jetzt über drei Flatcar Hosts, welche alle Daten aus dem Verzeichnis "/media/DATA", untereinander replizieren. Ob alles Synchron ist kann mittels folgendem Befehl geprüft werden.

docker exec -it glusterd gluster volume heal dockervol info summary

Hiermit endet Teil 1 meines Beitrags. Im nächsten Beitrag widme ich mich dann dem Thema Docker Swarm. Wie wird da Ganze installiert, konfiguriert und verwalltet. Funktionieren die Failover Tests und bleiben die Services aktiv?