Applikationen laufen dank Containern isoliert und damit stark entkoppelt vom Host-System. Geteilt wird sich vor allem der Kernel. Das ist auch so gewünscht, bringt es doch die Vorteile, weshalb wir Docker alle nicht mehr missen wollen. Beim Herausfinden des derzeitigen Status eines Containers, stellt die Betrachtungsweise jedoch ein Problem dar.

Lassen wir uns die Status-Informationen eines laufenden Docker-Containers anzeigen, sieht das Ergebnis formatiert wie folgt aus:

$ docker container inspect --format='{{json .State}}' CONTAINER_ID
{
  "Status": "running",
  "Running": true,
  "Paused": false,
  "Restarting": false,
  "OOMKilled": false,
  "Dead": false,
  "Pid": 13070,
  "ExitCode": 0,
  "Error": "",
  "StartedAt": "2017-06-14T13:13:42.55766548Z",
  "FinishedAt": "0001-01-01T00:00:00Z"
}

Hier stehen nun allerhand Infos drin, die man im Detail auch in Dockers API Dokumentation nachlesen kann. Die Wesentlichste steht gleich zuoberst und heißt Status. Sie kann den Wert running oder exited annehmen. Der Container und damit der Prozess läuft oder läuft nicht. Die Flags Running, Paused, Restarting, OOMKilled und Dead versuchen hier noch etwas zu differenzieren und Aussage darüber zu treffen, warum ein Container gerade nicht läuft. OOM bedeutet übrigens „Out of memory“. Man merkt, dass diese Aussagen aber immer stark am Prozess, den ein Docker-Container nun mal darstellt, festgemacht werden.

Nun stellen wir uns aber mal vor, dass in unserem Container eine Applikation läuft, die sich unter Umständen gerade in einer Endlosschleife befindet und somit gar nicht im Stande ist, korrekt zu arbeiten oder denkbar wäre es auch, dass in unserem Container mehrere Prozesse mittels Prozess-Manager, wie supervisord laufen. Docker selbst ist der Meinung, dass der angestoßene Prozess, also der Prozess-Manager, wunderbar läuft. Der Prozess-Manager selbst aber hat Mühe die Subprozesse am Laufen zu halten, startet sie wiederkehrend neu oder hat es nach mehrmaligen Neustarten vielleicht sogar aufgegeben. Die Applikation befindet sich also in keinem gesunden Zustand, Anfragen können nicht bedient werden, Docker sagt aber weiterhin: Läuft! Alles tutti!

An diesem Punkt setzt HEALTHCHECK an. Diese Direktive kann in ein Dockerfile integriert werden, um zu beschreiben, wie der Gesundheitszustand innerhalb eines Containers überprüft werden kann und wird wie folgt geschrieben:

HEALTHCHECK [OPTIONS] CMD command

Mögliche Optionen sind:

  • --interval=DURATION (default: 30s)
  • --timeout=DURATION (default: 30s)
  • --retries=N (default: 3)

Wir definieren damit also den Interval im dem der Health Check durchgeführt wird, wie lange gewartet werden soll bis der Versuch als gescheitert angesehen wird und nach wie vielen Fehlversuchen der Gesundheitszustand als "krank" gewertet wird. Nach dem Schlüsselwort CMD folgt das gewünschte Shell-Kommando. In einer Microservice-Architektur, die auf HTTP setzt, kommt hier häufig ein testender Aufruf mittels curl hinein, aber auch ein prüfender Verbindungsaufbau per TCP wäre denkbar. Weitere Infos dazu finden sich in der Docker Doku. Ein Dockerfile mit einem schlanken HTTP-Server in Golang könnte wie folgt aussehen:

FROM golang:1.8-alpine

RUN apk --no-cache add --update curl
COPY src /usr/src/app
RUN cd /usr/src/app && \
    go build && \
    chmod +x app
WORKDIR /usr/src/app
CMD ["./app"]
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s CMD curl -f --silent http://localhost/ping || exit 1

Dieses Docker-Image, basierend auf dem offiziellen Golang-Image im schlanken Alpine-Gewand, bekommt cURL mit hineingelegt, kopiert den Source-Code der Applikation, kompiliert die Software, führt diese aus und macht sie unter Port 80 verfügbar. Der abschließende HEALTHCHECKüberprüft alle 30 Sekunden und dabei wird 3 Sekunden darauf gewartet, ob das curl-Kommando etwas Erfolgreiches zurück liefert, andernfalls wird der Vorgang mit einem Exit-Code 1 quittiert. Schauen wir uns nun erneut den Container an, offenbaren sich Neuigkeiten:

$ docker container inspect --format='{{json .State}}' CONTAINER_ID
{
  "Status": "running",
  "Running": true,
  "Paused": false,
  "Restarting": false,
  "OOMKilled": false,
  "Dead": false,
  "Pid": 13378,
  "ExitCode": 0,
  "Error": "",
  "StartedAt": "2017-06-14T15:18:11.012108575Z",
  "FinishedAt": "0001-01-01T00:00:00Z",
  "Health": {
    "Status": "healthy",
    "FailingStreak": 0,
    "Log": [
      {
        "Start": "2017-06-14T15:18:56.499571598Z",
        "End": "2017-06-14T15:18:56.555418931Z",
        "ExitCode": 0,
        "Output": "Pong"
      },
      [...]
    ]
  }
}

Hinzugekommen ist eine neue Eigenschaft Health, die nun einen eigenen Status, unabhängig vom Prozess-Status, mit den letzten Ergebnissen liefert. Mein kleiner HTTP-Server liefert beim Aufruf der Route /ping ein simples Pong zurück, was sich ebenfalls in der Liste der letzten Versuche als Ausgabe finden lässt. Verfolgt man DevOps-Ansätze, wie "You build it, you run it", bekommt die Entwickler*in mit dem Health Check die Möglichkeit, den Gesundheitsstatus seiner eigenen Anwendung damit von außen einsehbar zu machen. Spannend ist dies natürlich vor allem in der Produktivumgebung, wo mit Monitoring-Tools, wie Prometheus der Gesundheitszustand abgegriffen und grafisch aufbereitet wird. Für einen Orchestrator, wie Kubernetes oder Docker Swarm (Mode) sind so Rückschlüsse möglich, um Maßnahmen, wie den Neustart des Containers, einleiten zu können und zwar nicht nur, weil der Prozess beendet ist, sondern eben auch aufgrund eines tieferen Blickes in die Applikation.

Bild von Hush Naidoo