Übung: Kubernetes

  1. Werkzeuge installieren
  2. Cluster anlegen
  3. kubectl-Konfiguration herunterladen
  4. Pod deployen
  5. Applikation im Internet freigeben
  6. Gesundheitszustand der App in Betracht ziehen
  7. App konfigurieren
  8. Geheimnisse
  9. Mehrere Instanzen
  10. Aufräumen
  11. Fehlermeldungen
  12. Sonstiges
  13. Fußnoten

Werkzeuge installieren

  • ibmcloud zur Interaktion mit der IBM Cloud: cloud.ibm.com
  • ibmcloud Container Service plugin:
    $ ibmcloud plugin install container-service
    
  • kubectl zur Interaktion mit Ihrem k8s-Cluster: kubernetes.io (nur kubectl)
  • jq zur Verarbeitung von JSON auf der Kommandozeile: stedolan.github.io/jq

Cluster anlegen

Benutzen Sie Ihren IBM Cloud Account, den Sie in der Vorbereitung zur Übung angelegt haben:

$ ibmcloud login

Jetzt können Sie einen neuen Kubernetes-Cluster anlegen, den Sie für die folgenden Übungen verwenden können:

$ ibmcloud ks cluster create classic --name webservices-lab

Erfahrungsgemäß dauert dieser Schritt ca. 15 min.

Applikation kennenlernen

Während Ihr Cluster angelegt wird, können Sie sich mit der App plaintoot, die wir in dieser Übung deployen wollen, vertraut machen:

  1. Lesen Sie das README
  2. Starten Sie plaintoot in einer lokalen Docker-Umgebung:

     $ docker run --rm -it suhlig/plaintoot /app/plaintoot print https://chaos.social/@nixCraft@mastodon.social/111108182085516402
     Reminder
    
     | ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|
     | Don't Push To Production On Friday |
     |_________________|
     \ (•◡•) /
     \ /
     ——
     | |
     |_ |_
    
     -- nixCraft@chaos.social
    

Sie können jede anderen öffentliche Mastoton-URL verwenden.

kubectl-Konfiguration herunterladen

Warten Sie, bis der Status Ihres Clusters normal ist:

$ ibmcloud ks cluster ls
OK
Name              ID                     State    …
webservices-lab   c908493f0hqeguesbo3g   normal   …

Jetzt können Sie die Cluster-Konfiguration herunterladen. kubectl verwendet diese, um mit dem Cluster (von Ihrem Rechner aus) zu interagieren:

$ ibmcloud ks cluster config --cluster webservices-lab
OK
The configuration for webservices-lab was downloaded successfully.

Falls Sie zur Verwaltung lieber eine Web-Applikation verwenden, können Sie Ihren frisch angelegten Cluster auswählen und dem Link “Kubernetes Dashboard” 1 folgen.

Pod deployen

Ein Pod als kleinste in Kubernetes handhabbare Einheit muss mindestens einen Container beinhalten, der den Applikationsprozeß darstellt. Beschreiben Sie das gewünschte Ergebnis in pod.yml:

apiVersion: v1
kind: Pod
metadata:
  name: plaintoot
  labels: { app: plaintoot }
spec:
  containers:
    - name: plaintoot
      image: suhlig/plaintoot
      command: [ /plaintoot ]
      args: [ serve ]
      ports: [ containerPort: 8080 ]

Die Angabe containerPort dokumentiert lediglich den Port, auf dem die Applikation (innerhalb des Containers) auf Verbindungen wartet. Entscheidend ist, auf welchem Port der Anwendungsprozeß auf Anfragen wartet.

Gewünschten Zustand beschreiben

Teilen Sie Ihrem Cluster den gewünschten Zustand mit:

$ kubectl apply -f pod.yml
pod/plaintoot created

Wirklichen Zustand beobachten

Der von Ihnen gewünschte Zustand wird sich nicht sofort einstellen - je nach Komplexität der Beschreibung und Auslastung des Clusters können einige Sekunden vergehen, bis aus dem Wunsch Wirklichkeit geworden ist.

