Dieser Workshop zeigt am Beispiel des Cloudstack Owncloud, wie stark die Entwickler das Integrieren etablierter Linux-Technologien wie LXC und Cgroups in Docker 1.0 abstrahiert und vereinfacht haben und wie Admins komplexe Szenarien in Containerumgebungen zähmen.
Wer sich heute mit Virtualisierung auseinandersetzt, kommt an der Containertechnologie nur schwer vorbei. Doch zu glauben, man müsse dazu Linux Containers (LXC) oder Cgroups studieren, ist falsch: Mit Docker, vor allem in der neuen Version 1.0, liegt ein mächtiges, Enterprise-fähiges Tool vor, das es Admins einfach macht, neue und leichtgewichtige Instanzen ihrer Applikationen auszurollen (siehe auch den Artikel in der Titelstrecke).
Der folgende Workshop zeigt am Beispiel von Owncloud, wie das auch mit komplexen Anwendungen, die einen Webserver und eine Datenbank sowie persistenten Storage verlangen, funktioniert.
Am Anfang war das Image
Es ist ganz egal, ob als Containermanager Docker [1] zum Einsatz kommt oder nicht: Die Basis für jedweden Container bildet immer ein Image, das aus dem Dateisystem einer Distribution besteht. Entweder holt sich der Nutzer fertige Images oder baut sie selbst. Der Eigenbau besitzt Vorteile und es lässt sich auf bereits erstellte oder von Policies vorgeschriebene Images aufbauen.
Im Falle von Docker beschreibt eine Anweisungsdatei, das so genannte Dockerfile, die Arbeitsweise für das Erstellen. Schon vorab ist es wichtig, einige Namenskonvention zu kennen: Der Dateiname jedes Dockerfile muss mit einem Großbuchstaben beginnen. So eine Datei besteht aus mehreren aufeinanderfolgenden Anweisungen, jede Zeile enthält ein Kommando, gefolgt von einem Parameter. Kommandos, zum Beispiel »RUN« , hat der Admin dabei stets und komplett in Großbuchstaben zu schreiben. Beim Parameter ist er freier, dieser kann auch aus mehreren Wörtern bestehen, so wie das typischerweise bei Linux-Kommandozeilen-Befehlen mit eigenen Parametern der Fall ist, etwa »chmod +x ./Mein_Skript.sh« .
Listing 1 zeigt das Dockerfile für das Image einer MySQL-Datenbank. Die Auswahl des Basis-Image erfolgt über die »FROM« Zeile. Der Name »centos:latest« bezieht sich hier auf das Image »centos« mit dem Tag »latest« als weitere, untergeordnete Spezifizierung. »latest« ist so immer die neueste Revision des Image. Auch Releases lassen sich so markieren, zum Beispiel »ubuntu:12.04« .
Listing 1
Dockerfile für MySQL
01 FROM centos:latest 02 MAINTAINER Tux <info@b1-systems.de> 03 RUN yum install -y mysql mysql-server 04 ADD start.sh /start 05 RUN chmod +x /start 06 EXPOSE 3306 07 CMD ["/start"]
Auf der Suche nach dem Image verbindet sich Docker mit der zentralen Registry beziehungsweise dem Hub (ehemals Index genannt). Über diese öffentlich zugängliche Plattform tauschen Benutzer Images aus, sodass der Admin meist nicht alles komplett neu selbst bauen muss.
Für viele Anwendungsfälle stehen fertige Images bereit, die sich direkt benutzen lassen, bequem auffindbar über die Suchfunktion der Webseite [2] oder den Befehl »docker search Suchmuster« . Ist das angegebene Image nicht lokal verfügbar, wird es Docker automatisch herunterladen. Alternativ ist es möglich, Images mit dem Befehl »docker pull Imagename« zu beziehen.
Beginnt eine Zeile mit »RUN« , wird Docker den nachfolgenden Befehl beim Anlegen des Image ausführen, nicht aber beim Ausführen des Containers. Der angegebene Befehl darf keine Interaktion mit dem Administrator erfordern. Kommandos, die interaktive Eingaben verlangen, muss er entsprechend kapseln – etwa mit »expect« – oder mit einem passenden Parameter konfigurieren, damit Docker sie ohne weitere Eingaben ausführen kann (Beispiel: »yum install -y« ). »RUN« -Befehle können in Dockerfiles beliebig oft auftreten.
Die Zeilen im Dockerfile werden bei der Erstellung eines Image mit »docker build« sequenziell abgearbeitet. Jeder Befehl im Dockerfile erzeugt automatisch ein neues Image, das Docker nur als Differenz (Diff) zu dem vorangegangen ablegt. Erneutes Aufrufen von »docker build« führt nicht zum erneuten Abarbeiten der Befehle, vielmehr liest Docker die Ergebnisse performant aus dem Cache.
Ändert sich aber ein im Dockerfile definierter Zwischenschritt, muss Docker auch alle darauf folgenden Schritte erneut ausführen. Den Zusammenhang zwischen verschiedenen Images zeigt eine von der Kommandozeile »docker images -viz | dot -T png -o docker-images.png« erzeugte Grafik (Abbildung 1).
Der Befehl »ADD« fügt Dateien und Verzeichnisse in das Image ein. Im MySQL-Dockerfile erledigt dies das Skript »start.sh« , das im Image als Datei »/start« gespeichert ist. Wer viele Dateien zu einem Image hinzufügen möchte, kann sie auch in einem Tar-Archiv bündeln und direkt als Argument an »ADD« übergeben. Auch URLs wie »http://Webserver.fqdn/myfile.sh« anzugeben ist hier möglich.
ENTRYPOINT vs. CMD
Das Schlüsselwort »CMD« beziehungsweise »ENTRYPOINT« legt fest, welches Kommando standardmäßig bei der Ausführung eines Containers laufen soll. Das ist in den meisten Fällen entweder das Binary eines Dienstes, ein Startskript oder ein Init-Ersatz wie »supervisor« oder »runit« .
Dabei ist ein feiner Unterschied zu beachten: »CMD /start« sorgt dafür, dass »docker« den Befehl mittels »/bin/sh -c« oder genauer mittels »/bin/sh -c ‘/start’« ausführt. Will der Admin das Kommando direkt aufrufen, muss er es wie eine Liste übergeben, zum Beispiel:
CMD [ 'Programm', '--Argu-ment1', '--Argument2' ]
Der wesentliche Unterschied zwischen »CMD« und »ENTRYPOINT« kommt erst beim Ausführen zum Tragen, Listing 2 zeigt das an einem Beispiel in der interaktiven Docker-Shell. Beide Images führen das Programm »/bin/echo« aus. Bei Angabe von »CMD« lässt sich das im Dockerfile angegebene Kommando überschreiben, bei »ENTRYPOINT« dagegen nicht (siehe Parameter »–entrypoint« bei »docker run« und das Beispiel im Listing 2).
Listing 2
Unterschied zwischen ENTRYPOINT und CMD
01 docker run cmd:latest hostname 02 3246bb700a21 03 docker run entrypoint:latest hostname 04 hostname 05 docker run --entrypoint hostname entrypoint:latest 06 57334f13ef76
Der mögliche Befehlssatz für Dockerfiles ist sehr umfangreich, die komplette Dokumentation findet sich unter [2]. Die Website von Michael Crosby [3] bietet weitere nützliche Informationen rund um die vielen Optionen im Dockerfile. Listing 3 zeigt ein komplettes Dockerfile für das Owncloud-Szenario [4].
Listing 3
Dockerfile für Owncloud
01 FROM hachque/opensuse 02 MAINTAINER Tux <info@b1-systems.de> 03 RUN zypper mr -ae 04 RUN zypper --non-interactive ar 05 'http://download.opensuse.org/repositories/isv:/Owncloud:/community:/6.0/openSuse_13.1/' owncloud 06 RUN zypper --non-interactive --gpg-auto-import-keys ref -f 07 RUN zypper --non-interactive update --auto-agree-with-licenses 08 RUN zypper --non-interactive install --auto-agree-with-licenses apache2 owncloud php5-mysql php5-fileinfo glibc-locale 09 ADD sysconfig /etc/sysconfig/ 10 CMD /usr/sbin/start_apache2 -f /etc/apache2/httpd.conf -DFOREGROUND
Images bauen
Nachdem der Admin sein Dockerfile fertiggestellt hat, muss er das eigentliche Image bauen. Das erledigt »docker build« , was zwingend den Pfad zu dem Verzeichnis verlangt, in dem sich das Dockerfile befindet. Ein Pfad könnte hier auch eine URL oder sogar die Standardeingabe sein. Daneben bietet es sich an, dem resultierenden Image direkt einen Namen sowie ein Tag zu verpassen, damit es später über dieses leicht referenziert werden kann:
$ cd /Pfad/zum/Dockerfile $ docker build -t myimage:test .
Nachdem ein Image anhand eines Dockerfile gebaut ist, lassen sich Container auf Basis des Image ausführen. Listing 4 zeigt die Ausgabe des Befehls »docker images« , der die lokal verfügbaren Images auflistet.
Listing 4
Ausgabe der lokal verfügbaren Images
01 docker images 02 REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE 03 owncloud_httpd latest a08339e14faf 3 days ago 869.1 MB 04 owncloud_database latest 0f12f4bfd2bd 3 days ago 254.5 MB 05 centos latest 0c752394b855 3 weeks ago 124.1 MB 06 fedora 20 3f2fed40e4b0 4 weeks ago 372.7 MB 07 hachque/opensuse 13.1 042c8bfd4d0e 11 weeks ago 151.5 MB 08 hachque/opensuse latest 042c8bfd4d0e 11 weeks ago 151.5 MB
Um einen Container mit dem im Image hinterlegten Kommando zu starten, genügt das Kommando »docker run owncloud_httpd:latest« , das den Container im Vordergrund ausführt. Der Parameter »-d« startet den Container im Hintergrund. Docker gibt nach der Ausführung die Prüfsumme des gestarteten Containers zurück.
docker run -d centos:latest sleep 30 a6c462a7999ab01db374ffe502b02d5aac1cd6
Das erweist sich vor allem beim Einsatz in Skripten als hilfreich. Für den Start einer interaktiven Sitzung in einem Container sind die Parameter »-i« und »-t« nötig. »-i« steht für »interactive« und bindet den Container an die Standardeingabe, »-t« alloziert ein Pseudo-Terminal, »–rm« lässt den gestoppten Container automatisch löschen:
docker run -i -t --rm fedora:latest /bin/bash bash-4.2# cat /etc/fedora-release Fedora release 20 (Heisenbug)
Das ist bei Tests mit Containern sinnvoll, die der Admin nach einem Stopp nicht mehr fortgesetzen möchte.
Diese Vorgehensweise ermöglicht es auch, eine interaktive Sitzung zu starten und das Ergebnis wie Paketinstallationen oder Konfigurationsänderungen in ein neues Image zu übernehmen (Listing 5). Der Parameter »–name« übergibt in diesem Beispiel direkt einen Namen, über den der Nutzer später auf exakt diesen Container zurückgreifen kann. Ist kein »–name« spezifiziert, dann erzeugt Docker einen zufälligen Namen.
Listing 5
Ein Image interaktiv bauen
01 docker run -t -i --name httpd-build fedora:20 /bin/bash 02 bash-4.2# yum install -y httpd htop 03 [...] 04 Installed: 05 htop.x86_64 0:1.0.3-3.fc20 httpd.x86_64 0:2.4.9-2.fc20 06 bash-4.2# vi /etc/httpd/conf/httpd.conf 07 bash-4.2# exit 08 docker commit -a 'Tux <info@b1-systems.de>' -m 'Erster Versuch' httpd-build myhttpd:latest 09 aeaee2e5070e60e1d8676d1fdc7340f4eff98f8de692d6eaadf2f84913b1d8e9
Nach einem erfolgreichen »docker commit« lässt sich nun ein neuer Container starten:
docker run -d myhttpd:latestapachectl -DFOREGROUND
Der Befehl »docker ps« zeigt nur Container an, die gerade laufen. Falls Docker einen Container gestoppt hat, weil zum Beispiel das ausgeführte Kommando beendet war, muss der Admin »docker ps -a« nutzen, um alle Container zu sehen.
Ports für den Zugriff übers Netz weiterleiten
Auf geht’s zum Netzwerkzugriff und dessen Konfiguration: »docker run« , ergänzt um den Schalter »-p« , startet das Image nun mit Netzwerkverbindung in den Container. Das folgende Beispiel aktiviert die Weiterleitung des Host-Ports »13306« in den Container auf den Port »3306« , also den MySQL-Server:
docker run -d -p 13306:3306 mysql
Die Weiterleitung realisiert Docker durch eine simple IPtables-NAT-Regel auf dem Hostsystem. Will der Admin den Weiterleitungsport zusätzlich auf eine IP-Adresse binden, muss er den Ports eine IP-Adresse mit abschließendem Doppelpunkt voranstellen. Für die Loopback-Adresse lautet der Parameterwert dann zum Beispiel »127.0.0.1:13306:3306« . Fehlt hier die »13306« , also etwa bei »127.0.0.1::3306« , dann wählt Docker einen Weiterleitungsport zufällig aus dem Bereich zwischen »49000« und »49900« .
In der Konfigurationsdatei definiert das Schlüsselwort »EXPOSE« , welche Netzwerkports im Container bereitstehen. Der Werteparameter für »EXPOSE« ist dabei eine durch Leerzeichen getrennte Liste von einzelnen Ports. Eine Konfigurationszeile könnte wie folgt aussehen:
EXPOSE 3306
Sie teilt Docker mit, dass der Container den Port »3306« verwendet. Wer die »EXPOSE« -Einstellung im Dockerfile nutzt, um alle im Container geöffneten Ports anzugeben, aktiviert diese auch alle gleichzeitig mit dem Schalter »-P« beim Containerstart. Docker kümmert sich dann um die dynamische Portbelegung am Host, eine Zuweisung ist – wie beim Schalter »-p« – nicht mehr möglich. Ein Beispiel auf dem Host könnte so aussehen:
docker run -d -P mysql
Setzt der Admin sowohl die Option »-p« als auch »-P« , haben die mit dem kleinen »p« gesetzten Werte Vorrang.
Abfragen lassen sich die aktuellen Weiterleitungen über zwei Befehle: Das Kommando »docker ps« liefert neben den Informationen zum Netzwerk auch Daten zur Laufzeit des Image, die gewünschte Angabe findet sich hier in der Spalte »PORTS« . Ist da ein Pfeil »->« vorhanden, handelt es sich um eine Weiterleitung. Steht dagegen nur eine Portnummer, dann ist dieser geöffnet und andere Container dürfen sie nutzen:
docker ps ... PORTS ... 0.0.0.0:5556->3306/tcp
Der Befehl »docker port« benötigt zusätzlich den Namen oder die ID des gestarteten Containers und die in ihm ansprechbare Portnummer. Das Kommando
docker port mysql 3306 0.0.0.0:13306
spricht zum Beispiel den Container mit dem Namen »mysql« an.
Container verbinden
Der Linking-Mechanismus verbindet Container sicher über das Netzwerk miteinander, ohne dabei Ports nach außen zu öffnen. Das geschieht über eine Netzwerk-Bridge (Abbildung 2), die der Dockarbeiter verwaltet und mit der jeder Container verbunden ist. Wer die im Beispiel gezeigte Verbindung zum Datenbank-Container »mysql« verwenden möchte, gibt beim Starten des Webcontainers zusätzlich die Option »–link mysql:db« an. Docker verlangt hier den Namen des zu verlinkenden Containers »mysql« und ein frei wählbares Alias, hier »db« (Listing 6).
Listing 6
Link zum Container mysql mit Alias db
01 docker run --name oc --link mysql:db owncloud_owncloud:latest env 02 [...] 03 DB_PORT=tcp://172.17.1.12:3306 04 DB_PORT_3306_TCP=tcp://172.17.1.12:3306 05 DB_PORT_3306_TCP_ADDR=172.17.1.12 06 DB_PORT_3306_TCP_PORT=3306 07 DB_PORT_3306_TCP_PROTO=tcp 08 DB_NAME=/oc/db

