Aus Linux-Magazin 03/2018

Docker-Container effektiv debuggen

© Andriy Popov, 123RF

Auch in Docker-Containern nisten sich Fehler ein. Der folgende Artikel gibt eine Übersicht, wie Admins Probleme per Debugging in den Griff bekommen.

Den Gipfelpunkt seines Hype hat Docker mittlerweile wohl überschritten: Neue Versionen lösen kein emsiges Treiben in der Community mehr aus und provozieren keine Flut von Fachartikeln mehr. Das ist nicht zuletzt deshalb so, weil sich ein beträchtlicher Teil des Interesses in den vergangenen Monaten von Docker weg und hin zu Lösungen verlagert hat, die Docker als Basis für größere Setups nutzen – etwa Kubernetes.

Docker ist im Mainstream angekommen, für Admins ist es heute nichts Außergewöhnliches mehr, die Containerlösung dort zu verwenden, wo man früher vielleicht noch auf echte VMs gesetzt hätte. So gesehen ist Docker nur noch eine Virtualisierungslösung von vielen. Doch wächst die Zahl der Nutzer, die sich mit Docker im produktiven Umfeld beschäftigen. Damit steigt auch die Zahl der Entwickler und Admins, die sich nicht gleich zurechtfinden und anfangs ihre liebe Not damit haben, einen Docker-Container zu bauen und zu starten.

Tatsächlich können beim Bauen und beim Betrieb eines Docker-Containers viele Dinge schiefgehen. Wer sich einem Docker-Problem mit dem Rüstzeug versierter KVM-Admins nähert, kommt aber nicht sehr weit: Das meiste funktioniert in Docker fundamental anders als bei echten VMs. Während man sich auf Letzteren einloggen und ein Problem in der gewohnten Shell-Umgebung untersuchen kann, bietet Docker eigene Interfaces, um etwa Logdateien auszulesen und Fehlermeldungen anzuzeigen.

Dieser Artikel geht im Detail auf die Möglichkeiten ein, die Admins beim Debuggen von Docker und von Docker-Containern haben. Am Anfang stehen diverse Tipps für den Admin, um Docker-Container von Anfang an Debugging-freundlich zu gestalten.

Debugging im Sinn – von Anfang an

Seine Container selber bauen ist deshalb prinzipiell sinnvoll, weil man sich mit fertigen Containern aus dem Docker-Hub immer dem Risiko des Blackbox-Modells aussetzt: Nach außen hin tut der Container oft, was er soll – was im Container drinsteckt und wie er gebaut worden ist, bleibt dem Admin allerdings unklar. Schon das Thema Compliance macht diesem Ansatz in vielen Unternehmen den Garaus. Wer seine Container aus dem Netz zieht, wird es zudem auch mit dem Debugging schwer haben.

Gar nicht schwer ist es hingegen, Docker-Container auf Basis eines Distributionsabbilds direkt vom Hersteller selbst zu bauen, wie etwa der Artikel zu Docker und Github im Linux-Magazin verdeutlicht hat [1]. Am Ende braucht es kaum mehr als ein ordentliches Docker-File, das die zentralen Anweisungen enthält. Wer diesen Weg geht und seine Container von Anfang an selbst baut, kann sie von der ersten Sekunde an so gestalten, dass sie leicht zu debuggen sind. Dafür gibt es mehrere Grundregeln, an die man sich halten sollte.

Ein Container pro Applikation

An erster Stelle steht eine Empfehlung, die logisch klingt, die aber oft selbst erfahrene Docker-Admins in den Wind schlagen: Es ist eine sehr gute Idee, mehrere Applikationen auch auf mehrere Docker-Container aufzuteilen, statt sie in einem großen Container zusammen auszurollen.

Die Auffassung, jeweils nur einen Container für mehrere Applikationen zu nutzen, spiegelt vermutlich noch die Denkweise, die echte VMs lange Zeit vorgaben: KVM & Co. produzieren ja einigen Overhead, der sich multipliziert, wenn viele kleine Programme in vielen einzelnen VMs ausgelagert sind.

