Über unsMediaKontaktImpressum
Dr. Thomas Fricke 16. Januar 2018

Kubernetes: Architektur und Einsatz – Eine Einführung mit Beispielen

Seit dem Erscheinen von Docker vor über vier Jahren haben Container DevOps revolutioniert. Kubernetes ist Googles Weg, die Cloud zu konsolidieren. Open Source, gedacht als Google Infrastructure For Everybody Else (GIFEE), hat die Platform as a Service (PaaS) die Cloud im Sturm erobert. Von eigener Hardware (on premises) bis hin zur Google Cloud Engine und allen anderen Cloud-Anbietern bieten sich alle Varianten von privaten und public clouds. Selbst die Konkurrenten folgen Kubernetes nun und liefern Beiträge, von denen es inzwischen über 3000 auf Github gibt, zum Ökosystem.

Die Anwendungsfälle für Kubernetes werden von den immer agiler werdenden Anforderungen an Development und Operations getrieben. Was muss ich beachten, wenn ich auf Container und Kubernetes umstellen möchte? Welches Wissen kann ich nutzen und was muss ich aufbauen. Was muss ich planen, womit kann ich direkt starten? Gibt es ein Sicherheitskonzept, wer sind die Major Player im Spiel?

Viele Fragen können in diesem Artikel nur angerissen werden, es soll eine Übersicht über den Stand der Entwicklungen in und um Kubernetes gegeben werden.

Container

Aus Sicht des Linux-Betriebssystems sind Container nichts anderes als Prozesse, eingeschränkt auf Namespaces. Wichtig an den Namespaces ist, dass ein Prozess in einem Namespace eine völlig andere Umgebung sieht als außerhalb, andere Netzwerkrouten, Control Group Limits, DNS-Einstellungen, sogar eine andere Prozess-ID etc.

Damit können sich Prozesse, die einen Namespace teilen, gemeinsame Ressourcen teilen und sind trotzdem von anderen Prozessen und dem Hostsystem isoliert.

Container-Images

Zusätzlich hat Docker noch ein CoW (Copy on Write)-Filesystem, das Images in Schichten organisiert. Im Idealfall werden nur wenige große Basis-Images durch kleine Unterschiede in verschiedenen Containern modifiziert. Standard ist das Linux Aufs (Advanced multi layered unification filesystem) [1], wobei die Images  übereinander gelegt werden. Andere Lösungen wie Logical Volumes sind über Plugins möglich.

Docker

Obwohl alle diese Techniken seit langem verwendet werden – LXC ist unter der Initiative von IBM 2008 entstanden [2] – und Google intern alle Applikationen durch ein System mit dem Namen Borg managed [3], hat sich erst mit dem Erscheinen von Docker vor über vier Jahren die Containerisierung auf breiter Front durchgesetzt.

Wer sich im DevOps-Umfeld mit verschiedenen Paketformaten, Configuration-Management-Tools und Deployment-Strategien herumschlagen musste, weiß die Vorteile eines einheitlichen Standards zu schätzen.

Docker hat – und das ist Segen und Fluch zugleich – alle zum Bauen, Transportieren und Starten von Containern notwendigen Tools in einem Kommando integriert [4]. Auf der einen Seite wird so die Schwelle für den Einstieg in die Containerwelt gesenkt, auf der anderen Seite kann keine Rede von einer Trennung der Aufgaben im Sinne der Separation of Concerns sein. Das widerspricht der Unix-Philosophie, die für eine Aufgabe ein Tool vorsieht. Während das Starten von Prozessen eine Aufgabe mit Systemprivilegien ist, gibt es keinen Grund, warum Transport und Bauen von Images (push/pull und build) nicht im Userspace ohne Root-Rechte durchgeführt werden sollten.

OCI und andere Container Runtimes

Das hat zu nicht unerheblichen Querelen hinter den Kulissen geführt, die wiederum zu einer Modularisierung von Docker, einem Container-Standard der OCI (Open Container Initiative) [5] und weiteren OCI compliante Runtimes geführt haben [6].

Rkt (sprich "Rocket") von CoreOS ist die älteste "alternative" Container-Engine [7], Intel hat die Clear Container Runtime [8] veröffentlicht, die den Container-Prozess noch durch einen auf der Basis von Qemu-Kvm implementierten Prozessor Hypervisor isoliert [9]. Durch Kubernetes selbst wurde mit cri-o [10] eine Runtime entwickelt, die optimal mit dem Container Runtime Interface zusammenspielt, einem Standard zum Starten von Container durch Orchestrierungs-Umgebungen.

Es dürfte nicht zuletzt der Vielzahl von Alternativen zu verdanken sein, dass Docker nicht nur alle Standardformate unterstützt, sondern schließlich auch Kubernetes [11].

DevOps: Orchestrierung von Containern

Seit Container sich als Standard durchgesetzt haben, war abzusehen, dass die Orchestrierung von Containern mit der Zahl der Container an Bedeutung gewinnt. Orchestrierung bedeutet in diesem Sinne, dass die Container nicht nur gestartet und gestoppt werden, sondern auch ihre Zusammenarbeit organisiert werden muss. Das fängt bei Containern an, die sich auf einem Host Ressourcen (Files, Netzwerk, etc) teilen, geht über ein einheitliches IP und DNS, das Management des gesamten Lebenszyklus und die Verbindung zu peripheren Systemen wie Storage (lokal oder im Netz) und externen Loadbalancern, die mit den IP-Adressen von Containern verbunden werden müssen. Hinzu kommen Routineaufgaben, Monitoring, Alerting, Versionsverwaltung, die im Rechenzentrum sonst auf verschiedene Tools verteilt werden.

Kubernetes-Grundlagen

Kubernetes [12] ist ein Google GIFEE-(Google Infrastructure for Everybody Else)-Projekt, das genau diese Orchestrierung liefern soll. Es ist vollständig Open Source und hat neben Google zahlreiche Beiträge anderer Firmen im Open Source-Umfeld integriert, u. a. von CoreOS, Red Hat und SuSE. Im Gegensatz zu anderen Projekten übt Google hier eine strikte Governance aus, die sich nicht so sehr auf die Inhalte, aber auf die drei monatlichen Release-Zyklen und die Bugfixes auch in älteren Branches auswirkt. Auf diese Weise gibt es eine klare Roadmap, stabile Versionen und verlässliche Update-Pfade. Das sind Eigenschaften, die im Enterprise-Umfeld unverzichtbar sind.

Architektur von Kubernetes

Die zentralen Einheiten sind der Master, der den API-Server enthält, gegen den alle Client Requests laufen. Der Zustand des Clusters wird im verteilten etcd Key Value Store abgelegt. Alle anderen Einheiten sind zustandslos, selbst der API-Server könnte redundant und verteilt mehrfach laufen. Der Master sollte für Produktionsumgebungen aus 3, 5, 7 oder 9 Instanzen bestehen, um einerseits ausfallsicher zu sein und andererseits bei Ausfall eines Nodes durch ein Leader Election einen neuen führenden etcd zu ermitteln.

Die Kubelets laufen auf den Worker Nodes, hier werden die Container in Pods zusammengefasst. Pods sind Container, die sich das gleiche Schicksal teilen. Sie werden gemeinsam erzeugt, teilen sich ein Netz und werden auch gemeinsam gestoppt, wenn ein Container terminiert. Der cAdvisor misst den Verbrauch von CPU-Leistung und Memory auf den Nodes und teilt ihn dem Master mit. Der Scheduler verteilt darauf neue Pods entsprechend den zur Verfügung stehenden  Ressourcen. Alle diese Instanzen laufen in den aktuellen Kubernetes-Installationen selber in Containern. Das Bootstrapping wird durch den Linux systemd durchgeführt, danach kann ein generisch unveränderliches System, wie Container Linux von CoreOS oder Atomic von Red Hat installiert werden, das ausschließlich Software als Container laufen lässt. Damit entfallen auf den Nodes Paket- und Konfigurationsmanagement und alle damit verbundenen Risiken.

Sofern die Container ausbruchsicher sind und die Nodes automatisch installiert werden können, haben wir damit einen erheblichen Sicherheitsgewinn. Aufgrund der Komplexität in mandantenfähigen Umgebungen stellt sich die Frage aber spätestens neu, wenn verschiedene konkurrierende Gruppen denselben Kubernetes-Cluster verwenden. Das Thema der Kubernetes Security geht aber über einen einführenden Artikel weit hinaus. Das gleiche gilt für die Netzwerke, die alle Pods verbinden. 

Am schnellsten lässt sich ein Kubernetes-Cluster in der Google Kubernetes Engine GKE erzeugen, hier läuft ein Cluster in weniger als zwei Minuten, vorausgesetzt man hat einen Google-Account [13]. In anderen Clouds ist der Aufwand etwas höher, hier helfen Tools wie Kops und Terraform beim Installieren des ersten Kubernetes Clusters [14].

Pods, Deployments und Services

Die wichtigsten Einheiten von Kubernetes sind die Pods, die im einfachsten Fall aus einem Image bestehen

apiVersion: v1

kind: Pod

metadata:

  name: nginx

spec:

  containers:

  - name: nginx

    image: nginx:1.7.9

    ports:

    - containerPort: 80

Das Beispiel lässt sich mit dem Client in einer Zeile deployen.

Wenn die Datei nginx.yaml heruntergeladen wurde [15], können wir einen Pod mit kubectl create -f nginx.yaml erzeugen. Alternativ kann auch die Url direkt angegeben werden.

Deployments

Um die Skalierbarkeit und Ausfallsicherheit zu erzeugen, wird der Pod in ein Replicaset und das wieder in ein Deployment eingebettet [16].

apiVersion: apps/v1beta2 # for versions before 1.8.0 use apps/v1beta1

kind: Deployment

metadata:

  name: nginx-deployment

  labels:

    app: nginx

spec:

  replicas: 3

  selector:

    matchLabels:

      app: nginx

  template:

    metadata:

      labels:

        app: nginx

    spec:

      containers:

      - name: nginx

        image: nginx:1.7.9

        ports:

        - containerPort: 80

Nach dem Deployment sehen wir, dass drei Pods gestartet werden, und das Replicaset garantiert, dass immer drei Pods laufen. Terminiert man mit kubectl pods delete einen einzelnen Pod, wird dieser sofort ersetzt:

kubectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
nginx-deployment-569477d6d8-l7gch   1/1       Running   0          1d
nginx-deployment-569477d6d8-mw4gf   1/1       Running   0          1d
nginx-deployment-569477d6d8-qxgpq   1/1       Running   0          1d

Alle Pods gehören zu einem Replicaset:

NAME                          DESIRED   CURRENT   READY     AGE
nginx-deployment-569477d6d8   3         3         3         1d

und das Replicaset zum Deployment:

kubectl get deployment
NAME               DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3         3         3            3           1d

Ändert man jetzt die Versionsnummer des Images:

kubectl set image deployment/nginx-deployment nginx=nginx:1.10.4


führt Kubernetes einen Rolling Update durch. Automatisch wird ein neues Replicaset erzeugt:

kunbectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
nginx-deployment-5d779dd45c-8ljkh   1/1       Running   0          1d
nginx-deployment-5d779dd45c-99dm9   1/1       Running   0          1d
nginx-deployment-5d779dd45c-bcx44   1/1       Running   0          1d

Und das erste Replicaset schrittweise auf 0 Container herunterskaliert, während das neue auf 3 Container heraufskaliert wird:

kubectl get replicaset  
NAME                          DESIRED   CURRENT   READY     AGE
nginx-deployment-569477d6d8   0         0         0         1d
nginx-deployment-5d779dd45c   3         3         3         1d

Im Falle eines Rollbacks lässt sich der Schritt schnell umkehren.

Services

Ein Service verbindet die Pods mit der Außenwelt. Zum einen holt er die Pods auf dem targetPort des über den Selector gelabelten Nodes ab. Zum anderen erzeugt er einen zufälligen Port auf dem Node. Dieser dient als Endpunkt für den LoadBalancer. In einer typischen Cloud-Umgebung sorgt der Cloud-Provider dafür, dass diese Information bei Kubernetes abgeholt und der richtige Endpunkt eingetragen wird.

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: NodePort
  ports:
  - port: 8000 # the port that this service should serve on
    targetPort: 80
    protocol: TCP
  # just like the selector in the replication controller,
  # but this time it identifies the set of pods to load balance
  # traffic to.
  selector:
    app: nginx
  type: LoadBalancer

Cattle and Pets

Diese einfachen Beispiele zeigen, dass Kubernetes eine Umgebung ist, die darauf angelegt ist, das Deployment und Management von Applikationen vollständig zu übernehmen. Kubernetes ist weitestgehend standardisiert [17] und damit sollten sich zustandslose Applikationen überall problemlos installieren lassen. Spannend wird es, wenn Daten mit ins Spiel kommen. Diese Unterscheidung läuft unter Cattle and Pets – während das Vieh im Stall keinen Namen bekommt und jederzeit geschlachtet und ersetzt werden kann, werden Haustiere liebevoll gepflegt. Das sind im Falle einer Applikation die Daten, die nicht verloren gehen dürfen: Datenbanken und Dateien.

Datenbanken

Es ist möglich, die Replikation auch in Kubernetes vollautomatisch aufzusetzen, aber die Orchestrierung einer vollautomatischen Replikation ist für jede Datenbank sehr speziell und muss sorgfältig getestet werden. Deswegen ist sehr zu empfehlen, mit zustandslosen Systemen zu starten, um erst einmal das notwendige Wissen aufzubauen, bevor dann auch die Systeme mit Daten in Kubernetes aufgebaut werden.

Auf der sicheren Seite ist zu empfehlen, die Services der Cloud-Provider zu nutzen. Der dadurch erzeugte Vendor Lock-In sollte bei SQL-Datenbanken keine Rolle spielen. Auf eigener Hardware können die bestehenden Datenbanken erst einmal ausserhalb von Kubernetes weiterbetrieben werden. Beispiele wie Patroni [18] zeigen zum einen die Komplexität der Replikation, zum andern, dass es möglich ist und sich automatisieren lässt.

Build Pipelines

Bereits das einfache Deployment-Beispiel zeigt, dass Continuous Delivery in Kubernetes eingebaut ist. Im Zusammenspiel mit Jenkins und Git-Servern zeigt sich das ganze Potential. Voll automatisierte Deployments sind eine natürliche Erweiterung, es gibt zahllose Beispiele, wie Jenkins dazu verwendet wird, Deployments in Kubernetes zu triggern. Eine vollständige Integration findet sich in Red Hats OpenShift [19]. Eingebaut ist ein Service, der automatisch aus Source Code Images generiert, testet und bei Erfolg direkt als Service ausrollt.

Für Java finden sich im fabric8 Beispiele, wie direkt aus einer integrierten Umgebung deployt werden kann [20]. Maven-Plugins können für annotierte Java-Klassen Deployments und Services generieren, sogar der Debugger kann sich mit einem Java-Programm in einem in Kubernetes laufenden Container verbinden.

Kubernetes und Sicherheit

Bei einer Sicherheitsanalyse von Kubernetes müssen die Container, das Netzwerk und die Zugriffsrechte beachtet werden. Applikationen, die gemeinsam auf einem Node laufen, müssen sich durch Linux Namespaces hinreichend gut isolieren lassen. Container müssen aus zuverlässigen Registries kommen, im Zweifel müssen sie selbst gebaut und über eine eigene Registry verteilt werden. Idealerweise laufen die Prozesse im Container nicht als Root, der Port wird sowieso mehrfach bei der Weiterleitung geändert, so dass es keine Notwendigkeit gibt, privilegierte Ports im Container zu verwenden. Das alles kann durch PodSecurityPolicies erzwungen werden.

Das Netzwerk in Kubernetes kennt Implementierungen wie Calico oder Weave [21], die NetworkPolicies implementieren. Damit lassen sich Kubernetes Pods und Services isolieren, die NetworkPolicies wirken wie Firewallregeln basierend auf Kubernetes Namespaces und Labeln.

Fazit

Kubernetes eröffnet unendliche Möglichkeiten, aber niemand sollte übersehen, dass ein Kubernetes-Cluster schnell die Komplexität eines Rechenzentrums erreicht. Deswegen ist es unerlässlich, Schritt für Schritt vorzugehen und am Aufbau einer verteilten Applikation zu lernen, sei es auf eigener Hardware, sei es in der Cloud. Schnelle Anfangserfolge werden den Aufbau motivieren, aber die Komplexität in verteilten mandantenfähigen Clustern darf nicht unterschätzt werden.

Isolation von Usern, Container und Services mit Hilfe von RBAC (Role Based Access Control) ist möglich, braucht aber vor allem ein tragfähiges Konzept. Werden die Applikationen komplexer, lohnt es sich, auch vor der Implementierung Zeit in die Architektur zu investieren. Es ist in komplexen Umgebungen wichtiger, in Pattern zu denken, als gleich drauf los zu implementieren [22]. Nur so haben sich unserer Erfahrung nach wartbare und stabile Applikationen entwickeln lassen.

Autor

Dr. Thomas Fricke

Thomas Fricke ist der CTO von Endocode, Cloud Architekt. Seine Lieblingsthemen sind das Skalieren von Applikationen und verteilte Datenbanken.
>> Weiterlesen
botMessage_toctoc_comments_9210