Wolltest du schon immer einmal Kubernetes zu Hause einsetzen, hast aber gerade keinen eigenen Serverraum zur Hand? Mit K3S von Rancher Labs kannst du ganz einfach einen leichtgewichtigen Kubernetes-Cluster auf einem oder mehreren Raspberry Pis aufsetzen.
In diesem Blogpost zeige ich dir, wie du einen Kubernetes-Cluster auf drei Raspberry Pis installierst, einen Webserver ausrollst und diesen samt gültigem Zertifikat aus dem Internet zugänglich machst. Alles, was du dazu brauchst, findest du in der nachfolgenden Liste.


Hostname | IP | Type |
---|---|---|
k3smaster | 192.168.0.130 | Master (Server) |
k3snode1 | 192.168.0.131 | Worker (Agent) |
k3snode2 | 192.168.0.132 | Worker (Agent) |
– | 192.168.0.240 | LoadBalancer IP |
Benötigte Komponenten
- 3 x Raspberry Pis 4 4GB (Raspberry Pi 3B+ gehen auch)
- 3 x Netzteil + SD Karte mit Raspberry Pi OS Buster (ehemals Raspbian) mit SSH Zugriff
- Router mit Port-Forwarding/Port-Freigabenmöglichkeit (z.B. eine Fritz!Box)
- Router mit Konfigurationsmöglichkeit für einen DynDNS-Dienst
- Einen Account bei einem DynDNS-Dienst (z. B. bei ddnss)
Vorarbeiten:
- OS der 3 Raspberry Pis sollte fertig eingerichtet sein
- mit statischen IP-Adressen gemäss Tabelle;
- grundkonfiguriert (Hostnames, Passwort für Benutzer pi, etc.)
kubectl
undhelm
lokal auf dem Notebook installiert.
Cluster installieren
Als ersten Task müssen wir die Raspberry Pis Container-fähig machen. Dafür müssen wir auf allen drei Systemen einige Kernel-Features aktivieren: Dazu hängen wir jeweils in der Datei /boot/cmdline.txt
die Features cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory
ans Ende der bereits existierenden Zeile an. Die Datei sollte dann in etwa so aussehen:
$ cat /boot/cmdline.txt
console=serial0,115200 console=tty1 root=PARTUUID=738a4d67-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait
cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory
Danach müssen alle drei Raspberry Pis einmal gebootet werden, damit die Änderung auch aktiv wird.
Installieren des Servers (master)
Nun sind wir bereit, die Installation des Kubernetes-Clusters zu beginnen. Dazu wird auf dem ersten Raspberry Pi der Server (master) installiert. Wir verwenden dabei nicht den standardmässigen Traefik als Ingress-Controller, sondern setzen auf Nginx Ingress. Als Load Balancer verwenden wir nicht den standardmässigen ServiceLB, sondern MetalLB. Sowohl Nginx, als auch MetalLB werden nach der Installation separat konfiguriert:
-
Via SSH als Nutzer pi auf den Server (master bzw. 192.168.0.130) verbinden.
-
Installieren des Servers. Dies installiert
kubectl
,crictl
,ctr
,k3s-killall.sh
undk3s-uninstall.sh
. Zusätzlich wird eine Kubeconfig-Datei nach/etc/rancher/k3s/k3s.yaml
geschrieben.
pi@k3smaster:~ $ export K3S_KUBECONFIG_MODE="644"
pi@k3smaster:~ $ export INSTALL_K3S_EXEC=" --disable servicelb --disable traefik"
pi@k3smaster:~ $ curl -sfL https://get.k3s.io | sh -
- Sobald das Kommando fertig ist, kann man überprüfen, ob k3s läuft (
sudo systemctl status k3s
) und ob die Installation erfolgreich war.
pi@k3smaster:~ $ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3smaster Ready master 5m v1.17.4+k3s1
- Nun müssen wir noch den Token herausfinden, um im nächsten Schritt die Agents (worker nodes) in den Cluster einzubinden.
pi@k3smaster:~ $ sudo cat /var/lib/rancher/k3s/server/node-token
K106edce2df174510a81gff7e49680fc556fad30173773a1ec1a5dc779a83d4e35b::server:6a9b70a1f5bc02a7cf775f97fa912789
Einbinden des k3s-Agent (Worker Nodes)
Nachdem wir nun den Master installiert haben, können wir die Agents (worker nodes) mit dem Server (master) verbinden. Dies muss für beide Agents (worker nodes) durchgeführt werden.
- Das definieren der Umgebungsvariable
K3S_URL
legt automatisch fest, dass der Node als Agent installiert werden soll und wo er den Server (master) finden kann.
pi@k3snode1:~ $ export K3S_KUBECONFIG_MODE="644"
pi@k3snode1:~ $ export K3S_URL="https://192.168.0.130:6443"
pi@k3snode1:~ $ export K3S_TOKEN="K104bef2f9c494f290d9b6c5a5d5d09c0c1698f7e126e08aa9f3d10324166cf2b77
:server:dd4bdd3ee3ac6cd84215931283e5d4be"
pi@k3snode1:~ $ curl -sfL https://get.k3s.io | sh -
- Sobald das Kommando abgeschlossen wurde, kann man auf dem Server (master) überprüfen, ob sich beide Agents verbunden haben.
pi@k3smaster:~ $ kubectl get nodes
NAME STATUS ROLES AGE VERSION
k3snode1 Ready <none> 3m46s v1.17.4+k3s1
k3smaster Ready master 19m v1.17.4+k3s1
k3snode2 Ready <none> 17s v1.17.4+k3s1
MetalLB (Load Balancer) installieren
Da wir nun alle Voraussetzungen für den Betrieb des Clusters geschaffen haben, können wir damit beginnen, MetalLB als Load Balancer zu installieren. MetalLB ist so ausgelegt, dass man bei der Installation einen IP-Adressbereich angibt, aus welchem MetalLB virtuelle IP-Adressen an Services mit dem Typ LoadBalancer verteilt. Somit sind Services des Typs LoadBalancer dann auf dieser IP-Adresse auch ausserhalb des Clusters erreichbar.
Für die Installation von MetalLB via Helm Chart übergeben wir folgende Parameter:
--namespace kube-system
: Wir installieren MetalLB in denkube-system
namespace.--set configInline...
.name
: Der Name den wir dem Addresspool geben wollen, hierdefault
..protocol
: Der Layer, auf welchem wir MetalLB konfigurieren wollen, hierlayer2
..addresses[0]
: Der IP-Adressbereich, aus welchem MetalLB virtuelle IP-Adressen an Services verteilen kann. Hier192.168.0.240-192.168.0.250
, also 11 Adressen.
- Mit diesen Optionen installieren wir MetalLB via Helm.
$ helm install metallb stable/metallb --namespace kube-system \
--set configInline.address-pools[0].name=default \
--set configInline.address-pools[0].protocol=layer2 \
--set configInline.address-pools[0].addresses[0]=192.168.0.240-192.168.0.250
- Überprüfen, ob MetalLB korrekt im
kube-system
namespace ausgerollt worden ist:
$ kubectl get pods -n kube-system -l app=metallb
NAME READY STATUS RESTARTS AGE
metallb-speaker-wx76w 1/1 Running 0 81s
metallb-speaker-l6cts 1/1 Running 0 81s
metallb-speaker-wz5t7 1/1 Running 0 81s
metallb-controller-75bf779d4f-r6wfn 1/1 Running 0 80s
Nginx (Ingress) installieren
Damit wir auch Routen verwalten und diese den Services zuweisen können, müssen wir noch Nginx als Ingress-Controller installieren. Dieser ermöglicht es, Applikationen über http/https von ausserhalb des Clusters verfügbar zu machen. Auch den Ingress-Controller werden wir via Helm Chart installieren.
- Installieren des Nginx Ingress in den Namespace
kube-proxy
.
$ helm install nginx-ingress stable/nginx-ingress --namespace kube-system \
--set controller.image.repository=quay.io/kubernetes-ingress-controller/nginx-ingress-controller-arm \
--set controller.image.tag=0.25.1 \
--set controller.image.runAsUser=33 \
--set defaultBackend.enabled=false
- Überprüfen, ob Nginx korrekt im Namespace
kube-system
ausgerollt worden ist.
$ kubectl get pods -n kube-system -l app=nginx-ingress
NAME READY STATUS RESTARTS AGE
nginx-ingress-controller-566c7cfbdd-jlm5h 1/1 Running 0 2m21s
- Nginx wird mit einem Service vom Typ LoadBalancer installiert. Somit bekommt der Nginx Ingress Service eine IP-Adresse von MetalLB zugewiesen, in diesem Fall
192.168.0.240
. Das heisst, der Service ist über diese IP-Adresse verfügbar.
$ kubectl get services -n kube-system -l app=nginx-ingress -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-ingress-controller LoadBalancer 10.43.144.115 192.168.0.240 80:31718/TCP,443:30045/TCP 3m27s
- Beim Zugriff auf diese IP-Adresse über den Browser sollte eine 404 Meldung zurück kommen.
Notiz: Möchte man Services nur vom lokalen Netzwerk verfügbar machen, könnte man DNS-Einträge (z. B. in der Datei /etc/hosts) auf diese virtuelle IP-Adresse 192.168.0.240
zeigen lassen und gleichzeitig dazu noch ein Nginx Ingress Objekt mit derselben Route wie der DNS-Name ausrollen. Dies werden wir hier aber nicht weiter ausführen, da wir die Services ja mit Zertifikaten aus dem Internet verfügbar machen wollen.
Cert-Manager installieren
Um nun Zertifikate für Ingress Objekte automatisch generieren zu lassen und den Traffic für unsere Applikationen mit TLS zu verschlüsseln, werden wir den Cert-Manager installieren.
-
Als erstes erstellen wir mit
kubectl create namespace cert-manager
einen neuen Namespace, worin wir den Cert-Manager ausrollen können. -
Danach wird das Helm Repository von Jetstack hinzugefügt, welches das cert-manager Helm Chart beinhaltet.
$ helm repo add jetstack https://charts.jetstack.io && helm repo update
- Nun können wir den Cert-Manager in den
cert-manager
namespace installieren.
$ helm install cert-manager jetstack/cert-manager --namespace cert-manager --set installCRDs=true
- Da der Cert-Manager nun installiert ist, müssen wir noch zwei Certificate Issuer erstellen, welche dann bei Let’s Encrypt Zertifikate für unsere Services beantragen. Wir erstellen je einen für das Testing und für die Produktion. Die Dateien sollten den Inhalt wie unten haben, wobei man die E-Mail-Adresse noch individuell anpassen muss.
# letsencrypt-staging.yaml
apiVersion: cert-manager.io/v1alpha3
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: <your-email-address>
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx
# letsencrypt-prod.yaml
apiVersion: cert-manager.io/v1alpha3
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: <your-email-address>
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
- Diese beiden Dateien spielen wir dann mit
kubectl apply -f <file>
in den Cluster ein.
Nun haben wir alle Cluster-Komponenten installiert, welche wir brauchen, um Applikationen auszurollen.
Web Server-Deployment
Nun geht es daran, einen simplen Webserver zu installieren.
-
Als erstes brauchen wir einen neuen Namespace, welchen wir mit
kubectl create namespace webserver
erstellen. -
Nun können wir ein Deployment für einen standardmässigen Webserver erstellen:
# webserver-deployment.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: httpd
name: httpd
namespace: webserver
spec:
replicas: 1
selector:
matchLabels:
app: httpd
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: httpd
spec:
containers:
- image: httpd:2.4.46
name: httpd
nodeSelector:
role: worker
status: {}
- Für das Deployment brauchen wir noch einen Service, welcher auf das Deployment zeigt.
# webserver-service.yaml
---
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: httpd
name: httpd
namespace: webserver
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: httpd
status:
loadBalancer: {}
- Nun können wir überprüfen, ob der Webserver ausgerollt ist, ob er korrekt läuft, und ob ein Service erstellt wurde.
$ kubectl get pods -n webserver
NAME READY STATUS RESTARTS AGE
httpd-68c587fd44-72chj 1/1 Running 0 47m
$ kubectl get svc -n webserver
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd ClusterIP 10.43.9.18 <none> 80/TCP 27m
- Falls nun auf der Service Cluster-IP
die Standard-Webseite des httpd zurück kommt, ist der Webserver richtig ausgerollt.
k3smaster:~ $ curl
<html><body><h1>It works!</h1></body></html>
Nun ist der Webserver vorerst nur über die Cluster-IP und den Service-Port verfügbar.
Aus dem Internet erreichbar machen
Jetzt gehen wir einen Schritt weiter und treffen die Vorbereitungen, um den Webserver via TLS aus dem Internet erreichbar zu machen.
DynDNS-Dienst
Standardmässig ändert sich die öffentliche IP-Adresse eines Heim-Routers, welche man vom Internet Service Provider zugewiesen bekommt, ab und zu. Aus diesem Grund braucht man einen dynamischen DNS-Dienst, welcher den Domänennamen automatisch auf die am Router gerade zugewiesene IP-Adresse wechselt. Somit wird der Router vom Internet her über einen festen Domänennamen verfügbar.
Um dies zu machen, registrieren wir uns bei einem DynDNS-Dienst – im Beispiel wurde dazu ein Account bei ddnss erstellt. Danach löst man eine DynDNS-Domäne. Dabei wählt man folgende Optionen:
- Name: Den Namen der gewünschten Domäne. Hier
foobar
, welche zum Domänennamenfoobar.ddnss.ch
führt. - IP-Mode: A (IPv4)
- Wildcard: Anwählen.

