Nicht zuletzt für eine zuverlässige Fehleranalyse benötigen Administratoren und Entwickler die vollständigen Protokolle ihrer Anwendungen. Moderne Scale-out-Umgebungen mit Containern erschweren jedoch das Einholen von Logs. Wir stellen Konzepte und Methoden vor, um die Logdateien von Applikationen in Kubernetes-Umgebungen mithilfe eines Sidecar-Containers einzusammeln.
Im Artikel "Planen, Packen und Verschiffen" [1] aus der Juli-Ausgabe haben wir Mittel und Wege vorgestellt, um Applikationen in Containern laufen zu lassen. An die Stelle einer großen monolithischen VM mit allen Diensten tritt hier eine Gruppe von mehreren kooperierenden Containern, die jeweils nur einen Dienst betreiben. Um die Verfügbarkeit und Performance zu verbessern, skalieren containerisierte Applikationen die Zahl der jeweiligen Container je nach Bedarf. So lassen sich auch einzelne Komponenten einer Applikation separat vom Rest ändern, beispielsweise für ein Update.
Das klassische Beispiel für die moderne Scale-out-Architektur sind Webapplikationen. Eine Reihe von Containern stemmt das Datenbank-Backend, wieder andere beherbergen ein klassisches Netzwerk-Dateisystem für statische Inhalte. Eine skalierende Gruppe betreibt das Web-Frontend der Anwendung. Redundante Container mit einem Message-Bus oder einem Key Value Store sorgen für die Kommunikation aller beteiligten Komponenten. Die Anwendungsentwickler des Frontends können ihren Teil der Anwendung dynamisch ändern und updaten, ohne dabei die Funktion des Backends oder anderer Komponenten zu behelligen.
Eine Scale-out-Architektur bringt aber auch eine ganze Reihe von Herausforderungen mit sich – und das nicht nur für die Anwendungsentwickler. Auch die Administratoren einer skalierenden Umgebung müssen stets den Überblick behalten. Das betrifft sowohl die Metriken als auch die Logs. In monolithischen Szenarien kann der Administrator einfach einen Logkollektor-Client in die Anwendungs-VM integrieren und diesen mit statischen Konfigurationen versehen. Bei dynamisch skalierenden Umgebungen funktioniert das nicht mehr, denn dort kommen die Logs von sehr vielen Containern mit stets wechselnden Namen und Adressen.
Im Artikel "Planen, Packen und Verschiffen" [1] aus der Juli-Ausgabe haben wir Mittel und Wege vorgestellt, um Applikationen in Containern laufen zu lassen. An die Stelle einer großen monolithischen VM mit allen Diensten tritt hier eine Gruppe von mehreren kooperierenden Containern, die jeweils nur einen Dienst betreiben. Um die Verfügbarkeit und Performance zu verbessern, skalieren containerisierte Applikationen die Zahl der jeweiligen Container je nach Bedarf. So lassen sich auch einzelne Komponenten einer Applikation separat vom Rest ändern, beispielsweise für ein Update.
Das klassische Beispiel für die moderne Scale-out-Architektur sind Webapplikationen. Eine Reihe von Containern stemmt das Datenbank-Backend, wieder andere beherbergen ein klassisches Netzwerk-Dateisystem für statische Inhalte. Eine skalierende Gruppe betreibt das Web-Frontend der Anwendung. Redundante Container mit einem Message-Bus oder einem Key Value Store sorgen für die Kommunikation aller beteiligten Komponenten. Die Anwendungsentwickler des Frontends können ihren Teil der Anwendung dynamisch ändern und updaten, ohne dabei die Funktion des Backends oder anderer Komponenten zu behelligen.
Eine Scale-out-Architektur bringt aber auch eine ganze Reihe von Herausforderungen mit sich – und das nicht nur für die Anwendungsentwickler. Auch die Administratoren einer skalierenden Umgebung müssen stets den Überblick behalten. Das betrifft sowohl die Metriken als auch die Logs. In monolithischen Szenarien kann der Administrator einfach einen Logkollektor-Client in die Anwendungs-VM integrieren und diesen mit statischen Konfigurationen versehen. Bei dynamisch skalierenden Umgebungen funktioniert das nicht mehr, denn dort kommen die Logs von sehr vielen Containern mit stets wechselnden Namen und Adressen.
Die meisten kommerziellen Kubernetes-Implementierungen kümmern sich von Haus aus um die Metriken. Denn der Management-Layer benötigt Informationen wie CPU-Last und Speicherbelegung einzelner Pods und Container, um beispielsweise Trigger für Scale-up- oder Scale-down-Aufgaben auszulösen. Anders sieht es jedoch bei den Logs aus. Um das Sammeln der Applikationsprotokolle muss sich der Nutzer selbst kümmern.
Alte Bekannte kommen an ihre Grenzen
OS-Templates für Container sollen so klein wie möglich bleiben und nur das Nötigste mitbringen. Es gibt kein ausführliches INIT-System wie Systemd und auch keine Logdienste wie Journald oder Syslog. Ziel ist es, mit dem Container nur einen Dienst zu starten. Und natürlich soll ein Container auch keine Loginformationen auf dem temporären lokalen Dateisystem sammeln. Viele Startskripte rufen den gewünschten Dienst daher einfach im "Vordergrund-Modus" auf, wie beispielsweise exec httpd -DFOREGROUND in einem Apache-Container.
Damit landen auch alle Logausgaben via den altbekannten Datenströmen "stdout" und "stderr" auf der Konsole. Von dort können sie Container-Manager wie Docker oder Podman abgreifen und weiter behandeln. Diese Dienste bringen dafür sogenannte Logtreiber mit. Diese leiten die stderr- und stdout-Ausgaben eines Containers zu einem Kollektor weiter.
In der simpelsten Form schreiben Docker, Podman oder Kubernetes die Container-Logs in ein eigenes Dateiformat, sodass sie der Nutzer mittels docker/podman/kubectl logs undlt;containeridundgt; abrufen kann.
In der Liste der verfügbaren Treiber finden sich unter anderem Syslog, Journald und Fluentd. Alle drei leiten die Logausgaben der laufenden Container an den jeweiligen Serverdienst weiter, der auf dem Container-Host selbst läuft. Syslog und Journald reichen für Einzelinstallationen und Entwickler- und Testumgebungen aus. Wer jedoch einen besseren Überblick benötigt, kommt um qualifizierte und gruppierte Logs kaum herum und sollte sich daher mit Fluentd auseinandersetzen. Wie die Konfiguration von Fluentd/Fluentbit für Container im Detail funktioniert, haben wir im Artikel "Container von oben" in der Ausgabe 11/2019 [2] ausführlich beschrieben.
Ein simples Fluentd-Szenario setzt jedoch voraus, dass der Fluentd-Dienst selbst auf dem Container-Host (oder in einem Container auf dem Host) läuft und dass der Container tatsächlich nur ein Log via stdout ausgibt. Das wiederum schränkt die Skalierung ein. In einer größeren Kubernetes-Umgebung wollen IT-Verantwortliche die Logs nicht pro Host, sondern basierend auf der zugehörigen Anwendung einsammeln. Erschwerend kommt hinzu, dass diverse Anwendungen ihre Loginformationen nicht so einfach auf stdout/stderr ausgeben.
Auch gibt es Szenarien, bei denen der Dienst im Container mehrere Logs in unterschiedlichen Formaten generiert. Das wiederum würde die Auswertung via stdout erheblich erschweren. Läuft beispielsweise eine PHP-Applikation auf einem Nginx-Webserver in einem Container, entstehen bis zu vier Logausgaben: Die Access- und Error-Logs des Nginx-Servers, das Log des PHP-Interpreters und das der PHP-Applikation selbst.
POD, Container, Replica Set und Deployment
Oftmals bringen selbst IT-Profis die Konzepte von PODs, Containern, Replica-Sets und Deployments durcheinander, deshalb hier noch einmal die Begriffe im Schnelldurchlauf: In einem Container läuft nach Möglichkeit nur ein Dienst mit der minimalen OS-Runtime und der Applikation selbst. Ein POD besteht aus mindestens einem Container und den für Kubernetes wichtigen Metadaten wie Environments und Variablen. Er kann mehrere direkt zusammengehörige Container enthalten. Die Container innerhalb eines PODs können aber nicht unabhängig voneinander skalieren. In unserem Beispiel enthält ein POD den Container mit der Applikation und einen Container mit dem Log-Shipper. Ein Replica Set definiert, wie Kubernetes die PODs skaliert und betreibt. Das Set legt Parameter fest wie die mindestens aktiven PODs auf separaten Hosts oder die Unter- und Obergrenze an laufenden PODs. Für nicht skalierende Applikationen kann das Replica Set aber auch Failover-Vorschriften wie den Betrieb eines aktiven und eines passiven PODs vorgeben. Das Deployment schließlich ist die deklarative Beschreibung, wie eine Applikation zu laufen hat. Es beschreibt die gewünschten PODs und gegebenenfalls mehrere Replica Sets (Frontend, Backend-Datenbank et cetera) und kümmert sich darum, dass immer ausreichend PODs laufen, solange das Deployment aktiv ist.
Beiwagen für den Log Collector
Alternativ zur Log-Collection via stdout mit Einholung über den Host kann der Nutzer seinen Applikations-Containern einen "Beifahrer" speziell für das Logging zur Seite stellen. Dieses Konzept nennt sich Sidecar-Container. Dieser Beiwagen betreibt dabei die Logsammlung für einen einzelnen oder eine ganze Gruppe von Containern. Er läuft dabei als zusätzlicher Container innerhalb eines PODs. Sollte die Applikation wachsen und weitere Pods starten, bekommt jeder Applikationsdienst seinen eigenen Beifahrer.
Als Beifahrer kann der Sidecar-Container die stdout-Ausgaben des zu loggenden Containers abfangen und verarbeiten. in diesem Modus muss dann jedoch der loggende Container in seine Standardausgabe den $HOSTNAME einbauen, damit der Sidecar-Container die Logs qualifizieren und mehrere Quellen auseinanderhalten kann. In der Praxis kommt es aber viel häufiger vor, dass die Anwendungen ihr Protokoll in eine eigene Logdatei schreiben. Auch hier kann der Sidecar-Container die Informationen abrufen, indem das Setup ein geshartes Volume für Applikation und Sidecar vorsieht. Hier konfiguriert der Nutzer seine containerisierte Anwendung so, dass diese ihre Logausgabe in verschiedene Verzeichnisse auf einem gesharten Speichermedium ablegt. Dieses Medium bindet ebenfalls der Sidecar-Container mit dem Log-Collector an. Ein entsprechendes k8s-Deployment kann dann (in Auszügen) so aussehen wie im ersten Listing-Kasten "Anbindung des Sidecar-Containers".
Im Log-Collector-Container können Dienste wie Fluentd, das Leichtgewicht Fluentbit oder auch Filebeat von Elastic selbst laufen. Der Administrator kann dazu bestehende Community-Images als Vorlage verwenden oder mit Buildah ein eigenes erstellen. Dazu bedarf es der passenden Dienstkonfiguration. Unter Docker oder Podman würde der Nutzer hier eine angepasste Konfigurationsdatei über die Option "COPY:" in den Container einbringen.
In einem Kubernetes-Deployment gelingt dies etwas eleganter: Der Administrator hinterlegt den Inhalt der Dienstkonfiguration dazu in einer sogenannten "Config Map". Das Deployment bindet diese Config Map dann über ein Volume (name: config) in den Log-Shipper-Container ein. So kann der User auf seiner Kubernetes-Umgebung eine ganze Reihe verschiedener Log-Shipper-Konfigurationen für verschiedene Dienste hinterlegen. Ein Beispiel für Fluentbit und Nginx im Applikations-Container finden Sie im zweiten Listing-Kasten "Einbinden von Fluentbit und Nginx".
Listing 2: Einbinden von Fluentbit und Nginx
Lik:apiVersion: v1
kind: ConfigMap
metadata:
name: my-service-config
data:
fluent-bit.conf: |
[INPUT]
Name tail
Tag nginx.access
Parser nginx
Path /var/log/1/access.log
[INPUT]
Name tail
Tag nginx.error
Parser nginx
Path /var/log/2/error.log
[OUTPUT]
Name forward
Match *
Host my.elasticsearch.host
Port 24224
Das "data"-Feld der Config Map kann dabei mehrere Dateien enthalten, falls der verwendete Log-Shipper dies benötigt. Eines der wenigen Probleme hierbei ist weniger das Sidecar-Konzept als die verwendeten Log-Shipper-Protokolle. Im genannten Beispiel leitet Fluentbit die qualifizierten Logdaten an den Host auf der fiktiven Adresse "my.elasticsearch.host" weiter, nutzt dazu aber den Non-Standard-Port 24224. Das funktioniert, solange der EFK-Stack außerhalb des Kubernetes-Clusters arbeitet und in der Lage ist, Daten auf Port 24224 aufzunehmen.
Innerhalb eines Kubernetes-Clusters gestaltet sich das jedoch etwas schwieriger. Den Inbound-Traffic leiten hier "Router" zum Ziel weiter, wobei der Begriff technisch nicht passt, handelt es sich doch um einen Reverse-Proxy. Der wiederum übermittelt in der Regel nur die Ports 80, 443 und eventuell 6443 (Kubernetes API) in den Cluster, nicht jedoch Non-Standard-Ports wie 24244. Hier muss der Administrator mit seinem jeweiligen Log-Shipper prüfen, ob dessen Protokoll sich wie HTTPS über einen Reverse-Proxy weiterleiten lässt. Alternativ erlauben einige Kubernetes-Distributionen, dass Nutzer Dienste an die statischen Adressen einzelner Kubernetes-Knoten binden. Damit lassen sich Daten ohne Router an Non-Standard-Ports schicken.
Weiterer Beifahrer fürs Löschen
Beim Loggen in Dateien verbraucht die Applikation Plattenplatz. Immer wieder fallen Systeme aus, weil volle Logfiles sämtlichen freien Plattenplatz belegen. Sobald containerisierte Anwendungen Shared Storage verwenden, droht auch hier der Überlauf. Im Beispiel aus dem Listing-Kasten schreibt die containerisierte Applikation munter Logdaten auf ein gemeinsames Volume. Der Log-Sidecar verarbeitet diese zwar, löscht sie aber nach getaner Arbeit nicht. Um den drohenden Plattenüberlauf zu verhindern, muss der Nutzer einen weiteren Sidecar in den POD integrieren, der sich um die Logrotation kümmert. Dazu gibt es im Internet eine ganze Reihe fertiger Logrotate-Images für Kubernetes, die sich um die Archivierung und Löschung alter Logdaten kümmern.
Fazit
Auf den ersten Blick mag es aufwendig erscheinen, jedem einzelnen Applikations-Container zwei weitere zur Seite zu stellen, die sich einzig und allein um das Logmanagement kümmern. Aber auch in klassischen Setups müssen Nutzer manuell die Logdienste in den jeweiligen VMs konfigurieren. Der initiale Aufwand mit eigenen Sidecar-Images wird sicher etwas mehr Zeit in Anspruch nehmen, als auf die Schnelle einen Logclient in einer VM einzurichten. Auf lange Sicht vereinfacht sich die Verwaltung jedoch. Denn bei neuen Applikationen müssen die Administratoren nur im Deployment auf den bereits bestehenden Sidecar und eine dazu passende Konfiguration verweisen. Zudem lassen sich diese Images von verschiedenen Mitarbeitern betreuen und unabhängig von der Applikation selbst updaten.