Abbildung 2: Schematische Darstellung der Verbindung der Container über Netzwerk-Bridge und Interfaces. Der Container mit der Webapplikation ist via »bridge0« mit dem Datenbank-Container verbunden. Über automatisch generierte Portforwardings ist der Port 80 des Owncloud-Containers auch von außen erreichbar.
Dieses Alias kann innerhalb des laufenden Webcontainers dazu dienen, die Verbindungsdaten zur MySQL-Datenbank zu erhalten. Docker stellt sie in Form von Umgebungsvariablen bereit, angezeigt von »env« , beginnend mit dem Alias des ersten gestarteten Prozesses.
Neben den gesetzten Umgebungsvariablen erhält der Kommunikationspartner einen Eintrag in der Datei »/etc/hosts« , hier ist die von Docker automatisch vergebene IP-Adresse dem gewählten Alias zugeordnet. Der direkte Zugriff auf den Datenbank-Container gelingt somit über den Hostnamen »db« . Da die verbundenen Container eine Art Eltern-Kind-Beziehung eingehen, ist es problemlos möglich, mehrere Kind-Container zu starten. So können auch mehrere Webcontainer mit einem Datenbank-Container kommunizieren, wie das im Owncloud-Beispiel der Fall ist.
Persistente Volumes
Ein Vorteil der Container und Images ist, dass der Admin immer von einem definierten Zustand ausgehen kann. Jede Veränderung, die in laufenden Containern passiert, wird verworfen, sobald der Container gestoppt und gelöscht ist. Das wird zum Nachteil, wenn im Container Nutzdaten liegen, die persistent gespeichert sein sollten. Hier helfen so genannte Volumes. Ein Volume ist ein Verzeichnis auf dem Hostsystem, das der Admin an beliebiger Stelle im Dateisystem des Containers einhängt (Listing 7).
Listing 7
Ein Volume erzeugen
01 mkdir /tmp/test 02 docker run -i -t -v /tmp/test:/foo --rm --name test1 fedora:20 /bin/bash 03 bash-4.2# echo Hello World > /foo/myfile 04 bash-4.2# exit 05 docker rm test1 06 cat /tmp/test/myfile 07 Hello World
Der Parameter »-v /tmp/test:/foo« sorgt dafür, dass das Verzeichnis »/tmp/test« im Container unter »/foo« zur Verfügung steht. Im Fall des Datenbankservers für Owncloud ist folgender Aufruf sinnvoll:
docker run -d -v /data/mysql:/var/lib/mysql
Docker legt die spezifizierte Verzeichnisstruktur auf dem Host automatisch an, falls sie noch nicht existiert. Der Admin muss Dateisystemberechtigungen aber eventuell anpassen, damit zum Beispiel ein Dienst im Container, der in einem unprivilegierten Benutzerkontext läuft, auch auf das Verzeichnis zugreifen darf.
Außerdem sollte stets nur ein Datenbank-Container auf jeweils ein Volume zugreifen. Leider geht damit auch ein wenig Flexibilität verloren, da es nun nicht mehr ausreicht, ein Image für einen Dienst auf einen anderen Computer zu verschieben – der Planer muss auch einen eigenen Mechanismus für die Verteilung der Nutzdaten einbauen.
Komplexe Szenarien mit Fig und Vagrant
Viele Entwicklungs- und Testumgebungen bestehen aus mehr als einem Dienst, der mitunter eine komplexe Konfiguration oder die Verbindung mit anderen Diensten erfordert. Die benötigten Images lassen sich zwar alle leicht mit Dockerfiles definieren, aber die Ausführung erfordert mitunter eigene Skripte und lange Kommandozeilen, damit die Verknüpfungen verschiedener Container-Volumes richtig funktionieren.
Zum Glück gibt es Provisionierungswerkzeuge wie Fig, das auf der Basis einer Konfigurationsdatei im Yaml-Format [5] die einzelnen Container mit ihren Parametern konfiguriert. Die Bereitstellung und Administration der Instanzen erfolgt dann mit dem »fig« -Kommandozeilenwerkzeug [6]. Listing 8 zeigt die Konfiguration für das Owncloud-Szenario, Listing 9 schematisch den Verzeichnisbaum der Konfigurationsdateien als Gedächtnisstütze.
Listing 9
Verzeichnisstruktur für das Owncloud-Projekt
01 owncloud/ 02 |-- fig.yml 03 |-- httpd 04 | ** ** |-- Dockerfile 05 | ** ** |-- sysconfig 06 | |-- apache2 07 |-- mysql 08 |-- Dockerfile 09 |-- start 10 |-- start.sh
Listing 8
fig.yml für das Owncloud-Szenario
01 database: 02 build: mysql/ 03 volumes: 04 - /data/mysql/:/var/lib/mysql 05 expose: 06 - "3306" 07 httpd: 08 build: httpd/ 09 links: 10 - database:db 11 volumes: 12 - /data/owncloud/files:/srv/www/htdocs/owncloud/data 13 - /data/owncloud/config:/srv/www/htdocs/owncloud/config 14 ports: 15 - "80:80"
Fig lässt sich ähnlich wie das populäre Deploymentwerkzeug Vagrant [7] bedienen, ist aber speziell auf Docker zugeschnitten. Das Kommando »fig up« zum Beispiel würde anhand der gegebenen Datei »fig.yml« die Images in den Ordnern »mysql« und »httpd« bauen, falls sie nicht bereits existieren, und danach die Container mit den definierten Parametern starten.
Danach sollte der Zugriff auf »http://$host-ip/owncloud« funktionieren. Abbildung 3 zeigt die Owncloud-Setup-Routine im Falle einer korrekten Konfiguration der Installation. Fig benennt die Images nach dem Muster »Hauptverzeichnis_Containername« , im konkreten Beispiel existieren nach dem Bau mit »fig build« zwei Images mit den Namen »owncloud_database« und »owncloud_httpd« .
Die Container selbst tragen den Namen des Image sowie eine laufende Nummer als Suffix. Wenn eine horizontal skalierbare Applikation im Container vorliegt, kann der Admin mit dem Befehl »fig scale« auch definieren, wie viele Container eines Typs (zum Beispiel »httpd« ) Docker startet. Analog zu »docker run -d« kann »fig up -d« auch die gesamte Umgebung im Hintergrund starten. »fig kill« und »fig rm« bilden das Aufräumkommando.
Infos
- Rob Knight, “Volle Ladung”: Linux-Magazin 08/13, S. 64
- Docker-Builder Referenz: http://docs.docker.com/reference/builder/
- Docker Best Practice:http://crosbymichael.com/dockerfile-best-practices.html
- Owncloud: http://owncloud.org
- YAML: http://www.yaml.org
- Fig https://orchardup.github.io/fig/
- Vagrant http://www.vagrantup.co








