In letzter Zeit kam das Thema Cloud häufiger in meiner Umgebung vor, als Virus oder Bit.
Wenn man dann erstmal anfängt, darüber nachzudenken, was das Wort bedeutet, kommt man schnell auf die Frage, wie ist Cloud eigentlich definiert. Und da fängt es schnell an, wolkig zu werden. Es gibt zwar Definitionen, aber die sind schwammig und bedeuten alles und nichts. Die einen kennen eine Private Cloud und meinen damit zumeist eine Umgebung mit Hosts und VMs. Die andere Seite spricht gerne auch mal von “Cloud-nativ” und meint damit Serverless-Systeme, bei denen jeder Request einen Script ausführt und dessen Ausführungszeit haargenau abgerechnet wird. Was eine Cloud ist - ist also beliebig wolkig zu sehen.
Ich bin nicht wirklich glücklich damit, zumal mich das Marketingdeutsch gewaltig stört. Mein Auto hat ‘ne Batterie. Ist es deshalb nicht auch ein E-Auto?…
Kubernetes
Taucht man etwas tiefer in die Welt der Cloud-afinen Menschen ein, beginnt sich der Wald etwas zu lichten. In der Realität muss man nämlich konkreter werden und da geht es dann schnell um die Themen, die wir auch einfach beim richtigen Namen nennen können. Nur sind die nicht so griffig. Ziel ist es, die Services maximal von einander zu trennen. Beim Thema Host+VMs handelt es sich um Virtualisierung. Hier kann man relativ einfach pro VM einen Service definieren. Der Vorteil ist. Der Service hat seine gesamte Landschaft, die ihm behagt, um sich herum gestrickt. Der Nachteil ist, dass der gesamte Overhead eines kompletten Betriebssystem jedesmal mitkommt. So wird aus einem simplen Java-Prozess gleich ein Thema um Kernel Updates, Firewalls und ich weiß nicht was.
Da der Overhead für große Umgebungen irgendwann unwirtschaftlich war und die Linuxwelt schon lange eine Alternative anbot, entwickelte sich mit der Zeit das Thema Paravirtualisierung zu einer besseren Lösung. Als Docker Inc. dann noch mit verhältnismäßig simplen Tools für CLI um die Ecke kam, war das Thema Cloud langsam geboren und benannt. Paravirtualisierung trennt die Services durch Prozessisolation. Ein Programm A kann in seiner Isolationsschicht nicht auf Programm B zugreifen, wenn dieser sich (leienhaft ausgedrückt) nicht im gleichen Context befindet. Was also eine CPU bei der Virtualisierung mit VMs macht, kann auch der Linux-Kernel auf Prozessebene. Er trennt die Programme hart voneinander. Durch den Einsatz von Images (eigentlich TAR-Balls) kann jeder Service in einen Container verpackt werden und liefert sein Ökosystem mit (also alle Libs, die ein Programm so während der Laufzeit braucht). Das OS wird aber nicht wirklich gestartet. Nur das Programm. Nun ist <a href="Docker ansich schon schön - aber auch hier gilt wieder das Problem - je mehr Services man betreibt, desto unübersichtlicher wird es.
Also fingen einige Leute an, ein System um Docker zu stricken. Sie nutzen die Ausführungsmöglichkeiten und die Tools um eine Management Umgebung drumherum zu bauen und fertig war Kubernetes.
Installation
Gefühlt ist die Installation von Kubernetes einfach… wenn man weiß, wie. In der Realität gibt es diverse Haken und Ösen. Benutzt man Docker als Unterbau, ist das Leben relativ einfach, weil sich das Ökosystem Kubernetes um Docker aufbaut. Da Docker aber langsam stirbt und die diversen Probleme ziemlich nerven, würde man gerne auf was anderes setzen. Mein erster Gedanke war Podman
. Leider braucht Kubernetes einen Dienst, gegen den er sich verbinden kann und da kam Podman anfangs nicht in Frage. Deshalb hab ich die Alternative Basis CRI-O
eingesetzt. Letztlich wrappen Docker und Podman CRI-O. Da Docker nun auch schon langsam sinkt, bedient man sich dem daraus geschaffenen Industriestandard containerd
.
Ich hab zwei Wege ausprobiert, um Kubernetes zu installieren.
Do-it-yourself
Der Ansible Script, den ich gebaut habe, tut eigentlich nicht viel mehr als folgendes:
- Repository http://apt.kubernetes.io/ hinzufügen
- Repository http://ppa.launchpad.net/projectatomic/ppa/ubuntu hinzufügen
- kubelet, kubectl, kubeadm, kubernetes-cni , cri-o-1.15 installieren
- overlay und br_netfilter Module im Kernel laden (dauerhaft)
- IPv4 und IPv6 Forward in der Firewall aktivieren (dauerhaft)
- CRI-O so konfigurieren, dass es Overlay benutzt, den Pfad auf conmon richtig setzt
- die Docker.Io und interne Registry erlaubt
- außerdem muss für Kubelet noch konfiguriert werden, dass es CRI-O verwenden soll und Systemd
Auf enem dem Master-Node wird dann mit kubeadm init ..
der Cluster gestartet und das Netzwerk vorbereitet. Die einzelnen Nodes joinen dann den Cluster mit kubeadmin join
und einem Token.
Dieses Basissetup ist dann erstmal ein Cluster. Es gibt eine Reihe von Services, die dem kube-system hinzugefügt werden sollten.
Ich gebe zu, Kubernetes alleine ist im produktiven Einsatz schwierig zu handhaben. Der Umgang mit den Yaml-Configs, die man “apply”‘n muss, ist gewöhnungsbedürftig. Deshalb kamen später auch Managementtools, wie OpenShift auf, um einem Admin das Leben in produktiven Umgebungen zu erleichtern.
Kubespray
Da ich sowieso überall und nirgends Ansible verwende, liefert eine Suche im Netz relativ schnell kubespray
ans Tageslicht. Kubespray ist ein spezielles Repository mit einem eigenen Ökosystem für Kubernetes. Es richtet nicht nur Kubernetes an sich ein, es bereitet die Systeme auch entsprechend vor. Leider lässt es sich schlecht in eigene Ansible Repositories integrieren. Das ist aber auch so ziemlich der einzige Nachteil.
Folgende Schritte sind zu erledigen.
- Kubespray von https://github.com/kubernetes-sigs/kubespray/archive/refs/heads/master.ziphttps://github.com/kubernetes-sigs/kubespray/archive/refs/heads/master.zip` herunterladen oder das Repository klonen.
- ein Inventar anlegen
cp -r inventory/sample inventory/mycluster
- ein Inventory mit drei
kube_nodes
, dreietcd
Nodes und ein oder zweikube_control_plane
s
- einige Default-Konfigs ändern
- group_vars/etcd.yml - den Wert
etcd_deployment_type
aufhost
stellen (statt docker) - group_vars/k8s_cluster/k8s-cluster.yml - den Wert
container_manager
aufcontainerd
stellen
- group_vars/etcd.yml - den Wert
Anschließend dann das Playbook cluster.yml
ausführen. Es wird ein etcd installiert. Dann wird der ausgewählte container manager ausgeführt (in unserem Fall containerd). Dann wird Kubernetes konfiguriert
Konfiguration
Anschließend kann man das Kubernetes Dashboard installieren.
Die Konfigdatei mit curl https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended.yaml > kubernetes-dashboard-deployment.yml
herunterladen. Die Datei enthält diverse fürs Dashboard notwendige Deklarationen. Eine davon ist der Service kubernetes-dashboard
. An diesem stellt man den spec-type Nodeport ein.
...
kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard
spec:
ports:
- port: 443
targetPort: 8443
selector:
k8s-app: kubernetes-dashboard
type: NodePort
...
Der Sinn dahinter ist, dass damit das Dashboard auf allen Nodes im Kubernetes auf einen Port erreichbar ist und keinen Ingress braucht. Die Datei dann mit kubectl apply -f kubernetes-dashboard-deployment.yml
einfügen.
Anschließend muss ein Admin-Account angelegt werden. Das ist etwas seltsam nur mit Tokens.
# acoby-admin.yml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: acoby-admin
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: acoby-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: acoby-admin
namespace: kube-system
Das auch einbinden mit kubectl apply -f acoby-admin.yml
. Dann das Token extrahieren.
SA_NAME="acoby-admin"
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep ${SA_NAME} | awk '{print $1}')
Denn relevanten Ports auf den Nodes findet man mit
kubectl get service -n kubernetes-dashboard
Dann brauchen wir einen Ingress Controller. Den gibt es hier
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.43.0/deploy/static/provider/baremetal/deploy.yaml
Ein Beispiel Service auf Basis von HTTPBin konfiguriert man wie folgt.
kubectl apply -f https://github.com/istio/istio/raw/master/samples/httpbin/httpbin.yaml
Damit ist der Service im Kubernetes ausgerollt. Um ihn über den Ingress Controller erreichbar zu machen, muss folgende Definition eingefügt werden.
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: httpbin-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: httpbin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: httpbin
port:
number: 8000
Relevant ist hier der Host und sein Name. Über die Domain httpbin.example.com
werden alle Anfragen an den Ingress an den Service httpbin auf Port 8000 weitergeleitet.
Hier ein paar hilfreiche Kommandos
kubectl get ing
kubectl get svc -n ingress-nginx
Der letzte Befehl zeigt uns den Quellport für unseren Kubernetes Ingress. Dieser ist von außen erreichbar.
Am Ende noch ein Beispiel für NFS. Die Einbindung für persistente Volumes ist trivial.
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs
spec:
capacity:
storage: 200Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
storageClassName: nfs
nfs:
path: /mnt/md1/shares/kubernetes
server: 192.168.10.2
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
labels:
demo: nfs-pvc
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Gi
Dabei wird das NFS Share /mnt/md1/shares/kubernetes
vom NFS Server 192.168.10.2
mit 200GB als PersistentVolume und Claim dem Cluster bekannt gemacht.
Nützliche Befehle
Hier eine Liste nützlicher Befehle
kubectl cluster-info
Zeigt einem den Status des Kubernetes Clusters an. Wichtig finde ich den Befehl, weil er den Einstiegspunkt (Master) aufzeigt.
kubectl completion bash > /etc/bash_completion.d/kubectl
Erzeugt die Bash-Completion und erstellt eine entsprechende Datei für Bash (gibts auch für zsh). Danach einmal neu einloggen und das Leben mit kubectl
ist deutlich einfacher.
kubectl get services --all-namespaces
Die Services in Kubernetes sind in Namespaces gruppiert. Mit diesem Befehl kann man sich alle Namespaces und die darin definierten Services anzeigen lassen. Sehr wichtig für den ersten Überblick.
kubectl get pods --all-namespaces
Mindestens genauso interessant ist die Liste aller Pods. In jedem Namespace, zu jedem Service gibt es eine Reihe von Pods. Jeder Pod enthält einen oder mehreren Container (also Docker-Prozess).
kubectl get secrets
Liefert eine Liste aller Tokens, die in dem Cluster im Namespace default konfiguriert sind. Kann man mit –all-namespace auch auf alle Namespaces anwenden.
kubectl get secrets default-token-....
Liefert dem Default-Token. Eigentlich das gleiche, wie get secret. Mit folgendem Befehl kann man sich aber das Zertifikat ziehen, was man zB. braucht, um Gitlab zu füttern.
kubectl get secret default-token-... -o jsonpath="{['data']['ca\.crt']}" | base64 --decode
Liefert das Zertifkat des Defaulttokens. Das wird zB. für Gitlab gebraucht.
kubectl apply -f patch.yaml
Mit dem Apply Parameter kann man den Kubernetes Cluster konfigurieren. Das ist ein hochgradig wichtiger Befehl, der so richtig nervig ist. Denn die Daten im YAML Format sind alles andere als hübsch und schlecht beschrieben. Beispiele folgen weiter unten.
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep gitlab-admin | awk '{print $1}')
Damit bekommt man eine Token für den externen Zugriff aufs System. Der innere Kubectl Befehl listet alle Tokens auf, grepped den User bzw. Tokenname, den man will und der zweite äußere Befehl zeigt mit describe secret die Daten darin an.
kubectl logs --namespace gitlab-managed-apps install-helm
Damit kann man sich die Logs eines Pods anschauen.
kubectl exec -it --namespace default shell-demo /bin/bash
Mit dem Befehl kann man in einen Pod einsteigen und einen Befehl ausführen. Das -it verursacht, dass der Befehl nicht gleich wieder beendet wird (also wirklich ein TTY öffnet). Ist sehr ähnlich zu docker/podman exec.
heml repo add stable https://kubernetes-charts.storage.googleapis.com
helm repo update
helm repo remove stable
Helm ist eine Art Meta-CLI-Befehl. Damit kann man in den Cluster einige vorgefertigte Pakete installieren. Ziel ist die Vereinfachung der Installation und des Managements. Ist wie ein Paketmanager zu betrachten. Die Pakete liegen als Charts vor. Meiner Meinung nach ein verwirrender Begriff.
Linksammlung
Hier finden sich einige nützliche und interessante Links an, die sich im Laufe der Recherche angesammelt haben.
Kubernetes & Ansible
- Ansible Kubernetes Modul https://docs.ansible.com/ansible/latest/modules/kubernetes_module.html
- https://kubernetes.io/blog/2019/03/15/kubernetes-setup-using-ansible-and-vagrant/
- https://www.digitalocean.com/community/tutorials/how-to-create-a-kubernetes-cluster-using-kubeadm-on-centos-7
- https://www.dev-eth0.de/blog/2019/01/04/kubernetes-cluster-ansible.html
- https://www.exoscale.com/syslog/kubernetes-ansible/
- https://www.howtoforge.com/tutorial/how-to-install-kubernetes-on-ubuntu/
Mal genauer reinschauen:
bootcamp2.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: kubernetes-bootcamp labels: app: bootcamp spec: selector: matchLabels: app: bootcamp template: metadata: labels: app: bootcamp spec: containers: - name: bootcamp image: gcr.io/google-samples/kubernetes-bootcamp:v1 ports: - containerPort: 8080
apiVersion: v1 kind: Service metadata: name: bootcamp-service spec: type: ClusterIP selector: app: bootcamp ports:
- port: 8080 targetPort: 8080
04 helm repo list 07 helm repo update 20 kubectl get pods 64 kubectl get pods -o wide 22 kubectl get nodes 30 kubectl get deployments 56 kubectl get services 57 kubectl get svc 26 kubectl proxy 44 kubectl describe pods 33 kubectl apply -f bootcamp.yml 42 kubectl exec kubernetes-bootcamp-fcc5bfb48-48pvr - sh 52 curl http://localhost:8001/api 53 curl http://localhost:8001/api/v1/namespaces/default/pods/kubernetes-bootcamp-fcc5bfb48-48pvr 54 curl http://localhost:8001/api/v1/namespaces/default/pods/kubernetes-bootcamp-fcc5bfb48-48pvr/proxy 55 curl http://localhost:8001/api/v1/namespaces/default/pods/kubernetes-bootcamp-fcc5bfb48-48pvr/proxy/ 65 more /run/flannel/subnet.env
120 kubectl get services my-nginx 121 kubectl delete services my-nginx 122 kubectl delete services hellok8s-service 123 kubectl delete services kubernetes 124 pico test.yaml 125 kubectl delete deployments.apps hellok8s-deployment 126 kubectl create -f test.yaml 127 kubectl get all 128 kubectl get all 129 kubectl get all 130 kubectl port-forward service/hellok8s-service 8080:8080 131 more test.yaml 132 kubectl get all 133 kubectl port-forward service/bootcamp-service 8080:8080 134 ll 135 ls -al 136 curl https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml 137 curl https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml 138 kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml 139 kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml 140 kubectl create secret generic -n metallb-system memberlist –from-literal=secretkey="$(openssl rand -base64 128)" 141 kubectl get all -n metallb-system 142 kubectl get all -n metallb-system 143 pico metallbconfig.yaml 144 kubectl apply -f metallbconfig.yaml 145 ls -al 146 more bootcamp2.yml 147 history