Copyright © KC Green

Bare-metal RaspberryPi Kubernetes Cluster mit HypriotOS

 infrastructure   howto 

Setup eines hochverfügbaren 'bare-metal' k3s Cluster unter HypriotOS (64bit)
TOC

Hier sehen wir uns an, wie man einen Multi-Master Kubernetes Cluster auf Basis von Rancher/k3s aufsetzt. Als Hardwareplattform verwende ich fünf Raspberry Pi 3b+ in einem Picocluster auf denen HypriotOS läuft.

Die Hochverfügbarkeit des Kubernetes Clusters wird durch die Verwendung von HA-Proxy und keepalived erreicht. Eine virtuelle Failover IP-Adresse zeigt so immer auf einen verfügbaren Kubernetes Apiserver. Persistenter Speicherplatz mit Unterstützung für read-write many Zugriff wird Dank Rook über ein verteiltes Clusterfilesystem bereitgestellt, der den verfügbaren Speicherplatz der RPi SD-Karten nutzt.

Mac-Nutzer aufgepasst: In manchen Befehlen, setze ich voraus, das GNU coreutils verfügbar und eine aktuelle Bash Version installiert sind!

Cloud-init Konfiguration

Nachfolgende cloud-init Templates verwende ich als Basis, wobei ich drei Raspberries als Controller definiere, auf diesen Nodes wird später zusätzlich zum workload die Kubernetes control plane laufen. Die anderen beiden sind normale Worker.

In allen cloud-init Konfigurationen müssen üblicherweise folgende Werte angepasst werden:

  1. Einzigartige Hostnamen. Meine Controller-Nodes heißen hier Bane, Loki und Mandarin. Die Worker Poison-Ivy und Venom
  2. Jeder Node bekommt eine statische IP-Adresse aus dem lokalen Netzwerk, ggf. muss die Netzmaske (CIDR) angepasst werden
  3. Und es muss eine virtuelle Failover Adresse definiert werden, die später auf den Kubernetes Apiserver zeigt
  4. Die Administrator Benutzerinformationen
  5. Der Öffentlicher SSH-Schlüssel des Administrators
  6. Optional kann aoe.local durch eine eigene DNS Zone ersetzen

Für den 2. und 3. Controller-Node müssen zusätzlich noch die Zeilen 108-109 angepasst werden:

# Suche nach:
state MASTER
priority 102

# Ersetze mit:
state BACKUP
priority 100

Der Einfachheit halber habe ich für jeden Node eine eigene cloud-init Datei angelegt, die ich im Ordner ./cloud-init ablege.

Expand to view: Controller Template

#cloud-config 
hostname: "bane"
manage_etc_hosts: false
locale: "en_US.UTF-8"
timezone: "Europe/Berlin"

users:

  • name: ff0x gecos: “Max Buelte” primary-group: aoe shell: /bin/bash sudo: ALL=(ALL) NOPASSWD:ALL groups: aoe,users,docker,adm,dialout,audio,plugdev,netdev,video lock_passwd: true ssh_pwauth: false ssh-authorized-keys:
    • ssh-rsa AAAAB3N…

apt: preserve_sources_list: true conf: | APT { Get { Assume-Yes “true”; Fix-Broken “true”; }; }; sources: kubernetes-xenial.list: source: “deb http://apt.kubernetes.io/ kubernetes-xenial main” keyid: 6A030B21BA07F4FB debian-unstable.list: source: “deb http://deb.debian.org/debian/ unstable main” keyid: 04EE7237B7D453EC #ignored0: # Just get the signing key # keyid: B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77 # keyserver: pgp.mit.edu

package_upgrade: true package_reboot_if_required: true packages:

  • vim
  • lrzsz
  • socat
  • dhcpcd5
  • psmisc
  • xfsprogs
  • nfs-common
  • libipset3
  • libnss-mdns
  • keepalived
  • haproxy

write_files:

  • content: | persistent slaac private interface eth0 static ip_address=192.168.2.241/24 static routers=192.168.2.1 static domain_name_servers=1.1.1.1 path: /etc/dhcpcd.conf

  • content: | 127.0.0.1 localhost.localdomain localhost ::1 localhost.localdomain localhost

    192.168.2.250 k3s.aoe.local k3s 192.168.2.241 bane.aoe.local bane 192.168.2.242 loki.aoe.local loki 192.168.2.243 mandarin.aoe.local mandarin 192.168.2.244 poison-ivy.aoe.local poison-ivy 192.168.2.245 venom.aoe.local venom path: /etc/hosts

  • content: | net.ipv4.ip_forward = 1 net.ipv4.conf.all.forwarding = 1 net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_nonlocal_bind = 1 path: /etc/sysctl.d/100-kubernetes.conf

  • content: | #!/bin/sh echo “All runlevel operations denied by policy” >&2 exit 101 path: /usr/sbin/policy-rc.d owner: root:root permissions: ‘0755’

  • content: | global_defs { enable_script_security script_user haproxy }

    vrrp_script chk_haproxy { script “/usr/bin/killall -0 haproxy” interval 2 }

    vrrp_sync_group SG_1 { group { INTERN } }

    vrrp_instance INTERN { interface eth0

    virtual_router_id 51
    state MASTER
    priority 102
    advert_int 1
    
    virtual_ipaddress {
      192.168.2.250/24
    }
    
    track_script {
      chk_haproxy
    }
    

    } path: /etc/keepalived/keepalived.conf owner: root:root permissions: ‘0640’

  • content: | [Unit] After= After=network-online.target

    [Service] ExecStartPre= ExecStartPre=/bin/sleep 10 path: /etc/systemd/system/keepalived.service.d/override.conf owner: root:root permissions: ‘0644’

  • content: | global maxconn 256000 log 127.0.0.1 local0 notice user haproxy group haproxy chroot /usr/share/haproxy pidfile /run/haproxy.pid stats socket /run/haproxy.sock mode 660 level admin stats timeout 30s daemon

    # SSL specific
    ca-base /etc/ssl/certs
    crt-base /etc/ssl/private
    tune.ssl.default-dh-param 2048
    ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
    ssl-default-bind-options no-sslv3
    

    defaults log global mode http option httplog option dontlognull timeout connect 15s timeout client 10m timeout server 10m

    STATS

    —–

    listen stats bind *:8081 mode http option httplog log global maxconn 5 stats enable stats refresh 5s stats show-node stats auth admin:WkjaBIdCJxqWJpoIE6hp7PdRIJpRnebQ stats uri /stats

    ANY

    listen k3s-ingress bind 192.168.2.250:80 mode tcp log global

    # Options
    option tcplog
    
    # Backend
    balance roundrobin
    default-server inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100
    server metallb1 192.168.2.251:80 check
    

    listen k3s-ingress-ssl bind 192.168.2.250:443 mode tcp log global

    # Options
    option tcplog
    option ssl-hello-chk
    
    # Backend
    balance roundrobin
    default-server inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100
    server metallb1 192.168.2.251:443 check
    

    INTERN

    ——

    frontend k8s-api bind 192.168.2.250:6443 mode tcp

    # Options
    option tcplog
    default_backend k8s-api
    

    backend k8s-api mode tcp option tcp-check balance roundrobin default-server inter 10s downinter 5s rise 2 fall 2 slowstart 60s maxconn 250 maxqueue 256 weight 100 server bane bane.aoe.local:6443 check server loki loki.aoe.local:6443 check backup server mandarin mandarin.aoe.local:6443 check backup path: /etc/haproxy/haproxy.cfg owner: root:root permissions: ‘0640’

bootcmd:

  • ‘date -s @1576147737’ # WTF
  • ‘printf “Package: *\nPin: release a=unstable\nPin-Priority: 150\n” > /etc/apt/preferences.d/limit-unstable’
  • [ cloud-init-per, once, prepare_storage-X, apt-get, update ]
  • [ cloud-init-per, once, prepare_storage-0, apt-get, install, -y, xfsprogs ]
  • [ cloud-init-per, once, prepare_storage-1, mkdir, -p, /data ]

