Zum Hauptinhalt springen
  1. Blog/

Kubernetes auf Hetzner Cloud: Einen Kubernetes-Cluster aufsetzen

Marcel Ryser
Autor
Marcel Ryser
Softwarearchitekt und Tech-Enthusiast aus Bern
Inhaltsverzeichnis

Dieser Blogbeitrag zeigt, wie man einen Kubernetes-Cluster auf Hetzner-Cloud-Servern aufsetzt. Der resultierende Cluster ist (noch) nicht vollständig, unterstützt aber bereits Persistent Volumes (Hetzner Block Storage Volumes). Ein Folgebeitrag beschreibt dann, wie wir einen (nginx-)Ingress und LoadBalancer einrichten (den Hetzner noch nicht anbietet).

Ich habe das nicht komplett allein gemacht, sondern konnte mich auf ein paar grossartige Quellen stützen:

Ich beschreibe die Schritte mit Fedora 31 als Betriebssystem für die Server. Warum Fedora? Mit Ubuntu bin ich nicht mehr so zufrieden wie noch vor ein paar Jahren. Warum also nicht eine andere grosse Distro ausprobieren und ihre Höhen und Tiefen kennenlernen?

Was wir tun
#

  • Kubernetes auf einem Setup mit einem Master und 2 Worker-Nodes aufsetzen, das etwa 13,24 € im Monat kostet (ohne Storage). Storage schlägt mit zusätzlich 4,76 €/100 GB pro Monat zu Buche
  • Die API-Kommunikation läuft über ein privates Netzwerk und nicht über das Internet
  • Du kannst Volumes auf Basis von Hetzner Cloud Block Storage Volumes für Datenspeicherung provisionieren

Was wir (noch) nicht tun
#

  • Einen Ingress einrichten, um Websites über eine öffentliche IPv4 auszuliefern
  • Einen LoadBalancer einrichten (für Ingress erforderlich)
  • Eine Backup-Strategie für die im Cluster und auf den Volumes gespeicherten Daten einrichten

Über diese Themen schreibe ich in ein paar Folgebeiträgen – schau also in ein paar Tagen/Wochen wieder auf meinem Blog vorbei.

Voraussetzungen
#

  • Grundlegendes Linux-Know-how, besonders Shell-Befehle
  • Ein Hetzner-Cloud-Konto

Schritt 1 – Das hcloud-CLI-Tool installieren
#

Beim Arbeiten mit Hetzner Cloud ist es oft praktisch, das meiste in der Shell zu erledigen. Dafür ist das hcloud-CLI-Tool nützlich.

Auf einem Mac wie meinem reicht:

brew install hcloud

Bist du nicht auf einem Mac, findest du die Installationsanleitung hier: https://github.com/hetznercloud/cli

Schritt 2 – Die Ressourcen auf Hetzner Cloud erstellen
#

Für das Setup brauchen wir mindestens 2–3 Server, die wir mit dem hcloud-Tool erstellen. Wir erstellen und richten ein:

  • 1 Hetzner Cloud Network
  • 1 Hetzner Cloud CX11 Server
  • 2 Hetzner Cloud CX21 Server

Der kleinere CX11-Server dient als Kubernetes-Master, die 2 CX21-Server sind Worker-Nodes.

2.1a – Ich habe noch keinen SSH-Key
#

Falls noch nicht geschehen, füge deinen öffentlichen SSH-Key zu Hetzner Cloud hinzu, damit du deine Server per Client-Zertifikat einrichten kannst.

hcloud ssh-key create --name <whateverthenameyoulike> --public-key-from-file ~/.ssh/id_rsa.pub

und notiere dir die Nummer des erstellten Keys, denn die brauchen wir später.

2.1b – Ich habe bereits einen Hetzner-Cloud-SSH-Key
#

Falls du schon einen SSH-Key erstellt hast, ermittle jetzt die Nummer/ID dieses Keys, denn die brauchen wir gleich.

hcloud ssh-key list

2.2 – Netzwerk und Nodes erstellen
#

Erstelle nun die Ressourcen auf Hetzner Cloud.

