Beziehung zwischen Kubernetes und Docker

Container-Orchestrierung: Was ist Kubernetes und wie verhält es sich zu Docker?

Matthew Gill

© NB_Factory/Shutterstock.com

Kubernetes wird am häufigsten zusammen mit von Docker verwalteten Containern verwendet. Doch ist es nicht strikt davon abhängig. Wie also funktionieren beide zusammen? Dies wird in diesem ausführlichen Kubernetes-Guide erläutert. Hierfür kommt zudem Payara Micro zum Einsatz, eine leichtgewichtige Open-Source-Plattform, um in einer Cloud-Kubernetes-Umgebung zu deployen.

Kubernetes definiert ein Container Runtime Interface (CRI), das Container-Plattformen implementieren müssen, um kompatibel zu sein. Diese Implementierungen werden umgangssprachlich als „Shims“ bezeichnetDas macht die Kubernetes-Plattform agnostisch, so dass anstelle von Docker auch andere Plattformen mit entsprechenden Shims, wie z. B. CRI-O oder KataContainers verwendet werden können. Die automatische Skalierung und die Failover-Sicherung sind nur zwei der Vorteile moderner Cloud-Plattformen, wie Amazon AWS, Microsoft Azure und Google Cloud Platform (GCP). Allerdings gibt es immer noch Schwierigkeiten mit dieser Architektur, wie z. B. die folgenden:

  1. Das Starten und Stoppen einer Instanz ist langsam.
  2. Die Kommunikation zwischen den Nodes in dieser Architektur kann sehr komplex sein.

Wenn wir eine monolitische Struktur über Microservices einsetzen, müssen wir uns wahrscheinlich nicht mit der Kommunikation zwischen den Nodes auseinandersetzen und werden diese Probleme daher nicht erleben. Wir müssen dennoch die Startzeit der Instanz im Blick haben, wenn wir ein Rolling Upgrade einführen oder einen Failover durchführen. Dies kann zusätzliche Kosten verursachen, da das Risiko, Anfragen zu verlieren, größer wird, je länger der Neustart dauert. Daher ist es nicht unwahrscheinlich, dass wir eine weitere Instanz haben wollen, um dieses Risiko zu minimieren.

An diesem Punkt kommt Kubernetes ins Spiel. Kubernetes ist eine Plattform für die Verwaltung von containerisierten Services. Dabei handelt es sich um ein Tool, mit dem Details wie z. B. das Separieren von Nodes abstrahiert werden können, während andere Dinge, wie Rolling Upgrades, Failover und Services automatisiert werden. Die Idee ist, auf sehr ähnliche Weise implementieren zu können, egal ob man dies lokal oder in der Cloud.

In diesem Artikel werden wir einige der Kubernetes-Grundlagen behandeln und eine einfache, lokale Microservice-Applikation einrichten, um deren Einsatz zu demonstrieren. Voraussetzung ist, dass die folgenden Tools bereits installiert und konfiguriert wurden:

  • Maven (v3+)
  • JDK 8
  • Docker

Was braucht man um Kubernetes zu installieren?

Kubernetes ist für den Einsatz mit einem Cloud-Anbieter vorgesehen. Es gibt dennoch mehrere Möglichkeiten, wie man einen Kubernetes-Cluster lokal betreiben kann. Grundsätzlich wird jede Implementierung am Ende dasselbe Interface über kubectl bereitstellen. Wenn man Windows nutzt, bietet sich Docker-For-Windows Unterstützung für Kubernetes. Verwendet man stattdessen Linux, so sind MicroK8s und Minikube die zwei Wegbereiter. Letzteres läuft unter Windows, MacOS und Linux. Minikube führt einen Kuberntes-Cluster in einer einzelnen VM aus, wohin hingegen MicroK8s mit Snap installiert wird und lokal mit minimalem Overhead läuft. Für eine einfache Installation sowie Einsatz, wird dieser Blog mit MicroK8s arbeiten.

MicroK8s wurde vom Kubernetes-Team Canonical entwickelt, deshalb muss es mit dem Canonical-Linux-Packetmanager installiert werden — ein Packetmanager auf vielen Linux-Betriebssystemen, der gewährleistet, dass Applikationen auf dieselbe Art installiert sowie automatisch aktualisiert werden. Snap lässt sich auf Ubunto folgendermaßen installieren:

sudo apt update
sudo apt install snapd

Es könnte sein, dass wir uns entweder ein- bzw. ausloggen oder unser System neustarten müssen, um sicherzustellen, dass der Systempfad auch richtig aktualisiert wird.

Wir können MicroK8s folgendermaßen installieren, sobald Snap installiert ist:

snap install microk8s –classic

Dies wird MicroK8s wie eine normale Systemanwendung installieren. Alle Micro8Ks-Utilitys werden microk8s.something genannt, einschließlich der Kubernets-Main-Utility, microk8s.kubectl. Da es sehr lästig ist, dies jedes Mal aufs neue eingeben zu müssen, und sofern wir noch keine andere Quelle für kubectl installiert haben, können wir mit dem folgenden Befehl die Kubernetes-Main-Utility umbenennen:

snap alias microk8s.kubectl kubectl

Dadurch können wir kubectl anstatt microk8s.kubectl benutzen. Dies kann auch rückgängig gemacht werden:

snap unalias kubectl

Einige wichtige Befehle, die nur von MicroK8s zur Verfügung gestellt werden, sind unten aufgelistet. Wir müssen sicherstellen, dass MicroK8s gestartet wird, bevor wir versuchen kubectl zum Starten von Services einzusetzen (es muss auch für den oben genannten kubectl get all-Befehl in Betrieb sein).