Die erstellte Domäne muss man nun im eigenen Router konfigurieren. Auf einer Fritz!Box 5490 sieht dies folgendermassen aus:
Nach dem Konfigurieren der Domäne auf dem Router sollte man im Webportal des DynDNS-Anbieters sehen, dass für die Domäne eine IP-Adresse konfiguriert wurde. Diese entspricht der öffentlichen IP-Adresse, welche dem Router vom Service Provider zugewiesen wurde.
Port-Forwarding
Damit vom Cert-Manager Let’s Encrypt Zertifikate gelöst werden können, ist es notwendig, dass Port 80 und 443 auf dem Router geöffnet sind. Des weiteren wollen wir dann vom Internet via Web-Adresse auf den Webserver zugreifen.
Nginx Ingress ist der Verwalter für unsere Routen und leitet ankommenden Traffic auf den Webserver-Service weiter. Da der Nginx Ingress-Service als Typ LoadBalancer auf der IP-Adresse 192.168.0.240 läuft, richten wir auch das Port-Forwarding dorthin ein.
Das Port-Forwadring konfigurieren wir auf der Fritzbox unter Internet -> Freigaben -> Portfreigaben und „Gerät für Freigaben hinzufügen“:
- IP-Adresse: 192.168.0.240
- Freigaben
- HTTP (80)
- Anwendung: HTTP-Sever
- Protokol: TCP
- Port an Gerät von: 80
- Port an Gerät bis: 80
- Port extern Gewünscht: 80
- HTTPS (443)
- Anwendung: HTTPS-Sever
- Protokol: TCP
- Port an Gerät von: 443
- Port an Gerät bis: 443
- Port extern Gewünscht: 443
- HTTP (80)
Nach dem Übernehmen der Port-Forwarding-Konfiguration sollte diese aktiv sein und in der Liste in etwa so wie auf dem Bild aussehen.
Ingress-Objekt
Nun ist alles vorbereitet, dass wir für den Webserver ein Ingress Objekt mit gültigen Let’s Encrypt-Zertifikaten lösen können.
- Erstellen der Ingress-Objektkonfiguration als Datei.
# webserver-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: httpd-ingress
namespace: webserver
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- webserver.<my-domain.ch>
secretName: httpd-prod-tls
rules:
- host: webserver.<my-domain.ch>
http:
paths:
- path: /
backend:
serviceName: httpd
servicePort: 80
-
Danach die Datei einspielen mit
kubectl apply -f webserver-ingress.yaml
. -
Nach einiger Zeit sollte man ein erfolgreiches Zertifikat erhalten haben, und ein Ingress-Objekt sollte erstellt worden sein.
$ kubectl get certificates -n webserver
NAME READY SECRET AGE
httpd-prod-tls True httpd-prod-tls 31m
$ kubectl get ingress -n webserver
NAME CLASS HOSTS ADDRESS PORTS AGE
httpd-ingress <none> webserver.<my-domain.ch> 80, 443 31m
Hurraa! Wir haben es geschafft! Nun sollte der Webserver unter webserver.<my-domain.ch>
aus dem Internet erreichbar sein. Der Kubernetes-Cluster ist damit getestet und bereit für weitere Applikationen!
Zusammenfassung
Mit K3S ist es sehr einfach möglich, sich zu Hause einen Kubernetes-Cluster aufzusetzen und eigene Applikationen auszurollen. Man muss sich des weiteren bei Verwendung von Cert-Manager keine Gedanken mehr machen über die mühselige Verwaltung von Zertifikaten.
Dominic – I’m working with Sebastian around a blog post about Rancher 2.5 – would you be OK with us posting a link to your article and broadcasting via our social media platforms?
Hi Pete
Sure, that would be cool, go on with that! 🙂
Hallo Dominic,
ich komme leider ab dem Punkt MetalLB nicht weiter. Wo soll MetalLB installiert werden? Es erscheint mir unlogisch, den Load Balancer auf meinem PC zu installieren. Danke im Vorraus! 🙂
Hallo Christopher,
Das ist richtig, MetalLB wird auf dem Cluster installiert. Dazu benötigst du bei dir auf dem PC „Helm“ und die Kube-config deines K3S Clusters. Helm setzt dann die yaml files dynamisch mit den von dir angegebenen Values (hier –set … parametern) zusammen und appliziert diese auf dem Cluster. Falls du also Helm bereits auf deinem PC installiert hast und du mit „kubectl get nodes“ die 3 Nodes deines K3S Clusters bekommst kannst du ohne bedenken bei Schritt 1 bei der Installation von MetalLB fortfahren 🙂 Ich hoffe ich konnte dir damit weiterhelfen, ansonsten kannst du gerne auf mich zukommen.
Hallo Dominic,
danke für deine Antwort! Nachdem ich die k3s.yaml aus /etc/rancher/k3s/ in die .kube\config kopiert habe, konnte ich MetalLB installieren. Leider kann der nginx-ingress-controller das Image nicht herunterladen. Laut $ kubectl describe pod liegt es an der Autorisierung. Der Status des Pod wechselt zwischen „ImagePullBackOff“ und „ErrImagePull“. Muss ich noch irgendwo einen Token angeben? Danke! 🙂
Der Error:
Failed to pull image „us.gcr.io/quay.io/kubernetes-ingress-controller/nginx-ingress-controller-arm:0.25.1“: rpc error: code = Unknown desc = failed to pull and unpack image „us.gcr.io/quay.io/kubernetes-ingress-controller/nginx-ingress-controller-arm:0.25.1“: failed to resolve reference „us.gcr.io/quay.io/kubernetes-ingress-controller/nginx-ingress-controller-arm:0.25.1“: failed to authorize: failed to fetch anonymous token: unexpected status: 400 Bad Request
Hallo Christopher, freut mich hat es nun geklappt. Hab wohl vergessen zu schreiben, dass man die k3s.yaml auf dem PC laden muss.
Da sieht mir die URL falsch aus. Es sollte „quay.io/kubernetes-ingress-controller/nginx-ingress-controller-arm“ sein. Ich habe gerade auf einem Raspi getestet, wenn ich „crictl pull quay.io/kubernetes-ingress-controller/nginx-ingress-controller-arm:0.25.1“ ausführe funktioniert es bei mir. Du benötigst keinen Token. Teste das doch mal auf einem deiner Raspis. Ansonsten könnte es auch sein, dass sich etwas im Helm Chart geändert hat.
Es wäre angenehmer zu lesen, wenn die Code Blöcke nicht gelb wären. 🙂
Danke für den Input, wir prüfen das fürs nächste Mal!
Hallo Christopher,
Hallo Dominic,
ich habe das gleiche Problem wie Dominic. crictl pull funktioniert :/
Habt ihr einen Tipp?
Nach ein bisschen Recherche habe ich folgenden Vorschlag:
helm install nginx-ingress ingress-nginx/ingress-nginx –namespace kube-system –set controller.image.repository=quay.io/kubernetes-ingress-controller/nginx-ingress-controller-arm –set controller.image.tag=0.27.1 –set defaultBackend.enabled=false –set controller.image.digest=““
Das mit dem digest habe ich noch nicht verstanden. Nachdem ich den runAsUser Parameter enternt habe und den default genommen habe, hat es funktioniert.