# Create private network (you can alter the ip range of course!. 10.98.0.0 is just a suggestion)
hcloud network create --name kubernetes --ip-range 10.98.0.0/16
hcloud network add-subnet kubernetes --network-zone eu-central --type server --ip-range 10.98.0.0/16

# Create Servers (this is where you'll need that ssh key from step 2.1)
hcloud server create --type cx11 --name master --image fedora-31 --ssh-key <ssh key id from 2.1>
hcloud server create --type cx21 --name worker-1 --image fedora-31 --ssh-key <ssh key id from 2.1>
hcloud server create --type cx21 --name worker-2 --image fedora-31 --ssh-key <ssh key id from 2.1>

# Attach the servers to the private network (we will use a easy-to-remember ip for the master, but this is optional)
hcloud server attach-to-network --network kubernetes --ip 10.98.0.10 master
hcloud server attach-to-network --network kubernetes worker-1
hcloud server attach-to-network --network kubernetes worker-2

Jetzt kannst du dich per SSH auf allen Servern einloggen:

hcloud server ssh <name-of-server>

# Example
hcloud server ssh master

Schritt 3 – Die Systeme für die Installation vorbereiten
#

Bevor wir Kubernetes installieren, müssen wir die Systeme vorbereiten. Das gilt nur für Fedora 31. Willst du Ubuntu nutzen, empfehle ich den Beitrag von Christian Beneke.

Wichtig! Diese Schritte musst du auf jedem Server ausführen (Master und die 2 Nodes).

Fedora kommt ohne aktivierte bash-completion. Das lässt sich leicht beheben mit

   dnf install -y nano bash-completion
   source /etc/profile.d/bash_completion.sh

fertig

3.1 Das System aktualisieren
#

Vor dem Start empfiehlt es sich, das System zu aktualisieren:

dnf update -y

3.2 Networking für Kubernetes aktivieren
#

cat <<EOF > /etc/sysctl.d/k8s.conf
# Allow IP forwarding for kubernetes
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF

Erstelle Konfigurationsdateien für Kubernetes und Docker, die wir gleich installieren.

cat <<EOF > /etc/sysconfig/kubelet
KUBELET_EXTRA_ARGS=--cloud-provider=external --runtime-cgroups=/systemd/system.slice --kubelet-cgroups=/systemd/system.slice
EOF

mkdir -p /etc/systemd/system/docker.service.d/
cat <<EOF > /etc/systemd/system/docker.service.d/00-cgroup-systemd.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd
EOF

systemctl daemon-reload

3.3 cgroups v1 statt v2 nutzen
#

Wenn du die Release Notes von Fedora 31 aufmerksam gelesen hast, ist dir vielleicht der Wechsel zu cgroups v2 aufgefallen. Da Docker und Kubernetes stark auf cgroups setzen, laufen wir definitiv in Probleme, weil sie cgroups v2 (noch) nicht unterstützen.

Also müssen wir zurück auf v1:

dnf install -y grubby
grubby --update-kernel=ALL --args="systemd.unified_cgroup_hierarchy=0"

Da es ein Kernel-Parameter ist, müssen wir das System vollständig neu starten:

reboot

3.4 Docker installieren
#

Folge https://docs.docker.com/install/linux/docker-ce/fedora/, um Docker zu installieren, im Wesentlichen also:

# Add Repo
dnf config-manager \
    --add-repo \
    https://download.docker.com/linux/fedora/docker-ce.repo

# Install packages
dnf install -y docker-ce docker-ce-cli containerd.io

# Enable the service
systemctl enable --now docker

3.5 Kubernetes installieren
#

Du kannst fast vollständig https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/ folgen, um Kubernetes zu installieren – mit ein paar Ausnahmen.

# Ensure iptables tooling does not use the nftables backend
update-alternatives --set iptables /usr/sbin/iptables-legacy

# Add repo (difference to the original is the "exclude=kube*" part, since we should do Kubernetes updates manually)
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF

# Set SELinux in permissive mode (effectively disabling it)
setenforce 0
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

# Install the packages for 1.17.3 (difference to the original is the addition of cni, since we we'll need that for flannel)
dnf install -y kubelet-1.17.3-0 kubeadm-1.17.3-0 kubectl-1.17.3-0 kubernetes-cni --disableexcludes=kubernetes

# Enable the Kubelet
systemctl enable --now kubelet

