Anwendungen mit Docker transportabel machen

Jede Anwendung hat Abhängigkeiten. Die Palette reicht von der Konfiguration des lokalen Betriebssystems bis zur Integration von Netzwerk- und Webdiensten. Das erschwert das Verteilen von Anwendungen auf unterschiedliche Systeme. Das Open-Source-Werkzeug Docker verspricht Abhilfe.

In Pocket speichern vorlesen Druckansicht 7 Kommentare lesen
Lesezeit: 18 Min.
Von
  • Golo Roden
Inhaltsverzeichnis

Jede Anwendung hat Abhängigkeiten. Die Palette reicht von der Konfiguration des lokalen Betriebssystems bis zur Integration von Netzwerk- und Webdiensten. Das erschwert das Verteilen von Anwendungen auf unterschiedliche Systeme. Das Open-Source-Werkzeug Docker verspricht Abhilfe.

Die grundlegende Problematik ist rasch erklärt: Jede Anwendung hängt von einigen Elementen ihrer Umgebung ab, die sich auf unterschiedlichen Systemen nur schwerlich garantieren lassen. Bereits die verwendete Programmiersprache kann ein Problem darstellen, wenn unterschiedliche Versionen gemischt zum Einsatz kommen: Brüche in der Abwärtskompatibilität sind in der Praxis keine Seltenheit. Ein solcher hat beispielsweise mit der Einführung der Version 0.10 von Node.js stattgefunden, deren Streams-Modul Inkompatibilitäten zu dem der Vorgängerversion aufwies.

Dieses Beispiel zeigt nur einen winzigen Ausschnitt aus der Palette der potenziellen Schwachstellen: Weitere Faktoren sind unter anderem das installierte Betriebssystem, dessen Version und Einstellungen, sämtliche hinzugefügten Pakete und Module oder auch die Konfiguration des Netzwerks. Die Liste der möglichen Störfaktoren, die die fehlerfreie Ausführung einer Anwendung gefährden können, ist quasi endlos. Daher lässt sich die Aufgabe, das eigene System für eine einzige Anwendung passend einzurichten, als enorme Herausforderung begreifen.

Zahlreiche Entwickler arbeiten in Teams und veröffentlichen ihre Anwendung in unterschiedlichen Umgebungen, beispielsweise auf einem Test- und einem Produktivsystem, was die Komplexität weiter steigert (siehe Tabelle).

Entwickler 1 Entwickler 2 Testsystem Produktivsystem
OS Ubuntu 13.10 Windows 7 CentOS 6.4 Ubuntu12.04 LTS
Node.js 0.10.23 0.10.21 0.10.17 0.8.16
Express 3.3.0 3.4.6 3.4.7 3.1.3
... ... ... ... ...


Die Komplexität der Abhängigkeiten einer Anwendung steigt mit der Anzahl der Systeme.

Die gezeigte Tabelle lässt sich gut mit dem Transport von Waren vergleichen: Im Transportwesen müssen Güter auf unterschiedlichen Wegen transportieren werden, wobei je nach Kombination von Gut und Transportmedium spezielle Bedingungen einzuhalten sind. Die Lösung hierfür besteht in genormten Containern, die sich gleichermaßen für den Transport per Bahn, Schiff oder Flugzeug eignen und die gewünschten Waren von dem gewählten Transportmedium abstrahieren.

Genau die Idee greift Docker auf: Statt die Anwendung direkt auf den Systemen einzurichten, packt man sie einschließlich all ihrer Abhängigkeiten in einen Container, der sich anschließend auf einheitlichem Weg verteilen und ausführen lässt. Bislang war solch ein Vorgehen virtuellen Maschinen vorbehalten, doch weisen Letztere einige Nachteile auf. Dazu zählen insbesondere der hohe Bedarf an Festplatten- und Arbeitsspeicher und der langwierige Ladevorgang beim Start: Die Anzahl der zeitgleich ausführbaren virtuellen Maschinen ist daher ausgesprochen gering. Werkzeuge wie Vagrant erleichtern zwar das Einrichten und Verwalten von virtuellen Maschinen, können an diesen Einschränkungen allerdings nichts ändern.

Docker basiert daher nicht auf virtuellen Maschinen, sondern auf Linux-Containern (LXC). Ein solcher ist eine vom Betriebssystem bereitgestellte virtuelle Umgebung zur isolierten Ausführung von Prozessen. Alle Container teilen sich allerdings einen gemeinsamen Kernel. Daher ist es mit den Containern beispielsweise nicht möglich, eine Windows-Anwendung auf Linux auszuführen: Hierfür benötigt man nach wie vor eine virtuelle Maschine.

Der größte Vorteil von Linux-Containern gegenüber virtuellen Maschinen liegt in ihrem äußerst sparsamen Umgang mit Ressourcen und einer kurzen Startzeit, die sich im Rahmen weniger Sekunden bewegt. Ihr größter Nachteil ist die für den unbedarften Anwender komplexe und aufwändige Konfiguration. Diese Aufgabe übernimmt Docker und stellt daher einen für jeden verständlichen und leicht benutzbaren Zugang zu Containern dar. Das Werkzeug steht kostenfrei zum Herunterladen bereit und ist unter der Apache License 2.0 lizenziert.

Die Installation erfolgt, abhängig vom verwendeten Betriebssystem, auf mehr oder minder aufwändigem Weg: Unter Umständen ist es nämlich erforderlich, zuvor den Kernel des Betriebssystems zu aktualisieren. Die Dokumentation leistet allerdings eine vergleichsweise gute Hilfestellung. Generell kann die Installation ausschließlich auf einem 64-Bit-basierten Linux erfolgen, wobei Ubuntu lange Zeit die erste Wahl der Entwickler von Docker war. Seit der am 5. Februar 2014 veröffentlichten Version 0.8 unterstützt Docker zudem OS X nativ. Das bezieht sich allerdings nur auf das Kommandozeilenwerkzeug, nicht auf den eigentlichen Docker-Server: Für dessen Einsatz ist unter dem Mac-Betriebssystem nach wie vor eine virtuelle Maschine erforderlich.

Äußerst hilfreich ist, dass Vagrant seit der im Dezember 2013 veröffentlichten Version 1.4 Docker
zum Provisionieren von virtuellen Maschinen unterstützt und in der Lage ist, es zu installieren. Daher lässt sich in wenigen Zeilen ein einfaches Vagrantfile schreiben, das eine mit Ubuntu und Docker arbeitende virtuelle Maschine einrichtet:

Vagrant.configure(2) do |config|
config.vm.box = "precise64"

# When running VMware use http://files.vagrantup.com/precise64_vmware.box
config.vm.box_url = "http://files.vagrantup.com/precise64.box"

# Insert network configuration here

config.vm.provision "docker" do |d|
d.pull_images "ubuntu"
end
end

Alternativ kann man dockervm nutzen, das im aktuellen Verzeichnis ein Vagrantfile anlegt und darauf aufbauend anschließend eine virtuelle Maschine erzeugt, die Docker enthält:

$ dockervm

Docker umfasst, wie im Zusammenhang mit OS X bereits erwähnt, eine Client- und eine Serverkomponente. Im einfachsten Fall sind beide auf dem gleichen System installiert, alternativ lässt sich der Server jedoch von einem Client über das Netzwerk steuern. Führt man das Kommando

$ docker version

aus, erhält man die Versionsnummer der beiden Komponenten und gegebenenfalls einen Hinweis auf eine verfügbare Aktualisierung.

Damit Docker eine Anwendung ausführen kann, benötigt es zunächst ein Abbild des Systems, das als Basis dienen soll. Ein solches bezeichnet Docker als "Image". Interessant ist, dass das Betriebssystem des Abbilds nicht jenem des Wirts entsprechen muss: Lediglich der verwendete Kernel muss zueinander passen. Daher ist es mit Docker beispielsweise problemlos möglich, Ubuntu auf CoreOS auszuführen. Ein Image lässt sich auf zwei Wegen beschaffen: Entweder entwickelt man ein eigenes von Grund auf, oder man lädt ein vorgefertigtes aus dem Web herunter. Letzteres entspricht der von Docker empfohlenen Vorgehensweise, weshalb einige offizielle Images zur Verfügung stehen.

Selbstverständlich ist das von Docker bereitgestellte Image, das Ubuntu enthält, nicht gleichermaßen für jeden geeignet. Daher lassen sich die Abbilder den individuellen Anforderungen und Wünschen anpassen. Allerdings werden die Modifikationen niemals innerhalb eines Images vorgenommen: Stattdessen entsteht ein neues, das auf seinen Vorgänger verweist und zusätzlich lediglich die Unterschiede zu ihm speichert. Auf die Weise bildet sich nach und nach das gewünschte System als Kette von Images. Eine Kette darf seit Version 0.7.2 von Docker maximal 127 Images umfassen, zuvor lag die Obergrenze bei 42. Startet man eine Anwendung, führt Docker die benötigte Kette von Images in ein einziges Abbild zusammen und instanziiert es: Die ausgeführte Instanz bezeichnet Docker als "Container". An ihm lassen sich Änderungen vornehmen, die beim Beenden verfallen.