Bei Containern ist dieser Faktor aber nur von untergeordneter Bedeutung, denn ein Container braucht lediglich etwas Platz auf der Platte – und die Containerimages der Hersteller sind so klein, dass sie kaum ins Gewicht fallen.

Wer den Overhead in Sachen Plattenplatz in Kauf nimmt, bekommt als Ausgleich erhebliche Erleichterungen in Sachen Debugging geboten. Denn einerseits zieht die Untersuchung eines Containers, in dem nur eine Applikation läuft, nicht gleich auch andere Applikationen in Mitleidenschaft. Und andererseits lässt sich ein einzelner Container mit einer Applikation leichter bauen und auch ersetzen als ein Konglomerat aus mehreren Komponenten.

Nicht zuletzt ist Docker ab Werk und durch die Art und Weise, wie Docker-Files aufgebaut sind, prädestiniert für eine Applikation pro Container. Weil es in Docker-Containern üblicherweise kein Init-System wie Systemd gibt, ruft der Admin das zu startende Programm im Docker-File meist direkt auf – es erhält innerhalb des Containers die virtuelle Prozess-ID 1, der Container betrachtet das Programm gewissermaßen als seinen Init-Prozess.

Pfercht der Admin wider besseres Wissen mehrere Dienste und Programme in den Container, führt das fast zwangsläufig zu Problemen. Dann muss er nämlich mit Shellskripten im Hintergrund arbeiten, was zwar zum erwünschten Effekt führt, das Debugging aber deutlich erschwert.

Container richtig starten

Apropos starten: Das Starten von Docker-Containern ist eine vielschichtige Angelegenheit und auch im Hinblick auf das Debugging von großer Bedeutung. Denn ob und wie ein Admin einen Container debuggen kann, hängt von der Frage ab, wie er die gewünschte Applikation aufruft. Gerade das Starten ist ein Thema, das beim Erstellen von Docker-Containern regelmäßig zur Verzweiflung führt, weil im schlimmsten Fall der Container nach dem »docker run«-Befehl sofort wieder abstürzt.

Grundsätzlich spielen beim Starten eines Docker-Containers zwei Einträge im Docker-File eine Rolle: »ENTRYPOINT« und »CMD«. Vielen Admins ist der Unterschied im ersten Moment gar nicht klar: »CMD« wird innerhalb des Containers direkt als Parameter an das Programm oder das Skript übergeben, das in »ENTRYPOINT« definiert ist. Falls der Container selbst keinen »ENTRYPOINT« festlegt, ist dieser üblicherweise »/bin/sh -c«. Steht in »CMD« also »/bin/ping« und der Admin definiert keinen separaten »ENTRYPOINT«, ruft der Container den Befehl »/bin/sh -c /bin/ping« beim Start auf (Abbildung 1).

Abbildung 1: Durch das Überschreiben des »ENTRYPOINT« – wie hier bei den Docker-Containern von Gitlab – lässt sich der Init-Prozess innerhalb des Containers bestimmen.

Abbildung 1: Durch das Überschreiben des »ENTRYPOINT« – wie hier bei den Docker-Containern von Gitlab – lässt sich der Init-Prozess innerhalb des Containers bestimmen.

Die beiden Werte sind also durchaus auch in Kombination nutzbar: So ist es etwa möglich und üblich, als »ENTRYPOINT« ein Programm festzulegen und es per »CMD« mit Parametern zu versorgen. Der Eintrag in »ENTRYPOINT« ist dann innerhalb des Containers der Prozess mit der virtuellen PID 1.

Wie schon erwähnt sollte es sich beim »ENTRYPOINT« idealerweise jedoch nicht um ein Shellskript handeln, das mehrere Prozesse startet. Falls sich das aus irgendwelchen Gründen nicht vermeiden lässt, müssen in jenem Shellskript alle Befehle bis auf den letzten entweder sauber terminieren oder sich mittels »&« in den Hintergrund verabschieden. Der letzte Befehl hingegen darf weder mit dem Rückgabewert »0« auslaufen, noch darf er in den Hintergrund – denn dann terminiert der gesamte Docker-Container.

Wenn der Start schiefgeht

Hat der Admin alle Vorsichtsmaßnahmen wie beschrieben getroffen und der Container stürzt trotzdem gleich beim Start ab, gibt es mehrere Wege, sich dem Problem zu nähern. Was viele nicht wissen: Der Docker-Aufruf auf der Kommandozeile, mit dem der Admin den Container startet, kennt auch den Parameter »-e« – mit dem lässt sich der »ENTRYPOINT« des Docker-File direkt auf der Kommandozeile überschreiben.

Ergänzend dazu sollte der Admin auch die Parameter »-it« angeben und sicherstellen, dass der Parameter »-d« im Aufruf des Containers fehlt. Dann startet der Docker-Container mit der Shell als Programm, auf der der Admin im nächsten Schritt den Befehl eingeben kann, der im eigentlichen »ENTRYPOINT« im Docker-File definiert ist.

Dabei sollte er jedoch sicherstellen, dass der Befehl sich nicht als Daemon gleich wieder in den Hintergrund zu verabschieden versucht. Sind diese Bedingungen erfüllt, wird schnell klar, was das Problem ist. Eine entsprechende Änderung des Docker-File schafft Abhilfe.

Übrigens: Auch der Wert von »CMD« Lässt sich auf der Kommandozeile überschreiben. Dazu genügt es, den erwünschten Wert einfach an das Docker-Kommando anzuhängen. Das ist auch deshalb praktisch, weil sich bei der Definition des »ENTRYPOINT« Parameter kaum definieren lassen – die übergibt der Admin stattdessen mittels »CMD«.

Die Ausgabe von Programmen

Ein zentrales Thema beim Debuggen von Applikationen in Docker-Containern ist deren Ausgabe auf Stdout und Stderr. Denn wenn das Programm im »ENTRYPOINT« mit den Parametern aus »CMD« startet, merkt der Admin außerhalb eben jenes Containers davon erst mal gar nix. Auf normalen Systemen ist das Lesen von Logs bekanntlich keine große Herausforderung: Hier läuft ein Syslog-Daemon mit, der Ausgaben der laufenden Programme sammelt und in diverse Logdateien schreibt. In einem Docker-Container läuft jedoch kein separater Syslog-Daemon.

Das Thema Logging funktioniert deshalb in Docker fundamental anders, als es auf normalen Systemen der Fall ist. Wegen des schon beschriebenen Umstands, dass in Docker-Containern auch kein Systemd oder überhaupt ein Init-System vorhanden ist, sind Anwendungen in Docker-Containern meist auf den Betrieb im Vordergrund ausgelegt. Das funktioniert so lange gut, wie der Admin den Grundsatz “eine Applikation pro Container” beherzigt. Dann landen die Logs meistens automatisch auf dem Standard-Ausgabekanal »stdout«.

Der Clou: Docker kennt eine »logs«-Operation, die alle Ausgaben eines Containers auf seinem Standard-Ausgabekanal auf der Kommandozeile des Hosts anzeigt. Wenn die Applikation innerhalb des Containers also diesen Kanal fleißig benutzt, macht sie die Ausgaben für den Admin auf dem Host sichtbar. Der Befehl lässt sich denkbar einfach nutzen: »docker logs test« zeigt die Ausgabe von »stdout« des Docker-Containers »test« an. Wer die Logdateien fortlaufend sehen möchte, schiebt noch ein »-«f zwischen »logs« und »test« oder den jeweiligen Namen des Containers. Im Grunde funktioniert »docker logs« also ganz ähnlich wie »tail -f« .

Alternativ zu »docker logs -f« steht übrigens auch der Befehl »docker attach« zur Verfügung: Der gibt den Input des Standard-Ausgabekanals einfach in Echtzeit im Terminal des Admin wieder und ist damit »docker logs -f« sehr ähnlich. Im Gegensatz zu den Inhalten von »attach« lässt sich jedoch der Inhalt von »docker logs« auch dann noch anzeigen, wenn ein Container nicht läuft – nur das lokale Abbild des speziellen Containers darf nicht mittels »docker rm« gelöscht worden sein.

Der Befehl eignet sich so ideal für jene Szenarien, in denen ein Container gleich nach dem Aufruf von »docker run« abstürzt. In der Ausgabe von »docker logs« steht danach zumeist der Grund, »attach« hingegen ist nur mit dem laufenden Container zu verwenden (Abbildung 2).

Abbildung 2: Der »logs«-Befehl von Docker gibt den Inhalt von »stdout« aus, also des Standard-Ausgabekanals des Containers.

Abbildung 2: Der »logs«-Befehl von Docker gibt den Inhalt von »stdout« aus, also des Standard-Ausgabekanals des Containers.

Sehen, was los ist

Ein für viele Admins unverzichtbares Tool ist das Werkzeug »top«, das die Prozesstabelle anzeigt, also eine Liste aller Prozesse, die auf dem System gerade laufen, samt ihrer jeweiligen Zustände. Wer Docker für Containerisierung nutzt, muss auf eine solche Prozessübersicht nicht verzichten: »docker top Name« zeigt sie für den Container »Name« auf der Kommandozeile an – und zwar kontinuierlich.

Freilich: Folgt der Admin dem Mantra, dass pro Container nur ein Prozess laufen sollte, wird die Ausgabe des Kommando nicht sonderlich umfangreich sein. Es kommt aber vor, dass ein Container Child-Prozesse startet oder Threads anlegt, die sich mit »top« beobachten lassen. Äquivalent dazu funktioniert auch »docker ps«, das eine Ausgabe ähnlich der von »ps« auf der Shell produziert.

Container temporär anhalten

Nicht so sehr auf die Innenperspektive des Containers bezieht sich der nun folgende Tipp: Docker kennt »pause«- und »unpause«-Befehle. Sie erlauben es dem Admin, laufende Container anzuhalten, ohne sie ganz zu stoppen oder gar zu löschen. Im Alltag kann das praktisch sein, wenn der Admin etwa einen Container debuggt und einen anderen anhalten muss, damit dieser nicht weiter Daten an den untersuchten Container sendet. Es wäre erheblich mehr Aufwand, den anderen Container komplett anzuhalten und danach zu starten. Die Kombination von »pause« und »unpause« hingegen sorgt dafür, dass die Applikation im Container am Ende des Vorgangs einfach weiterläuft, als sei in der Zwischenzeit nichts gewesen.

Debugging im Container

Selbst wenn der Start des Containers erfolgreich war, können noch Dinge schiefgehen. Wer wenig Erfahrung mit Docker hat, steht dann erst mal wie der sprichwörtliche Ochse vor dem Tor: Anders als bei einer echten virtuellen Maschine kann man sich in Container ja nicht einfach per SSH einloggen, um direkt vor Ort zu schauen, was los ist. Keine Panik: Docker bietet auch für diesen Zweck eine Möglichkeit, die auch das wohl mächtigste Debugging-Werkzeug für Docker überhaupt ist: »docker exec«.

Noch am besten vergleichen lässt sich »docker exec« mit »chroot« auf der Kommandozeile. Nach dem Aufruf von »docker exec Name Befehl« auf der Kommandozeile ruft Docker den ganzen Befehl innerhalb des Containers auf. Auf Systemebene bedeutet das, dass »docker« den bezeichneten Befehl innerhalb der für jenen Container definierten Namespaces aufruft, also etwa in den Netzwerk- und Prozess-Namespaces, die für den Container bereits existieren.

Wenn der Admin an »exec« die Parameter »-it« anhängt, funktioniert »docker exec« auch interaktiv: Der Befehl »docker exec -it /bin/bash« etwa ruft dann eine Docker-Shell innerhalb des Containers »ping« auf, die der Admin genau so nutzen kann, wie es bei einer normalen Shell auch der Fall wäre. Wer also auf der Suche nach einer Möglichkeit ist, sich in die laufenden Docker-Container “einzuloggen”, findet diese in »docker exec« (Abbildung 3).

Abbildung 3: »docker exec« katapultiert den Admin in eine Shell innerhalb des Containers, wo er sich dann umsehen kann.

Abbildung 3: »docker exec« katapultiert den Admin in eine Shell innerhalb des Containers, wo er sich dann umsehen kann.

Aus dem Glashaus raus

Entsprechend hat der Admin die Möglichkeit, sämtliche Binaries, die im Container vorhanden sind, aus einer so gestarteten Shell heraus aufzurufen. Klappt etwa in Sachen Netzwerk etwas nicht, helfen die üblichen Verdächtigen wie »ss« oder »ip« weiter – vorausgesetzt sie sind im Docker-Image enthalten, das dem Container zugrunde liegt. Wer die Container selbst baut, sollte hier auf eine etwaige Grundausstattung achten, denn den Basis-Images der Linux-Distributoren liegen längst nicht alle Werkzeuge bei, die der Admin aus seiner alltäglichen Arbeit gewohnt ist.

Auch andere Linux-Werkzeuge wie »ls« oder »ps« funktionieren im Docker-Container problemlos. Nicht vergessen sollte der Admin jedoch, dass er innerhalb des Containers wirklich nur diese Sicht hat, nämlich jene, in die die verschiedenen Namespaces und Sicherheitspolicies ihn zwingen. Will er Komponenten außerhalb des Containers debuggen, muss er ihn zuvor wieder verlassen.

Klar sein muss dem Admin außerdem, dass Dateisystem-Veränderungen lediglich das Overlay-Image des laufenden Docker-Containers betreffen. Löscht er diesen Container also und startet ihn aus dem Basis-Image neu, sind auch etwaige Modifikationen weg. Stößt der Admin auf Änderungen, die er vornehmen möchte, baut er idealerweise das Basis-Image des Containers neu.

Wichtig ist in diesem Kontext auch, dass sich der Hauptprozess eines laufenden Containers aus diesem heraus nicht einfach neu starten lässt. Nach dem Ändern einer Konfigurationsdatei lässt sie sich höchstens mit »SIGHUP« neu laden, falls das Tool das Signal unterstützt. Ein »SIGKILL« mit dem Plan, den Prozess anschließend neu zu starten, würde die laufende Shell sofort beenden, denn der Dienst ist schließlich die virtuelle Prozess-ID 1 des Containers. Stürzt »init« auf einem Linux-System ab, führt das zu einer Kernelpanic. Bei Docker terminiert der betroffene Container sofort.

Forensische Untersuchungen

Wer bei der Untersuchung eines Docker-Containers auf Inhalte stößt, die er gern genauer untersuchen möchte, steht vor einer Herausforderung: Aus gutem Grund sind die Dateien des Containers von jenen des Hosts strikt getrennt. Der »docker«-Befehl liefert aber ein »cp«-Kommando, das Dateien aus laufenden Containern auf den Host kopieren kann. Wer etwa vermutet, dass nach einem Einbruch in einen in Docker betriebenen Webserver Malware über diesen verteilt worden ist, kommt so an die besagten Dateien ran und kann sie eingehender Forensik unterziehen.

Neben dem schon erwähnten »logs«-Kommando ist auch »docker stats« ausgesprochen nützlich. Es zeigt laufend Metrikdaten eines Containers an, also zum Beispiel den Ressourcenverbrauch im Hinblick auf CPU oder RAM oder Netzwerk. Amok laufende Container lassen sich so schnell identifizieren und aus dem Verkehr ziehen.

Container-Inspektion

Viele Tricks und Kniffe kamen bereits zu Sprache. Docker selbst bietet aber auch noch diverse hilfreiche Informationsquellen. Ein sehr mächtiges Werkzeug ist der »inspect«-Befehl von Docker, der über beinahe jede Eigenschaft eines Containers Auskunft gibt.

Wenn der Admin einen Container mit »docker« startet, spricht das auf der Kommandozeile aufgerufene Werkzeug »docker« mit dem API des Docker-Daemon. Der Befehl zum Start eines Containers erfolgt in Form eines REST-Requests im Json-Format, den der Client an den Server schickt. Der Server arbeitet den Request im Anschluss ab und nutzt das Json-File auch für die interne Verwaltung des Containers.

Er aktualisiert das File zudem, falls sich Änderungen des Zustands des Containers ergeben, etwa weil der Admin ein neues Docker-Volume an diesen angeschlossen hat. Mit »inspect« holt sich der Admin den Inhalt jener Json-Datei wieder auf den Bildschirm und erschließt so eine zuverlässige Informationsquelle.

Infos in Feldern

Interessant ist für den Admin etwa der Eintrag im »State«-Feld, der Auskunft über den Zustand des Containers gibt. Hat der Admin beim Start des Containers Ports für diesen exponiert, stehen die in »NetworkSettings.Ports«. Praktisch ist auch, dass in der Inspect-Ausgabe die aktuell verbundenen Volumes des Containers sowie deren Pfade im Dateisystem des Hosts sichtbar sind.

Gleiches gilt für die Logdatei des Containers: Die Ausgabe von »stdout« zeichnet der Docker-Daemon für jeden Container auf, damit der Admin etwa per »logs«-Befehl darauf zugreifen kann. In der Json-Datei des Containers steht der automatisch generierte Pfad zu jener Datei, die die Ausgabe von »stdout« enthält, sodass der Admin also auch direkt auf diese zugreifen kann.

Die mit Abstand wichtigste Information auf der Inspect-Ausgabe ist jedoch die Umgebung, die der Container vom Docker-Daemon beim Start übergeben bekommen hat. Es ist durchaus üblich, Einfluss auf die Konfiguration des Containers über Umgebungsvariablen zu nehmen, die »docker run« mittels des Parameters »-e« übergeben bekommt. Im Docker-File lassen sich diese Variablen mit den gesetzten Werten dann entsprechend verarbeiten.

Klappt beim Übergeben der Umgebungsparameter etwas nicht, bekommt der Admin mit der Ausgabe von Inspect immerhin heraus, welche Variablen für den Container bei dessen Start gesetzt waren (Abbildung 4).

Abbildung 4: Das Kommando »docker inspect« zeigt detaillierte Informationen über den Container an, etwa die verbundenen Netzwerke oder dessen aktuellen Zustand.

Abbildung 4: Das Kommando »docker inspect« zeigt detaillierte Informationen über den Container an, etwa die verbundenen Netzwerke oder dessen aktuellen Zustand.

Layer untersuchen

Kommt der Admin in die Verlegenheit, ein Setup zu debuggen, das er nicht selbst gebaut hat und das auf fertigen Containern vom Docker-Hub basiert, steht er vor einer echten Herausforderung. Hier hat er es nicht selten mit einem so genannten Layer Cake zu tun: Das ursprüngliche Image vom Docker Hub haben verschiedene Nutzer verändert und modifiziert, sodass sich kaum nachvollziehen lässt, wer wann welche Änderung zu verantworten hat.

Eine kleine Hilfe bietet hier Docker selbst: Mit dem »docker history«-Befehl lässt sich für ein Image anzeigen, welche Veränderungen es im Laufe seiner Existenz erfahren hat. Inklusive etwaiger Kommentare sowie entsprechender Anmerkungen – die möglicherweise sogar etwas Licht ins Dunkel zu bringen vermögen (Abbildung 5).

Abbildung 5: »docker history« legt offen, welche Veränderungen an einem Docker-Abbild vorgenommen wurden.

Abbildung 5: »docker history« legt offen, welche Veränderungen an einem Docker-Abbild vorgenommen wurden.

Infos

  1. Martin Loschwitz, “Containersicherheit”: Linux-Magazin 01/18, S. 30

Der Autor

Martin Gerhard Loschwitz ist Telekom Public Cloud Architect bei T-Systems und beschäftigt sich beruflich vorrangig mit Themen wie Open Stack, Ceph und Kubernetes.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben