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.

Docker – „Build, Ship, and Run“

Problemstellung

Ein großes Problem stellen beim Testen und Ausliefern von Software die leider häufig unterschiedlich konfigurierten Ablaufumgebungen dar. Die Entwicklerrechner, der Testserver und der Produktivserver werden sich höchst wahrscheinlich deutlich unterscheiden, was die installierte Software und weitere Konfiguration anbelangt. Dies führt immer wieder zu Problemen, welche nur auf einer dieser Umgebung auftreten und sich nicht einfach nachvollziehen lassen. Vor allem aber ist natürlich gefährlich, dass sich so ein einwandfreies Funktionieren in der Produktivumgebung nicht durch ein erfolgreiches absolvieren der Tests in einer Integrationsumgebung garantieren lässt.

Um dieses Problem zu lösen könnte man nun mit virtuellen Maschinen als Umgebung arbeiten, was allerdings den Nachteil mit sich bringt, dass diese relativ schwergewichtig sind. Diese Schwergewichtigkeit ergibt sich zum einen daraus, dass die Images ein komplettes Betriebssystem enthalten und daher entsprechend groß sind, zum anderen aber auch, das auf dem Host-Rechner entsprechend viel Ressourcen für das Bereitstellen der virtuellen Ablaufumgebungen benötigt wird. Container wie sie von Docker angeboten werden stellen hier eine leichtgewichtigere Alternative dar.

Docker

Docker ist eine offene Container-Plattform, welche es ermöglich Anwendungen isoliert in sogenannten Containern ausführen zu können. Die Applikationen werden dafür mit all ihren Abhängigkeiten und Konfigurationen in einem Image definiert, welches dann zwischen den verschiedenen Umgebungen ausgetauscht werden kann.

Umgesetzt ist Docker selbst, also die Docker-Engine, als eine Client-Server-Anwendung. Zum einen gibt es die serverseitige Laufzeitumgebung, die als Daemon-Prozess läuft und die eigentliche Arbeit verrichtet. Von dieser Komponente wird per REST eine API bereitgestellt, welche von dem clientseitigen CLI-Werkzeug angesprochen wird. Der Client kann entweder direkt auf dem Server laufen, oder sich remote verbinden.

Container teilen sich das Betriebssystem des Hostrechners und stellen nicht selber ein vollwertiges Betriebssystem bereit. Es werden lediglich die Prozesse isoliert ausgeführt. Dabei bringt jedoch jeder Container sein eigenen Dateisystem mit, was sich die Container also mit dem Host teilen ist somit hauptsächlich nur der Kernel. Unter der Haube verwendet Docker Linux-Technologien wie Namespaces und CGroups um die Prozesse in den Containern Isoliert ausführen zu können.

Installation

Derzeit läuft Docker nativ nur auf Linux, es gibt aber auch Installer für Windows und Mac-Os, welche dann allerdings auf eine Virtualisierung angewiesen sind. Auf Linux muss lediglich die docker-engine installiert werden, wo in der Regel schon entsprechende Pakete für die jeweiligen Distributionen bereitstehen.

Beispiel für Installation auf Debian:

apt-get update
apt-get install -y apt-transport-https ca-certificates
apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
cat <<EOF >> /etc/apt/sources.list.d/docker.list
deb https://apt.dockerproject.org/repo debian-jessie main
EOF

apt-get update
apt-get install -y docker-engine
service docker start

Images

Anwendungen werden definiert in Form von sogenannten Images. Ein Image beschreibt ein Dateisystem mit allen für die Anwendung relevanten Dateien und Einstellungen. Das Dateisystem setzt sich immer aus mehreren Schichten zusammen. Jede Änderung an einem Image wird in einer eigenen Schicht angelegt. Durch die Aufteilung in Layer ist es nun möglich, dass bei Änderungen an einem Image auch nur diese Änderungen ausgetauscht werden müssen.

Es wird in jedem Fall ein Basisimage benötigt, welches einer Linux Distribution entspricht wie z.B. Debian. Diesem Basisimage können nun eigene Dateien, Umgebungsvariable usw. in einer neuen Schicht hinzugefügt werden, wodurch dann ein neues Image entsteht. Images erhalten werden mit einem Namen getaggt über welchen sie dann verwendet werden können.

Auflisten von Images auf dem Docker-Host:

docker images

Entfernen von Images auf dem Docker-Host:

docker rmi image_name

Registries

Abgelegt und Verteilt werden Images von sogenannten Registries. Es gibt die öffentliche Standard-Registry DockerHub im Internet, in welcher eine Vielzahl verwendbarer Basisimages zu finden ist. Es besteht auch die Möglichkeit dort eigene Images hochzuladen. Falls Images nur unternehmensintern verfügbar sein sollen, besteht auch die Möglichkeit eine eigene Registry zu betreiben. Die Images werden über ihren Tagnamen von der Registry angefordert. Der Tagname kann die Adresse der Registry als Prefix enthalten, was dazu führt, dass Docker versucht das Image von der entsprechenden Adresse anzufordern. Wenn kein Prefix gegeben und das Image nicht lokal vorhanden ist, wird automatisch auf Dockerhub geschaut.

Docker Images können mit dem Pull-Befehl geladen und mit dem Push-Befehl abgelegt werden. Dies erinnert an Versionsverwaltungssysteme wie Git. Wenn ein Image benötigt wird um z.B. einen Container zu starten, so wird, falls das Image noch nicht geladen wurde, automatisch ein Pull durchgeführt.

Commands für Pull und Push:

Docker pull repo/image/version

Docker push repo/image/version

Containers

Container sind in Docker die lauffähigen Komponenten, die gestartet, gestoppt und auch wieder gelöscht werden können. Erzeugt werden Container immer von einem Image. Man kann sich daher Images auch als eine Art Templates für Container vorstellen. Da die Images nicht direkt beschreibbar sind (was auch gut ist!) wird für jeden Container nochmal ein drüber liegender Layer angelegt, auf welchen geschrieben werden kann. Dadurch können sich mehrere Container ein Image teilen, ohne sich dabei gegenseitig zu beeinflussen.

Ein Container wird mit dem Run-Befehl erzeugt und gestartet. Dieser benötigt als Information ein Image, welches im Bedarf noch gepulled wird. Weitere Startparameter können Netzwerkkonfiguration, Interaktiv- oder Hintergrund-Modus, Umgebungsvariablen, Volumes, Containername und auszuführender Prozess sein.

Beispiel startet das Image localhost:5000/mysql und gibt dem Container den Namen mysql_server:

docker run -d --name=mysql_server -t localhost:5000/mysql

Ein Docker Container stoppt, sobald der Prozess, welcher als Einstiegspunkt definiert ist, beendet wird. Ein Container kann manuell gestoppt werden über den Stop-Befehl

docker stop mysql_server

Falls nicht mehr benötigt kann der Container wieder entfernt werden:

docker rm mysql_server

Es besteht auch die Möglichkeit ein neues Image aus einem Container zu bauen. Dies enthält dann alle Informationen des Container-Images und zusätzlich alle Änderungen die im Container-Layer stattgefunden haben. Allerdings sollten Dockerfiles für das Erstellen von Images bevorzugt werden, da so die Änderungen besser dokumentiert sind.

Dockerfiles

Dockerfiles beschreiben die Änderungen die an einem Image durchgeführt werden sollen, um ein neues zu erstellen. Dabei gibt es nur eine Handvoll Anweisungen die in einem Dockerfile genutzt werden können:

· From: Gibt das Basisimage an, welches erweitert werden soll.

· ADD/COPY: Fügt eine Datei aus dem Dateisystem des Hosts oder von einer Netzwerkadresse hinzu

· ENV: Definiert eine Umgebungsvariable

· RUN: Führt Befehle aus, wobei jedes RUN zu einem neuen Layer führt, daher werden häufig viele Befehle gebündelt in einem RUN ausgeführt

· VOLUME: Definiert einen Pfad der für das Einbinden von Volumes genutzt werden kann

· EXPOSE: Gibt Ports an welche der Container nach außen zur Verfügung stellen soll

· CMD/ENTRYPOINT: Definiert die Aktion, die standardmäßig beim Starten eines Containers ausgeführt wird

Beispiel für ein Mysql Image:

FROM localhost:5000/base

ADD my.cnf /etc/mysql/my.cnf
ADD startmysql.sh /var/startmysql.sh

RUN apk add --update mysql && \
    mkdir -p /etc/mysql/conf.d && \
    mkdir /run/mysqld && \
    rm -rf /var/cache/apk/* && \
    chmod 644 /etc/mysql/my.cnf

VOLUME ["/var/data/mysql"]

EXPOSE 3306

CMD ["/var/startmysql.sh"]

Mit dem Build-Befehl kann mit Hilfe eines Dockerfiles ein neues Image gebaut werden. Dabei kann auch der Tagname, den das Image bekommen soll, definiert werden:

docker build –t mytagname /pathtodockerfile/

Volumes

Es ist nicht angebracht, Daten, wie z.B. die einer Datenbank, direkt in den Docker-Containern abzuspeichern. Diese Informationen würden nämlich verloren gehen, sobald der Container zerstört wird und neu aus dem Image heraus erzeugt wird (z.B. nach einem Update des Images).

Damit Daten erhalten bleiben, werden sie in sogenannten Volumes abgelegt. Diese Volumes werden dann in dem Container unter einem bestimmten Pfad eingehängt (z.B. Datenverzeichnis einer Datenbankinstallation). Das Zuweisen eines Volumes kann beim Erzeugen eines Container mit dem Run-Befehl erfolgen:

docker run -d --name=mysql_server -v /data/mysql:/var/data/mysql -t localhost:5000/mysql

Es gibt verschiedene Arten von Volumes. Bei einem Container der Volumes definiert, ohne dass im Run-Befehl explizit eines zugewiesen wird, bekommen einen temporären Speicher, der nicht nach Zerstören des Containers erhalten bleibt. Sollen die Daten persistent bleiben, so muss ein anderer Volume-Driver verwendet werden. Dies kann z.B. ein Verzeichnis auf dem Host sein, ein Netzwerk- oder Cloudspeicher.

Networking

Docker Container müssen in der Lage sein miteinander zu kommunizieren, außerdem müssen sie Verbindungen nach außen aufbauen und in einigen Fällen auch anbieten können. Es gibt daher in Docker eine Reihe von Möglichkeiten Netzwerke für die Container zu konfigurieren. Per Default gibt es in Docker ein Bridge und ein None Netzwerk, welche direkt bei der Installation der Docker Engine angelegt werden. Das Default-Bridge-Netzwerk wird standardmäßig verwendet. Es erlaubt es Container miteinander per Link zu Verknüpfen und Ports auf den Hostrechner zu Mappen, sodass die entsprechenden Services direkt über die Adresse des Hostrechners verfügbar sind.

Beispiel für Portmapping auf host:

docker run -d -p=3306:3306 --name=mysql_server -v /data/mysql:/var/data/mysql -t localhost:5000/mysql

Dies reicht für das Testen in der Regel erstmal aus. Es besteht aber auch die Möglichkeit ein eigenes Netzwerk zu definieren. Dies kann entweder ein Bridge-Netzwerk sein, welches genutzt werden kann, wenn alle Container auf demselben Hostrechner laufen, oder ein Overlay-Netzwerk, wenn die Container auf mehrere Rechner verteilt sind.

Netzwerke können mit Hilfe des docker network-Befehls verwaltet werden.

Eigene Registries

Es besteht die Möglichkeit eine eigene Registry für Images zu betreiben. Dafür muss eine entsprechende Registry-Software, welche Docker-Images in der standardisierten Form verwalten kann, installiert werden. Einfachste Möglichkeit das auszuprobieren ist es, eine Registry über das offizielle Image zu starten:

docker run -d -p 5000:5000 --name registry registry:2

Nun können Images in diese Registry hinzugefügt oder von da bezogen werden:

docker push localhost:5000/mysql
docker pull localhost:5000/mysql

Eine Registry kann auch als Cache für den zentralen Dockerhub definiert werden. In diesem Fall muss Docker auch noch entsprechend konfiguriert werden sodass nicht standardmäßig direkt bei Dockerhub angefragt wird, sondern bei der eigenen Registry.

Best-Practices

Images sollten immer mit Hilfe von Dockerfiles beschrieben werden, da somit immer alles Änderungen dokumentiert sind. Diese Dockerfiles können dann in einer Versionsverwaltung, z.B. ein Git-Repository, abgelegt werden.

Das wählen des richtigen Basisimages ist wichtig. Dabei sollte man darauf achten, dass alle benötigten Funktionen enthalten sind, aber auch, dass die Image-Größe nicht unnötig groß wird. Sollen die Images möglichst klein bleiben, so kann z.B. ein Alpine-Linux als Basis verwendet werden. Von Images kann immer weiter „abgeleitet“ werden, sodass man viel wiederverwenden kann, z.B. kann man ein eigenes Java-Basisimage erstellen, von dem alle Anwendungs-Images, die Java benötigen, abgeleitet sind.

Eine Anwendung sollte in einem eigenen Container Laufen. Bei zusammengesetzten Anwendungen, z.B. Webanwendung + Datenbank, bedeutet das, dass jeder Bestandteil in einem eigenem Container laufen sollte. Es soll so aufgebaut sein, dass die gleichen Images in allen Stages (Entwicklung, Test, Produktion) ohne Änderungen verwendet werden können. Alle zur Laufzeit der Anwendung entstehenden Daten und Konfiguration, die dauerhaft sein sollen, werden in Persistent-Volumes abgelegt.

Docker kann sowohl für das definieren von Infrastruktur, als auch für eigene Anwendungen verwendet werden. Es macht in beiden Fällen Sinn, da so die komplette Anwendungslandschaft gut Dokumentiert und durch die Images immutable definiert worden ist.

Docker-Compose

Bei Anwendungen die aus mehreren Teilen bestehen, z.B. Webanwendung und Datenbank, bietet es sich an mit dem zusätzlichen Werkzeug Docker Compose zu arbeiten. Damit ist es möglich die Container-Konfiguration, die benötigten Images, das Networking zwischen den Container usw. deklarativ in einer gemeinsamen Datei zu beschreiben. Diese kann dann z.B. mit dem Projekt in die Versionsverwaltung und wird dann von Docker-Compose genutzt um die Container mit einem einfachen Befehl zu erzeugen und zu starten.

Als Format für die Konfigurationsdatei wird YAML verwendet. Beispiel für ein Compose-File:

version: '2'
services:
  mysqldb:
    image: localhost:5000/mysql
    volumes:
      - "/data/mysql:/var/data/mysql"
    restart: always
    expose:
      - &amp;quot;3306&amp;quot;

  demoapp:
    depends_on:
      - mysqldb
    build: .
    image: localhost:5000/demo
    links:
      - mysqldb
    ports:
      - "8080:8080"
    restart: always
    environment:
      DB_CONNECTION_URL: mysqldb:3306
      DB_SCHEMA: demo
      DB_USER: demouser
      DB_PASSWORD: demopassword

Gestartet werden kann diese Anwendung jetzt einfach über den Up Befehl. Dieser kann als Parameter auch ein neues Bauen der Images anstoßen (Sofern im File ein Build-Pfad zu einem Dockerfile angegeben wurde). Es werden nur die Container die noch nicht Laufen, oder deren Image sich geändert hat gestartet.

docker-compose up -d --build

Beispiel Einsatz von Docker in Java-EE Projekt

Gehen wir von einer einfachen Anwendung aus, die auf einem Wildfly läuft und an eine MySql Datenbank angebunden ist. Die Anwendung und die Datenbank sollen in jeweils einem eigenen Container laufen. Es wird also einmal ein Image für die Datenbank definiert, welches als Einstiegspunkt die Datenbankanwendung startet. Als zweites benötigen wir ein Image mit dem Wildfly, welches als Einstiegspunkt den Applicationserver hochfährt. Der Wildfly in dem Image ist hier bereits so konfiguriert, dass er an die Datenbank, die in einem anderen Container läuft, angebunden ist. Dieses Wildfly-Image wird nun als Basisimage für das Image unserer eigentlichen Anwendung verwendet. Bei dem Projekt Handelt es sich um ein einfaches Webprojekt als WAR-File. Das Docker-Image für unsere Anwendung wird nun durch ein Dockerfile beschrieben, was nichts weiter tut, dem Wildfly-Image das War-File hinzuzufügen. Das Dockerfile wird einfach in dem Projekt neben dem Sourcecode miteingecheckt. Das Dockerfile wie es aussehen könnte:

FROM localhost:5000/wildfly

ADD target/demo.war $JBOSS_HOME/standalone/deployments/

Neben dem Dockerfile gibt es jetzt noch zusätzlich die Datei für Docker-Compose in welchem die Container-Konfiguration beschrieben ist. Um jetzt eine neue Version zu bauen und zu starten muss lediglich nach dem Maven-Build der Anwendung, der ein WAR hinterlässt, der docker-compose up –build Befehl abgesetzt werden. Dieser sorgt für ein Erstellen eines aktuellen Anwendungsimages, sowie ein Stoppen des alten Containers und anschließendes Erzeugen und Hochfahren des neuen Containers.

Demo

Ein Demoprojekt ist auf Github zu finden: https://github.com/GEDOPLAN/docker-demo
Das Projekt ist folgendermaßen aufgebaut:
Für jedes Image existiert hier ein Verzeichnis mit einem Dockerfile. Es gibt ein Basis-Image (Abgleitet von Apline-Linux) und darauf aufbauende Images für Java, Wildfly und Mysql. Das eigentliche Software Projekt ist in dem Ordner demo-project untergebracht. Es handelt sich um eine normale Webanwendung, welche als Maven-Projekt aufgesetzt wurde. Das Dockerfile des Projektes erzeugt einfach ein neues Image indem es dem Wildfly-Image das WAR hinzufügt. Ein Docker-Compose-File beschreibt die Container für Mysql und die Anwendung selbst.

Wie nun mit Docker gearbeitet werden kann, ist in dem Vagrant-File dokumentiert. Das File beinhaltet:

Installation von Docker
Starten von Docker
Starten einer eigenen Registry
Bauen der Images und pushen in Registry
Starten des Mysql-Containers (Danach Connecten und Datenbankeinrichten)
Stoppen des Mysql-Containers
Bauen des Projektes
Starten der Anwendung mit Docker-Compose

Für das Ausführen per Vagrant wird VirtualBox und Vagrant benötigt. Das Vagrant Plugin vagrant-vbguest muss ebenfalls installiert sein. Zum ausführen einfach in das Hauptverzeichnis wechseln (Wo das Vagrantfile liegt) und „vagrant up“ ausführen.