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.
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:
- Einzigartige Hostnamen. Meine Controller-Nodes heißen hier Bane, Loki und Mandarin. Die Worker Poison-Ivy und Venom
- Jeder Node bekommt eine statische IP-Adresse aus dem lokalen Netzwerk, ggf. muss die Netzmaske (CIDR) angepasst werden
- Und es muss eine virtuelle Failover Adresse definiert werden, die später auf den Kubernetes Apiserver zeigt
- Die Administrator Benutzerinformationen
- Der Öffentlicher SSH-Schlüssel des Administrators
- 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
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.
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
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:
- Entweder man fügt der metallb Konfiguration einen Pool mit mehreren virtuellen IP-Adressen zu
- 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
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