runcmd:

  • ‘sed -i “s+cgroup_memory=1+cgroup_memory=1 cgroup_enable=memory+” /boot/cmdline.txt’
  • ‘apt-get purge –yes docker-ce docker-ce-cli containerd.io; apt-get autoremove –yes’
  • ‘apt-get purge –yes isc-dhcp-client isc-dhcp-common; apt-get autoremove –yes’
  • ‘mkdir -p /usr/share/haproxy; chown haproxy:haproxy -R /usr/share/haproxy’
  • ‘killall ntp >/dev/null 2>&1; apt-get purge –yes ntp >/dev/null 2>&1’
  • ‘timedatectl set-ntp true’
  • ‘systemctl disable –now wpa_supplicant’
  • ‘systemctl disable –now bluetooth’
  • ‘systemctl mask bluetooth’
  • ‘systemctl enable keepalived’
  • ‘systemctl enable haproxy’
Expand to view: Worker Template

#cloud-config 
hostname: "venom"
manage_etc_hosts: false
locale: "en_US.UTF-8"
timezone: "Europe/Berlin"

users:

  • name: ff0x gecos: “Max Woelfing” primary-group: aoe shell: /bin/bash sudo: ALL=(ALL) NOPASSWD:ALL groups: aoe,users,docker,adm,dialout,audio,plugdev,netdev,video lock_passwd: true ssh_pwauth: false ssh-authorized-keys:
    • ssh-rsa AAAAB3N…

apt: preserve_sources_list: true conf: | APT { Get { Assume-Yes “true”; Fix-Broken “true”; }; }; sources: kubernetes-xenial.list: source: “deb http://apt.kubernetes.io/ kubernetes-xenial main” keyid: 6A030B21BA07F4FB debian-unstable.list: source: “deb http://deb.debian.org/debian/ unstable main” keyid: 04EE7237B7D453EC #ignored0: # Just get the signing key # keyid: B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77 # keyserver: pgp.mit.edu

package_upgrade: true package_reboot_if_required: true packages:

  • vim
  • lrzsz
  • socat
  • dhcpcd5
  • psmisc
  • xfsprogs
  • nfs-common
  • libnss-mdns

write_files:

  • content: | persistent slaac private interface eth0 static ip_address=192.168.2.245/24 static routers=192.168.2.1 static domain_name_servers=1.1.1.1 path: /etc/dhcpcd.conf

  • content: | 127.0.0.1 localhost.localdomain localhost ::1 localhost.localdomain localhost

    192.168.2.250 k3s.aoe.local k3s 192.168.2.241 bane.aoe.local bane 192.168.2.242 loki.aoe.local loki 192.168.2.243 mandarin.aoe.local mandarin 192.168.2.244 poison-ivy.aoe.local poison-ivy 192.168.2.245 venom.aoe.local venom path: /etc/hosts

  • content: | net.ipv4.ip_forward = 1 net.ipv4.conf.all.forwarding = 1 net.bridge.bridge-nf-call-iptables = 1 path: /etc/sysctl.d/100-kubernetes.conf

  • content: | #!/bin/sh echo “All runlevel operations denied by policy” >&2 exit 101 path: /usr/sbin/policy-rc.d owner: root:root permissions: ‘0755’

bootcmd:

  • ‘date -s @1576147737’ # WTF
  • ‘printf “Package: *\nPin: release a=unstable\nPin-Priority: 150\n” > /etc/apt/preferences.d/limit-unstable’
  • [ cloud-init-per, once, prepare_storage-X, apt-get, update ]
  • [ cloud-init-per, once, prepare_storage-0, apt-get, install, -y, xfsprogs ]
  • [ cloud-init-per, once, prepare_storage-1, mkdir, -p, /data ]

runcmd:

  • ‘sed -i “s+cgroup_memory=1+cgroup_memory=1 cgroup_enable=memory+” /boot/cmdline.txt’
  • ‘apt-get purge –yes docker-ce docker-ce-cli containerd.io; apt-get autoremove –yes’
  • ‘apt-get purge –yes isc-dhcp-client isc-dhcp-common; apt-get autoremove –yes’
  • ‘killall ntp >/dev/null 2>&1; apt-get purge –yes ntp >/dev/null 2>&1’
  • ‘timedatectl set-ntp true’
  • ‘systemctl disable –now wpa_supplicant’
  • ‘systemctl disable –now bluetooth’
  • ‘systemctl mask bluetooth’

Anschließend erstelle ich die Datei env im aktuellen Ordner, mit Informationen zu meiner Netzwerkumgebung. Sie wird später von diversen Bash Scripten eingelesen werden.

local_domain='aoe.local'
declare -A ADRESSES=( [cluster]='192.168.2.250' [cluster_fqdn]="k3s.${local_domain}" [ingress]='192.168.2.251' )
declare -A NODES=( [bane]='192.168.2.241' [loki]='192.168.2.242' [mandarin]='192.168.2.243' [poison-ivy]='192.168.2.244' [venom]='192.168.2.245' )

HypriotOS mit 64bit Kernel

UPDATE: Nachdem es mittlerweile eine offizielle 64bit Version von HypriotOS gibt, ist der nachfolgende Teil nur noch erforderlich, wenn man eigene Kernelparameter aktivieren möchte, wie z.B. nativen Wireguard-Support.

Ein etwas älteres Projekt auf Github erklärt Schritt-für-Schritt, wie man einen 64bit Kernel für HypriotOS kompiliert. An dieser Stelle macht es nichts, dass das Projekt schon ein paar Tage auf dem Buckel hat, man kann selbstverständlich einen aktuellen Kernel erzeugen. Der große Vorteil ist hierbei, dass man eigene Kernelmodule wie die native Unterstützung für Wireguard fest in den Kernel backen kann - oder unbenötigte Module, bzw. Features entfernen, um den Kernel kleiner zu machen.

Installation des Betriebssystems

Flashen der SD-Karten

Abhängigkeiten

Zum flashen der RPi Micro SD-Karten verwende ich flash.

Bekannte Probleme

Aktuell gibt es einen Bug, der verhindert das apt-get update ordentlich aufgerufen werden kann, wenn der Zeitunterschied zu groß ist. Hier geht es zum offenen Github Issue. Als quick ‘n dirty Lösung, setze ich die aktuelle Systemzeit über cloud-init.

Der Workaround über die cloud-init Konfiguration erfordert, dass der Zeitstempel immer aktualisiert wird, bevor sie verwendet werden kann und auch dann ist sie nicht länger als ein paar Stunden gültig, da ansonsten der Zeitunterschied wieder zu groß wird!

Mit folgendem Script kann man die Konfigurationen aktualisieren:

source ./env
date="$(date +%s)"

for host in ${!NODES[@]}; do
  sed -ri "s/date -s @[[:digit:]]+/date -s @${date}/" ./cloud-init/${host}.yaml
done

Micro SD-Karte für jeden Node bereitstellen

Unbedingt sicherstellen, dass /dev/mmcblk0 das korrekte Gerät ist, in dem sich die Micro SD-Karte zum flashen befindet!

Natürlich muss die SD Karte nach jedem Vorgang gewechselt werden.

export IMAGE="$(pwd)/hypriotos-rpi64-4.19.86-custom.img.zip"

flash --userdata ./cloud-init/bane.yaml --device /dev/mmcblk0 "$IMAGE"
flash --userdata ./cloud-init/loki.yaml --device /dev/mmcblk0 "$IMAGE"
flash --userdata ./cloud-init/mandarin.yaml --device /dev/mmcblk0 "$IMAGE"
flash --userdata ./cloud-init/poison-ivy.yaml --device /dev/mmcblk0 "$IMAGE"
flash --userdata ./cloud-init/venom.yaml --device /dev/mmcblk0 "$IMAGE"

RPi Nodes booten

SD-Karten einstecken, wenn noch nicht geschehen. Anschließend Picocluster, bzw. jeden einzelnen Raspberry neustarten und warten. Es kann eine ganze Weile dauern, bis die Konfiguration von HypriotOS komplett abgeschlossen ist.

SSH-Verbindung aufbauen

Um den Konfigurationsfortschritt abzufragen, kann man folgendes Script ausführen. Ich setze voraus, dass der avahi-daemon (vgl. service discovery via mDNS, Zeroconf DNS, Bonjour) aktiviert ist:

source ./env

for host in ${!NODES[@]}; do
  echo "--- $host ---"
  \ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 $host.local | awk '{print $2}') "sudo tail -n5 /var/log/cloud-init-output.log"
done

Nachdem die Konfiguration via cloud-init abgeschlossen ist, müssen alle Nodes einmalig manuell neugestartet werden. Dieser Schritt ist erforderlich, damit die statischen IP-Adressen korrekt zugewiesen werden:

source ./env

for host in ${!NODES[@]}; do
  ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 $host.local | awk '{print $2}') "sudo reboot"
done

Sobald man in der Lage ist, die zuvor vergebene Failover IP-Adresse anzupingen, sind alle RPis bereit für die Installation von Kubernetes.

Installation von Kubernetes

Zur Verwaltung des Kubernetes Clusters ist eine lokale Installation von kubectl notwendig. Nachfolgender Befehl läd die letzte Version herunter:

curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"

Controller

Mit folgendem Befehl wird k3s auf dem ersten Controller bane installiert. Das hier generierte TOKEN wird später bei der Installation weiterer Nodes benötigt. Wenn der HypriotOS Kernel keine Unterstützung für WIreguard mitbringt, muss der Parameter --flannel-backend entfernt werden.

source ./env

export CLUSTER_TOKEN="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-64} | head -n 1)"

\ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 bane.local | awk '{print $2}') \
  "sudo curl -sfL https://get.k3s.io | sh -s - server --tls-san ${ADRESSES[cluster_fqdn]} \
    --advertise-address ${ADRESSES[cluster]} --bind-address ${NODES[bane]} --node-ip ${NODES[bane]} \
    --token $CLUSTER_TOKEN --flannel-backend wireguard --no-deploy servicelb \
    --cluster-init --write-kubeconfig '/tmp/kubeconfig'"

Die Installation dauert nur wenige Sekunden. Nach Abschluss kann man sich die KUBECONFIG herunterladen und entweder die globale ~/.kube/config überschreiben oder die Datei unter anderem Namen in ~/.kube ablegen und die Umgebungsvariable $KUBECONFIG auf die Konfiguration zeigen lassen. Wenn der Ordner .kube noch nicht im Heimatverzeichnis existiert, muss er angelegt werden.

source ./env

mkdir -p ~/.kube

\ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 bane.local | awk '{print $2}') \
  "sudo cat /tmp/kubeconfig" > ~/.kube/config

chmod 600 ~/.kube/config

sed -i "s+server:.*$+server: https://${ADRESSES[cluster_fqdn]}:6443+" ~/.kube/config

Jetzt muss sichergestellt werden, dass der Kubernetes Apiserver Hostname zur Failover IP-Adresse auflöst. Hierfür müssen wir folgende Einträge in der lokalen /etc/hosts Datei hinzufügen. Wenn man einen Nameserver betreibt, können die A-Records natürlich in der Zonendatei des Servers angelegt werden.

source ./env

sudo echo

echo "# k3s cluster" | sudo tee -a /etc/hosts
echo "${ADRESSES[cluster]} ${ADRESSES[cluster_fqdn]}" | sudo tee -a /etc/hosts
echo "${ADRESSES[ingress]} minio.${local_domain}" | sudo tee -a /etc/hosts

Die beiden nachfolgenden Controller loki und mandarin werden nun mit folgendem Befehl eingerichtet:

source ./env

export TOKEN="$CLUSTER_TOKEN" # Generiertes Token von oben

for host in ${!NODES[@]}; do
  [[ "${host}" =~ (bane|poison-ivy|venom) ]] && continue
  \ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 ${host}.local | awk '{print $2}') \
    "sudo curl -sfL https://get.k3s.io | sh -s - server --advertise-address ${ADRESSES[cluster]} --bind-address ${NODES[$host]} --node-ip ${NODES[$host]} \
    --token $TOKEN --server 'https://${ADRESSES[cluster_fqdn]}:6443'"
done

Wenn die Installation abgeschlossen ist, sollte man alsbald alle drei Master Nodes angezeigt bekommen:

kubectl get nodes -w

Worker

Die restliches RPis fügen wir als normale Kubernetes Worker dem Cluster hinzu:

source ./env

export TOKEN="$CLUSTER_TOKEN" # Generiertes Token von oben

for host in ${!NODES[@]}; do
  [[ "${host}" =~ (bane|loki|mandarin) ]] && continue
  \ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 ${host}.local | awk '{print $2}') \
    "sudo curl -sfL https://get.k3s.io | sh -s - agent --node-ip ${NODES[$host]} \
    --token $TOKEN --server 'https://${ADRESSES[cluster_fqdn]}:6443'"
done

Nach wenigen Minuten sollte der Kubernetes Cluster aus 5 Nodes bestehen:

kubectl get nodes -w

Installation von Kubernetes Addons

Alles nachfolgende ist optional, der Kubernetes Cluster besteht und kann nach Belieben verwendet werden.

Kubernetes Dashboard

Installation vom Kubernetes Dashboard.

Benutzer für die Administration hinzufügen:

kubectl apply -f - <<END
apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kube-system
END

Das Dashboard für ARM-Systeme ausrollen:

curl -sLo - https://raw.githubusercontent.com/kubernetes/dashboard/v1.10.1/src/deploy/recommended/kubernetes-dashboard.yaml | sed -E 's@(image:.*)-amd64:(.*$)@\1-arm64:\2@g' | kubectl apply -f -

Token des Administrator-Benutzers anzeigen:

kubectl get secret -n kube-system $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}') -o jsonpath="{.data.token}" | base64 --decode

Um auf das Dashboard zugreifen zu können, starten wir einen Proxy in den Kubernetes Cluster mit kubectl proxy. Dadurch sind wir in der Lage uns mit dem Token von oben über http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#!/login einloggen zu können.

Loadbalancer

Wenn k3s - wie oben - ohne Rancher “servicelb” installiert wurde, macht es Sinn mit metallb Unterstützung für “Loadbalancer” Services bereitzustellen. Hierfür wird eine weitere virtuelle Failover IP-Adresse aus dem lokalen Netzwerk (analog zu der für den Apiserver) benötigt.

Installation des Manifests:

kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.1/manifests/metallb.yaml

Neuere metallb Version erwarten ein Kubernetes Secret mit einem Zufallswert:

kubectl create secret -n metallb-system generic memberlist --from-literal=secretkey="$(openssl rand -base64 128)" --dry-run=client -o yaml | kubectl apply -f -

metallb konfigurieren (Failover Adresse und L2-Routing Modus):

source ./env

kubectl create -f - <<END
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: home
      protocol: layer2
      addresses:
      - ${ADRESSES[ingress]}/32
END

Ich gehe hier davon aus, dass alle in Kubernetes laufende Dienste über den durch Rancher/k3s installierten Standard-Ingress-Controller traefik freigegeben werden. Sollte das nicht der Fall sein gibt es zwei Möglichkeiten:

  1. Entweder man fügt der metallb Konfiguration einen Pool mit mehreren virtuellen IP-Adressen zu
  2. Alternativ - wenn man weiterhin nur eine einzige virtuelle Failover-Adresse für metallb verwenden möchte - kann man IP address sharing aktivieren. Allerdings müssen hierfür gewisse Bedingungen erfüllt sein.

Ingress Controller

Wie schon erwähnt verwenden wir hier der Einfachheithalber den Ingress Controller, der mit k3s kommt. Möchte man direkt nach der Installation einen eigenen Controller verwenden, kann man die Standardinstallation mit --no-deploy traefik unterdrücken. Sollte man traefik später durch einen anderen Controller ersetzten wollen, muss man auf allen Controller Nodes die Datei /etc/systemd/system/k3s.service editieren und entsprechenden Parameter einfügen. Danach kann man den laufenden Controller mit kubectl delete -n kube-system helmcharts traefik entfernen.

Persistenter Speicherplatz

Rancher/k3s kommt standardmäßig mit Unterstützung für persistente Volumes. Diese StorageClass ist als default markiert. Um jedoch Datenverlust durch ein verteiltes Dateisystem, bzw. Redundanz der Daten entgegenzuwirken und zusätzlich read-write many Zugriff zu ermöglichen, kann man mit Rook einen exzellenten cloud-native Storage Operator in den Cluster installieren.

Minio

UPDATE: Die Minio-Unterstützung in Rook wurde vor Kurzem abgekündigt. Das war zu erwarten, da der Code nicht ordentlich gepflegt wurde und wohl auch der Rückhalt in der Community nicht da war. Wenn man nun object storage benötigt und Rook einsetzen möchte, kann man alternativ Ceph verwenden.

ARM Container images erzeugen

Workaround für Minio aarch64 Image:

docker pull --platform linux/arm64 webhippie/minio
docker tag webhippie/minio ff0x/minio-aarch64

cd $GOPATH/src
mkdir -p github.com/rook/ && cd github.com/rook/
git clone https://github.com/rook/rook.git
cd rook/

sed -i 's+ARCH=$(GOARCH)+ARCH=arm64v8 --platform linux/arm64+' images/minio/Makefile
sed -i 's+-t $(MINIO_IMAGE)+-t ff0x/minio-arm64+' images/minio/Makefile
sed -i 's+FROM minio/minio.*+FROM ff0x/minio-aarch64+' images/minio/Dockerfile 

GOARCH=arm64 IMAGES=minio make build.all
docker push ff0x/minio-arm64

Installation und Konfiguration von Rook

Rook installieren und Minio S3-kompatiblen Storage konfigurieren. Mit $MINIO_USER wird einem Benutzer Zugriff auf Minio gewährt.

cd /tmp && git clone https://github.com/rook/rook.git
cd rook/cluster/examples/kubernetes/minio/

export MINIO_USER='minio_user'
export MINIO_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-32} | head -n 1)

sed -i 's+rook/minio:master+ff0x/minio-arm64:latest+g' operator.yaml
kubectl create -f ./operator.yaml

sed -i "s/\(username:*: \).*/\1$(echo -n ${MINIO_USER} | base64)/" object-store.yaml
sed -i "s/\(password:*: \).*/\1$(echo -n ${MINIO_PASS} | base64)/" object-store.yaml
kubectl create -f ./object-store.yaml

Konfiguration der Ingress Route

Eine Ingress Route für den Minio webserver hinzufügen:

source ./env

kubectl create -f - <<EOF
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: minio-my-store
  namespace: rook-minio
  annotations:
    kubernetes.io/ingress.class: traefik
spec:
  rules:
  - host: minio.${local_domain}
    http:
      paths:
      - path: /
        backend:
          serviceName: minio-my-store
          servicePort: 9000
EOF

Wenn alle Pods gestartet sind und die Konfiguration abgeschlossen ist, kann man http://minio.${local_domain} im Browser aufrufen, sich mit den erzeugten Zugangsdaten einloggen und versuchen ein Volume zu allokieren.

Je nach Applikation muss der S3-kompatible object storage anders angebunden werden. Mehr Informationen gibt es in der Rook Dokumentation.

HELM

Rancher/k3s kommt von Haus aus mit integriertem Helm Controller, es ist also nicht erforderlich die Cluster-Komponente “Tiller” (bei HELMv2) manuell zu installieren.

Verwaltung der Cluster-Nodes

Nachfolgend ein kurzes Bash Script, das beispielhaft zeigt, wie man komfortabel und gracefully alle Raspberry Pis herunterfahren kann:

#!/usr/bin/env bash

source ./env

function print_style() {
  if [ "$2" == "info" ] ; then
    COLOR="96m"
  elif [ "$2" == "success" ] ; then
    COLOR="92m"
  elif [ "$2" == "warning" ] ; then
    COLOR="93m"
  elif [ "$2" == "danger" ] ; then
    COLOR="91m"
  else #default color
    COLOR="0m"
  fi

  STARTCOLOR="\e[$COLOR"
  ENDCOLOR="\e[0m"

  printf "$STARTCOLOR%b$ENDCOLOR" "=== $1\n"
}

function service_active() {
  local _service="$1"

  if systemctl -q is-active "$_service"; then
    return 0
  else
    return 1
  fi
}

service_active "avahi-daemon.service" || sudo systemctl start avahi-daemon.service

for host in ${!NODES[@]}; do
  print_style "Powering off ${host}" "danger"
  \ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 ${host}.local | awk '{print $2}') \
    "sudo shutdown -h now"
done