Beobachten Sie den wirklichen Zustand (incl. Übergang zwischen den Pod-Phasen):

$ kubectl get pods
NAME         READY   STATUS    …
plaintoot   1/1     Running   …

Sie können das gleiche Kommando auch mit dem Parameter --watch aufrufen, um die Veränderungen kontinuierlich zu beobachten.

Fragen zur Kontrolle

  • Was passiert, wenn Sie einen Schreibfehler in der Image-Referenz plaintoot haben, z.B. image: suhlig/keintweet und das mit kubectl apply anwenden?

  • Wie lautet die Versionsnummer von plaintoot, die Sie deployt haben?

Applikation im Internet freigeben

Jetzt ist die App zwar gestartet, aber noch nicht von außen (per Internet) erreichbar. Dafür wird eine Resource vom Typ Service benötigt, die sie in service.yml beschreiben können:

apiVersion: v1
kind: Service
metadata:
  name: plaintoot-service
spec:
  type: NodePort
  ports:
    - name: http
      port: 9090
      targetPort: 8080
  selector:
    app: plaintoot

$ kubectl apply -f service.yml

Die Verbindung zwischen einem “freigegebenen” (exposed) Port und den Pods, die die Anfragen beantworten, wird über den selector gesteuert. Hier werden alle eingehenden Anfragen an Port 9090 zu den Pods (auf deren Port 8080) weitergeleitet, die das Label app mit dem Wert plaintoot haben. Sie können diese Entscheidung mit folgendem Befehl nachvollziehen:

$ kubectl get pods --selector app=plaintoot
NAME         READY   STATUS    …
plaintoot   1/1     Running   …

Um die App zu erreichen, benötigen Sie die öffentliche IP Ihres Clusters sowie den extern sichtbaren Port (nodePort):

  1. Öffentliche IP Ihres Clusters abfragen:

     $ ibmcloud ks worker ls --cluster webservices-lab --output json \
       | jq --raw-output '.[].publicIP'
    
  2. nodePort Ihres Services herausfinden:

     $ kubectl get service plaintoot-service -o json \
       | jq --raw-output '.spec.ports[].nodePort'
    

Jetzt können Sie die öffentliche URL Ihrer App aufrufen:

$ curl http://$public-ip:$port

Beispiel:

$ curl http://159.122.183.244:32733
Serves a plain-text representation of a single tweet via HTTP

🎉 Herzlichen Glückwunsch 🎉, Sie haben erfolgreich eine Web-Applikation auf Kubernetes deployt und für die Öffentlichkeit zur Verfügung gestellt!

Gesundheitszustand der App in Betracht ziehen

Kubernetes kann Ihre Applikation in regelmäßigen Abständen auf ihre Gesundheit (liveness probe) überprüfen und mögliche Probleme durch einen Neustart des Pods (und damit der Container) lösen.

Es gibt mehrere Wege für eine App, diese Frage zu beantworten. Für eine Web-Applikation wie plaintoot bietet sich eine Abfrage mit HTTP an.

liveness probe deklarieren

apiVersion: v1
kind: Pod
metadata:
  name: plaintoot
  labels:
    app: plaintoot
spec:
  containers:
    - name: plaintoot
      image: suhlig/plaintoot
      command: [ /plaintoot ]
      args: [ serve ]
      ports: [ containerPort: 8080 ]
      livenessProbe:
        httpGet:
          path: /liveness
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 10
        timeoutSeconds: 3

Teilen Sie Ihrem Cluster den gewünschten neuen Zustand mit dem obigen Inhalt in pod-with-liveness-probe.yml mit. Kubernetes wird die Aktualisierung des vorhandenen Pods aber verweigern:

$ kubectl apply -f pod-with-liveness-probe.yml
The Pod "plaintoot" is invalid…

Eine Aktualisierung von Pods ist nur eingeschränkt möglich (pets vs. cattle2). Eine bessere Lösung wäre z.B. ein Deployment, mit dem dieses und noch andere Probleme gelöst werden. Das ist Thema eines späteren Kapitels.

Vorerst können Sie den Pod manuell löschen und erneut anlegen:

$ kubectl delete pod plaintoot
$ kubectl apply -f pod-with-liveness-probe.yml

Pod-Fehler simulieren

Um einen Fehlerzustand innerhalb der Applikation zu simulieren, kann plaintoot mit der Umgebungsvariable MAX_UPTIME angewiesen werden, nach Ablauf dieser Zeitspanne einen Fehler zu berichten. Kubernetes wird einen Neustart des Pods erzwingen, falls die Antwort der liveness probe einen Statuscode ergibt, der nicht 200 ist.

apiVersion: v1
kind: Pod
metadata:
  name: plaintoot
  labels:
    app: plaintoot
spec:
  containers:
    - name: plaintoot
      image: suhlig/plaintoot
      command: [ /plaintoot ]
      args: [ serve ]
      ports: [ containerPort: 8080 ]
      livenessProbe:
        httpGet:
          path: /liveness
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 10
        timeoutSeconds: 3
      env:
      - name: MAX_UPTIME
        value: 60s

  1. Löschen Sie den Pod und legen Sie ihn mit der oben stehenden aktualisierten Konfiguration erneut an:

     $ kubectl delete pod plaintoot
     $ kubectl apply -f pod-with-liveness-probe-failing.yml
    
  2. Beobachten Sie die Zustandsübergänge des plaintoot-Pods:

     $ kubectl get pods --selector app=plaintoot --watch
    
  3. Beobachten Sie parallel dazu (in einem zweiten Terminal-Fenster oder -Tab) die Log-Nachrichten des Pods:

     $ kubectl logs --selector app=plaintoot --follow
    

Achtung: Da die plaintoot-App deterministisch nach der konfigurierten Zeit in den Zustand unhealthy geht, wird Kubernetes die Neustarts immer länger hinauszögern:

After containers in a Pod exit, the kubelet restarts them with an exponential back-off delay (10s, 20s, 40s, …), that is capped at five minutes. Once a container has executed for 10 minutes without any problems, the kubelet resets the restart backoff timer for that container.

Sie können weitere Möglichkeiten nutzen, um sich über den Zustand des Pods zu informieren:

  • Schauen Sie sich die Beschreibung des Pods an:

      $ kubectl describe pod --selector app=plaintoot
    
  • Nur Ereignisse:

      $ kubectl get events --field-selector involvedObject.name=plaintoot
    

App konfigurieren

Um ihren Sinn zu erfüllen und die Text-Version eines Tweets mithilfe der Twitter-API abzurufen, muss die Applikation konfiguriert werden. Im bisherigen Zustand gibt die Applikation eine Fehlermeldung aus, sobald Sie versuchen, einen Tweet (hier mit der ID 20) zu erfragen:

$ curl http://$public-ip:$port/20
Error: oauth2: cannot fetch token: 403 Forbidden

Unsere Application ist zwar healthy (antwortet grundsätzlich per HTTP), aber nicht ready (kann ihre Aufgabe erfüllen), weil die Twitter-API nur mit API-Key und -Secret erreichbar ist. Diese Konfiguration erwartet die plaintoot-Anwendung in der Umgebungsvariablen $TWITTER_BEARER_TOKEN; bisher haben wir aber nichts dergleichen konfiguriert.

Bevor wir den eigentlichen Fehler (fehlende Konfigurationsparameter) beheben, müssen wir zunächst dafür sorgen, daß die Applikation im unkonfigurierten Zustand den richtigen Fehler zeigt und Kubernetes darauf aufmerksam macht, daß die Applikation noch keine Anfragen erhalten soll.

readiness probe hinzufügen

Eine Applikation in einem Fehlerzustand, der nicht durch einen Neustart behoben werden kann, sollte keine Anfragen per HTTP erhalten3. Um Kubernetes diesen Zustand mitzuteilen, können Sie eine readiness probe konfigurieren:

apiVersion: v1
kind: Pod
metadata:
  name: plaintoot
  labels:
    app: plaintoot
spec:
  containers:
    - name: plaintoot
      image: suhlig/plaintoot
      command: [ /plaintoot ]
      args: [ serve ]
      ports: [ containerPort: 8080 ]
      readinessProbe:
        httpGet:
          path: /readiness
          port: 8080
        initialDelaySeconds: 5
        timeoutSeconds: 1

Beschreiben Sie den gewünschten Zustand:

$ kubectl apply -f pod-with-readiness-probe.yml

Beachten Sie, daß wir die liveness-Probe vorerst wieder entfernt haben.

Zustandsübergänge beobachten

  1. Beobachten Sie die Zustandsübergänge des plaintoot-Pods:

     $ kubectl get pods --selector app=plaintoot --watch
    
  2. Beobachten Sie parallel dazu (in einem zweiten Terminal-Fenster oder -Tab) die Log-Nachrichten des Pods:

     $ kubectl logs --selector app=plaintoot --follow
    

Fragen zur Kontrolle

  • Welchen Zustand erreicht der oben beschriebene Pod (mit readiness probe, aber ohne die nötigen Umgebungsvariablen), und warum?

Geheimnisse

Der Wert der oben besprochenen Umgebungsvariablen TWITTER_BEARER_TOKEN soll möglichst geheim bleiben; im Idealfall hat nur der Pod selbst darauf Zugriff. Statt die Werte im Klartext zu konfigurieren (siehe oben), können wir die wertvollen Geheimnisse separat verwalten.

Geheimnis anlegen

$ kubectl create secret generic twitter-credentials \
  --from-literal twitter_bearer_token=deadbeef

Konzeptionell legen Sie damit eine Resource vom Typ Secret an. Dafür wäre eigentlich das gleiche kubectl apply wie für einen Pod möglich:

apiVersion: v1
kind: Secret
metadata:
  name: twitter-credentials
type: Opaque
stringData:
  TWITTER_BEARER_TOKEN: deadbeef

Machen Sie sich aber bitte bewusst, daß Sie in diesem Fall ein wertvolles Geheimnis in eine Datei auf Ihrer lokalen Festplatte schreiben.

Weitere Möglichkeiten, um die Werte für Secrets zu lesen, finden Sie mit der Hilfe: kubectl create secret generic -h.

Geheimnis verwenden

Jetzt ist das Geheimnis im Cluster4 bekannt, und wir können es in der Definition des Pods verwenden:

apiVersion: v1
kind: Pod
metadata:
  name: plaintoot
  labels:
    app: plaintoot
spec:
  containers:
    - name: plaintoot
      image: suhlig/plaintoot
      command: [ /plaintoot ]
      args: [ serve ]
      ports: [ containerPort: 8080 ]
      env:
        - name: TWITTER_BEARER_TOKEN
          valueFrom:
            secretKeyRef:
              name: twitter-credentials
              key: twitter_bearer_token
      livenessProbe:
        httpGet:
          path: /liveness
          port: 8080
        initialDelaySeconds: 10
        periodSeconds: 10
        timeoutSeconds: 3
      readinessProbe:
        httpGet:
          path: /readiness
          port: 8080
        initialDelaySeconds: 5
        timeoutSeconds: 1

Fragen zur Kontrolle

  • Warum ist das Ablegen eines Geheimnisses in einer lokalen Datei keine gute Idee? Was könnte schiefgehen?

  • Wie schützt Kubernetes Geheimnisse in einem Cluster vor unberechtigtem Zugriff?

Mehrere Instanzen

Jetzt haben wir eine Instanz unserer Applikation. Kubernetes wird sie neu starten, wenn sie unerwartet beendet wird, aber nicht unendlich oft. Außerdem ist die Anwendung zwischen den Neustarts nicht verfügbar.

In der Vorlesung haben wir das Konzept von Deployments kennengelernt, womit sich diese Nachteile beheben lassen.

Gewünschten Zustand beschreiben

apiVersion: apps/v1
kind: Deployment
metadata:
  name: plaintoot
spec:
  replicas: 5
  selector:
    matchLabels:
      app: plaintoot
  template:
    metadata:
      labels:
        app: plaintoot
    spec:
      containers:
        - name: plaintoot
          image: suhlig/plaintoot
          command: [ /plaintoot ]
          args: [ serve ]
          ports: [ containerPort: 8080 ]
          livenessProbe:
            httpGet:
              path: /liveness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 3
          readinessProbe:
            httpGet:
              path: /readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 1
          resources:
            requests:
              memory: "10Mi"
              cpu: "10m"
            limits:
              memory: "20Mi"
              cpu: "100m"

$ kubectl apply -f deployment.yml

Wirklichen Zustand beobachten

$ kubectl get deployments
$ kubectl get pods

Fragen zur Kontrolle

  • Was ist am Betreiben eines einzelnen Pods problematisch?

  • Beschreiben Sie stichwortartig die Vorgänge, die nach der Änderung des Deployments

    • von replicas: 3 auf replicas: 5 sowie
    • von replicas: 3 auf replicas: 1 ablaufen:

Aufräumen

Wenn Sie den Cluster nicht mehr benötigen, sollten Sie ihn löschen:

$ ibmcloud ks cluster rm --cluster webservices-lab

Beachten Sie bitte, daß Cluster, die Ihnen im Rahmen der Vorlesung zur Verfügung gestellt wurden, nach spätestens 30 Tagen automatisch gelöscht werden.

Fehlermeldungen

  • Unable to connect to the server: failed to refresh token: oauth2: cannot fetch token: 400 Bad Request

    Die auf Ihrem Rechner gespeicherte $KUBECONFIG (~/.kube/config) ist nicht mehr gültig (bzw. das darin enthaltene Token). Aktualisieren Sie diese mit dem Kommando:

    $ ibmcloud ks cluster config --cluster webservices-lab
    

    Gegebenenfalls müssen Sie sich erneut mit ibmcloud login authentifizieren.

    Sonstiges

  • Etwas eleganteres Warten, bis der Cluster bereit ist (macOS):

    $ while [[ "$(ibmcloud ks cluster get --cluster webservices-lab --output json | jq --raw-output .state | tee .cluster-state)" != "normal" ]]; do
      echo "$(gdate --rfc-3339=s) $(< .cluster-state)"
      sleep 1
    done
    osascript -e 'display notification "Los gehts!" with title "Cluster ist bereit"'
    rm .cluster-state
    
  • Statt Benutzername und Passwort können Sie sich auch mit einem API-Key bei der IBM Cloud anmelden:

    1. Anlegen (einmalig):

       $ ibmcloud iam api-key-create "$USER@$(hostname)"
       Creating API key suhlig@lux under ac1c1612b0ef457692995a7bbcc67709 as s+webservices20211006@uhlig.it...
       OK
      
    2. Verwenden:

       $ export IBMCLOUD_API_KEY=********
       $ ibmcloud login # Benutzername und Passwort nicht nötig
      

    Natürlich sollten Sie auch diesen Schlüssel geheimhalten!

  • Was ist so besonders am Tweet mit der ID 20?

  • Lesenswert:

Fußnoten

  1. siehe auch die Dokumentation des Kubernetes Dashboards 

  2. siehe Pets vs. Cattle: The Future of Kubernetes in 2022 

  3. Alternativ können Sie diesen Zustand auch innerhalb der Applikation behandeln. 

  4. eigentlich nur innerhalb des Namespaces, aber in dieser Übung arbeiten wir der Einfachheit halber immer im default-Namespace. 

letzte Änderung: 12. Dezember 2023