Über unsMediaKontaktImpressum
Peter-Josef Meisch 19. Dezember 2017

Kubernetes – Funktioniert das wirklich?

Kubernetes ist in aller Munde – und das nicht erst, seit Docker die native Unterstützung von Kubernetes als Orchestrierungstool neben Docker-Swarm angekündigt hat. Trotzdem scheint Kubernetes für viele noch ein Buch mit sieben Siegeln zu sein. Oft höre ich Fragen wie "Habt ihr das im Einsatz?" oder "Funktioniert das wirklich?". 

Anwendungen in Container zu packen und dort laufen zu lassen, hat sich mittlerweile in der Entwicklergemeinde etabliert. So lässt sich schnell mal eine Datenbank hochfahren oder ein Webserver starten, mit dem die lokale Entwicklung getestet werden kann. Sobald aber mehrere Anwendungen in Docker gestartet werden oder wenn man gar eine Anwendung mehrfach redundant starten will und diese Anwendungsinstanzen miteinander kommunizieren müssen, stellt sich die Frage, wie man die unterschiedlichen Anwendungen für den Test konfiguriert und orchestriert.

In diesem Artikel zeige ich, wie eine Anwendung aus drei Komponenten, die auf unterschiedlichen Technologien basieren, in Kubernetes konfiguriert, ausgerollt und skaliert wird.

Voraussetzungen

Um die Beispiele mit Kubernetes [1] nachzuvollziehen, sind eine Kubernetes-Installation und eine lokal installierte kubectl-Anwendung notwendig.

Die verwendeten Kubernetes-Konfigurationen sowie der komplette Programmcode für die Beispielanwendungen sind auf GitHub [2] verfügbar. Die Docker-Images mit den Anwendungen sind im offiziellen Docker-Repository abgelegt und werden von den Beispielkonfigurationen verwendet. Dadurch können die Beispiele auch in Kubernetes getestet werden, ohne dass die Anwendungen lokal gebaut und in Docker-Images gepackt werden müssen.

Auf das Erstellen der Anwendungen sowie der Docker-Images gehe ich in diesem Artikel nicht weiter ein. Das Quellcode-Repository enthält sowohl auf der obersten Ebene als auch in den Unterverzeichnissen für die einzelnen Anwendungen jeweils detaillierte Anweisungen in den readme.md-Dateien.

Grundlagen

Docker

Docker soll in diesem Artikel nur so weit beschrieben werden, wie es für das Verständnis von Kubernetes notwendig ist. Docker bietet die Möglichkeit, Prozesse wie z. B. Webserver, REST-Services oder Datenbanken in einer beschränkten Umgebung ablaufen zu lassen. Beschränkt bedeutet hier, dass ein Prozess im Container die anderen Prozesse außerhalb nicht sieht, seine eigene IP-Adresse hat und sein eigenes Filesystem. Definiert wird diese Containerumgebung in einem Docker-Image, welches in einer Docker-Registry wie z. B. hub.docker.com[3] zur Verfügung gestellt wird.

Während ein Docker-Image die Umgebung für einen Prozess beschreibt, ist ein Docker-Container quasi eine lebende Instanz dieses Images. Beim Start eines Container verwendet der Docker-Service die Beschreibung im Docker-Image, um den gewünschten Prozess zu starten und ihn in den Grenzen auszuführen (Dateisystem, Ports etc.), die durch das Docker-Image vorgesehen sind.

Kubernetes

Kubernetes ist ein Tool zur Orchestrierung von Anwendungen, welche in Containern (meistens Docker) ausgeführt werden, auch über verschiedene Rechnerknoten hinweg. In diesem Abschnitt möchte ich die wichtigsten Konzepte von Kubernetes vorstellen.

Label

Label werden in Kubernetes bei fast allen Komponenten verwendet. Ein Label ist ein key-value-Paar, wobei sowohl der key als auch der value frei gewählt werden können. Beispiele für Label sind app=myservice oder environment=test.

Nodes

Nodes oder Knoten sind die Rechner, auf denen Kubernetes installiert wird, und die zusammen den Kubernetes-Cluster bilden. Dabei spielt es keine Rolle, ob es sich hierbei um physikalische Rechner oder um virtuelle Instanzen handelt, die z. B. von einem Cloud-Provider zu Verfügung gestellt werden. Nodes können im laufenden Betrieb zu einem Kubernetes-Cluster hinzugefügt werden, ebenso ist es möglich, Nodes aus einem Kubernetes-Cluster im laufenden Betrieb zu entfernen.

Pod und Deployment

Die grundlegende Einheit im Kubernetes-Universum ist ein "Pod". Ein Pod enthält im Normalfall einen Docker-Container. Es gibt zwar auch Pods, welche mehr als einen Container enthalten, aber dies sind Spezialfälle, die hier nicht weiter berücksichtigt werden sollen.

Ein Pod hat eine eigene IP-Adresse, unter der er im Cluster erreichbar ist, er hat einen Namen und er wird mit Hilfe von Labeln gekennzeichnet. Man kann sich einen Pod vorstellen wie einen eigenen kleinen Rechner, auf dem nur ein einzelner im Docker-Container spezifizierter Prozess läuft.

Der Anwender erzeugt im Normalfall die Pods nicht selber, sondern er erzeugt ein "Deployment", welches die Beschreibung des Pods enthält sowie zusätzliche Angaben wie z. B. die Anzahl der Pods, die gleichzeitig laufen sollen (Skalierung der Pods). Das Deployment startet die gewünschte Anzahl Pods und überwacht diese. Sollte eine Anwendung in einem Container z. B. abstürzen und damit den Container und den Pod beenden, sorgt das Deployment wieder dafür, dass ein neuer Pod gestartet wird und somit wieder die gewünschte Anzahl an Pod-Instanzen zur Verfügung steht.

Für die Pods bzw. Container können – und sollen – Beschränkungen für CPU und Speicherbedarf definiert werden, damit zum einen Kubernetes die Pods auf den Nodes laufen lassen kann, auf denen noch genügend Ressourcen zur Verfügung stehen, und zum anderen können Pods, die mehr als das erlaubte Limit an Ressourcen verbrauchen, gestoppt und neu gestartet werden.

Service

Während ein Deployment dafür sorgt, dass die Pods in der gewünschten Anzahl im Cluster laufen, gibt es noch keine Möglichkeit, auf diese Pods zuzugreifen. Zwar hat jeder Pod eine IP-Adresse, aber es gibt für den Anwender noch keine Komponente, welche ihm den Zugriff auf die Pods ermöglicht. Diese Funktionalität übernehmen in Kubernetes die Services.

Ein Service hat einen Namen und eine Konfiguration, die ihm mitteilt, welche Pods ihm zugeordnet sind. Diese Zuordnung geschieht über die Label, die an die Pods konfiguriert wurden. Es gibt drei wichtige Arten von Services:

  • cluster-interne Loadbalancer: Dieser Service besitzt eine eigene IP-Adresse. Er kann über seinen Namen im DNS gefunden werden und routet den Netzwerkverkehr an die Pods weiter, für die er zuständig ist.
  • headless-Service: Dieser Service hat keine eigene IP-Adresse, eine DNS-Anfrage nach seinem Namen liefert alle IP-Adressen der zum Service gehörenden Pods zurück.
  • globale Loadbalancer: Diese Services haben zum einen die gleiche Funktionalität wie cluster-interne Loadbalancer-Services, sind aber auch von außerhalb des Clusters über eine vom Cloudprovider zur Verfügung gestellte Adresse erreichbar.

ConfigMaps und Secrets

Eine ConfigMap ist eine Sammlung von Key-Value-Werten, die z. B. als Environment-Variable oder als Konfigurationsdatei in den Pods verwendet werden können. Secrets bieten die gleiche Funktionalität, werden aber kodiert in Kubernetes gespeichert. In den Beispielanwendungen erläutere ich die genaue Verwendung beider Typen.

Die Beispielanwendung

Als Beispiel dient eine Anwendung, die es ermöglicht, Dokumente zu speichern und später zu durchsuchen. Ein Dokument hat in diesem Kontext lediglich zwei Eigenschaften: einen Titel und den Inhalt. Die Anwendung besteht aus drei Komponenten, die mit unterschiedlichen Technologien implementiert sind:

Das Frontend ("documentui") ist eine mit Angular 5 geschriebene Webanwendung, welche über einen in JavaScript geschriebenen Server mit node.js ausgeliefert wird. In diesem Frontend gibt es drei über ein Menü erreichbare Bereiche: eine filterbare Liste aller Dokumente, eine Detailansicht und ein Bereich zum Hinzufügen neuer Dokumente.

Die Webanwendung enthält keinerlei fachliche Logik, sie dient nur der reinen Repräsentation der Daten und der Nutzerinteraktion. Der in JavaScript geschriebene Server, welcher die Anwendung zur Verfügung stellt, dient auch als Proxy für den Backend-REST-Service (unter dem URL-Pfad /api/). Über diesen Pfad greift zum einen die Webanwendung auf den Service zu, zum anderen kann hierüber mit einem Tool wie z. B. curl auch auf den REST-Service zugegriffen werden.

Die Kernanwendung ("documentservice"), welche die Dokumentdaten entgegennimmt, speichert und durchsucht, ist eine in Java geschriebene Anwendung, welche auf Spring-Boot basiert. Diese Anwendung stellt eine REST-API zur Verfügung.

Als dritte Komponente wird Elasticsearch für die Speicherung der Dokumente verwendet. Abb.1 zeigt die Listenansicht der Dokumente im Browser.

Deployment der Komponenten im Cluster

Start des Clusters

Für das Beispiel in diesem Artikel verwenden wir eine minikube-Installation [4]. Das genaue Vorgehen zur Installation von minikube ist in der verlinkten Dokumentation ausführlich beschrieben. Die aktuell verwendeten Versionen sind:

  • minikube v0.23.0
  • kubectl v1.8.3
  • kubernetes v1.8.0

Damit im Beispiel der Cluster auch genug Hauptspeicher hat, um die Anwendungen skalieren zu können, starten wir ihn mit 4GB Speicher, ferner aktivieren wir das von minikube mitgelieferte heapster-Addon, um den Ressourcenverbrauch der einzelnen Komponenten im Dashboard anzeigen zu können:

$ minikube start --vm-driver virtualbox --memory 4096
Starting local Kubernetes v1.8.0 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Setting up certs...
Connecting to cluster...
Setting up kubeconfig...
Starting cluster components...
Kubectl is now configured to use the cluster.
    
$ minikube addons enable heapster
heapster was successfully enabled

Der erfolgreiche Start können wir mit minikube status überprüfen. Für den Rest dieses Artikels verwenden wir keine minikube-Kommandos mehr, sondern nur kubectl-Kommandos, damit die Beispiele auch auf anderen Kubernetes-Installationen nachvollziehbar sind:

$ kubectl cluster-info
Kubernetes master is running at https://192.168.99.100:8443

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'

Das Kubernetes-Dashboard stellen wir durch den Befehl kubectl proxy & auf dem lokalen Rechner unter der Adresse http://127.0.0.1:8001/ui zur Verfügung. Hier können wir uns einen Überblick über die in Kubernetes laufenden Komponenten und z. B. auch über die Auslastung der Kubernetes-Nodes anzeigen lassen (s. Abb.2).

Elasticsearch

Die erste Komponente, die wir im Kubernetes-Cluster installieren, ist der Service, der den Zugriff auf die Elasticsearch-Pods ermöglicht. Dieser Service wird vor dem Deployment installiert, da in der Konfiguration der Pods auf diesen Service verwiesen wird, schließlich müssen die Elasticsearch-Instanzen sich ja gegenseitig finden, um einen Elasticsearch-Cluster zu bilden, und hierfür wird der Service verwendet.

Hier nun die Definition des Service in der Datei elasticsearch-service.yaml:

# (1)
kind: Service
apiVersion: v1
metadata:
  name: elasticsearch
spec:
  # (2)
  selector:
    app: elasticsearch
  # (3)
  type: ClusterIP
  clusterIP: None  
  # (4)
  ports:
    - name: cluster
      port: 9300
      targetPort: 9300 
  1. Die Konfiguration beschreibt einen Service, der den Namen "elasticsearch" erhält.
  2. Der Selector gibt an, dass dieser Service alle Pods zur Verfügung stellt, welche ein Label mit dem Key app und dem Value elasticsearch haben.
  3. Der Service ist nur innerhalb des Kubernetes-Clusters verfügbar, er hat aber keine eigene IP-Adresse, sondern stellt als headless-service über den cluster-internen Nameserver die IP-Adressen der Pods zu Verfügung.
  4. Die Elasticsearch-Knoten verwenden Port 9300 um miteinander zu kommunizieren, dieser Port wird auch im Service spezifiziert.

Durch das folgende Kommando installieren wir den Service in Kubernetes. Der anschließende Aufruf zum Anzeigen der vorhandenen Services zeigt den neu angelegten Service sowie den im Cluster vorhandenen Kubernetes API-Server (der in diesem Beispiel nicht verwendet wird).

$ kubectl apply -f elasticsearch-service.yaml
service "elasticsearch" created

$ kubectl get svc
NAME            TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
elasticsearch   ClusterIP   None         <none>        9300/TCP   24s
kubernetes      ClusterIP   10.0.0.1     <none>        443/TCP    1h

Als nächstes installieren wir das Deployment der Elasticsearch-Pods. Die dafür notwendige Konfiguration sieht wie folgt aus (elasticsearch-deployment.yaml):

# (1)
kind: Deployment
apiVersion: apps/v1beta2
metadata:
  name: elasticsearch
spec:
  # (2)
  replicas: 2  
  # (3)
  selector:
    matchLabels:
      app: elasticsearch
  template:
    metadata:
      labels:
        app: elasticsearch
    # (4)
    spec:
      # (5)
      initContainers:
        - name: sysctl-vm
          image: busybox
          command: ["sysctl", "-w", "vm.max_map_count=262144"]
          securityContext:
            privileged: true
      # (6)
      containers:
        - name: elasticsearch
          image: docker.elastic.co/elasticsearch/elasticsearch:5.6.3
          # (7)
          env:
            - name: cluster.name
              value: "es-cluster"
            - name: ES_JAVA_OPTS
              value: "-Xms300m -Xmx300m"
            - name: discovery.zen.ping.unicast.hosts
              value: "elasticsearch"
            - name: xpack.security.enabled
              value: "false"
          # (8)
          ports:
            - name: cluster
              containerPort: 9300
              protocol: TCP
          # (9)
          resources:
            limits:
              memory: 350Mi
              cpu: 0.4
            requests:
              memory: 250Mi
              cpu: 0.2
          # (10)
          volumeMounts:
            - name: storage
              mountPath: /usr/share/elasticsearch/data
      # (11)
      volumes:
        - name: storage
          emptyDir: {}
  1. Es wird ein Deployment mit dem Namen elasticsearch angelegt.
  2. Das Deployment soll zwei Instanzen der spezifizierten Pods enthalten.
  3. Zu diesem Deployment gehören alle Pods, welche das Label app mit dem Wert elasticsearch haben; unter template wird dieses Label für die Pods spezifiziert. In unserem Beispiel sind die Einträge identisch, es kann aber auch komplexere Fälle geben, in denen der Selector zusätzliche Pods enthalten kann, dies wird hier nicht weiter berücksichtigt.
  4. Es folgt die Spezifikation des Pods.
  5. Dieser Pod enthält einen Docker-Container, der vor dem eigentlichen Container ausgeführt wird. Dies ist notwendig, da für Elasticsearch eine Systemvariable (vm.max_map_count) gesetzt werden muss. Genauere Info findet sich in der Elastic-Dokumentation [5]
  6. Der Pod enthält einen Docker-Container mit dem Namen elasticsearch, der Parameter image gibt an, von wo dieses Image zu laden ist, in diesem Fall aus der Docker-Registry von elastic.
  7. Beim Start des Containers werden die angegebenen Environment-Variablen gesetzt, hier wird in discovery.zen.ping.unicast.hosts als Wert der Name des Services übergeben, der im vorherigen Schritt im Kubernetes-Cluster angelegt wurde.
  8. Der Container stellt Port 9300 zur Verfügung, dies ist der Standardport von Elasticsearch für die Elasticsearch-Cluster-Kommunikation.
  9. Der Container wird auf einen Speicherverbrauch von 350 MB und einen CPU-Verbrauch von 0,4 CPUs beschränkt. Sollte er mehr Speicher verwenden, wird er von Kubernetes beendet und neu gestartet. Bei Verbrauch von mehr CPU wird er gedrosselt. Die Werte bei den requests verwendet Kubernetes, um zu prüfen, auf welchem Knoten noch Ressourcen frei sind, um den Pod dort laufen zu lassen.
  10. Unter dem angegebenen Pfad wird ein Volume zum Speichern der Daten in den Container eingebunden.
  11. Das in (10) verwendete Volume wird hier definiert.

Das Deployment rollen wir mit dem folgenden Befehl im Cluster aus:

$ kubectl apply -f elasticsearch-deployment.yaml
deployment "elasticsearch" created

Nach kurzer Zeit sind die beiden Pods des Deployment verfügbar, was mit dem entsprechenden kubectl-Befehl überprüft werden kann:

$ kubectl get deploy
NAME            DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
elasticsearch   2         2         2            2           15m

$ kubectl get pod -w
NAME                             READY     STATUS    RESTARTS   AGE
elasticsearch-67d494cc79-brsgt   1/1       Running   0          2m
elasticsearch-67d494cc79-kbm6r   1/1       Running   0          2m

An der Ausgabe sehen wir, dass die Pods einen Namen erhalten haben, der aus dem Namen des Deployments, der Kennung des ReplicaSets und einer Pod-Kennung zusammengesetzt ist. Auch im Dashboard können wir den Status des Deployment überprüfen (s. Abb.3).

Es kann übrigens durchaus ein paar Minuten dauern, bis die Pods im Status READY sind, da ja zuerst das Docker-Image in den Kubernetes-Cluster geladen werden muss. Bei Neustarts oder Erhöhen der Anzahl von Pods ist das natürlich nicht mehr notwendig.

REST-Service

Für den "documentservice" installieren wir als erstes ein Secret (documentservice-secret.yaml), welches die Anwendungskonfiguration enthält:

# (1)
kind: Secret
apiVersion: v1
metadata:
  name: config-documentservice
# (2)
stringData:
  application.yaml: |
    spring:
      data:
        elasticsearch:
          cluster-nodes: "elasticsearch:9300"
  1. Das Secret wird in Kubernetes mit dem Namen config-documentservice angelegt.
  2. Es enthält einen Eintrag mit dem key application.yaml. Der value dieses Eintrages ist der Inhalt der Spring-Boot-Konfiguration für die Anwendung.

Die Installation führen wir wie schon gewohnt mit kubectl apply -f durch:

$ kubectl apply -f documentservice-secret.yaml
secret "config-documentservice" created

Bei der Beschreibung der Deployment-Konfiguration (documentservice-deployment.yaml) erläutere ich nur die Elemente genauer, welche bisher noch nicht beschrieben wurden:

kind: Deployment
apiVersion: apps/v1beta2
metadata:
  name: documentservice
spec:
  replicas: 1
  selector:
    matchLabels:
      app: documentservice
  template:
    metadata:
      labels:
        app: documentservice
    spec:
      containers:
        - name: documentservice
          image: codecentric/informatik-aktuell-usingk8s-documentservice:1.0
          env:
            - name: JAVA_OPTS
              value: "-Xmx128m"
            - name: SPRING_CONFIG_LOCATION
              value: "file:/config/"
          ports:
            - name: api
              containerPort: 8080
              protocol: TCP
          resources:
            limits:
              memory: 300Mi
              cpu: 0.2
            requests:
              memory: 100Mi
              cpu: 0.1
          volumeMounts:
            - name: config-volume
              mountPath: /config
      # (1)
      volumes:
        - name: config-volume
          secret:
            secretName: config-documentservice
  1. Das Volume mit dem Namen config-volume, welches für diesen Pod spezifiziert wird, wird aus dem Secret config-documentservice erzeugt, das wir gerade installiert haben und wird im Container unter dem Pfad /config zur Verfügung gestellt. Im konkreten Fall führt das dazu, dass es im Container eine Datei /config/application.yaml gibt, welche den im Secret spezifizierten Inhalt hat. Und über den Environment-Eintrag SPRING_CONFIG_LOCATION instruieren wir die Spring-Boot Anwendung, diese Konfiguration zu verwenden.

Die Installation erledigen wir mit:

$ kubectl apply -f documentservice-deployment.yaml
deployment "documentservice" created

Der Service für den documentservice konfigurieren wir mit der Datei documentservice-service.yaml:

kind: Service
apiVersion: v1
metadata:
  name: documentservice
spec:
  selector:
    app: documentservice
  # (1)
  type: ClusterIP
  ports:
    - name: api
      port: 8080
      targetPort: 8080
  1. Im Gegensatz zum Service für elasticsearch wird hier durch Kubernetes eine IP-Adresse für den Service vergeben, da wir die ClusterIP "nicht" setzen.
$ kubectl apply -f documentservice-service.yaml
service "documentservice" created

$ kubectl get svc
NAME              TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)    AGE
documentservice   ClusterIP   10.0.0.104   <none>        8080/TCP   19s
elasticsearch     ClusterIP   None         <none>        <none>     2h
kubernetes        ClusterIP   10.0.0.1     <none>        443/TCP    13h

Wählen wir im Dashboard den Service aus, sehen wir, dass der Service die IP-Adresse 10.0.0.104 hat und auf den Pod mit der IP 172.17.0.8 routet (die IP-Adressen sind je nach Installation unterschiedlich).

Webanwendung

Für die Webanwendung documentui gibt es ebenfalls drei Konfigurationsdateien. Die erste ist documentui-configmap.yaml, diese enthält die konfigurierbaren Werte für die Anwendung – dies ist nur ein Wert, der angibt, unter welchem Namen und Port der REST-Service erreichbar ist.

kind: ConfigMap
apiVersion: v1
metadata:
  name: config-documentui
data:
  apihost: documentservice:8080

Die zweite Datei für das Deployment der Webanwendung ist documentui-deployment.yaml:

kind: Deployment
apiVersion: apps/v1beta2
metadata:
  name: documentui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: documentui
  template:
    metadata:
      labels:
        app: documentui
    spec:
      containers:
        - name: documentui
          image: codecentric/informatik-aktuell-usingk8s-documentui:1.0
          # (1)
          env:
            - name: API_HOST
              valueFrom:
                configMapKeyRef:
                  name: config-documentui
                  key: apihost
          ports:
            - name: server
              containerPort: 3000
              protocol: TCP
          resources:
            limits:
              memory: 100Mi
              cpu: 0.2
            requests:
              memory: 50Mi
              cpu: 0.05
  1. Neu in dieser Konfiguration ist, dass für die Environmentvariable API_HOST für den Container die zuvor installierte ConfigMap verwendet wird.

Den Service für die Webanwendung konfigurieren wir mit Hilfe der Datei documentui-service.yaml:

kind: Service
apiVersion: v1
metadata:
  name: documentui
spec:
  selector:
    app: documentui
  # (1)
  type: LoadBalancer
  # (2)
  ports:
    - name: api
      port: 80
      targetPort: 3000
  1. Da die Webanwendung von außerhalb des Kubernetes-Clusters erreichbar sein soll, wird der Service als LoadBalancer definiert. Er hat damit eine cluster-interne IP, aber auch eine externe, welche vom Cloud-Provider zur Verfügung gestellt wird.
  2. Der Service stellt nach außen den Port 80 zur Verfügung, routet aber den Verkehr auf Port 3000 der Pods.

Alle drei Dateien installieren wir wie gehabt:

$ kubectl apply -f documentui-configmap.yaml
configmap "config-documentui" created

$ kubectl apply -f documentui-deployment.yaml
deployment "documentui" created

$ kubectl apply -f documentui-service.yaml
service "documentui" created

$ kubectl get po
NAME                               READY     STATUS    RESTARTS   AGE
documentservice-665757d4c8-rjwwc   1/1       Running   0          15m
documentui-6bc456ff4d-mtvf5        1/1       Running   0          1m
elasticsearch-67d494cc79-brsgt     1/1       Running   0          27m
elasticsearch-67d494cc79-kbm6r     1/1       Running   0          27m

$ kubectl get svc
NAME              TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
documentservice   ClusterIP      10.0.0.104   <none>        8080/TCP       8m
documentui        LoadBalancer   10.0.0.31    <pending>     80:32436/TCP   1m
elasticsearch     ClusterIP      None         <none>        9300/TCP       33m
kubernetes        ClusterIP      10.0.0.1     <none>        443/TCP        47m

Bei der Anzeige der Services sehen wir, dass durch den Cloud-Provider noch keine externe IP zugewiesen wurde (PENDING). Bei minikube ist das auch nicht möglich, hier muss auf das Kommando minikube service zurückgegriffen werden:

$ minikube service documentui --url
http://192.168.99.100:32436

Unter der angezeigten URL ist dann die Webanwendung verfügbar. Bei einem "echten" Cluster wäre die Anwendung unter http://EXTERNAL-IP:80 erreichbar.

Unser Kubernetes-Cluster sieht zur Zeit so aus (Abb.5 enthält nur die Services und Pods, die Deployments, welche die Pods verwalten sind nicht dargestellt, da sie für den Netzwerkverkehr keine Rolle spielen).

Ein Aufruf von außen mit dem Browser landet beim documentui-Service, dieser routet weiter an den documentui-Pod. Der Pod greift auf den documentservice-Service zu, der an den documentservice-Pod routet. Die gestrichelten Linien zu elasticsearch zeigen, dass der documentservice-Pod vom elasticsearch-Service die IP-Adressen der elasticsearch-Pods erhält und sich zu einem der elasticsearch-Pods verbindet.

Laden der Beispieldaten

Durch Aufruf der URL http://192.168.99.100:32436/api/documents/load – die IP-Adresse und der Port sind entsprechend anzupassen – laden wir die in der Anwendung vorhandenen Beispieldokumente in Elasticsearch. Dies können wir entweder im Browser oder mit curl erledigen:

$ curl http://192.168.99.100:32436/api/documents/load
5 Datensätze geladen.

Wenn wir die Anwendung im Browser aufrufen, sehen wir, dass in der Kopfzeile die Namen der Pods ausgegeben werden, welche die Anfrage verarbeitet haben (processed on documentservice-abc via documentui-xyz). An dieser Stelle können wir später sehen, wie Anfragen geroutet werden, wenn die Anzahl der Pods erhöht wird (s. Abb.6).

Skalieren der Deployments

Nach der Basisinstallation laufen insgesamt 4 Pods im Cluster:

$ kubectl get po -o wide
NAME                               READY     STATUS    RESTARTS   AGE       IP           NODE
documentservice-665757d4c8-rjwwc   1/1       Running   0          42m       172.17.0.8   minikube
documentui-6bc456ff4d-mtvf5        1/1       Running   0          27m       172.17.0.9   minikube
elasticsearch-67d494cc79-brsgt     1/1       Running   0          53m       172.17.0.6   minikube
elasticsearch-67d494cc79-kbm6r     1/1       Running   0          53m       172.17.0.7   minikube

Um die Verfügbarkeit und Performance unserer Anwendung zu erhöhen, skalieren wir die Webanwendung auf 3 Instanzen und den REST-Service auf 2 Instanzen:

$ kubectl scale deployment documentui --replicas=3
deployment "documentui" scaled

$ kubectl scale deployment documentservice --replicas=2
deployment "documentservice" scaled

$ kubectl get pod -o wide
NAME                               READY     STATUS    RESTARTS   AGE       IP            NODE
documentservice-665757d4c8-rjwwc   1/1       Running   0          43m       172.17.0.8    minikube
documentservice-665757d4c8-zcpsd   1/1       Running   0          4s        172.17.0.12   minikube
documentui-6bc456ff4d-2hfc2        1/1       Running   0          10s       172.17.0.10   minikube
documentui-6bc456ff4d-mtvf5        1/1       Running   0          28m       172.17.0.9    minikube
documentui-6bc456ff4d-scszf        1/1       Running   0          10s       172.17.0.11   minikube
elasticsearch-67d494cc79-brsgt     1/1       Running   0          55m       172.17.0.6    minikube
elasticsearch-67d494cc79-kbm6r     1/1       Running   0          55m       172.17.0.7    minikube

$ kubectl get deploy
NAME              DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
documentservice   2         2         2            2           43m
documentui        3         3         3            3           29m
elasticsearch     2         2         2            2           55m

Dieses Skalieren der Anwendungen geschieht im laufenden Betrieb, die neuen Pods werden von den Services erkannt und dem Loadbalancing hinzugefügt. Unser Kubernetes-Cluster sieht jetzt wie in Abb.7 dargestellt.

Wenn wir jetzt mit der Anwendung arbeiten, sehen wir durch Auswahl von Einträgen aus der Liste oder auch durch Hinzufügen eines neuen Eintrages, dass die Anfragen über die Frontend-Pods und Backend-Pods verteilt werden. Die als LoadBalancer arbeitenden Services verteilen die Aufrufe übrigens in der Default-Einstellung zufällig an die Pods, so dass durchaus auch mehrere Aufrufe hintereinander vom gleichen Pod bearbeitet werden.

Die Skalierung der Pods in einem Deployment können wir auch so definieren, dass die Anzahl der Pods dem aktuellen CPU-Verbrauch angepasst wird, das heißt, wenn die Container unter Last stehen, können automatisch neue hinzugefügt werden, und wenn die Last wieder sinkt, werden Pods wieder gelöscht. Das im Rahmen dieses Artikels zu demonstrieren geht jedoch zu weit, hier verweise ich auf die ausführliche Dokumentation von Kubernetes.

Wir können das Dashboard nutzen, um uns den Zustand der einzelnen Komponenten anzeigen zu lassen oder auch um einen allgemeinen Überblick zu bekommen (s. Abb.8).

Fazit

Wir haben in diesem Artikel gesehen, wie verschiedene Anwendungen, die mit unterschiedlichen Technologien entwickelt wurden, in einem Kubernetes-Cluster deployed werden und wie sie miteinander kommunizieren. Wir haben die Anwendungen im Kubernetes-Cluster durch einen einfachen Befehl horizontal skaliert und gesehen, wie Kubernetes das Loadbalancing und Routing automatisch anpasst. Die Beispiele haben gezeigt, wie auch auf einem einzelnen Rechner ein lokaler Kubernetes-Cluster aufgesetzt werden kann, dies kann zum Beispiel für lokale Tests bei der Entwicklung verwendet werden.

Ich hoffe, mit diesem Artikel das Interesse an Kubernetes geweckt zu haben und möchte ausdrücklich auf die sehr umfangreiche Kubernetes-Dokumentation verweisen.

Autor

Peter-Josef Meisch

Peter-Josef Meisch entwickelt in Java, ist aber immer offen für neue Sprachen und Technologien. Zur Zeit arbeitet er bei codecentric als Entwickler und Consultant.
>> Weiterlesen
botMessage_toctoc_comments_9210