Betreiber von IT-Services haben meist zwei Ziele im Auge: Eine hohe Verfügbarkeit und ein geringes Risiko beim Einspielen von neuen Funktionen und Sicherheitsaktualisierungen. Infrastruktur als Code und Immutable Deployment sind Vorgehensmuster für Entwicklung und Betrieb, mit denen sich diese Ziele in unserer Erfahrung sehr gut erreichen lassen. Um diese Muster bei unseren firmeneigenen Diensten anzuwenden, haben wir uns für Docker entschieden und teilen in diesem Beitrag die Erkenntnisse aus der Umstellung von vollständig virtualisierten Servern auf schlanke Docker-Container.
Anforderungen an eine solche DevOps-Umgebung sind im Einzelnen:
Sicher gibt es mit Puppet, Chef, Ansible, chroot, Bash-Script-Sammlungen, KVM, XEN, Linux containers (LXC), VMWare vSphere etc. pp. schon länger Technologien, die hiervon Teilaspekte abdecken. Aber erst das Gespann Git, Debian GNU/Linux und eben Docker machen das Entwickeln und Betreiben eines Dienstes so leichtgewichtig und komfortabel, wie wir es von “normaler” Software-Entwicklung gewöhnt sind.
Docker umgibt schon seit längerer Zeit ein Hype, den man in seinem Ausmaß nicht immer nachvollziehen konnte. Beispielsweise ätzte @hipsterhacker:
Best features of Docker:
- Easy to get a project to the top of HackerNews
- New Github stars for old ideas
- Has lots of Twitter followers
Also, was ist dran am Hype? Aus unserer Sicht gibt es drei Schlüsseleigenschaften, die es vor Docker so nicht gab:
docker run -it Y apt-get install X
Diese drei Merkmale, insbesondere das dritte, haben zu einem Ökosystem geführt, in dem es geprüfte, stets aktuelle Images für alle Lebenslagen gibt.
Mit Docker ist es also praktisch ohne weitere Infrastruktur-Tools möglich den von der Software-Entwicklung gewohnten Continuous-Delivery-Arbeitsfluss auf den Betrieb von Diensten auszudehnen. Alle Schritte von einem Linux Standard-Image bis zum fertig konfiguriertem Dienst können im Dockerfile
bzw. in Hilfsskripten in einem für diesen Dienst speziellen Git-Repository verwaltet werden. Wie bei Git üblich nutzen wir dazu einen Entwicklungs-Branch (development
) und einen Produktionsbranch (master
). Vom Master-Branch lässt sich immer und überall eine produktive Instanz des Dienstes erzeugen und starten durch:
docker build --tag myService . && docker run -it myService
Statt Software wie Jira, oder Gitlab inkl. Abhängigkeiten wie Datenbank und Mailserver manuell zu installieren und zu konfigurieren, entwickelt man Dockerfile
s und Hilfsskripte. Zum Teil scheint das komplizierter, bei genauerem Betrachten stellt sich aber heraus: Je komplizierter eine solche Einrichtung ist – desto mehr lohnt es sich solche Dienste mit Docker zu automatisieren, denn die Komplexität ist jetzt besser dokumentiert, langfristig pflegbar und in Betrieb zu nehmen.
Zunächst sehen die Phasen eines Dienstes bei Verwendung von Docker wie folgt aus:
Aus dem versionkontrollierten Quellcode entsteht mit docker build
ein Image, vergleichbar mit einem Festplatten-Abbild einer virtuellen Maschine. docker images
listet lokal bekannt Images auf. docker create
erzeugt einen Container, das entspricht einer Instanz der Virtuellen Maschine basierend auf einem bestimmten Image – docker ps
zeigt lokale Container an. docker start
startet einen einzelnen Container. create
und start
lassen sich durch docker run
zusammenfassen; nutzt man ein Image des Docker Hubs entfällt sogar der build
-Schritt.
Sobald der Hauptprozess eines Containers beendet ist, beendet sich der Container – er existiert jedoch weiterhin einschließlich der Daten, die seit dem Start im virtuellen Dateisystem des Container geändert wurden. Container sind aber konzeptionell flüchtig, d.h. es ist zwar möglich einen gestoppten Container mit docker ps --all
anzuzeigen oder erneut zu starten, üblicher ist aber eine der beiden folgenden Aktionen:
docker rm
löscht einen Container einschließlich der angefallen Datendocker commit
erzeugt ein permanentes Image, von dem aus weitere Container abgeleitet werden können.Aus Entwicklersicht wirkt dieser Ablauf nachvollziehbar und umfassend. Aus der Betriebssicht fällt sofort auf, dass im produktiven Betrieb das Persistieren von Daten unbedingt nötig aber hier so noch nicht berücksichtigt ist.
Um persistente Daten mit flüchtigen, unveränderbaren Containern zu vereinen, nutzt man bei Docker in der Regel sogenannte Data-Only-Container. Das sind Container, die mit dem Parameter --volume <MyPath>
erzeugt wurden – sie müssen nie gestartet oder geändert werden. Daher spielt auch keine Rolle auf welchem Image ein Data-Container basiert. Der eigentlich Service-Container kann mit -volumes-from <MyDataContainer>
angewiesen werden, den oben genannten MyPath
zu mounten und somit Betriebsdaten in den Datencontainer zu schreiben.
Die Idee an diesem Vorgehen ist, dass so der Service-Container jederzeit verworfen und neu erzeugt werden kann, aber immer auf die aktuellen Daten zugreift. Diese frühe und deutlich Trennung von Dienst-Software und Dienst-Daten hat außerdem den Vorteil, dass Entwicklung und Betrieb sich von Anfang an darüber klar sind, wo die Daten liegen, und wie sie aus einem Docker-Dienst gesichert und wieder hergestellt werden: Typischerweise sind diese Sicherungen feingranularer und bessert testbar als z.B. VM-Snapshots.
Mit Docker wird das Einrichten und Betreiben eines Dienstes zum versionskontrollierten Software-Entwicklungsprojekt. Schon das Migrieren einer herkömmlichen Webanwendung mit Datenbank zu einer Container-Gruppe inklusive Data-Only-Container bietet allerdings schon einige Fallstricke, auf die wir in unserem nächsten Post anhand eines konkreten Beispiels eingehen werden.