3.6 Versionen pinnen
#

Der Befehl dnf upgrade würde auch Docker- und Kubernetes-Pakete upgraden. Das wollen wir vielleicht nicht, denn für jedes Kubernetes-Upgrade gibt es spezifische Schritte: https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/

Es ist also sinnvoll, die installierte Version festzunageln und diese Pakete manuell zu upgraden. Um dnf am Upgrade aller Pakete zu hindern, kannst du Excludes in der Konfiguration definieren. Es gibt aber einen anderen Weg: dnf-plugin-versionlock. Dieses dnf-Plugin fügt Pakete zu einer Liste hinzu, die bei dnf upgrade nicht aktualisiert werden.

dnf install -y dnf-plugin-versionlock

und dann fügen wir alle installierten Pakete für Kubernetes und Docker zu dieser Lock-Liste hinzu:

dnf versionlock add kube* docker-* containerd*

Wenn wir upgraden müssen, entfernt man einfach den Version-Lock

dnf versionlock delete <package>

und installiert die gewünschte Version

dnf install -y kubeadm-1.17.3-0

und fügt das Paket danach wieder dem Version-Lock hinzu. Mach das mit jedem Paket entlang der offiziellen Upgrade-Anleitung: https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/

Schritt 4 – Den Cluster initialisieren – Control Plane installieren
#

Wenn du bis hierher alle Schritte auf jedem Server befolgt hast, haben wir alles, um den Cluster zu initialisieren. Die folgenden Schritte machst du nur auf dem Master-Node.

Du brauchst die öffentliche IP-Adresse deines Master-Servers und die Adresse im privaten Netzwerk. Da der API-Server-Traffic zwischen Master und Nodes nicht vollständig gesichert ist (siehe https://kubernetes.io/docs/concepts/architecture/master-node-communication/), will ich das private Netzwerk zwischen allen Servern nutzen. Wir sagen kubeadm also, den API-Server über die private IP (10.98.0.10, erstellt in Schritt 2.2) zu advertisen, aber auch die Authentifizierung über die öffentliche IP des Servers zu erlauben. Denn ich will auch von meinem Mac aus deployen (dieser Traffic ist durch das selbstsignierte Zertifikat in .kube/config verschlüsselt). Das --ignore-preflight-errors=NumCPU brauchen wir nur auf einem CX11-Server, da er nur eine CPU hat. Ein Kubernetes-Master empfiehlt mindestens 2 CPUs.

kubeadm init \
  --apiserver-advertise-address=10.98.0.10 \
  --service-cidr=10.96.0.0/16 \
  --pod-network-cidr=10.244.0.0/16 \
  --kubernetes-version=v1.17.3 \
  --ignore-preflight-errors=NumCPU \
  --apiserver-cert-extra-sans <public ip of your master server>

Jetzt solltest du eine Erfolgsmeldung erhalten wie

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 10.98.0.10:6443 --token 9bsd1v.jl1351231kk53 \
    --discovery-token-ca-cert-hash sha256:aca30f96f58ea1479fe421936e232d5dc46fe19f03fe9d7888821154bb457039

Notiere dir den Join-Befehl in der letzten Zeile, denn den brauchen wir später.

Kopiere die Cluster-Config in dein Home, um mit kubectl auf den Cluster zuzugreifen:

mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

Schritt 5 – Ein Container-Netzwerk deployen
#

Damit die Container/Pods kommunizieren können, brauchen wir ein Container-Netzwerk (CNI). Eine Option ist Cilium, du kannst aber auch ein anderes wählen: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#pod-network

Update 20. Jan. 2020: Flannel hat auf meinem Cluster Probleme verursacht – warum? Lies hier

kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/v1.7.1/install/kubernetes/quick-install.yaml

Optional

Prüfe nun, ob das Netzwerk läuft: Du kannst einen gründlichen Connectivity-Test deployen, um zu sehen, ob jeder Container auf jedem Node die anderen über verschiedene Network Policies erreichen kann.

kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/1.7.1/examples/kubernetes/connectivity-check/connectivity-check.yaml

# after a while
kubectl get pods
NAME                                                     READY   STATUS             RESTARTS   AGE
echo-a-9b85dd869-292s2                                   1/1     Running            0          8m37s
echo-b-c7d9f4686-gdwcs                                   1/1     Running            0          8m37s
host-to-b-multi-node-clusterip-6d496f7cf9-956jb          1/1     Running            0          8m37s
host-to-b-multi-node-headless-bd589bbcf-jwbh2            1/1     Running            0          8m37s
pod-to-a-7cc4b6c5b8-9jfjb                                1/1     Running            0          8m36s
pod-to-a-allowed-cnp-6cc776bb4d-2cszk                    1/1     Running            0          8m36s
pod-to-a-external-1111-5c75bd66db-sxfck                  1/1     Running            0          8m35s
pod-to-a-l3-denied-cnp-7fdd9975dd-2pp96                  1/1     Running            0          8m36s
pod-to-b-intra-node-9d9d4d6f9-qccfs                      1/1     Running            0          8m35s
pod-to-b-multi-node-clusterip-5956c84b7c-hwzfg           1/1     Running            0          8m35s
pod-to-b-multi-node-headless-6698899447-xlhfw            1/1     Running            0          8m35s
pod-to-external-fqdn-allow-google-cnp-667649bbf6-v6rf8   1/1     Running            0          8m35s

Wenn alles im Status ready ist, hast du Glück – alles funktioniert wie erwartet. Du kannst den Check löschen:

kubectl delete -f https://raw.githubusercontent.com/cilium/cilium/1.7.1/examples/kubernetes/connectivity-check/connectivity-check.yaml

Schritt 6 – Worker-Nodes beitreten lassen
#

Führe den in Schritt 4 ausgegebenen Join-Befehl auf beiden Worker-Nodes aus.

Etwa so:

kubeadm join 10.98.0.10:6443 --token 9bsd1v.jl1351231kk53 \
    --discovery-token-ca-cert-hash sha256:aca30f96f58ea1479fe421936e232d5dc46fe19f03fe9d7888821154bb457039

Schritt 7 – Den Hetzner Cloud Controller Manager deployen
#

Für die Integration mit Hetzner Cloud haben sie einen “Kubernetes Cloud Controller Manager” entwickelt. Der braucht Zugriff auf die Hetzner Cloud, also erstellen wir ein weiteres API-Token.

Um ein Hetzner-Cloud-API-Token zu erstellen, melde dich im Web-Interface an und navigiere zu deinem Projekt -> Access -> API tokens und erstelle ein neues Token.

Nun erstellen wir in Kubernetes ein Secret mit diesem Token. Dafür brauchst du die Network-ID, die wir in Schritt 2.2 erstellt haben:

hcloud network list
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: hcloud
  namespace: kube-system
stringData:
  token: "<the api token you created just yet>"
  network: "<network-id>"
EOF

Jetzt können wir den Controller deployen:

kubectl apply -f  https://raw.githubusercontent.com/hetznercloud/hcloud-cloud-controller-manager/master/deploy/v1.5.1.yaml

Schritt 8 – Hetzner Cloud Storage (CSI) deployen
#

Um die PVC zu nutzen, die die meisten Anwendungen brauchen, musst du ein CSI deployen. Auch dafür hat Hetzner einen Treiber geschrieben.

Erstelle das API-Token aus dem vorherigen Schritt neu oder nutze es wieder und erstelle ein Secret für den Treiber.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: hcloud-csi
  namespace: kube-system
stringData:
  token: "<the api token you created just yet>"
EOF

Jetzt deploye die API und den Treiber:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/csi-api/release-1.14/pkg/crd/manifests/csidriver.yaml
kubectl apply -f https://raw.githubusercontent.com/kubernetes/csi-api/release-1.14/pkg/crd/manifests/csinodeinfo.yaml

kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.2.3/deploy/kubernetes/hcloud-csi.yml

Fazit
#

Jetzt solltest du neue Anwendungen mit Storage-Fähigkeit (Volumes) deployen können. Um darauf zuzugreifen, bräuchtest du aber einen Einstiegspunkt aus dem Internet wie NodePorts – doch es gibt bereits eine bessere Lösung: LoadBalancer/Ingress.

Das nächste Thema ist, wie wir auf die in Kubernetes deployten Anwendungen zugreifen können.