Kubernetes Grundlagen

Für den weitere Verlauf des Tutorials werde ich einen Control-Plane starten. Siehe ~/kubernetes-tutorial/src/opentofu/k3s-installation/k3s-installation-single/main.tf.

Danach werden alle Teilnehmer die Möglichkeit haben, sich mit dem Cluster zu verbinden und die folgenden Schritte durchzuführen. Es ist wichtig, dass alle Teilnehmer Zugriff auf den Cluster haben, damit sie die Übungen durchführen können.

$ export IP_CONTROL_PLANE=hier_ip_addresse_des_control_planes_eintragen
$ export K3S_TOKEN=$(ssh -i ../opentofu/schulung root@$IP_CONTROL_PLANE 'cat /var/lib/rancher/k3s/server/agent-token')
$ curl -sfL https://get.k3s.io | K3S_URL=https://$IP_CONTROL_PLANE:6443 K3S_TOKEN=$K3S_TOKEN sh -
$ scp -i ../opentofu/schulung root@$IP_CONTROL_PLANE:/etc/rancher/k3s/k3s.yaml ~/.kube/config
$ vim ~/.kube/config # hier die IP-Adresse des Control-Planes eintragen, damit kubectl mit dem Cluster kommunizieren kann
$ kubectl get nodes

Deployments

Wir fangen mit den Kubernetes Grundlagen an und zwar mit einem PgAdmin4 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgadmin4
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pgadmin4
  template:
    metadata:
      labels:
        app: pgadmin4
    spec:
      containers:
      - name: pgadmin4-container
        image: dpage/pgadmin4:9.14.0
        env:
        - name: PGADMIN_DEFAULT_EMAIL
          value: admin@admin.com
        - name: PGADMIN_DEFAULT_PASSWORD
          value: admin
        ports:
        - containerPort: 80
        resources:
          limits:
            memory: 512Mi
            cpu: 500m
          requests:
            memory: 256Mi
            cpu: 250m
$ cd ~/kubernetes-tutorial/src/deployments
$ kubectl apply -f pgadmin4-deployment.yaml
$ kubectl get pod -o wide
$ kubectl get rs
$ kubectl get deploy
$ kubectl get all

Wie kann man nun die Applikation erreichen? Für Debugging:

$ kubectl port-forward pod/pgadmin4-[id] 8080:80 --address=0.0.0.0

Warnung

kubectl port-forward ist nicht für die Produktion gedacht, sondern nur für Entwicklungszwecke. In einer Produktionsumgebung sollte man NIE kubectl port-forward verwenden!

Nun wird unser Deployment skaliert:

$ kubectl scale rs/pgadmin4-[id] --replicas=2 # funktioniert nicht, da die Replikas von einem Deployment verwaltet werden
$ kubectl get all
$ kubectl scale deploy/pgadmin4 --replicas=2 # funktioniert, da das Deployment die Replikas verwaltet
$ kubectl get pod -o wide

Nun können wir über Port-Forwarding beide Pods erreichen.

Services

Services ermöglichen es, eine Gruppe von Pods als einen einzigen Dienst zu exponieren. Sie ermöglichen es, den Zugriff auf die Pods zu load-balancen.

apiVersion: v1
kind: Service
metadata:
  name: pgadmin4
spec:
  type: ClusterIP
  selector:
    app: pgadmin4
  ports:
    - protocol: TCP
      port: 9090
      targetPort: 80
  sessionAffinity: ClientIP
  sessionAffinityConfig:
    clientIP:
      timeoutSeconds: 10800 # 3 hours
  
$ kubectl apply -f pgadmin4-service.yaml
$ kubectl get svc
$ kubectl port-forward svc/pgadmin4 8080:9090 --address=0.0.0.0 # und im Browser aufrufen
$ kubectl describe svc/pgadmin4
$ kubectl get endpoints pgadmin4
$ kubectl get endpointslices

StatefulSets

StatefulSets sind sehr ähnlich zu Deployments, aber sie sind für Anwendungen gedacht, die einen stabilen Netzwerk-Identität und persistenten Speicher benötigen. Sie werden oft für Datenbanken verwendet. Wir wollen nun eine PostgreSQL-Datenbank mit einem StatefulSet deployen:

apiVersion: apps/v1
kind: StatefulSet	
metadata:
  name: postgres
spec:
  replicas: 1
  serviceName: postgres
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: "postgres-container"
        image: "postgres:18.3-alpine3.23"
        env:
        - name: POSTGRES_PASSWORD
          value: "secret"
        ports:
        - containerPort: 5432
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
          requests:
            memory: "256Mi"
            cpu: "250m"
        
$ kubectl apply -f postgres-statefulset.yaml
$ kubectl get statefulsets
$ kubectl get pods -o wide
$ kubectl describe statefulset/postgres

Wir wollen einen Service erstellen, um die Datenbank zu erreichen:

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
    type: ClusterIP
    ports:
    - protocol: TCP
      port: 5432
      targetPort: 5432
    selector:
      app: postgres
$ kubectl apply -f postgres-service.yaml
$ kubectl get svc
$ kubectl describe svc/postgres
$ kubectl port-forward svc/postgres 5432:5432 --address=0.0.0.0 # und aus einem PG Client heraus auf die Datenbank zugreifen

Nun kann man in der PgAdmin4 UI die Postgres-Datenbank einrichten und sich mit ihr verbinden.

$ kubectl port-forward svc/pgadmin4 8080:9090 --address=0.0.0.0

und im Browser eine DB Verbindung in PgAdmin4 einrichten, um die Postgres-Datenbank zu erreichen. Der Hostname ist postgres (der Name des Services), der Port ist 5432, der Benutzername ist postgres und das Passwort ist secret.

Nun kann man sich erneut aus Visual Studio Code mit der Postgres-Datenbank verbinden, um zu sehen, dass die Daten persistent sind:

$ kubectl port-forward svc/postgres 5432:5432 --address=0.0.0.0

Was passiert aber, wenn wir der postgres-Pod aus dem StatefulSet gelöscht wird?

$ kubectl delete pod postgres-0
$ kubectl get pods -o wide

Der Pod wird automatisch neu erstellt, da er von einem StatefulSet verwaltet wird. Und da der Pod eine stabile Netzwerk-Identität hat, wird er immer den Namen postgres-0 haben. Ist die Datenbanktabelle, die wir vorhin erstellt haben, immer noch da?

Persistent Volumes Claims

Persistent Volumes Claims (PVCs) sind eine Möglichkeit, persistenten Speicher für Pods bereitzustellen. Sie ermöglichen es, Speicher von einem Storage-Provider zu reservieren und diesen Speicher dann in einem Pod zu verwenden.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres
spec:
  accessModes:
    - ReadWriteOncePod # siehe auch https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes
  storageClassName: local-path
  resources:
    requests:
      storage: 1Gi
$ kubectl apply -f postgres-pvc.yaml
$ kubectl get pvc
$ kubectl describe pvc/postgres

Nun können wir den PVC in unserem StatefulSet verwenden:

apiVersion: apps/v1
kind: StatefulSet	
metadata:
  name: postgres
spec:
  replicas: 1
  serviceName: postgres
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: "postgres-container"
        image: "postgres:18.3-alpine3.23"
        env:
        - name: POSTGRES_PASSWORD
          value: "secret"
        #envFrom:
        #- configMapRef:
        #    name: postgres
        #- secretRef:
        #    name: postgres
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
          requests:
            memory: "256Mi"
            cpu: "250m"
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: postgres
        
        
$ kubectl get statefulsets
$ kubectl delete statefulset postgres
$ kubectl apply -f postgres-statefulset-with-pvc.yaml
$ kubectl describe pvc/postgres
$ kubectl get statefulsets
$ kubectl get pods -o wide
$ kubectl describe statefulset/postgres
$ kubectl exec -it pod/postgres-0 -- bash
$ psql -U postgres
$ CREATE TABLE person (name VARCHAR(255));
$ INSERT INTO person (name) VALUES ('Christian Trutz');
$ SELECT * FROM person;
$ \\q
$ exit
$ kubectl delete pod postgres-0
$ kubectl get pods -o wide
$ kubectl exec -it pod/postgres-0 -- bash
$ psql -U postgres
$ SELECT * FROM person;

Zu einem PVC gehört immer ein Persistent Volume (PV), das den tatsächlichen Speicher repräsentiert. In unserem Fall wird der PV automatisch von Kubernetes erstellt, da wir einen StorageClass mit dem Namen local-path verwenden, der standardmäßig in k3s enthalten ist. Der PV wird auf dem Node erstellt, auf dem der Pod läuft, und der Speicher wird auf dem lokalen Dateisystem des Nodes bereitgestellt.

$ kubectl get pv
$ kubectl describe pv [id pv]
$ kubectl get pv [id pv] -o json | jq .spec.local.path
$ ls -lah [path_aus_obigem_befehl]

ConfigMaps und Secrets

ConfigMaps und Secrets sind Möglichkeiten, Konfigurationsdaten und sensible Daten in Kubernetes zu speichern. ConfigMaps werden für Konfigurationsdaten verwendet, Secrets für sensible Daten wie Passwörter oder API-Schlüssel verwendet werden. ConfigMaps und Secrets können in Pods als Umgebungsvariablen übergeben oder als Dateien gemountet werden.

Für unser PostgreSQL-Deployment könnten wir zum Beispiel die Konfigurationsdaten wie den Benutzernamen, die Datenbank in einer ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres
data:
  POSTGRES_USER: postgres
  POSTGRES_DB: postgres

und das Passwort in einem Secret speichern:

apiVersion: v1
kind: Secret
metadata:
  name: postgres
type: Opaque
data:
  POSTGRES_PASSWORD: c2VjcmV0 # base64 encoded 'secret'
$ kubectl apply -f postgres-configmap.yaml
$ kubectl apply -f postgres-secret.yaml
$ kubectl get configmaps
$ kubectl get secret postgres -o json | jq .data
$ kubectl get secret postgres -o json | jq .data.POSTGRES_PASSWORD | tr -d "\"" | base64 -d
$ vim postgres-statefulset-with-pvc.yaml # hier die ConfigMap und das Secret in das StatefulSet einbinden
$ kubectl apply -f postgres-statefulset-with-pvc.yaml
$ kubectl describe statefulset/postgres
$ kubectl describe pod/postgres-0
$ kubectl exec pod/postgres-0 -it -- bash
$ env
$ env | grep POSTGRES_PASSWORD

Wichtig

Es ist wichtig zu beachten, dass Secrets in Kubernetes nicht wirklich sicher sind, da sie Base64-kodiert und nicht verschlüsselt sind. In einer Produktionsumgebung sollten zusätzliche Sicherheitsmaßnahmen ergriffen werden, um Secrets zu schützen, wie zum Beispiel Rollenbasierte Zugriffskontrolle (RBAC) und die Verwendung von externen Secret-Management-Lösungen.

RBAC

RBAC (Role-Based Access Control) ist ein Mechanismus, um den Zugriff auf Ressourcen in Kubernetes zu steuern. Es ermöglicht es, Rollen zu definieren, die bestimmte Berechtigungen haben, und diese Rollen dann Benutzern oder Service Accounts zuzuweisen.

$ kubectl get roles
$ kubectl get rolebindings
$ openssl req -new -key chris.key -out chris.csr -subj "/CN=chris/O=developers"
$ vim developer-csr.yaml # hier den Namen des CSR und den Pfad zur CSR Datei anpassen
$ kubectl apply -f developer-csr.yaml
$ kubectl get csr
$ kubectl certificate approve [csr name]
$ kubectl get csr
$ kubectl get csr chris -o jsonpath='{.status}' | jq
$ kubectl get csr [csr name] -o jsonpath='{.status.certificate}' | base64 -d > [name des developers].crt
$ openssl x509 -noout -text -in [name des developers].crt
$ kubectl config get-contexts
$ kubectl config get-clusters
$ kubectl config get-users
$ kubectl config set-credentials [name des developers] --client-certificate=[name des developers].crt --client-key=[name des developers].key
$ kubectl config get-users
$ kubectl config set-context developer --cluster=default --user=[name des developers]
$ kubectl config use-context developer
$ kubectl get nodes # funktioniert nicht, da der Entwickler keinen Zugriff auf die Nodes hat
$ kubectl get pods # funktioniert nicht, da der Entwickler keinen Zugriff auf die Pods hat
$ kubectl get secrets # funktioniert nicht, da der Entwickler keinen Zugriff auf die Secrets hat
$ kubectl config use-context default
$ kubectl get secrets # funktioniert
$ kubectl apply -f developer-role.yaml
$ kubectl describe roles developer
$ vim developer-rolebinding.yaml # hier die Namen der Entwickler anpassen und mehrere Entwickler hinzufügen, wenn nötig
$ kubectl apply -f developer-rolebinding.yaml
$ kubectl describe rolebindings developer
$ kubectl config use-context developer
$ kubectl get pods # funktioniert, da der Entwickler nun Zugriff auf die Pods hat
$ kubectl get secrets # funktioniert nicht, da der Entwickler keinen Zugriff auf die Secrets hat
$ kubectl exec pod/postgres-0 -- env | grep POSTGRES_PASSWORD # funktioniert nicht, da der Entwickler keinen exec-Zugriff auf die Pods hat
$ kubectl config use-context default
$ kubectl exec pod/postgres-0 -- env | grep POSTGRES_PASSWORD # funktioniert, da der default=Admin user Zugriff auf die Pods hat

Ingress

Ingress ist eine Möglichkeit, HTTP- und HTTPS-Verkehr zu einem Service in Kubernetes zu routen. Es ermöglicht es, mehrere Services unter derselben IP-Adresse und demselben Port zu exponieren.

apiVersion: networking.k8s.io/v1
kind: Ingress	
metadata:	
  name: pgadmin4
spec:
  ingressClassName: traefik	
  rules:
    - host: pgadmin4.trutz.cloud
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: pgadmin4
                port:
                  number: 9090

Die Ingress-Ressource definiert, dass Anfragen an http://pgadmin4.trutz.cloud/ an den Service pgadmin4 weitergeleitet werden sollen.

Ingress alleine reicht nicht aus, um den Verkehr zu routen. Es wird ein Ingress-Controller benötigt, der die Ingress-Ressourcen überwacht und die entsprechenden Regeln konfiguriert. In unserem Fall verwenden wir den Traefik-Ingress-Controller, der standardmäßig in k3s enthalten ist.

$ kubectl apply -f pgadmin4-ingress.yaml
$ kubectl get ingress
$ kubectl describe ingress/pgadmin4
$ http://pgadmin4.trutz.cloud/  # im Browser aufrufen

Tipp

Der Einstiegspunkt von aussen in das Cluster ist der Ingress-Controller, dessen Aufgabe ist den Verkehr zu den Services im Cluster zu routen. Das Mapping URL zu Service wird typischerweise auf Domain-Ebene gemacht, also zum Beispiel http://pgadmin4.trutz.cloud/ zum Service pgadmin4, aber es ist auch möglich, das Routing auf Context-Pfad-Ebene zu machen, zum Beispiel http://trutz.cloud/pgadmin4/ zum Service pgadmin4, aber hier ist zu beachten, dass die Applikation dann so konfiguriert sein muss, dass sie den obigen Context-Pfad unterstützt.

Kubernetes Controller und CRDs

Kubernetes Controller sind Prozesse, die den aktuellen Zustand des Clusters überwachen und sicherstellen, dass er dem gewünschten Zustand entspricht. Sie reagieren auf Änderungen im Cluster und führen die notwendigen Aktionen aus, um den gewünschten Zustand zu erreichen. Custom Resource Definitions (CRDs) ermöglichen es, benutzerdefinierte Ressourcen in Kubernetes zu erstellen, die von Controllern verwaltet und überwacht werden können.

$ kubectl get crds
$ kubectl describe crd [name_der_crd]
$ kubectl api-resources