# Starts MicroK8s and allows provisioning nodes.
microk8s.start
# Stops MicroK8s
microk8s.stop
# Clear all resources and revert kubernetes to a 'clean' state
microk8s.reset

Um zu prüfen, ob MicroK8s richtig installiert wurde, rufen wir eine Auflistung aller Ressourcen auf:

kubectl get all

Unsere Microservices-Beispielanwendung

Um zeigen zu können, wie sich eine Microservice-Anwendung in Aktion verhält, brauchen wir zunächst eine Anwendung, mit der wir arbeiten können. Die Anwendung, die wir verwenden werden, wird zufällige Payara-Micro-Instanznamen generieren. Sie kann im Payara-Beispielrepository gefunden werden. Ich rate dazu, dass Repository zu klonen, damit man auch weiterhin die Anwendungsausführung verfolgen kann.

Die Anwendung beinhaltet 3 Komponenten:

  1. Frontend: Eine Vue.js-Anwendung, die das API des Backends benutzt, um eine Nachricht zu erstellen
  2. Backend: Ein Backend-API, das sich um die Anfragen des Frontends kümmert
  3. NameGenerator: Ein Name-Generator-Service, der zufällige Instanznamen erstellt

Als erstes werden wir die Anwendung lokal und ohne Kubernetes laufen lassen, um zu beobachten, wie das aussieht.

Das Frontend starten

Der Frontend Code kann im Ordner frontend/ gefunden werden. Wer bereits NodeJS (v9+) und NPM installiert hat, kann das Projekt auch folgendermaßen starten:

# Install dependency packages to node_modules/
npm install
# Start the server
npm run serve

Verfügt man nur über Maven, kann man das Projekt hiermit ausführen:

# Download a local NodeJS installation and use it to start the server
mvn test -Pserve

Alle Änderung, die am Code vorgenommen wurden, werden augenblicklich erscheinen, unter Verwendung des Hot-Reloading-Features von Vue Loaders.

Nun können wir innerhalb des Terminals auf den Link zusteuern (standardmäßig handelt es sich hier um http://localhost:8080/). Wir werden jetzt die folgende Seite sehen:

Quelle: <a href="https://blog.payara.fish/what-is-kubernetes" rel="noopener" target="_blank">Payara-Blog</a>

Quelle: Payara-Blog

Sobald wir GENERATE NAME betätigen, werden wir eine Fehlermeldung sehen. Die Entwicklerkonsole wird uns den vollständigen Fehler anzeigen und deutlich machen, was schief gelaufen ist. In Google Chrome kann man sich diese über die F12-Taste und durch Anklicken des Console-Tabs anzeigen lassen.

Quelle: <a href="https://blog.payara.fish/what-is-kubernetes" target="_blank" rel="noopener">Payara-Blog</a>

Quelle: Payara-Blog

Der Button ruft das Backend-API auf dem Port 8081 auf, aber wird diesen nicht erreichen! Das liegt daran, dass wir den Backend-Server nicht gestartet haben, um diese Anfragen zu bearbeiten.

Das Backend starten

Wir nutzen ein anderes Terminal, aus dem backend/-Ordner und führen den folgenden Befehl aus:

# Build the application, bundle it with Payara Micro and then run it
mvn verify -Pserve

Dadurch wird der Server auf Port 8081 gestartet, um mit dem Bearbeiten von Anfragen zu beginnen. Wenn wir nun auf den Button auf der Website klicken, erhalten wir einen anderen Fehler in der Konsole:

GET http://localhost:8081/api/greeting 500 (Internal Server Error)

Das bedeutet, dass das Backend erreicht wurde, aber es eine 500 zurückgibt. Der Grund dafür wird im Hauptteil des Codes für diesen Service erläutert:

@Path("/greeting")
@RequestScoped
public class BackendService {

    private NameService nameService;

    @Inject
    public void initNameService(@ConfigProperty(name = "name.service.url") URI nameServiceUrl) {
        nameService = RestClientBuilder
                .newBuilder()
                .baseUri(nameServiceUrl)
                .build(NameService.class);
    }

    @GET
    public String getGreetinExisting Backupsg() {
        return "Hello " + nameService.getRandomName();
    }

}

Dieser Code nutzt die MicroProfile-Konfiguration sowie REST-Client-APIs, um den Zufallsnamen vom Name-Service einzuholen, und gibt den „Hello“-String mit dem angehängten Zufallsnamen zurück. Da der dritte Service nicht verfügbar ist, wird ein HTTP-500-Fehler an das Frontend weitergeleitet.

Auf unseren Blogs kann man mehr über MP Rest Client und MP Config API oder unsere Dokumentation nachlesen, um mehr Information zu beiden zu erhalten.

Die Server-URL für den Namenservice wird aus der Konfigurationseigenschaft name.service.url eingeholt, die standardmäßig http://localhost:8082 lautet, wie in src/main/resources/META-INF/microprofile-config.properties angegeben. Die MicroProfile-Konfigurationsspezifikation erlaubt es, diese zu überschreiben, indem die Umgebungsvariable NAME_SERVICE_URL vor dem Ausführen der Anwendung konfiguriert wird.

Den Name-Generator-Service starten

Der letzte Teil der Anwendung ist der Name-Service. Wir gehen in den Ordner name-generator und führen den folgenden Befehl aus:

# Build the application, bundle it with Payara Micro and then run it
mvn verify -Pserve

Dies startet den Server auf Port 8082, um mit der Bearbeitung von Anfragen zu beginnen. Wenn wir jetzt den Button der Webseite betätigen, dann werden wir sehen, wie Zufallsnamen ausgegeben werden!

Quelle: <a href="https://blog.payara.fish/what-is-kubernetes" target="_blank" rel="noopener">Payara-Blog</a>

Quelle: Payara-Blog

Dies bedeutet, dass alle 3 Services richtig funktionieren! Da wir jetzt gesehen haben, wie diese Anwendung aussieht, können wir damit beginnen, die Anwendung auf Container zu portieren, die dann mit Kubernetes eingesetzt werden können.

Services auf Container portieren

Jeder Service hat eine Docker-Datei in seinen entsprechenden Ordnern. Wir führen den folgenden Befehl von der Root des Stammverzeichnis des Kubernetes-Beispielordners aus, um diese zu erstellen:

mvn install -Pdocker

In der Annahmen, dass wir Docker lokal installiert haben und auch ohne sudo ausführen können, wird dieser Befehl drei Docker-Container erstellen — jeweils einen pro Service.

  • payara/kubernetes-frontend
  • payara/kubernetes-backend
  • payara/kubernetes-name-generator

Alternativ können wir die Images auch manuell erstellen, indem wir die Docker-Dateien in jedem Unterordnern verwenden.

Ein Docker-Netzwerk erstellen

Es war bisher möglich, Container über ihren Namen zu verknüpfen, mit Hilfe des --link-Parameters. Dies ist nun zugunsten der Erstellung benutzerdefinierter Bridges nicht mehr empfehlenswert. Dies macht es Containern grundsätzlich möglich, sich gegenseitig mit ihren Container-Namen (diejenigen, die mit --name zugewiesen sind) zu referenzieren. Um ein Docker-Netzwerk zu erstellen, führen wir den folgenden Befehl aus:

docker network create kubernetes-network

Mit dem --network können wir der dem Netzwerk auch Container hinzufügen, wenn wir Container anlegen. Sobald wir mit dem Netzwerk fertig sind, köönne wir es mit Hilfe des entsprechenden Befehls entfernen:

docker network rm kubernetes-network

Den Name-Service-Container ausführen

Der erste Container, der gestartet wird, ist der Name-Generator-Service. Die Dockerdatei startet die Payara-Micro-Instanz, wie wir es zuvor machten, aber ohne den Port zu verändern (das bedeutet, dass es auf Standardport 8080 laufen wird). Dies ist deswegen so, da Docker jeden Container als seinen eigenen virtuellen Host aussortiert, sodass nicht die Notwendigkeit für jeden Container besteht, einen anderen Port auszuwählen.

Wir können den Container folgendermaßen ausführen:

docker run -d --rm --name name-generator --network kubernetes-network payara/kubernetes-name-generator 

Das wird den Container im Hintergrund starten. Mit diesem Befehl wird sichergestellt, dass der Container über einen geeigneten Namen mit --name verfügt und der Container mit dem neuen Netzwerk per --network verbunden wird. Das ist notwendig, da der Container später vom backend-Container referenziert wird. Wir können prüfen, ob der Container gestartet wurde, indem wir die Container-Protokolle überprüfen:

docker container logs name-generator

Als Alternative können wir die Container-IP  mit dem folgenden Befehl erhalten:

docker inspect name-generator

Anschließend können wir den Endpunkt testen, indem wir einen zufälligen Instanznamen einholen:

curl <container-ip>/name/random

Den Backend-Container ausführen

Mit dem zweiten Container ist es exakt so, wie mit dem ersten, diesmal allerdings für das Backend Service JAR. Wir können den Container folgendermaßen ausführen:

docker run -d --rm --name backend -p 8081:8080 --network kubernetes-network --env NAME_SERVICE_URL=http://name-generator:8080 payara/kubernetes-backend

Hier gibt zwei zusätzliche Argumente, die wir bemerken werden:

Der erste, neue Teil ist '-p 8081:8080'. Das ist das Docker-Argument, um einen Container-Port auf einem localhost-Port abzubilden. Es bildet den Port 8080 des Container in diesem Fall ab (der von Payara Micro benutzt wird), um Port 8081 auf die Host-Maschine zu portieren. Der Grund dafür ist, dass das JS auf der Webseite in der Lage sein muss, auf den Backend-Service zuzugreifen. Der einfachste Weg, um dies zu tun, besteht darin, das Backend auf localhost zu hosten. Dies wird ebenfalls gelöst, wenn wir anfangen Kubernetes einzusetzen.

Der zweiter Teil ist '--env NAME_SERVICE_URL=http://name-generator:8080'. Ihr werdet euch von vorhin noch erinnern, dass dieser Service es erlaubt, dass die URL des Name-Services durch diese Umgebungsvariable überschrieben wird. Dies ist der letzte Teil, der es dem Backend möglich macht, den Name-Service zu finden und mit diesem zu kommunizieren.

Den Frontend-Container ausführen

Der Frontend-Container ist der letzte Container. Dieser unterscheidet sich leicht von den anderen Containern, da er dem Client kompilierten JS-Code über Nginx zustellt und daher vorher kompiliert werden muss. Als Teil des Maven Builds wird der Befehl npm run build ausgeführt, der die statischen Dateien generiert, die dem Benutzer zur Verfügung gestellt werden sollen. Unter anderem wird dadurch das dynamische Neuladen sowie die ausführliche Protokollierung deaktiviert, und der Code in der Regel für die Produktion bereit gemacht. Es ist dieser Zeitpunkt, an dem alle Umgebungsvariablen in den kompilierten Code eingegeben werden sollte. Aus diesem Grund, da der Payara-Micro-Service die erforderlichen Umgebungsvariablen überschreiben kann, verlangt der JS-Code, dass er für den Maven-Build angegeben wird. Wir müssen die Variable, die sich in der Datei 'frontend/.env.production' befindet, bearbeiten. Dadurch wird die URL des Backend-Service bestimmt, die vom Benutzer aufgerufen werden soll. Normalerweise würden wir dies durch den Domainnamen für unsere Website-API ersetzen, aber in diesem Fall werden wir es einfach auf’http://localhost:8081′ ändern, da dort der Docker-Container gehostet wird.

Sobald wir dies getan haben, können wir das Frontend mit 'mvn clean install' neu erstellen und den Container wie gewohnt starten:

docker run -d --rm --name frontend -p 8080:80 --network kubernetes-network payara/kubernetes-frontend

Dadurch wird der Container gestartet und an den Port 8080 auf dem Host gebunden. Wenn wir http://localhost:8080/ besuchen, wird die Webseite wie bisher angezeigt, außer dass sie jetzt in einem Container läuft!

Wenn wir mit den Containern fertig sind, können wir sie mit dem folgenden Docker-Befehl stoppen:

docker stop frontend backend name-generator

Wir können das Netzwerk dann wie vorher beschrieben löschen.

Die Services nach Kubernetes portieren

Nun, da wir bewiesen haben, dass die Docker-Container funktionieren, können wir mit der Portierung der Komponenten auf Kubernetes beginnen. Würden wir jetzt mit dem aktuellen Setup deployen, dann würden wir auf Probleme stoßen, denn wir müssten entweder einen Load-Balancing-Node für jeden Containertyp erstellen oder wir müssten jeden Container manuell mit einem anderen verknüpfen, was bei Skalierung schwierig zu handhaben wäre. Ebenfalls würden wir auf Probleme stoßen, sobald wir ein Rolling Upgrade durchführen wollten, da das Load-Balancing dieses nicht berücksichtigt, sodass wir zuerst das Load-Balancing konfigurieren, daraufhin einen Container aktualisieren und dann das Load-Balancing neu konfigurieren müssten und so weiter.

Wir gehen davon aus, dass wir MicroK8s gemäß den Anweisungen zu Beginn dieses Blogs installiert haben.
Ist das nicht der Fall, sehen wir dort noch einmal nach, ob wir alles richtig installiert haben, bevor wir fortfahrfen. Bevor wir anfangen, alle Kubernetes-Schritte durchzugehen, ist es nützlich, einen schnellen Crashkurs über Kubernetes-Konzepte zu absolvieren:

  • Container: Ein ausführbares Image, das eine einzelne Software und alle ihre Abhängigkeiten enthält. In unserem Fall handelt es sich um einen Docker-Container.
  • Node: Eine virtuelle oder physische Maschine, die als Worker für Kubernetes verwendet wird. Kubernetes wird sein Management in der Praxis abstrahieren. Ein Kubernetes Cluster enthält einen Master Node, der verwendet wird, um den Zustand des Clusters in Echtzeit zu verwalten und Benutzerinteraktionen zu ermöglichen.
  • Cluster: Eine Reihe von Nodes, die Container ausführen, die von Kubernetes verwaltet werden. Dies ist analog zu einer Domain im Payara-Server.
  • Pod: Das kleinste Kubernetes-Objekt. Ein Pod betreibt einen Satz von Containern. Normalerweise haben Pods jeweils einen Container, aber in einigen Fällen können sie auch zusätzliche Container Services beinhalten, wie z.B. einen, um Protokollierungsfunktionen hinzuzufügen.
  • Deployment: Ein Objekt, das einen Satz von replizierten Pods verwaltet. Bei der Skalierung eines Microservice werden weitere Pods zu einem Deployment hinzugefügt.
  • Service: Ein Objekt, das beschreibt, wie man über einen gemeinsamen Endpunkt auf Deployments oder Gruppen von Pods zugreift. Kann Dinge wie Load-Balancer erstellen, um auf die Ressource zuzugreifen.
  • Label: Kubernetes-Objekte (Pods, Deployments, Services, etc.) werden mit Labels versehen, um gemeinsam ausgewählt zu werden.
  • Selector: Ein Selektor referenziert ein Label, um die Interaktion mit einer Reihe von Objekten zu ermöglichen, wie z.B. mehrere Container zum Zwecke des Load-Balancing.
  • Kubectl: Das Befehlszeilen-Utility, das zur Interaktion mit dem Kubernetes Master Node verwendet wird.

Kubernetes kann YAML-Manifeste einsetzen, um damit alle Ressourcen zu beschreiben (wie z. B. Services, Deployments oder Pods), die an es deployt werden. Diejenigen, die mit dem Docker-Compose-Werkzeug vertraut sind, wird die YAML-Konfiguration bekannt vorkommen. Als Beispiel: Die pod.yaml-Datei im Frontend zeigt ein Beispiel für einen Pod-Deskriptor:

apiVersion: v1
kind: Pod
metadata:
  name: frontend
  labels:
    app: frontend
spec:
  containers:
    - image: payara/kubernetes-frontend
      name: frontend
      ports:
        - containerPort: 80

Dieser Deskriptor verweist auf einen Pod, der an Kubernetes deployt werden soll. Er erhält den Namen ‚Frontend‘ und das Label ‚app: Frontend‘. Dies kann von Selektoren verwendet und zwischen den Pods geteilt werden. Das Docker-Image wird dann referenziert und der Deskriptor bestimmt den vom Container verwendeten Port. Beachtet, dass man nicht direkt mit Pods arbeiten sollte; dies wird nur gezeigt, um einen grundlegenden Deskriptor für das kleinste Kubernetes-Objekt zu geben.

Der Anwendungsversuch wird jedoch zu einem Problem führen: Kubernetes wird nicht in der Lage sein, dass benannte Image zu finden, da es keinen Zugriff auf die lokale Docker-Registry hat. Würde das Image in die Docker-Hub-Container-Registry verschoben werden, dann würde Kubernetes es finden können. Wir können unseren Kubernetes Cluster auch mit privaten Registrys verbinden. Weitere Informationen zur Nutzung privater Register finden Sie auf der Kubernetes-Webseite. Aber MicroK8s beinhaltet eine Funktion, die uns bei der lokalen Entwicklung hilft: die eigene lokale Registry. Damit können wir Docker in diese Registry pushen und dann das Image in MicroK8s benutzen.

Verwendung der lokalen MicroK8s-Registry

Um die lokale Registry von MicroK8s zu initialisieren, führen wir den folgenden Befehl aus:

microk8s.enable registry

Dadurch wird das lokale Repository initialisiert, so dass wir beginnen können, Images dorthin zu pushen. Wir erinnern uns, dass wir 3 Docker Images haben, die verwendet werden können:

  • payara/kubernetes-frontend
  • payara/kubernetes-backend
  • payara/kubernetes-name-generator

Mit den folgenden Befehlen können diese in die lokale Registry gepusht werden:

docker tag payara/kubernetes-frontend 127.0.0.1:32000/kubernetes-frontend
docker tag payara/kubernetes-backend 127.0.0.1:32000/kubernetes-backend
docker tag payara/kubernetes-name-generator 127.0.0.1:32000/kubernetes-name-generator

docker push 127.0.0.1:32000/kubernetes-frontend
docker push 127.0.0.1:32000/kubernetes-backend
docker push 127.0.0.1:32000/kubernetes-name-generator

Die ersten 3 Befehle werden das Image mit dem Link zu der Registry taggen. Dies sagt Docker, wohin es das Image pushen soll.

Die nächsten 3 Befehle werden das Image zu der Registry pushen.

Diese Registry ist für die lokale Entwicklung vorgesehen, daher ist sie standardmäßig unsicher. Das könnte bedeuten, dass Kubernetes einen Fehler ausgeben wird, wenn es dahin verschoben wird. Um dem entgegen zu wirken, müssen wir eine Konfigurationsdatei bei /etc/docker/daemon.json erstellen, mit dem folgenden Inhalt:

{
    "insecure-registries": ["127.0.0.1:32000"]
}

Diese Konfiguration wird Docker sagen, dass unsichere Verbindungen zu der MicroK8s-Registry erlaubt sind. Wir müssen den Docker Daemon neustarten, damit die Änderung aktiv werden:

sudo systemctl restart docker

Sobald wir das erledigt haben, wir das Image korrekt in die Micro8Ks-Registry verschoben. MicroK8s enthält eine Referenz zu dieser Registry, die 'local.insecure-registry.io' heißt. Wir müssen sicherstellen, dass wenn wir einen Kubernetes-Deskriptoren benutzen, wir anstelle des alten Repositorynamens (payara/<image-name>), die neue Registry verwenden (local.insecure-registry.io/<image-name>).

Nach der Bearbeitung des Pod-Deskriptors können wir Kubernetes mit dem folgenden Befehl anweisen, es zu erstellen:

kubectl create -f pod.yaml

Dies wird den Pod erstellen, gemäß den Anweisungen des Manifests. Wir können sehen, dass es damit begonnen hat, den describe-Befehl zu benutzen:

kubectl describe pods

Jetzt stellen wir allerdings fest, dass es keinen Weg gibt, um auf die Pods zuzugreifen! Um das zu lösen, werden wir zuerst ein Deployment erstellen. Und wie wir es bereits vorher gesagt haben, ist die direkte Interaktion mit Pods eine allgemein schlechte Idee, daher wird unser Deployment das Basisobjekt sein, von dem aus wir arbeiten werden. Die nächste Sektion wird beschreiben, was für Deployments eingesetzt werden und wie man sie einsetzt. Wir müssen allerdings zunächst die Pods löschen:

Erstellung unseres ersten Kubernetes-Deployments

Wie im Termininologieteil beschrieben, ist ein Deployment ein Objekt, das benutzt wird, um Pods zu einzuschließen und beim Skalieren von Microservices zu helfen. Unser erstes Deployment befindet sich im Frontend-Verzeichnis, namens deployment.yaml. Die Inhalte werden unten angezeigt:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
        - image: local.insecure-registry.io/kubernetes-frontend
          name: frontend
          ports:
            - containerPort: 80
          readinessProbe:
              httpGet:
                  path: /
                  port: 80
              initialDelaySeconds: 3
              periodSeconds: 3
              failureThreshold: 2

Dieser Deployment-Deskriptor ersetzt den Pod-Deskriptor und kann ohne ihn eingesetzt werden. Wir werden sehen, dass der Inhalt dem Pod-Deskriptor weitgehend ähnlich ist, bis auf einige wenige Unterschiede:

  1. Das Replicas-Tag gibt vor, wie viele Pods anfänglich für dieses Deployment erstellt werden. In diesem Fall sind 2 Pods zu erstellen.
  2. Der Strategy-Block: Dieser Block gibt die Einschränkungen für die Aktualisierung oder das Zurücksetzen des Deployments an. Der Wert max unavailable definiert, wie viele Pods nicht erreichbar sein werden an einem Zeitpunkt. Der Wert max surge definiert, wie viele Pods über den Replica Count erstellt werden können, während eines Updates. In diesem Fall bedeutet dies, dass der Replica Count ein Maximum von 3 haben könnte.
  3. Der Template-Block: Dieser definiert, wie die Pods unter diesem Deployment benannt werden sollen.
  4. Der Selector-Block: Dieser definiert die Kriterien für einen Pod, der von diesem Deployment verwaltet werden soll. In diesem Fall ist es das gleiche, wie die in den Templates angegebenen Labels.
  5. Die Readiness-Probe: Diese legt fest, wie Kubernetes prüfen soll, ob die Instanz als „bereit“ gilt. Dies wird durch die Rollback-Strategie sowie alle Dienste, die diese Pods nutzen, genutzt.
    • Type: In diesem Fall pingen wir einfach die Kontext-Root an und suchen nach einer 200. Eine vollständige Liste der möglichen Readiness-Prüfungen finden man in der Kubernetes-Dokumentation.
    • Inital Delay: Dies bedeutet, dass Kubernetes nach dem Start mindestens 3 Sekunden warten sollte, bevor es den Container als „bereit“ betrachtet.
    • Period: Alle 3 Sekunden sollte eine Readiness-Kontrolle durchgeführt werden.
    • Failure Threshold: Wenn 2 aufeinanderfolgende Readiness-Prüfungen fehlschlagen, sollte der Container als nicht bereit angesehen und neu gestartet werden.

Wir können dieses Deployment mit dem folgenden Befehl einsetzen:

kubectl apply -f deployment.yaml

Dabei wird die Kubernetes deklarative Management API verwendet. Während wir im vorherigen Abschnitt den Befehl create verwendet haben, kann apply auch zur Skalierung und Durchführung der meisten anderen Änderungen an Containern verwendet werden. Wenn wir Änderungen an einem Deskriptor vornehmen müssen, editeren wir einfach die Datei und führen den Befehl apply erneut aus. Für den Rest dieses Leitfadens werden wir diesen Befehl zur Vereinfachung verwenden.

Man muss ein paart Sekunden warten, bis die Container starten und wir sehen zwei Pods, die unter dem Frontend-Deployment erstellt wurden, wenn wir Kubernetes-Ressourcen auflisten.

kubectl get all
NAME                            READY   STATUS    RESTARTS   AGE
pod/frontend-65756df4c8-fkrjd   1/1     Running   0          12s
pod/frontend-65756df4c8-jcq5x   1/1     Running   0          12s

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/kubernetes   ClusterIP   10.152.183.1   <none>        443/TCP   170m

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/frontend   2/2     2            2           12s

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/frontend-65756df4c8   2         2         2       12s

Ein entscheidender Punkt, den wir hier beachten sollten, ist, dass es keine IP-Adresse gibt, die dem Deployment zugewiesen ist.

Wir erstellen unseren ersten Kubernetes-Service

Der Frontend-Ordner beinhaltet einen finalen YAML-Deskriptor: service.yaml.

apiVersion: v1
kind: Service
metadata:
  name: frontend-lb
spec:
  type: LoadBalancer
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: frontend

Das ist unser erster Service. Es wird dafür eingesetzt, auf das zuvor erstellte Frontend-Deployment zu zugreifen. Wir werden feststellen, dass das Deployment selbst hier überhaupt nicht erwähnt wird. Stattdessen wird ein Selektor verwendet, der es uns ermöglicht, alle Pods auszuwählen, die einem Label entsprechen. Das ist einer der Gründe, warum Kubernetes so flexibel ist.

Die einzige Neuerung in diesem Deskriptor ist die Portkonfiguration. Dieser Dienst leitet TCP-Verbindungen von Port 80 auf seiner zugewiesenen IP an den Zielport 80 auf den referenzierten Pods weiter.

Wir können diesen Service mit dem folgenden Befehl anwenden:

kubectl apply -f service.yaml

Wir werden nun eine neue Ergänzung zum Ergebnis von 'kubectl get all' sehen:

NAME                            READY   STATUS    RESTARTS   AGE
pod/frontend-65756df4c8-fkrjd   1/1     Running   0          86m
pod/frontend-65756df4c8-jcq5x   1/1     Running   0          86m

NAME                  TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/frontend-lb   LoadBalancer   10.152.183.109   <pending>     80:31415/TCP   5s
service/kubernetes    ClusterIP      10.152.183.1     <none>        443/TCP        4h16m

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/frontend   2/2     2            2           86m

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/frontend-65756df4c8   2         2         2       86m

Von hier aus können wir den neuen Load-Balancer Service sehen. Wir werden feststellen, dass die externe IP auf „Pending“ eingestellt ist. In einer Cloud-Umgebung wird diese IP vom Provider automatisch vergeben und mit dem entsprechenden DNS synchronisiert. In MicroK8s können wir einfach die Cluster-IP von unserem Browser aus aufsuchen, um den Endpunkt zu sehen. Der Besuch der Cluster-IP von frontend-lb führt uns über einen der frontend-Pods zum Frontend der Website.

Erstellen des Backend Kubernetes Service

Der Backend-Verzeichnis enthält zwei YAML-Deskriptoren: deployment.yaml und service.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate: 
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
        - image: local.insecure-registry.io/kubernetes-backend
          name: backend
          ports:
            - containerPort: 8080
          env:
            - name: NAME_SERVICE_URL
              value: "http://name-generator"
          readinessProbe:
              httpGet:
                  path: /health
                  port: 8080
              initialDelaySeconds: 20
              periodSeconds: 3
              failureThreshold: 3

Der Deployment-Deskriptor ist, bis auf ein wenige Veränderungen, ähnlich dem Frontend-Service formatiert.

Zunächst einmal wird jedes Images mit der Umgebungsvariable gestartet, die für das Docker-Image benötigt wird. Da Kubernetes-Services namentlich zugänglich sind, wird der Name des Name-Generator-Servers als entsprechender Wert übergeben. Die andere Änderung besteht darin, dass die anfängliche Verzögerung für die Readiness-Probe erhöht wird. Der Grund dafür ist, dass Payara Micro länger braucht, um zu starten, als der im Frontend verwendete nginx-Server, so dass Kubernetes länger warten muss, um zu sehen, ob er bereit ist.

apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  type: LoadBalancer
  ports:
    - port: 80
      protocol: TCP
      targetPort: 8080
  selector:
    app: backend

Das Manifest service.yaml ist dem Manifest des Frontends sehr ähnlich. Der einzige Unterschied besteht darin, dass der Zielport für den Container 8080 ist. Trotzdem verwendet der Service für das Deployment weiterhin Port 80. Dadurch wird eine gewisse Konsistenz zwischen den Services erreicht.

Wir können diese beiden Konfigurationsdateien anwenden, und wir werden sehen, dass der Kubernetes-Cluster so aussieht:

NAME                            READY   STATUS    RESTARTS   AGE
pod/backend-5b5bcf5dcd-gn4kk    1/1     Running   0          27s
pod/backend-5b5bcf5dcd-vdmxt    1/1     Running   0          27s
pod/frontend-65756df4c8-fkrjd   1/1     Running   0          115m
pod/frontend-65756df4c8-jcq5x   1/1     Running   0          115m

NAME                  TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/backend-lb    LoadBalancer   10.152.183.141   <pending>     80:31931/TCP   24s
service/frontend-lb   LoadBalancer   10.152.183.109   <pending>     80:31415/TCP   29m
service/kubernetes    ClusterIP      10.152.183.1     <none>        443/TCP        4h45m

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/backend    2/2     2            2           28s
deployment.apps/frontend   2/2     2            2           115m

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/backend-5b5bcf5dcd    2         2         2       28s
replicaset.apps/frontend-65756df4c8   2         2         2       115m

Wir können sehen, dass die Backend-Pods erstellt wurden und durch den backend-lb-Service lastverteilt werden. Alles, was wir jetzt tun müssen, ist, den Name-Generator-Service zu erstellen.

Den Name-Generation-Kubernetes-Service erstellen

Der Name-Generation-Service deployment.yaml ist genau derselbe wie der für den Backend-Service, ändert sich aber mit dem Namen und den Image-Namen. Sie können die Bereitstellung auf die gleiche Weise anwenden. Der einzige Unterschied zwischen diesen Deskriptoren und denen des Backend-Service besteht in der service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: name-generator
spec:
  ports:
    - port: 80
      protocol: TCP
      targetPort: 8080
  selector:
    app: name-generator

Dies ähnelt zwar weitgehend dem vorherigen Service-Deskriptor, aber es fehlt eine Zeile:

type: LoadBalancer

Diese Zeile weist dem Service eine IP zu, die als externer Load-Balancer verwendet werden soll. Wir dieser Deskriptor ohne diese Zeile verwendet, ist der Service trotzdem weiterhin namentlich für andere Pods zugänglich, aber es wird keine externene IP zugewiesen. Da es sich um eine interne IP handelt, muss sie nicht extern zugänglich sein (und das wäre in der Tat eine Sicherheitsschwäche, wenn das der Fal wäre).

Sobald wir die Dateien deployment.yaml und service.yaml aus diesem Ordner angewendet haben, sind wir fast fertig!

Das Ergebnis

Sobald wir alle 3 Deployments und alle 3 Services deployt haben, sollten wir den folgenden Kubernetes-Cluster sehen:

NAME                                  READY   STATUS    RESTARTS   AGE
pod/backend-5b5bcf5dcd-gn4kk          1/1     Running   0          131m
pod/backend-5b5bcf5dcd-vdmxt          1/1     Running   0          131m
pod/frontend-65756df4c8-fkrjd         1/1     Running   0          4h6m
pod/frontend-65756df4c8-jcq5x         1/1     Running   0          4h6m
pod/name-generator-855f8f9c59-dckd4   1/1     Running   0          30s
pod/name-generator-855f8f9c59-vllns   1/1     Running   0          30s

NAME                     TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/backend-lb       LoadBalancer   10.152.183.141   <pending>     80:31931/TCP   130m
service/frontend-lb      LoadBalancer   10.152.183.109   <pending>     80:31415/TCP   160m
service/kubernetes       ClusterIP      10.152.183.1     <none>        443/TCP        6h57m
service/name-generator   ClusterIP      10.152.183.115   <none>        80/TCP         26s

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/backend          2/2     2            2           131m
deployment.apps/frontend         2/2     2            2           4h6m
deployment.apps/name-generator   2/2     2            2           30s

NAME                                        DESIRED   CURRENT   READY   AGE
replicaset.apps/backend-5b5bcf5dcd          2         2         2       131m
replicaset.apps/frontend-65756df4c8         2         2         2       4h6m
replicaset.apps/name-generator-855f8f9c59   2         2         2       30s

Ein Besuch der IP des Frontend Load-Balancers (lila) führt uns zur Website. Es gibt jedoch noch ein paar Probleme. Erinnerst ihr euch daran, als wir dies mit Docker neu erstellt haben und wir den Frontend-Code ändern mussten, um auf das Backend zu verweisen? Dies würde in der Produktion mit einem Domainnamen gelöst werden, aber da es lokal ist, müssen wir es umgehen! Glücklicherweise haben wir die Backend-URL auf localhost:8081 gesetzt und Kubernetes bietet eine nützliche Utility:

kubectl port-forward service/backend-lb 8081:80

Dadurch wird der Port 8081 auf dem Host vorübergehend mit dem Serviceport 80 des Backend-Lastausgleichs verbunden. Dadurch können die AJAX-Aufrufe korrekt an den Backend-Service weitergeleitet werden.

Wenn wir ein Terminalfenster nicht geöffnet lassen möchten, können wir stattdessen die Umgebungsvariable für das Frontend-Image ändern, das wir zuvor von 'http://localhost:8081' auf 'http://backend' geändert haben, und die folgende Zeile zu Ihren /etc/hosts hinzufügen:

<backend-url> backend

Die Backend-URL befindet sich in grün über der violetten Frontend-URL. Dies wird den den gleichen Effekt haben, aber ohne Terminalfenster!

Das letzte Problem ist, dass MicroK8s standardmäßig keinen laufenden DNS-Service hat (der Service, der es ermöglicht, andere Dienste namentlich und nicht über IP zu referenzieren). Wir können dies mit dem folgenden Befehl aktivieren:

microk8s.enable dns

Sobald dies aktiviert ist, können wir die Frontend-URL erneut aufsuchen und die App sollte funktionieren! Wir können anhand der Cluster-Konfiguration sehen, dass wir 6 Instanzen in dieser Konfiguration haben und diese Konfiguration kann leicht skaliert werden, um den großen Anwendungsbedarf zu decken.

Etwas kubernetische Coolness

Angenommen, unsere Backend-Entwickler entscheiden sich für die Veröffentlichung einer neuen Version. Sie sind nicht glücklich darüber, dass die App immer wieder „Hallo“ sagt, und denken, dass es eine gute Verbesserung sein werden würde, wenn sie stattdessen „Hey“ sagen würde. Sie pushen ein neues Docker-Image und bitten uns, es zu deployen. Ohne Kubernetes würden wir wahrscheinlich in jede Instanz gehen und die neue Anwendung deployen, was überhaupt keine praktikable Lösung ist. Kubernetes ermöglicht Rolling Upgrades für aktuell laufende Container.

Ein Update mit Kubernetes durchzuführen ist so einfach, wie die Version des Images der entsprechenden Container im Deskriptor zu aktualisieren und den allmächtigen Befehl auszuführen:

kubectl apply -f deployment.yaml

Dann können wir einfach warten, bis Kubernetes den Rolling-Neustart durchführt. Wir können auch die Anzahl der Replikate in jedem Deployment ändern, indem wir den Wert im Deskriptor bearbeiten und auf diese Weise anwenden.

Aber halt! Die Backend-Entwickler haben ein kaputtes Image erstellt! Keine Sorge, wir können das Deployment einfach zurücksetzen. Wir listen die vorherigen Deployments mit dem folgenden Befehl auf:

kubectl rollout history deployment/backend

Wir wählen eine Deploymentnummer aus dieser Liste aus. Falls wir hier keine Änderungsinformation sehen möchten, sollten wir beim Anwenden neuer Deployments die --record-Markierung hinzufügen. Um einen einzelnen Revisionslauf zu prüfen benutzen wir den folgenden Befehl:

kubectl rollout history deployment/backend --revision=2

Sobald wir ein Deployment ausgewählt haben, können wir mit diesem Befehel einen Rollback zu dieser Version ausführen:

kubectl rollout undo deployment/backend --to-revision=2

Wenn wir die --to-Revision auslassen, wird die vorherige Version des Deployments wieder hergestellt.

Payara Micro: Gut geeignet für das Deployment in einer Cloud-Kubernetes-Umgebung

Hoffentlich hat dieser Artikel geholfen, etwas Licht auf das zu werfen, was Kubernetes ist, sowie auf einige der Vorteile, die es im Einsatz unserer eigenen Produktionsumgebung hat. Jeder Cloud-Anbieter bietet eine Kubernetes-Deployment-Option an, sowie eine eigene Methode zur automatischen Skalierung von Clustern. Jeder Anbieter hat seine eigenen Vorteile, aber alle verfolgen die gleichen Grundkonzepte und folgen der gleichen Architektur, so dass das Wissen, das wir hier gelernt haben, auf alle anwendbar ist. Mit Kubernetes können wir unsere Implementierungen fast vollständig unabhängig von der zugrundeliegenden Infrastruktur gestalten und müssen uns nur um die Auswahl der Image-Tags kümmern, die zu jeder Zeit laufen.

Payara Micro ist gut geeignet für den Einsatz in einer Kubernetes-Cloud-Umgebung. Um mehr über die Skalierung in dieser Umgebung zu erfahren, besuchen Sie unseren Blog zu diesem Thema.

Geschrieben von
Matthew Gill
Matthew Gill
Matthew Gill ist Senior Apprentice bei Payara.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: