Puppet & Co. im Einklang mit der Docker-Technologie

Docker: Konfigurationsmanagement im Container
Kommentare

Docker setzt beim Aufbau der Container auf einen Image-basierten Ansatz und fordert damit eingeschliffene Vorgehensweisen des Konfigurationsmanagements mit Tools wie Puppet oder Chef heraus. Je nach Anwendungsfall lassen sich auch beide Vorgehensweisen kombinieren. In der Ferne grüßt aber bereits der „konfigurationsfreie“ Microservice.

In der täglichen Arbeit mit Docker gibt es verschiedene Wege, um zu lauffähigen Anwendungen bzw. Containern zu gelangen. Neben dem direkten Arbeiten „im Container“ und nachträglichem Commit eines Versionsstands lassen sich vordefinierte Container sehr einfach über das Dockerfile erzeugen. Die Nutzung dieses Ansatzes führt zu einem definierten und nachvollziehbaren Weg der Containererstellung, der für viele Teile der Lieferkette in der IT sehr hilfreich ist: Jetzt können Bausteine mit klar definierten Funktionen entstehen und in Betrieb genommen werden.

Dabei besitzt das Dockerfile eine sehr einfache Syntax, die an den Shell-Stil angelehnt ist. Der Nutzer beschreibt dabei, auf welchem Basis-Image sein Container aufbaut, welche Shell-Kommandos zur Provisionierung ausgeführt werden sollen und welche weiteren Ressourcen wie Volumes, Umgebungsparameter oder Ports Verwendung finden. Details hierzu erfahren Sie im Grundlagenartikel „Docker Basics“.

Der einfache Aufbau eines Dockerfiles stellt eine niedrige Hürde beim Einstieg in die Containererstellung dar. Gleichzeitig steht der Ansatz in Kontrast zu Vorgehensweisen, die innerhalb der letzten Jahre im Zuge von Continuous Delivery eingeführt wurden. Viele Unternehmen haben Ressourcen in den Aufbau ihrer Deployment-Pipelines und die Umsetzung und Vereinheitlichung von Konfigurationsmanagement (KM) investiert. In Deutschland zählen Werkzeuge wie Puppet oder Chef zu den bekanntesten Vertretern in diesem Bereich, mit Ansible und Salt stehen schon die nächsten Herausforderer in den Startlöchern. Für Unternehmen stellt sich die Frage, ob für den Einsatz von Docker eine neue Vorgehensweise notwendig wird, oder ob sich bewährte Vorgehensweisen weiterverwenden lassen, und in welchem Rahmen das sinnvoll sein kann.

Nun gibt es mehrere Möglichkeiten, wie sich die Konfiguration für Docker und Docker-Container anwenden lässt:

  1. Konfiguration des Docker Hosts
  2. Konfiguration der Container
  3. Dynamische Konfiguration der Container

Docker Host

Der erste Bedarf für Konfiguration stellt sich auf dem Host ein, auf dem der Docker Daemon beheimatet ist. Hier geht es darum, das Softwarepaket Docker zu installieren und gemäß den eigenen Anforderungen, etwa an Sicherheit oder die eigene Umgebung (Beispiel: IP-Adressen für den API-Zugriff) anzupassen (Abb. 1).

Abb. 1: Installation und Konfiguration von Docker über Puppet

Abb. 1: Installation und Konfiguration von Docker über Puppet

Diese Aufgabe ist dem klassischen Server-Provisioning zuzurechnen und stellt sich immer dann, wenn Hardware integriert bzw. ersetzt werden muss. Weiterhin sollte die Provisionierung nachvollziehbar und prüfbar sein, d. h. bekannte Versionsstände von Betriebssystem und auch von Docker selbst installieren. Dabei ist es für den laufenden Betrieb immer interessant, Versionen konfigurierbar zu halten, um Updates einfach installieren zu können.

Dafür bieten sich die bekannten KM-Werkzeuge unbestreitbar an. Für Puppet ist beispielsweise in der Puppetforge ein Modul enthalten, mit dem sich Docker auf einem Host installieren, konfigurieren und mit Container-Images bestücken lässt. Listing 1 zeigt ein einfaches Beispiel eines Puppet-Moduls, das Docker in einer festgelegten Version installiert und IP/Port für die Serviceschnittelle konfiguriert.

Listing 1

include 'docker'
class { 'docker':
    version       => '1.2.0',
    tcp_bind      => 'tcp://127.0.0.1:2375',
}

Die Docker-Klasse erlaubt es, Version, Binding des API (lokal über Socket oder über HTTP), DNS-Einträge und vieles mehr zu konfigurieren. In einer Standardinstallation von Docker werden für die Konfiguration des Servers eine Reihe von Annahmen getroffen, um die Nutzung zu vereinfachen. Dies deckt sich in der Regel aber nur zum Teil mit betrieblichen Anforderungen wie etwa aus den Bereichen Security und Netzwerk. Hier wird es mit hoher Wahrscheinlichkeit notwendig, eine individuelle Konfiguration für die eigene Umgebung zu erstellen, und dies sollte, wo immer möglich, in einer testgetriebenen Weise mit KM-Werkzeugen geschehen. Als Basis für eigene, testbare Implementierungen kann ein Blogpost über Docker, Puppet und Serverspec herangezogen werden.

Neben der Konfiguration von Docker selbst ist es auch interessant, den Host mit den richtigen Docker Images zu bestücken und diese dann zu starten. Die Abhängigkeiten zwischen Hosts, d. h. im Beispiel von Docker, wo und in welcher Reihenfolge Container gestartet werden sollen, decken KM-Werkzeuge unterschiedlich gut ab. Die Bestückung mit Images lässt sich im obigen Docker-Modul von Puppet ebenfalls umsetzen. Listing 2 zeigt einen Ausschnitt, in dem das Image von Ubuntu 14.04 von der öffentlichen Docker Registry geladen wird.

Listing 2

docker::image { 'ubuntu':
  image_tag => '14.04'
}

Der Typ docker::image erlaubt es auch, Images über den Parameter ensure => ‚absent’ wieder zu entfernen. Für den Unternehmenseinsatz ist es wichtig, den Docker-Host so zu konfigurieren, dass Images von einer lokalen, sicheren Registry geladen werden. Dabei muss die Registry aber individuell auf Sicherheitsaspekte angepasst werden.

Um das Starten von Containern zu verwalten, bietet das Modul den Typ docker::run an. Hierbei gibt es zahlreiche Einstellungsmöglichkeiten, die den Parametern des Aufrufs von docker run entsprechen, wie etwa Ports, Volumes, Environment-Einträge usw. Das Modul übersetzt dabei die Konfiguration in upstart-Aufrufe, sodass die Container auf Betriebssystemebene gestartet werden können. Eine Orchestrierung oder Abhängigkeitssteuerung von Containern liegt im Moment allerdings außerhalb der Möglichkeiten des Puppet-Moduls. Andere Orchestrierungs- und Konfigurationswerkzeuge, beispielsweise Salt, werden in der Zukunft vermutlich Möglichkeiten anbieten.

Containerkonfiguration

Nachdem der Docker Host betriebsbereit gemacht wurde, lassen sich die ersten Container starten. In der Regel werden die Images dazu in einem Build-Prozess erstellt, und hier bieten sich Ansätze zur Ausführung von Konfigurationsmanagement. Das kann an mehreren Stellen geschehen:

  • Zur Buildzeit, d. h. beim Bauen eines Container-Images
  • Beim Starten eines Containers, d. h. vor dem Start des eigentlichen Prozesses kann ein Konfigurationslauf durchgeführt werden, oder während der Laufzeit eines Containers kann (auch kontinuierlich) die Konfiguration angepasst werden.

Allen Ansätzen ist gemein, dass sie sowohl mit lokaler Konfiguration (masterless: Puppet apply, chef-solo) als auch mit ausgelagerter Konfiguration (Puppetmaster, Chef Server) umsetzbar sind. Die folgenden Beispiele beziehen sich auf Puppet in einer masterless-Konfiguration, d. h. die Puppet-Klassen und Manifeste liegen lokal auf einem Dateisystem.

Zum Build-Zeitpunkt

Damit docker build das Container-Image aufbauen kann, liest der Docker-Server das Dockerfile ein und wendet die enthaltenen Anweisungen auf die Dateisysteme an. Je komplexer die Applikation aufgebaut ist, desto mehr RUN-Anweisungen müssen ins Dockerfile, und das hat Auswirkungen auf die Dateisystem-Layer. Es bietet sich an, Teile davon zu einer RUN-Anweisung zusammenzufassen, die Puppet ausführt. (Abb. 2, siehe dazu auch hier)

Abb. 2: Puppet zum Build-Zeitpunkt ausführen

Abb. 2: Puppet zum Build-Zeitpunkt ausführen

Listing 3

FROM ubuntu:14.04
RUN apt-get update -yqq && apt-get install -yqq puppet && rm -rf /var/lib/apt/lists/*
ADD ./puppet /tmp/build-puppet-provisioning
RUN ( cd /tmp/build-puppet-provisioning; puppet apply --logdest console container.pp )
CMD /bin/bash

In Listing 3 wird ein Ubuntu-Image als Basis verwendet (FROM…). Die folgenden beiden RUN-Anweisung bringen den Package-Index auf den neuesten Stand und sorgen dafür, dass Puppet installiert ist. Die ADD-Anweisung fügt das lokale Verzeichnis puppet/ in das Dateisystem des Containers ein (dort unter /tmp/build-puppet-provisioning). Dort befinden sich dann die auszuführenden Puppet-Manifeste. Die folgende RUN-Zeile wechselt in das Verzeichnis und führt ein puppet apply auf das Manifest container.pp (s.u.) aus, um die Konfiguration anzuwenden. Listing 4 zeigt ein alternatives Beispiel mit Chef als Provisionierungswerkzeug.

Listing 4

FROM ubuntu:14.04
RUN apt-get update –yqq && apt-get -y install curl build-essential libxml2-dev libxslt-dev git && rm -rf /var/lib/apt/lists/*
RUN curl -L https://www.opscode.com/chef/install.sh | bash
RUN echo "gem: --no-ri --no-rdoc" > ~/.gemrc
ADD ./chef /chef
RUN chef-solo -c /chef/solo.rb -j /chef/solo.json

Um das Beispiel einfach zu halten, lassen wir das Puppet-Manifest nur eine Datei mit einem dynamischen Inhalt füllen, Listing 5 zeigt die Datei container.pp.

Listing 5

notice("BEGIN puppet container provisioning ...")
file { '/tmp/provisioned':
  ensure  => file,
  mode  => '0750',
  content => "Provisioned with puppet, container $fqdn"
}
notice("END   puppet container provisioning ...")

Im Build-Lauf (Listing 6) kann man bei der Ausführung sehen, an welcher Stelle Puppet zum Zuge kommt.

Listing 6

# docker build .
Step 5 : RUN ( cd /tmp/build-puppet-provisioning; puppet apply --logdest console container.pp )
 ---> Running in 873cc0f4c498
Notice: Scope(Class[main]): BEGIN puppet container provisioning ...
Notice: Scope(Class[main]): END   puppet container provisioning ...
Notice: Compiled catalog for b756a5b3138f in environment production in 0.06 seconds
Notice: /Stage[main]/Main/File[/tmp/provisioned]/ensure: defined content as '{md5}50901c6a5c9ccc93ff45bb3348a4a14c'

Dieser Ansatz ist vorteilhaft, wenn innerhalb einer Build-Pipeline ein Image gebaut wird, das dann mit seinem Stand „fertig“ ist und keine Nachkonfiguration erfordert. Dann ist man in der Lage, Dockerfiles kompakt zu halten und die komplexe Konfiguration auszulagern.

Ein Nachteil besteht darin, dass Images nach der Bestückung immer noch das KM-Werkzeug enthalten. Um kleine Images zu erhalten, müsste nach erfolgreicher Konfiguration Puppet wieder entfernt und das Image verkleinert werden. Diese Vorgehensweise wird auch als Squashing bezeichnet. Falls zum Startzeitpunkt des Containers weitere Konfiguration, wie etwa die Anpassung von IP-Adressen an die Umgebung notwendig ist, reicht die Konfiguration zur Build-Zeit nicht aus. Hier sind moderne Konfigurationsansätze wie etwa Environment-Variablen sinnvoll, gegebenenfalls kollidieren sie allerdings mit der existierenden Anwendungsarchitektur und müssen auf herkömmliche Weise in die Konfiguration eingebracht werden, beispielsweise zum Startzeitpunkt des Containers.

Zum Startzeitpunkt

Die Anwendung der Konfiguration lässt sich auch aus dem Build heraushalten und erst beim Start des Containers ausführen. Dazu wird der Puppet-Lauf nicht in eine RUN-Anweisung, sondern in die CMD-Anweisung kodiert (Listing 7).

Listing 7

FROM ubuntu:latest
(...)
CMD /bin/sh -c '( cd /mnt/build-puppet-provisioning; puppet apply --logdest console container.pp ) && cat /tmp/provisioned'

Ein Shell-Wrapper führt das Kommando in den Klammern aus. Es wechselt in das Provisionierungsverzeichnis und ruft Puppet mit dem Manifest auf. Falls dies erfolgreich war, wird das nachfolgende Kommando ausgeführt (im Beispiel nur die Anzeige der Testdatei, im richtigen Anwendungsfall die Zielapplikation). Listing 8 zeigt die Ausgabe.

Listing 8

# docker run -ti aschmidt75/t2
Notice: Scope(Class[main]): BEGIN puppet container provisioning ...
Notice: Scope(Class[main]): END   puppet container provisioning ...
Notice: Compiled catalog for 5224b67daad4 in environment production in 0.06 seconds
Notice: /Stage[main]/Main/File[/tmp/provisioned]/ensure: defined content as '{md5}02bb35cff7f13934edbffbff78495888'
Notice: Finished catalog run in 0.02 seconds
Provisioned with puppet, container 5224b67daad4

Dieser Ansatz erscheint nur sinnvoll, wenn der auszuführende Puppet-Code zum Startzeitpunkt des Containers geändert werden kann, d. h. er befindet sich in der Regel nicht im Image, sondern auf einem Docker Volume (oder auf einem Puppetmaster). Dafür sind unabhängig von der Build-Pipeline nachträgliche Konfigurationsänderungen möglich.

Weiterhin führt es dazu, dass von der Idee eines unveränderlichen Images abgewichen wird. Eine Build-Pipeline liefert ein Anwendungs-Image, aber zur Laufzeit ist nicht dieselbe Konfiguration aktiv wie zur Build-Zeit. Nachteilig wirkt sich außerdem aus, dass man in der Wahl des CMD-Parameters nicht mehr frei ist, sondern auf das KM-Werkzeug eingehen muss. Ebenso passt der Ansatz nicht mehr mit dem ENTRYPOINT-Parameter zusammen, d. h. die Nutzung wird diesbezüglich eingeschränkt. Letzten Endes ist diese Vorgehensweise technisch möglich, aber nicht empfehlenswert.

Zur Laufzeit

Wenn der Bedarf besteht, während der Laufzeit eines Containers Konfiguration durchzuführen, muss dafür gesorgt werden, dass das KM-Tool Teil der Prozesskette im Container ist. Dies bietet sich an, wenn man den Container nicht im Microservicegedanken mit nur einem einzigen Service (z. B. Apache Tomcat) laufen lässt, sondern im Zusammenspiel mit mehreren Services (sshd, monitoring/logging, supervisor.d, siehe auch Abb. 3). Dann kann auch Puppet im Daemon-Modus mitlaufen und in regelmäßigen Abständen eine gegebenenfalls geänderte Konfiguration anwenden. Der Aufbau einer solchen Lösung ist allerdings komplexer und berührt die grundlegenden Nutzungsszenarien von Containern.

Der Ansatz ist vorteilhaft, wenn man Container „wie VMs“, das heißt Log-in-fähig über SSH und mit mehreren Prozessen verwendet. Dann besteht aus Sicht des Konfigurationsmanagements wenig Unterschied zur Bestückung eines klassischen Servers.

Abb. 3: Puppet-Daemon-Prozess als Teil des Containers

Abb. 3: Puppet-Daemon-Prozess als Teil des Containers

Alle drei vorgestellten Ansätze lassen sich prinzipiell auch kombinieren, z. B. zum Build- und Startzeitpunkt. Dabei steigt allerdings die Konfigurationskomplexität, wenn man sicherstellen möchte, dass sich Konfigurationen zu verschiedenen Zeitpunkten und aus verschiedenen Quellen nicht widersprechen.

Allerdings lässt sich der Aspekt der Orchestrierung von Docker-Containern hiermit nicht ausreichend lösen. In Puppet lassen sich Abhängigkeiten von Containern untereinander nicht modellieren, und eine Host-übergreifende Orchestrierung bleibt mit diesem und anderen Konfigurationsmanagementtools schwierig. Die Abhängigkeiten von Containern und damit deren Orchestrierung ist aktuell eine Domäne von spezialisierten Werkzeugen wie etwa Fig oder decking.io.

Dynamische Konfiguration der Container

Durch die Fortschritte im Cloud- und Containerumfeld kommen im Docker-Ökosystem neuartige Werkzeuge ans Tageslicht, die das Potenzial besitzen, die Konfiguration von Anwendungen deutlich zu vereinfachen.

Neben der Installation von Paketen gehört das Modifizieren von Konfigurationsdateien zu den häufigsten Aufgaben von KM-Werkzeugen: Ein- und Ausschalten von Feature-Toggles, Setzen von Pfaden, IP-Adressen und Host-Namen anderer Komponenten und Services sowie umgebungsspezifische Anpassungen in Dateien (z. B. keine Dummy-Werte in Liveumgebungen).

Alternativ erhalten sie ihre Konfiguration über den ENV-Mechanismus von Docker. Dabei werden beim Start eines Containers mit dem Flag -e/–env Einträge mitgegeben, die innerhalb des Containerprozesses als Environment-Variablen zugreifbar sind. Dies entspricht der Konfigurationsidee der „Twelve-Factor Apps“. Der Erfolg dieser Variante ist allerdings vom verwendeten Framework bzw. der Sprache abhängig. So müssen z. B. bei Java-Applikationen die Environmentvariablen mit einzelnen –D-Parametern als System-Properties durchgereicht werden.

Moderne Werkzeuge wie etcd oder consul lösen eine Reihe der obigen Konfigurationsprobleme durch einen verteilten, hochverfügbaren Key/Value-Store, der Konfigurationseinträge enthalten darf. Darauf aufbauende Anwendungen müssen dann in der Lage sein, ihre Konfiguration beim Programmstart nicht mehr aus Dateien, sondern vom nächstgelegenen Cluster-K/V-Mitglied einzulesen. Dabei können sie neben einfachen Dingen wie Schalter auch automatisiert die IP-Adressen abhängiger Services wie etwa einer Datenbank erhalten – so ersetzt dynamisches Service-Discovery eines der Haupteinsatzgebiete von KM-Werkzeugen.

Fazit

Das Bestücken von Hosts mit Docker sowie die Konfiguration des Docker-Servers sind klassische Aufgaben für ein Konfigurationsmanagementwerkzeuge. Docker lässt dem Nutzer beim Bau der Container-Images genügend Freiheiten, um an geeigneten Stellen KM-Tools einzuklinken. Investitionen in KM-Code lassen sich in die Docker-Welt hinüberretten und erlauben so eine Transition vom VM-Aufbau zum Containeraufbau. Allerdings übernimmt man damit auch die teilweise gewachsene Komplexität dieser Werkzeuge. Eine Alternative ist es, die Herausforderungen wie Service-Discovery und dynamische Konfiguration in seiner Umgebung umzusetzen und so konfigurationsarmen bzw. sogar -freien Containern zu gelangen.

Aufmacherbild: Crane lifter handling container box loading to depot von Shutterstock / Urheberrecht: Prasit Rodphan

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -