Docker Orchestrierung mit Kubernetes

Docker bietet uns eine leichtgewichtige, containerbasierte Form der Virtualisierung, welche sich gut dafür eignet, Anwendungen zu paketieren und auszuliefern. Dabei wird das Verwalten der Images mit Hilfe von Dockerfiles und Registries schon gut von Docker direkt unterstützt und gestaltet sich sehr einfach. Etwas mehr Aufwand ist bei der Verwaltung der Container selber zu Laufzeit notwendig. Zwar bietet Docker hier natürlich entsprechende Möglichkeiten für das mitgeben von Parametern beim Containerstart, was so direkt aber nicht die Wiederverwendbarkeit der Laufzeitkonfiguration unterstützt und sehr viel Eigeninitiative bei Themen wie Networking etc. erfordert. Docker-Compose kann uns hier bereits einiges an Arbeit abnehmen, wobei auch hier noch mehr Unterstützung für die alltäglichen Aufgaben bei Entwicklung und Betrieb der Anwendungen wünschenswert wäre.

Es gibt eine Reihe von Plattformen die uns bei der sogenannten Orchestrierung von Dockercontainern unterstützen. Diese Technologien ermöglichen uns unter anderem die Container in einer geclusterten Umgebung auszuführen, wobei die Anzahl Instanzen der Container ganz einfach zur Laufzeit beliebig erhöht werden kann. Die Plattform übernimmt dabei das Hochfahren und Verteilen der Container in dem Cluster. Darüber hinaus werden viele weitere Features geboten, wie z.B. wiederverwendbare Konfiguration, Loadbalancing, Rolling Updates, Mechanismen für Austausch von Passwörten/Zertifikaten und Servicediscovery. Im folgendem wird nun Kubernetes als eine Plattform zur Docker Orchestrierung vorgestellt unter dem Hauptgesichtspunkt, wie uns diese Vorgehensweise bei dem Entwickeln und Betreiben von eigenen Anwendungen unterstützen kann.

Kubernetes

Kubernetes ist eine ursprünglich von Google entwickelte opensource Plattform für das Clustermanagement und Betreiben dockerbasierter Applikationen. Es zeichnet sich durch seine deklarativ gehaltene Art der Konfiguration aus und erfreut sich mittlerweile auch schon einer recht großen Verbreitung. Als grundlegende Technologie wird Kubernetes von einigen Cloud-Anbietern genutzt, wie z.B. Openshift und Google Containerengine.

Kubernetes ist im Grunde gemäß dem Master-Slave-Prinzip aufgebaut. Ein Cluster setzt sich also zusammen aus einem Master und einer Reihe von Nodes. Der Master bietet die API Schnittstelle an und ist für das Verwalten der Nodes und das Hochfahren und Aufteilen der Container auf diese zuständig (scheduling). Die Nodes benötigen jeweils einen Docker-Runtime und sind für das tatsächliche Starten der Dockercontainer zuständig. Auf diese Weise ist ein elastisches Clustering möglich, dass es ermöglicht, bei Bedarf neue Nodes hinzuzufügen, welche dann vom Master genutzt werden können um Containerinstanzen zuzuweisen.

API

Die Schnittstellen für die Kommunikation mit dem Kubernetes Cluster werden per REST bereitgestellt. Das Kommandozeilenprogramm kubectl kann hierfür als komfortabler Client verwendet werden. Es gibt in Kubernetes verschiedene Arten von Objekten, die angelegt und Konfiguriert werden können. Alle diese Objekte können in Form von YAML Files definiert und über die Schnittstellen eingespielt werden. Dadurch, dass alle Konfigurationsobjekte durch deskriptive YML Strukturen beschrieben werden können, ist es einfach diese Konfigurationen wiederzuverwenden und in einem Versionsverwaltungssystem wie Git zu unterzubringen.

Pods

Ein Pod ist das grundlegendste Objekt in Kubernetes und kapselt einen (oder mehrere) Docker Container. Als Einheit für das Scheduling innerhalb von Kubernetes werden enstprechend Pods verwendet und nicht Dockercontainer direkt. Man kann sich einen Pod als Wrapper um einen Container vorstellen. Zwar ist es möglich mehrere Container in einem Pod unterzubringen, allerdings wird nach der am meist verbreiteten Vorgehensweise immer ein Pod für einen Container genutzt. Es handelt sich bei einem Pod um eine nicht trennbare Einheit, kann also immer nur komplett auf einem Clusterknoten laufen. An Konfiguration enthält ein Pod einen Verweis auf das damit Verknüpfte Dockerimage und als weitere Parameter Hauptsächlich Angaben zu Volumes, Umgebungsvariablen und Ports. Also im Großen und Ganzen das, was ansonsten als Information beim Docker run Befehl mitgegeben wird.

Beispiel für einen Pod:

apiVersion: v1
kind: Pod
metadata:
  labels:
    name: demoapp
spec:
  containers:
  - image~: demoapp:1
    name: demoapp
    ports:
      - containerPort: 3306
        protocol: TCP

Dieses Konfigurationsobjekt würden wir jetzt im Falle einer eigenen Anwendung einfach im Git-Repository des Projektes mit einchecken. Über einen Befehl kann es eingespielt werden:

kubectl create –f mypod.yml

Metadata: Labels + Selectors und Annotations

Den Objekten wie Pods können in Kubernetes Metadaten angehängt werden, sogenannte Labels und Annotations. Dabei Handelt es sich um Key-Value-Paare. Objekte können mit Hilfe dieser Labels durch Selector-Queries gefunden werden. Dies ist wichtig, da im Cluster ja viele verschiedene oder auch mehrere Instanzen einer Podkonfiguration laufen können. Über diese Selektor-Queries ist es möglich bestimmte Elemente im Cluster nach bestimmten Kriterien zu finden. Zum Beispiel könnte dies für Discovery genutzt werden, ein Pod könnte ein bestimmtes Label haben um zu kennzeichnen, dass metrische Daten per REST bereitgestellt werden. Der Value von einer Annotation könnte dann die URL sein. Ein Werkzeug für das Sammeln der Daten könnte sich so jetzt alle Anwendungen im Cluster heraus suchen, die metrische Daten anbieten.

Services

Wenn nun mehrere Anwendungen miteinander kommunizieren sollen, z.B. unsere Anwendung mit einer MySQL Datenbank, so brauchen wir dafür eine Schnittstelle, über welche diese Applikation zentral angesprochen werden kann. Direkt mit dem Pod Verbinden geht nicht, da wir in einer geclusterten Umgebung sind und nicht wissen, wo der Pod läuft und außerdem soll es ja später mal möglich sein, mehrere Instanzen eines Pods zu betreiben. Diese zentralen Schnittstellen werden über sogenannte Services abgebildet. Ein Service besteht aus einem Namen und ein oder mehreren Ports unter welchem der Zugriff auf die Anwendung dahinter Clusterweit möglich ist. Der Service-Endpoint ist dabei auch in der Lage ein Loadbalancing durchzuführen, es könnten also mehrere Instanzen der Anwendung dahinter laufen. Die Pods die mit diesem Service verknüpft sind werden mit Hilfe einer Selector-Query festgelegt, benötigen also entsprechende Metadaten in der Podkonfiguration.

apiVersion: v1
  kind: Service
  metadata:
    name: demoapp
  spec:
    selector:
      name: demoapp
    ports:
    - port: 8080
      protocol: TCP

Der Service wird auf einer IP im gesamten Cluster bereitgestellt, die mit dem Namen des Service aufgelöst werden kann. Die Adresse des Service kann in anderen Pods aus Umgebungsvariablen, die automatisch von Kubernetes gesetzt werden, ausgelesen werden:

DEMOAPP_SERVICE_HOST=10.0.0.11

DEMOAPP_SERVICE_PORT=8080

Services sind zunächst nur innerhalb des Cluster erreichbar, können aber auch nach außen hin sichtbar gemacht werden. Dies wird über den Service Typ festgelegt. Auf einer Cloud-Plattform wie Openshift besteht hier die einfache Möglichkeit über Type:LoadBalancer eine IP über einen externen LoadBalancer der Plattform zu bekommen. Services können auch genutzt werden, um Schnittstellen zu externe Anwendungen für Applikationen innerhalb des Clusters anzubieten.

Dies bedeutet, wenn wir eine eigene Anwendung betreiben, welche auf eine Mysql Datenbank angewiesen ist, müssten wir zwei Services bereitstellen. Die Datenbank mit einem normalen Service Clusterintern, die eigentliche Anwendung mit einem Service der eine Adresse extern bereitstellt. Die Anwendung kann jetzt über die Umgebungsvariablen an die Adresse für die Datenbank gelangen.

ReplicationControllers / ReplicaSets

ReplicationControllers erlauben es auf einfach Art und Weise mehrere Instanzen einer Podkonfiguration zu betreiben. Der ReplicationController stellt dabei sicher, dass immer eine gewünschte Anzahl der Pods im Cluster läuft. Erzeugt werden die Pods vom Controller dabei aus einem Pod-Template. Es kann eine gewünschte Anzahl Replicas direkt in der Konfiguration angegeben werden, diese kann dann später aber auch zur Laufzeit geändert werden.

apiVersion: v1
  kind: ReplicationController
  metadata:
    name: mysql-1
  spec:
    replicas: 1
    selector:
      name: mysql
    template:
      apiVersion: v1
      kind: Pod
      metadata:
        labels:
          name: mysql
      spec:
        containers:
        - image~: mysql
          name: mysql
          ports:
            - containerPort: 3306
              protocol: TCP

Wenn es nun einen Service gibt, der per Selector die Pods einsammelt, so sind alle Replicas die der Controller hochfährt mit diesem Service verknüpft.

ReplicaSets sind eine neuere Variante der ReplicationController in Kubernetes, die im Großen und Ganzen die gleichen Möglichkeiten bieten, aber zusätzlich neuere Selector-Features nutzen können.

Deployments

Die Funktionalität eines ReplicationControllers/ReplicaSets ist für das einfache Betreiben und Ausrollen einer Anwendung noch ein wenig zu low-level. Für diese Aufgabe gibt es daher spezielle Objekte, die sogenannten Deployments. Von der grundlegenden Konfiguration her sieht das Objekt so aus wie ein ReplicationController, aber ermöglicht uns darüber hinaus weitere Funktionalität.

Wenn wir eigene Anwendungen betreiben brauchen wir eine Möglichkeit, einfach eine neue Version auszurollen. Dies bedeutet im Dockerumfeld in der Regel, dass wir eine neue Version des Images erstellt haben. Nun müssen die alten Container gestoppt und die neuen gestartet werden. Ein Deployment ermöglicht genau dies über einen einfachen Befehl:

kubectl set image deployment/demoapp-deployment demoapp=demoapp:1.1

Nicht nur das Umstellen der Imageversion, auch andere Änderungen an der Deploymentconfig (außer Replica Scaling) bewirken einen Rollout. Dabei wird eine Änderungshistorie geführt, sodass es immer möglich ist, im Problemfall auf eine frühere Version des Deployments zurückzurollen.

Für das Ausrollen werden mehrere Verfahren angeboten, hier ist vor allem die Strategie RollingUpdate interessant, die uns ermöglicht ein Update ohne Ausfallzeit durchzuführen. Dabei würden zunächst die Pods mit dem neuen Image gestartet und erst anschließend wenn diese erfolgreich hochgefahren wurden, werden die alten Pods angehalten. Dabei bleibt immer die gewünschte Mindestanzahl Replicas verfügbar.

Ob ein Pod erfolgreich gestartet wurde wird zunächst davon abhängig gemacht, ob der Container ohne Fehler gestartet wurde und noch läuft. Interessant ist hier aber natürlich auch, ob die Anwendung schon läuft, denn erst dann sollen ja die alten Instanzen gestoppt werden. Dies ist über Angabe einer entsprechenden livenessProbe möglich. Hierfür kann entweder ein Command oder eine URL angegeben werden, mit welchem die Verfügbarkeit der Anwendung getestet werden kann.

Für unsere Anwendung bedeutet das nun, dass wir für diese ein Deployment definieren und immer wenn wir eine neue Version gebaut haben und das Dockerimage in die Registry gepushed ist, ein Rollout mit dem neuen Imagetag auf dem Deployment antriggern.

Persistent Volumes

Daten werden wie in Docker gewohnt in Volumes abgelegt, welche in den Pod Konfigurationen den Containern zugeordnet werden können. Genutzt werden können hier viele verschiedene Arten von Volumes, angefangen von einem Pfad auf dem Node, einem Netzwerklaufwerk über verschiedene Cloudspeicherlösungen bis hin speziellen Lösungen wie Git-Repositories.

Zusätzlich bietet Kubernetes ein eigenes Konzept bestehend aus speziellen Objekten, den PersistenVolumes und PersistenVolumeClaims. PersistentVolumes sind dabei Objekte die vom Administrator eingetragen werden und einen Zugriff auf einen Speicher (auch hier verschiedene Typen möglich) mit einer bestimmten Größe definieren.

apiVersion: v1
  kind: PersistentVolume
  metadata:
    name: mysqlvolume
  spec:
    capacity:
      storage: 2Gib
    accessModes:
      - ReadWriteOnce
    hostPath:
      path: /data/volumes/mysql
    persistentVolumeReclaimPolicy: Retain

