Der Cloud-Native-Stack - Teil 3

Steuermann, grüß mir die Wolken: Cloud-native Anwendungen mit Kubernetes

Dr. Joseph Adersberger, Mario-Leander Reimer

© iStock / gremlin

Kubernetes (griechisch „Steuermann“) ist ein Open-Source-Projekt aus der Feder von Google und im Prinzip ein Applikationsserver der Ära Cloud: für Microservices und alle anderen Anwendungen, die sich in Container zwängen lassen und auf einem Cluster laufen. Anwendungen sollen dabei so skalieren, resilient und effizient zu betreiben sein wie diejenigen von Google. Ein großes Versprechen, das wir im Rahmen dieses Artikels beleuchten.

Kubernetes (kurz: K8s) ist ein quelloffener Clusterorchestrierer, der maßgeblich von Google entwickelt wird und Mitte 2015 in der Version 1.0 erschienen ist. Das bedeutet, dass K8s für den Einsatz in Produktion freigegeben ist, was Google selbst und weitere Unternehmen wie die New York Times bereits tun. Rund um K8s hat sich mittlerweile die Cloud Native Computing Foundation (CNCF) unter dem Dach der Linux Foundation formiert. K8s ist die CNCF-Referenzimplementierung eines Clusterorchestrierers. Damit stehen nun hinter K8s neben Google auch weitere namhafte Unternehmen wie Mesosphere, Cisco, IBM und Intel.

Doch was ist K8s genau? K8s ist ein Clusterorchestrierer, ein Applikationsserver, der Anwendungen auf einem potenziell sehr großen Cluster ausführt – auf einer Cloud. K8s agiert dabei auf der Abstraktionsebene von Anwendungen und ihren angebotenen Services, sitzt also exakt an der Schnittstelle zwischen Devs und Ops. Die Anwendungen sind dabei oft Microservices. Es sind jedoch auch andere Anwendungen gern auf K8s gesehen, Hauptsache man kann sie in einem Docker- oder rkt-Container verpacken, wie es auch für klassische JEE-Anwendungen gelingt.

K8s betreibt Anwendungen automatisch. Er besitzt eine Steuerschnittstelle (REST-API, Kommandozeile und Web-UI), mit der die Automatismen angestoßen werden können und der aktuelle Status abgerufen werden kann. Was K8s automatisiert:

  • Container auf dem Cluster ausführen
  • Netzwerkverbindungen zwischen Containern aufbauen
  • Persistenten Speicher (Persistent Volumes) für zustandsbehaftete Container bereitstellen
  • Konfigurationsparameter, Schlüssel und Passwörter definieren, ändern und bereitstellen
  • Roll-out-Workflows wie Canary Roll-outs automatisieren
  • Performance und Verfügbarkeit von Serviceendpunkten überwachen und Container bei zu geringer Performance skalieren (Auto-Scaling) und im Fehlerfall reschedulen (Auto-Healing)
  • Services managen: Service Discovery, Naming und Load Balancing

DevOps Docker Camp 2017

Das neue DevOps Docker Camp – mit Erkan Yanar

Lernen Sie die Konzepte von Docker und die darauf aufbauende Infrastrukturen umfassend kennen. Bauen Sie Schritt für Schritt eine eigene Infrastruktur für und mit Docker auf!

Anwendungen deklarativ beschreiben

K8s macht all das auf Basis einer Anwendungsblaupause, die beim Deployment einer Anwendung mit überreicht wird. Diese Anwendungsblaupause beschreibt den Zielzustand einer Applikation im Cluster. Aufgabe von K8s ist es dann, das Cluster vom aktuellen Zustand in den Zielzustand zu überführen. Die Anwendungsblaupause macht dabei keine Annahmen über das Cluster, sondern stellt lediglich Ressourcenforderungen. Somit sind Anwendungen portierbar: Egal ob K8s nur einen Laptop, eine Private Cloud oder gar die große weite Public Cloud unter seinen Fittichen hat, solange genügend Ressourcen zur Verfügung stehen, läuft die Anwendung ohne Anpassung überall. Eine Anwendungsblaupause besteht bei K8s aus den folgenden Elementen, die über YAML- oder JSON-Dateien beschrieben werden (Abb. 1):

  • Pod: Gruppe an Containern, die auf demselben Knoten laufen und sich eine Netzwerkschnittstelle inklusive einer dedizierten IP, persistente Volumes und Umgebungsvariablen teilen. Ein Pod ist die atomare Scheduling-Einheit in K8s. Ein Pod kann über so genannte Labels markiert werden. Das sind frei definierbare Schlüssel-Wert-Paare.
  • Service: Endpunkt unter einem definierten DNS-Namen, der Aufrufe an Pods verteilt. Die für einen Service relevanten Pods werden über ihre Labels selektiert (z. B. role = apache, env != test, tier in (web, app))
  • ReplicaSet (bzw. veraltetes Konstrukt Replication Controller): Stellt sicher, dass eine spezifizierte Anzahl an Instanzen pro Pod ständig läuft. Ist für Reaktionen im Fehlerfall, Skalierung und Roll-outs zuständig.
  • Deployment: Klammer um einen gewünschten Zielzustand im Cluster in Form eines Pods mit dazugehörigem ReplicaSet. Ein Deployment bezieht sich nicht auf Services, da diese in der K8s-Philosophie einen von Deployments unabhängigen Lebenszyklus haben.
adersberger_kubernetes_1

Abb. 1: Elemente einer Anwendungsblaupause

Die Architektur von Kubernetes

K8s selbst ist eine verteilte Anwendung. Abbildung 2 zeigt die Architektur von K8s.

adersberger_kubernetes_2

Abb. 2: Architektur von Kubernetes

Der Masterknoten ist das Gehirn des Clusters, die normalen Nodes die Muskeln. Der Master orchestriert das Cluster, auf den Nodes laufen die Pods und Services. Auf dem Masterknoten läuft ein API-Server, über den die K8s-Automatismen per REST-API angestoßen werden können. Er bietet die zentrale Administrationsschnittstelle. Der Controllermanager steuert die eigentliche Orchestrierung. Er führt Deployments und ReplicaSets (bzw. Replication-Controller) aus. Der Scheduler ist dafür zuständig, Pods auf die passenden Knoten zu verteilen. Er macht das entsprechend des Ressourcenbedarfs der Pods und den noch freien Ressourcen auf den Nodes. etcd ist ein verteilter konsistenter Konfigurationsspeicher – das Gedächtnis von K8s, in dem der aktuelle Clusterzustand verwaltet wird.

Auf den Nodes laufen drei Komponenten: der kube-proxy, der sich um die Weiterleitung von Serviceaufrufen an die passenden Container kümmert; die Container-Engine, im Default ist es Docker, das Container ausführt; und das kubelet, ein Agent des Masters, der den Knoten verwaltet, steuert und mit dem Master kommuniziert. Neben K8s als Kernplattform ist mittlerweile ein breites Ökosystem an Werkzeugen, Erweiterungen und Frameworks rund um K8s entstanden:

  • Prometheus, Weave Scope und sysdig für das Monitoring und die Fehlerdiagnose
  • Helm als Paketmanager für wiederverwendbare Applikationsteile
  • Maven- und Gradle-Plug-ins sowie ein Java-API zur Fernsteuerung von K8s

Erste Schritte: Hands-on Kubernetes

Der Schritt hin zu ersten Erfahrungen mit K8s ist ein leichter. Es stehen mehrere vorgefertigte K8s-Installationen zur Verfügung, z. B. über die Google-Container-Engine, für diverse Public IaaS Clouds oder als Vagrant-Boxen. Unter dem URL steht ein Shellskript für die automatische Installation bereit. Listing 1 zeigt ein Skript, mit dem ein lokales K8s-Cluster per Vagrant erstellt wird. Das funktioniert nur unter Linux und Mac, eine Installation unter Windows wird aktuell noch nicht unterstützt.

#!/bin/sh
export KUBERNETES_PROVIDER=vagrant
export KUBE_ENABLE_CLUSTER_MONITORING=none
curl -sS https://get.k8s.io | bash

Sobald die Installation erfolgreich abgeschlossen ist, kann anschließend über das kubectl.sh-Kommandozeilenwerkzeug mit dem Cluster interagiert werden (Listing 2). In Zeile 1 und 2 werden sowohl Informationen über das Cluster als auch die aktuell laufenden Pods, Services und Deployments abgefragt. In Zeile 4 legen wir ein Deployment und einen Service an und in Zeile 6 skalieren wir das Deployment von zwei auf fünf Replicas.

kubectl.sh cluster-info
kubectl.sh get pods,services,deployments

kubectl.sh create -f nginx-deployment.yml
kubectl.sh get pods,services,deployments
kubectl.sh scale deployment nginx – replicas=5
kubectl.sh get pods

Listing 3 zeigt die hierfür verwendete Anwendungsblaupause: einen nginx-Server in zwei Instanzen. Zusätzlich zum eigentlichen Deployment ist ein Service definiert, um auf die nginx-Instanzen über eine externe IP zugreifen zu können und um die Last auf die beiden laufenden Pods zu verteilen. Über das Templateelement im Deployment wird der Pod definiert. Dabei können auch mehrere Container mit angegeben werden. Das ReplicaSet wird implizit über die Anzahl der Replicas definiert.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
        tier: web
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
    tier: web
spec:
  # external load-balanced IP if provider supports it
  type: LoadBalancer
  ports:
  - port: 80
  selector:
    app: nginx
    tier: web

Die Kubernetisierung von Zwitscher

Im nächsten Schritt wollen wir nun unseren Zwitscher-Showcase aus den vorherigen Artikeln dieser Serie mithilfe von K8s orchestrieren. Der erste Schritt ist die Containerisierung: Hier werden die einzelnen Microservices in Docker-Container gepackt und per Docker Registry verfügbar gemacht. Listing 4 zeigt beispielhaft das Dockerfile für einen Zwitscher-Microservice, Listing 5, wie man daraus einen Container erzeugt und in eine Docker Registry lädt. Abbildung 3 zeigt alle Container sowie die verwendeten Basis-Images von Zwitscher.

FROM qaware-oss-docker-registry.bintray.io/base/debian8-jre8
MAINTAINER QAware GmbH <qaware-oss@qaware.de>

RUN mkdir -p /opt/zwitscher-service

COPY build/libs/zwitscher-service-1.1.0.jar /opt/zwitscher-service/zwitscher-service.jar
COPY src/main/docker/zwitscher-service.* /opt/zwitscher-service/

RUN chmod 755 /opt/zwitscher-service/zwitscher-service.jar; chmod 755 /opt/zwitscher-service/zwitscher-service.sh
EXPOSE 8761
ENTRYPOINT exec /opt/zwitscher-service/zwitscher-service.sh
$ docker build -t zwitscher-service:1.1.0 .
$ docker tag <IMAGE_ID> qaware-oss-docker-registry.bintray.io/zwitscher/zwitscher-service:1.1.0
$ docker push qaware-oss-docker-registry.bintray.io/zwitscher/zwitscher-service:1.1.0
adersberger_kubernetes_3

Abb. 3: Dockerized Zwitscher

Bei der Containerisierung sollte ein Punkt dringend beachtet werden: Kenne dein Basisimage! So bringt z. B. das java:8-Docker-Basis-Image stolze 800 MB auf die Waage. Bei sechs Microservices-Containern ist das eine Menge Holz, der Download der Images durch K8s dauert später unnötig lange. Abhilfe schafft hier z. B. die dockerisierte und kubernetisierte Version von Alpine Linux oder ein eigenes maßgeschneidertes Image wie in Listing 5 zu sehen.

Nun steht die eigentliche Kubernetisierung unserer Anwendung an. Hierfür teilen wir unsere Container auf Pods auf. Alle Container eines Pods teilen dasselbe Schicksal: Sie werden gemeinsam gestartet und gemeinsam skaliert. Stirbt ein Container, sterben alle. Welche Container man zusammen in einen Pod packt, ist eine Architekturentscheidung: Welche Container haben einen gemeinsamen Lebenszyklus? Welche Container sollten möglichst nah beieinander laufen, um Overhead zu vermeiden? Ein bekanntes Pattern ist das Sidekick-Pattern, bei dem neben einem funktionalen Container (wie einem Microservice) noch weitere Container laufen, die dessen Funktionalität anreichern (z. B. ein Monitoring-Agent oder ein Logextraktor).

Abbildung 4 zeigt die Pods sowie die definierten Services und Abhängigkeiten von Zwitscher in der Übersicht. Listing 6 zeigt die Definition für den Service und das Deployment unserer Zwitscher-Applikation.

adersberger_kubernetes_4

Abb. 4: Kubernized Zwitscher

apiVersion: v1
kind: Service
metadata:
  name: zwitscher-service
  labels:
    zwitscher: service
spec:
  # use NodePort to be able to access the port on each node
  type: NodePort
  ports:
  - port: 8080
  selector:
    zwitscher: service
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: zwitscher-service
spec:
  replicas: 3
  minReadySeconds: 30
  template:
    metadata:
      labels:
        zwitscher: service
    spec:
      containers:
      - name: zwitscher-service
        image: "qaware-oss-docker-registry.bintray.io/zwitscher/zwitscher-service:1.1.0"
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            memory: "256Mi"
            cpu: "500m"
          limits:
            memory: "512Mi"
            cpu: "750m"
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /admin/health
            port: 8080
          initialDelaySeconds: 90
          timeoutSeconds: 30
        env:
        - name: EUREKA_HOST
          value: zwitscher-eureka
        - name: JAVA_OPTS
          value: -Xmx196m

Der Service kann später direkt über seinen Namen angesprochen werden. Beim Typ stehen ClusterIP, NodePort und LoadBalancer zur Auswahl. ClusterIP exponiert den Service nur innerhalb des Clusters. Im Fall von NodePort werden die Containerports eines Pods auf jeden K8s Node exportiert. Bei Verwendung von LoadBalancer wird eine externe, lastverteilte IP für diesen Service akquiriert, sofern das vom Cluster unterstützt wird. Das Deployment definiert die Eigenschaften der Pods mit ihren Containern. In unserem Beispiel setzen wir die initiale Anzahl an Replicas auf drei. Über die Template Spec definieren wir das Docker Image, die benötigten Ressourcen, Ports, Liveness Probes und die Umgebungsvariablen unserer Container. Die Liveness Probes nutzt K8s, um herauszufinden, ob ein Pod erfolgreich gestartet ist.

Alle YAML-Dateien für das kubernetisierte Zwitscher sind im Zwitscher-GitHub-Repository zu finden. Nun können wir die Deployments und Services in K8s per Kommandozeile erzeugen (Listing 7).

$ kubectl.sh create -f zwitscher-eureka/k8s-zwitscher-eureka.yml
$ kubectl.sh create -f zwitscher-config/k8s-zwitscher-config.yml
$ kubectl.sh create -f zwitscher-service/k8s-zwitscher-service.yml
...
$ kubectl.sh get deployments,pods
$ kubectl.sh scale deployment zwitscher-service --replicas=3
$ kubectl.sh get deployments,pods

K8s kennt derzeit keine Startabhängigkeiten und keine Startreihenfolge der einzelnen Deployments. Dennoch benötigt der Zwitscher-Microservice eine gewisse Spring-Cloud-Infrastruktur, um sauber zu starten, wie Eureka und den Configuration-Service. Eine Lösung für dieses Problem ist es, den Start der Services per Retry-Mechanismus resilienter zu gestalten (Listing 8). Spring versucht dann in definierten, immer länger werdenden Abständen den Configuration-Service zu erreichen. Erst wenn man eine maximale Anzahl an Versuchen erreicht, schlägt der Start fehl.

spring:
  cloud:
    config:
      enabled: true
      failFast: true
      retry:
        initialInterval: 1500
        maxInterval: 5000
        maxAttempts: 5
        multiplier: 1.5
      discovery:
        enabled: true
        serviceId: ZWITSCHER-CONFIG

Eine Alternative zu dieser Lösung ist es, nichts weiter zu tun: Sollte ein Service aufgrund von fehlenden Infrastrukturdiensten nicht starten, wird K8s nach einer gewissen Zeit den Dienst neu starten, da er nicht antwortet. Das passiert so lange, bis alle Dienste die benötigte Infrastruktur vorfinden und sauber starten. Das klingt zunächst merkwürdig, aber das System heilt sich quasi allein.

Für Zwitscher haben wir die gesamte Spring-Cloud-Infrastruktur eins zu eins auf K8s portiert. Alternativ dazu kann auch auf Teile der Spring-Cloud-Infrastruktur verzichtet und stattdessen die K8s-Infrastruktur genutzt werden. Der Netflix-Eureka-Server kann durch den K8s-DNS-Server ersetzt werden. Der Zuul-Edge-Server lässt sich außerdem durch Ingress von K8s ersetzen, etcd kann als Konfigurationsmechansimus genutzt werden. Es gibt ein Spring-Cloud-Projekt, das die K8s-Bausteine besser integrieren soll, um genau so etwas zu bewerkstelligen. Es befindet sich aktuell in einem sehr frühen Stadium und ist noch nicht für ernsthafte Projekte geeignet.

Fazit

Kubernetes ist ein Applikationsserver für die Ära Cloud. Dadurch, dass K8s unter dem Dach der Linux Foundation weiterentwickelt wird, ist es wahrscheinlich, dass sich damit ein Standard für Clusterorchestrierung etabliert. Die Aufmerksamkeit und die Entwicklungsgeschwindigkeit ist bei K8s schon jetzt sehr hoch. K8s ist produktionsreif, wenngleich auch manche Features wie Auto-Scaling nur in der Google Cloud verfügbar sind. Das wird sich aber schnell ändern.

K8s ist eine mächtige Technologie. So konnten wir auf einige spannende Features nicht eingehen, wie Health Checks, Netzwerk- und Storage-Virtualisierung, Daemon Sets für querschnittlich verteilte Container, Namespaces, um auf einem Cluster verschiedene Umgebungen (z. B. Test, Preprod und Prod) zu realisieren oder die zentrale Verwaltung von Schlüsseln. Bei Interesse sei hierfür auf die K8s-Dokumentation und das Buch von Kelsey Hightower zu K8s verwiesen [1], das demnächst erscheinen wird.

Geschrieben von
Dr. Joseph Adersberger
Dr. Joseph Adersberger
Dr. Joseph Adersberger ist technischer Geschäftsführer der QAware GmbH, einem IT-Projekthaus mit Schwerpunkt auf Cloud-nativen Anwendungen und Softwaresanierung.
Mario-Leander Reimer
Mario-Leander Reimer
Mario-Leander Reimer ist Cheftechnologe bei der QAware. Er ist Spezialist für den Entwurf und die Umsetzung von komplexen System- und Softwarearchitekturen auf Basis von Open-Source-Technologien.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.