K3S on Raspberry Pi

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 und helm 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:

  1. Via SSH als Nutzer pi auf den Server (master bzw. 192.168.0.130) verbinden.

  2. Installieren des Servers. Dies installiert kubectl, crictl, ctr, k3s-killall.sh und k3s-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 -
  1. 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
  1. 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.

  1. 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 -
  1. 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 den kube-system namespace.
  • --set configInline...
    • .name: Der Name den wir dem Addresspool geben wollen, hier default.
    • .protocol: Der Layer, auf welchem wir MetalLB konfigurieren wollen, hier layer2.
    • .addresses[0]: Der IP-Adressbereich, aus welchem MetalLB virtuelle IP-Adressen an Services verteilen kann. Hier 192.168.0.240-192.168.0.250, also 11 Adressen.
  1. 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
  1. Ü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.

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

  1. Als erstes erstellen wir mit kubectl create namespace cert-manager einen neuen Namespace, worin wir den Cert-Manager ausrollen können.

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

  1. Als erstes brauchen wir einen neuen Namespace, welchen wir mit kubectl create namespace webserver erstellen.

  2. 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: {}
  1. 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: {}
  1. 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
  1. 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änennamen foobar.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:

dyndns fritzbox

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.

domain erstellen

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

Nach dem Übernehmen der Port-Forwarding-Konfiguration sollte diese aktiv sein und in der Liste in etwa so wie auf dem Bild aussehen.

Portforwarding konfigurieren

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.

  1. 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
  1. Danach die Datei einspielen mit kubectl apply -f webserver-ingress.yaml.

  2. 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.

 

10 Kommentare

  • Peter Dalziel, 25. Oktober 2020

    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?

  • mm
    Dominic Gabriel, 25. Oktober 2020

    Hi Pete
    Sure, that would be cool, go on with that! 🙂

  • Christopher Monje, 14. November 2020

    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! 🙂

  • mm
    Dominic Gabriel, 16. November 2020

    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.

  • Christopher Monje, 16. November 2020

    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

  • mm
    Dominic Gabriel, 16. November 2020

    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.

  • Random Dude, 20. November 2020

    Es wäre angenehmer zu lesen, wenn die Code Blöcke nicht gelb wären. 🙂

  • mm
    Saraina Jenni, 20. November 2020

    Danke für den Input, wir prüfen das fürs nächste Mal!

  • Michael, 20. November 2020

    Hallo Christopher,
    Hallo Dominic,

    ich habe das gleiche Problem wie Dominic. crictl pull funktioniert :/

    Habt ihr einen Tipp?

  • Michael, 20. November 2020

    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.