Auf der anderen Seite legen die Nutzer dieses Speichers ein PersistentVolumeClaim Objekt an, welches dann über Selektoren mit einem PersistentVolume Verknüpft wird. Diese Claims können dann wie normale Volumes in den Pods eingebunden werden.

 apiVersion: v1
  kind: PersistentVolumeClaim
  metadata:
    name: mysql
  spec:
    accessModes:
      - ReadWriteOnce
    resources:
      requests:
        storage: 2Gib
    volumeName: mysqlvolume
kind: Pod
metadata:
  labels:
    name: mysql
spec:
  containers:
  - image~: mysql
    name: mysql
    ports:
      - containerPort: 3306
        protocol: TCP
    volumeMounts:
      - mountPath: /var/lib/mysql
        name: storage
  restartPolicy: Always
  volumes:
  - name: storage
    persistentVolumeClaim:
      claimName: mysqlvolume

Die Idee hier ist also, dass das tatsächliche bereitstellen des Speichers eine Aufgabe der Administration ist und die Anwender sich über die Claims diese bereitgestellten Volumes anfordern, ohne über die technischen Hintergründe des dahinterliegenden Speichermediums informiert sein zu müssen.

Secrets

Falls mehrere Anwendungen miteinander kommunizieren sollen, müssen in der Regel Passwörter, Tokens und Zertifikate ausgetauscht werden. Kubernetes bietet hierfür die sogenannten Secrets als spezielle Objekte an. In einem Secret werden die Daten in Key-Value Form gespeichert. Diese Secrets können nun entweder als Volume in die Pods eingebunden werden, oder Werte aus ihnen können dem Pods als Umgebungsvariable übergeben werden. Kubernetes selbst hängt in alle Pods Secrets für den Zugriff auf die Cluster API als Volume ein, sodass aus den Containern heraus Anfragen an die Kubernetes API möglich sind.

Definieren eines Secrets:

apiVersion: v1
kind: Secret
metadata:
  name: mysqlsecret
type: Opaque
data:
  password: ZGVtb3Bhc3N3b3Jk
  username: ZGVtb3VzZXI=

Einbinden als Umgebungsvariable:

containers:
          - name: demoapp
            image~: "demoapp:1"
            ports:
              - containerPort: 8080
                protocol: "TCP"
            env:
              - name: DB_SCHEMA
                valueFrom:
                  secretKeyRef:
                    name: mysqlsecret
                    key: database

Entsprechend könnten Secrets eingesetzt werden um z.B. die Zugriffsdaten für die Datenbank bereitzustellen. So müssen sie nicht in der Anwendung hart kodiert sein, sie müssen noch nicht einmal bekannt sein. Die Anwendung bindet sich einfach das Secret ein und holt sich die Werte daraus für die Anmeldung an der Datenbank. Verschiedene Passwörter für Produktions- und Testdatenbank sind somit einfach zu handhaben.

ConfigMaps

Ähnlich wie die Secrets aber etwas allgemeiner, für nicht sensible Daten gehalten, sind die Configmaps. Diese bieten die Möglichkeit zentrale Konfiguration anzubieten, welche dann von den Pods konsumiert werden kann. Dadurch, dass die Konfiguration so in die Plattform ausgelagert wird und nicht direkt in den Container-Images verdrahtet ist, sind die Anwendungsimages besser portabel/wiederverwendbar.

Namespaces

Kubernetes bietet uns zu Zwecken der Isolation an, sogenannte Namespaces zu definieren. Die können dafür genutzt werden, um mehrere voneinander getrennte Umgebungen innerhalb des Clusters zu definieren. Objekte werden immer in einem Namespace angelegt wo sie zunächst Zugang zu den anderen Objekten dieses Namespaces haben (Zugriffe auf andere Namespaces sind auch möglich). Wenn eine Anwendung einen Service über seinen Namen anspricht, so wird der Service aus demselben Namespace angezogen. Man kann sich das auch bei sonstigen Selector-Queries oder Zuordnungen zunutze machen, z.B. PersistentVolumes, Secrets, Configmaps usw.

Wofür die Namespaces sich daher gut einsetzen lassen ist also das Bereitstellen mehrerer Umgebungen wie z.B. test, qualitätsicherung und produktion. Wenn die zu dem Projekt gehörenden Objekte alle entsprechend portabel geschrieben sind, kann also eine neue Umgebung aufgesetzt werden in dem ein neuer Namespace erstellt wird und alle Objekte in diesem angelegt werden. Wodurch auch das duplizieren sehr komplexer, aus mehreren Anwendungen bestehenden, Umgebungen mit einigen Befehlen umzusetzen ist.

Templates

Wenn es darum geht, eine Umgebung, die aus vielen verschiedenen Objekten besteht, in einem Schritt einzuspielen, können Templates von großer Hilfe sein. Ein Template wird definiert in einem YAML File, welches beliebig viele Kubernetes Objekte enthalten darf. Es ist möglich Parameter für das Template zu definieren und diese Parameter in der Konfiguration der Objekte zu verwenden. Das Template kann nun entweder direkt aus dem File heraus angewendet, oder im Cluster für die einfache Wiederverwendung eingespielt werden. Damit ist es dann möglich über anwenden des Template alle darin enthaltenen Objekte auf einen Schlag in einem Namespace einzuspielen (Unter eventueller Angabe der benötigten Parameter).

Weiteres

Es gibt darüber hinaus noch viele weitere Features in Kubernetes. Zugriff auf Ressourcen, Namespaces, oder Aktionen im Cluster kann beschränkt werden. Anwendungen können sich über ServiceAccounts am API Server anmelden so wir normale Benutzer und haben dann Aktionen gemäß der ihnen zugeteilten Rollen zur Verfügung.

Automatisches horizontales anpassen der Replicas anhand von metrischen Daten, wie CPU Nutzung, ist möglich

Cron Jobs können definiert werden, die um eine bestimmte Zeit einen Prozess anstoßen, der die für die Durchführung notwendigen Pods hochfährt, welche nach abarbeiten der Aufgabe wieder gestoppt werden.

Fazit

Kubernetes bietet uns viele Funktionen, die für den Betrieb containerbasierter Anwendungen hilfreich sind. Alle Objekte können zusammen in einem Template mit der Anwendung als YAML Datei mit in das Repository eingecheckt werden. Mit dem Template können mehrere gleichartige Umgebungen aufgesetzt werden. Neue Images können einfach ausgerollt werden mit Hilfe der Deployments. Viele Instanzen der Anwendung können parallel gestartet werden und ein elastisches Clustering wird ermöglicht. Es gibt außerdem Mechanismen für Service-Discovery und das Verwalten von zentraler Konfiguration und Secrets.

Für das einfache Ausprobieren von Kubernetes auf einem lokalen Rechner empfiehlt sich Minikube, welches einen 1Knoten-Cluster(Master+Node) in einer virtuellen Maschine wie VirtualBox startet.

Als Alternative zur direkten Nutzung von Kubernetes kann auch Openshift empfohlen werden. Dies ist eine opensource Cloudplattform welche auf Kubernetes aufbaut und dieses um einige Features erweitert. Zum Beispiel ist es hier einfach Möglich Services über Routen nach außen hin sichtbar zu machen, dies wird von dem enthaltenen externen Loadbalancer übernommen.

Zusammenfassung Vorgehensweise für Anwendungsentwicklung

Als Beispiel gehen wir von eine einfachen Anwendung bestehend aus einer Java-Webanwendung und einer Mysql Datenbank aus.

Wir benötigen ein Docker-Image für die Datenbank und eines welches als Basis für unsere Anwendung dient (eventuell mit ApplicationServer).
Desweiteren benötigen wir nun :

Einen ReplicationController für die Datenbank und einen Service der die Datenbank innerhalb des Clusters zur Verfügung stellt. Für die eigene Anwendung nutzen wir ein Deployment mit der RollingUpdate-Strategie. Diese Anwendung wird über einen Service nach draußen zur Verfügung gestellt. Auf die Datenbank greift die Anwendung über die entsprechenden Umgebungsvariablen für den Service zu. Zugangsdaten für die Datenbank werden als Secret bereitgestellt und in den Pod der Anwendung eingebunden. Als Volume wird ein PersistentVolumeClaim in dem Pod für die Datenbank eingebunden. Alle diese Objekte werden zusammen in ein Template verpackt und mit der Anwendung eingecheckt. Nun können mit Hilfe von Namespaces verschiedene Stages für Test und Produktion erzeugt werden. In diesen müssen jeweils die PersistentVolumes angelget werden, sowie eventuell die Secrets für die Datenbank (Sofern die unterschiedlich sein müssen.). Anschließend kann das Template genutzt werden um die Umgebung in den Namespace einzuspielen.

Für das Bereitstellen einer neuen Version in einer der Umgebung muss nun das Dockerimage der Anwendung gebaut und getaggt (und in eine Registry gepushed) werden. Danach wird über einen Aufruf der Kubernetes API ein erneutes Ausrollen des Deployments mit dem neuen Imagetag angestoßen.

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: