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.

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: