Bare-metal RaspberryPi Kubernetes Cluster mit HypriotOS

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

Hier sehe ich mir an, wie ich einen Multi-Master Kubernetes Cluster auf Basis von Rancher/k3s aufsetze. 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.

Die folgende Skizze zeigt den Aufbau im Überblick: Eine Keepalived-VIP mit HA-Proxy verteilt die Apiserver-Anfragen auf die drei Controller, Web-Traffic läuft über MetalLB zu Traefik.

flowchart TB KC["kubectl<br/>:6443"] WEB["Web<br/>:80 / :443"] HAP["Keepalived-VIP 192.168.2.250<br/>+ HA-Proxy<br/>VRRP-Failover über die Controller"] subgraph CP["Controller — k3s-server (control plane + workload)"] B["Bane"] L["Loki"] M["Mandarin"] end subgraph WK["Worker — k3s-agent"] PI["Poison-Ivy"] VE["Venom"] end MLB["MetalLB 192.168.2.251<br/>→ Traefik Ingress"] ROOK[("Rook — verteilter Storage<br/>über alle SD-Karten")] KC --> HAP WEB --> HAP HAP -->|"API: bane aktiv · loki/mandarin backup"| B & L & M HAP -->|":80/:443"| MLB CP --- ROOK WK --- ROOK
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:

1# Suche nach:
2state MASTER
3priority 102
4
5# Ersetze mit:
6state BACKUP
7priority 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.

1local_domain='aoe.local'
2declare -A ADRESSES=( [cluster]='192.168.2.250' [cluster_fqdn]="k3s.${local_domain}" [ingress]='192.168.2.251' )
3declare -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:

1source ./env
2date="$(date +%s)"
3
4for host in ${!NODES[@]}; do
5  sed -ri "s/date -s @[[:digit:]]+/date -s @${date}/" ./cloud-init/${host}.yaml
6done

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.

1export IMAGE="$(pwd)/hypriotos-rpi64-4.19.86-custom.img.zip"
2
3flash --userdata ./cloud-init/bane.yaml --device /dev/mmcblk0 "$IMAGE"
4flash --userdata ./cloud-init/loki.yaml --device /dev/mmcblk0 "$IMAGE"
5flash --userdata ./cloud-init/mandarin.yaml --device /dev/mmcblk0 "$IMAGE"
6flash --userdata ./cloud-init/poison-ivy.yaml --device /dev/mmcblk0 "$IMAGE"
7flash --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, führe ich folgendes Script aus. Ich setze voraus, dass der avahi-daemon (vgl. service discovery via mDNS, Zeroconf DNS, Bonjour) aktiviert ist:

1source ./env
2
3for host in ${!NODES[@]}; do
4  echo "--- $host ---"
5  \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"
6done

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:

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

Sobald ich in der Lage bin, 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:

1curl -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.

1source ./env
2
3export CLUSTER_TOKEN="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-64} | head -n 1)"
4
5\ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 bane.local | awk '{print $2}') \
6  "sudo curl -sfL https://get.k3s.io | sh -s - server --tls-san ${ADRESSES[cluster_fqdn]} \
7    --advertise-address ${ADRESSES[cluster]} --bind-address ${NODES[bane]} --node-ip ${NODES[bane]} \
8    --token $CLUSTER_TOKEN --flannel-backend wireguard --no-deploy servicelb \
9    --cluster-init --write-kubeconfig '/tmp/kubeconfig'"

Die Installation dauert nur wenige Sekunden. Nach Abschluss lade ich mir die KUBECONFIG herunter und überschreibe entweder die globale ~/.kube/config oder lege die Datei unter anderem Namen in ~/.kube ab und lasse die Umgebungsvariable $KUBECONFIG auf die Konfiguration zeigen. Wenn der Ordner .kube noch nicht im Heimatverzeichnis existiert, muss er angelegt werden.

 1source ./env
 2
 3mkdir -p ~/.kube
 4
 5\ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 bane.local | awk '{print $2}') \
 6  "sudo cat /tmp/kubeconfig" > ~/.kube/config
 7
 8chmod 600 ~/.kube/config
 9
10sed -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 muss ich 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.

1source ./env
2
3sudo echo
4
5echo "# k3s cluster" | sudo tee -a /etc/hosts
6echo "${ADRESSES[cluster]} ${ADRESSES[cluster_fqdn]}" | sudo tee -a /etc/hosts
7echo "${ADRESSES[ingress]} minio.${local_domain}" | sudo tee -a /etc/hosts

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

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

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

1kubectl get nodes -w

Worker

Die restliches RPis füge ich als normale Kubernetes Worker dem Cluster hinzu:

 1source ./env
 2
 3export TOKEN="$CLUSTER_TOKEN" # Generiertes Token von oben
 4
 5for host in ${!NODES[@]}; do
 6  [[ "${host}" =~ (bane|loki|mandarin) ]] && continue
 7  \ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 ${host}.local | awk '{print $2}') \
 8    "sudo curl -sfL https://get.k3s.io | sh -s - agent --node-ip ${NODES[$host]} \
 9    --token $TOKEN --server 'https://${ADRESSES[cluster_fqdn]}:6443'"
10done

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

1kubectl 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:

 1kubectl apply -f - <<END
 2apiVersion: v1
 3kind: ServiceAccount
 4metadata:
 5  name: admin-user
 6  namespace: kube-system
 7---
 8apiVersion: rbac.authorization.k8s.io/v1
 9kind: ClusterRoleBinding
10metadata:
11  name: admin-user
12roleRef:
13  apiGroup: rbac.authorization.k8s.io
14  kind: ClusterRole
15  name: cluster-admin
16subjects:
17- kind: ServiceAccount
18  name: admin-user
19  namespace: kube-system
20END

Das Dashboard für ARM-Systeme ausrollen:

1curl -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, starte ich einen Proxy in den Kubernetes Cluster mit kubectl proxy. Dadurch bin ich in der Lage mich 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:

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

Neuere metallb Version erwarten ein Kubernetes Secret mit einem Zufallswert:

1kubectl 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):

 1source ./env
 2
 3kubectl create -f - <<END
 4apiVersion: v1
 5kind: ConfigMap
 6metadata:
 7  namespace: metallb-system
 8  name: config
 9data:
10  config: |
11    address-pools:
12    - name: home
13      protocol: layer2
14      addresses:
15      - ${ADRESSES[ingress]}/32
16END

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 verwende ich 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:

 1docker pull --platform linux/arm64 webhippie/minio
 2docker tag webhippie/minio ff0x/minio-aarch64
 3
 4cd $GOPATH/src
 5mkdir -p github.com/rook/ && cd github.com/rook/
 6git clone https://github.com/rook/rook.git
 7cd rook/
 8
 9sed -i 's+ARCH=$(GOARCH)+ARCH=arm64v8 --platform linux/arm64+' images/minio/Makefile
10sed -i 's+-t $(MINIO_IMAGE)+-t ff0x/minio-arm64+' images/minio/Makefile
11sed -i 's+FROM minio/minio.*+FROM ff0x/minio-aarch64+' images/minio/Dockerfile 
12
13GOARCH=arm64 IMAGES=minio make build.all
14docker 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.

 1cd /tmp && git clone https://github.com/rook/rook.git
 2cd rook/cluster/examples/kubernetes/minio/
 3
 4export MINIO_USER='minio_user'
 5export MINIO_PASS=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-32} | head -n 1)
 6
 7sed -i 's+rook/minio:master+ff0x/minio-arm64:latest+g' operator.yaml
 8kubectl create -f ./operator.yaml
 9
10sed -i "s/\(username:*: \).*/\1$(echo -n ${MINIO_USER} | base64)/" object-store.yaml
11sed -i "s/\(password:*: \).*/\1$(echo -n ${MINIO_PASS} | base64)/" object-store.yaml
12kubectl create -f ./object-store.yaml

Konfiguration der Ingress Route

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

 1source ./env
 2
 3kubectl create -f - <<EOF
 4apiVersion: extensions/v1beta1
 5kind: Ingress
 6metadata:
 7  name: minio-my-store
 8  namespace: rook-minio
 9  annotations:
10    kubernetes.io/ingress.class: traefik
11spec:
12  rules:
13  - host: minio.${local_domain}
14    http:
15      paths:
16      - path: /
17        backend:
18          serviceName: minio-my-store
19          servicePort: 9000
20EOF

Wenn alle Pods gestartet sind und die Konfiguration abgeschlossen ist, kann ich http://minio.${local_domain} im Browser aufrufen, mich 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 ich komfortabel und gracefully alle Raspberry Pis herunterfahren kann:

 1#!/usr/bin/env bash
 2
 3source ./env
 4
 5function print_style() {
 6  if [ "$2" == "info" ] ; then
 7    COLOR="96m"
 8  elif [ "$2" == "success" ] ; then
 9    COLOR="92m"
10  elif [ "$2" == "warning" ] ; then
11    COLOR="93m"
12  elif [ "$2" == "danger" ] ; then
13    COLOR="91m"
14  else #default color
15    COLOR="0m"
16  fi
17
18  STARTCOLOR="\e[$COLOR"
19  ENDCOLOR="\e[0m"
20
21  printf "$STARTCOLOR%b$ENDCOLOR" "=== $1\n"
22}
23
24function service_active() {
25  local _service="$1"
26
27  if systemctl -q is-active "$_service"; then
28    return 0
29  else
30    return 1
31  fi
32}
33
34service_active "avahi-daemon.service" || sudo systemctl start avahi-daemon.service
35
36for host in ${!NODES[@]}; do
37  print_style "Powering off ${host}" "danger"
38  \ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no $(avahi-resolve-host-name -4 ${host}.local | awk '{print $2}') \
39    "sudo shutdown -h now"
40done