IoT und Edge-Computing mit Podman

Seite 2: Podman und Edge-Computing

Inhaltsverzeichnis

Ähnlich wie Cloud-Computing vor gut zehn Jahren ist der Begriff Edge-Computing heute nicht eindeutig definiert – es existieren verschiedene Blickwinkel darauf. Das Podman-Team versteht darunter den Betrieb außerhalb klassischer Server im Rechenzentrum (On Premises) oder der Cloud. Diese Definition schließt typische Systeme aus dem Internet der Dinge oder dem Industrial Internet of Things (IIoT) ein, vom Getränkeautomaten über PKW bis zur Hochsee-Boje.

Um Container und auch Podman in solchen Umgebungen mit einer Vielzahl verteilter Systeme sinnvoll nutzen zu können, müssen eine Reihe von Anforderungen erfüllt sein. Das Podman-Entwicklungsteam hat sich dabei auf drei kritische Eigenschaften konzentriert:

  • Zuverlässigkeit: Die Workloads müssen zuverlässig laufen und sich nach einem Absturz direkt neu starten. Service-Ausfallzeiten sind teuer und können im Einzelfall sogar gefährlich sein, sofern kritische Systeme betroffen sind. Falls ein Container auf einer Hochsee-Boje abstürzt, muss er ohne manuelles Zutun wieder anlaufen.
  • Automatisierung: Die Gegebenheiten im Edge-Computing verlangen einen hohen Grad an Automatisierung. Nicht nur auf hoher See ist die Netzanbindung oft unzuverlässig, sodass sich Edge-Workloads automatisiert regelmäßig selbstständig updaten sollten. Fehlgeschlagene Updates müssen erkennbar sein, um Zuverlässigkeit garantieren zu können.
  • Fehlererkennung: Im Sinne der Service-Updates geht es darum, den korrekten Betrieb der Workloads zu überwachen. Health-Checks helfen dabei, nicht nur Abstürze, sondern auch darüber hinaus gehende Fehler automatisch zu erkennen – und die Container neu zu starten.

Um die genannten Anforderungen zu erfüllen, spielt die Integration zwischen Podman und systemd eine tragende Rolle. Erste Versuche, Container innerhalb eines systemd-Service mit Docker auszuführen, unternahm das Container-Team von Red Hat bereits im Jahr 2016. Das Unterfangen scheiterte jedoch an der fehlenden Unterstützung durch die Maintainer von Docker, die andere Pläne verfolgten und nicht auf die eingereichten Pull Requests eingingen.

Damit eröffnete sich auch die Lücke, die Podman ab 2018 füllen konnte. Dessen traditionelle Fork-Exec-Architektur gestaltet die Integration mit systemd vergleichsweise einfach. Container sind darin Kind-Prozesse von Podman. Zudem agiert conmon als Hauptprozess (main PID) des systemd-Service (siehe Abbildung 2). conmon ist ein kleines, in C geschriebenes Programm, das unter anderem für das Monitoring der Container zuständig ist. Es kümmert sich um Logs, hält bestimmte Deskriptoren und Ports offen und beendet sich mit dem Exit-Code des Containers. Damit eignet sich conmon perfekt für den Einsatz in systemd. Die Client-Server-Architektur von Docker lässt sich deutlich schwieriger mit systemd integrieren. Zum einen läuft der Docker-Daemon standardmäßig mit Root-Rechten und die Überwachung aller Container übernimmt containerd. Einen eindeutigen, mit conmon vergleichbaren Hauptprozess gibt es bei Docker nicht – und das gilt bis heute.

Integration von Podman mit systemd: Container sind Kind-Prozesse von Podman und conmon agiert als main PID (Abb. 2).

Um containerisierte Workloads mit Podman innerhalb von systemd auszuführen, bedarf es der sogenannten Units. Vergleichbar zur Deklaration eines Service durch eine Kubernetes-YAML-Datei lassen sich systemd-Services auch als instanziierte systemd-Unit ausführen. Die gestalten sich jedoch schnell sehr komplex, insbesondere wenn sie zuverlässig und fehlertolerant arbeiten sollen. Hilfe beim Erstellen von Units bietet podman generate systemd. Als Eingabe benötigt das generate-systemd-Kommando einen Container oder Pod und gibt dann eine systemd-Unit in Form einer Textdatei aus. Sollen im weiteren Verlauf auch neue Features einfließen, müssen sowohl die Units als auch die Container beziehungsweise Pods immer wieder neu generiert werden. Der Workflow mit generate-systemd gestaltet sich dadurch etwas holprig.

Podman liefert seit der im April 2023 erschienenen Version 4.5 ein neues Werkzeug namens Quadlet, das zur Vereinfachung des Workflows beiträgt. Quadlet ist vergleichbar mit Docker Compose oder Kubernetes-YAML, aber gezielt auf das Ausführen von Containern mit Podman in systemd zugeschnitten. Die Arbeitsweise von Quadlet lässt sich am einfachsten anhand eines Beispiels nachvollziehen (die folgende Datei liegt unter $HOME/.config/containers/systemd/heise.container parat).

[Container]
Image=nginx:latest
ContainerName=heise-container
PublishPort=8080:80

[Service]
Restart=on-failure

Der Inhalt der Datei erinnert nicht zufällig an die herkömmliche Syntax einer systemd-Unit. Quadlet-Dateien erweitern die systemd-Syntax um zusätzliche Tabellen. Das Beispiel enthält eine "Container"-Tabelle, die einen Container und dessen Eigenschaften deklariert. Im Beispiel wird ein nginx-Container namens heise-container erstellt, der auf Port 8080 auf dem Host erreichbar ist, und die Restart-Policy ist auf "on-failure" gesetzt. Das führt dazu, dass der systemd-Service – und damit der Container – neu startet, wenn der Container sich mit einem Exit-Code ungleich 0 beendet. Das ist eine einfache Maßnahme, um die gewünschte Zuverlässigkeit im Edge-Computing zu garantieren.

Quadlet selbst ist ein systemd-Generator, der bei einem Boot, User-Login oder auch einem daemon-reload von systemd startet. Um die heise.container-Datei in einen systemd-Service zu übersetzen, lässt sich die User-Session von systemd einfach neu laden:

$ systemd --user daemon-reload
$ systemctl --user status heise.service
heise.service
Loaded: loaded (/home/vrothberg/.config/containers/systemd/heise.container; generated)
Drop-In: /usr/lib/systemd/user/service.d
         └─10-timeout-abort.conf
Active: inactive (dead)

Der generierte Service übernimmt bequemerweise den Namen der Quadlet-.container-Datei und lässt sich direkt starten.

$ systemctl --user start heise.service
$ podman ps
CONTAINER ID    IMAGE    COMMAND    CREATED    STATUS    PORTS    NAMES
1cd75f3516e4   docker.io/library/nginx:latest   nginx -g daemon o...2 minutes ago  Up 2 minutes 0.0.0.0:8080->80/tcp heise-container

Wie in den Manpages von Podman beschrieben, unterstützt Quadlet das Erstellen von Volumes sowie Netzwerken und kann neben Containern auch Kubernetes-Workloads ausführen. Um das Gästebuch in Quadlet auszuführen, genügt eine simple guestbook.kube-Datei:

[Kube]
Yaml=/home/vrothberg/guestbook.yaml

Ähnlich wie bei .container-Quadlets gibt es eine dedizierte [Kube]-Tabelle, die das gewünschte Kubernetes-YAML referenziert. Das Ausführen von Kubernetes-YAML innerhalb von systemd ist deutlich komplexer als das eines einzelnen Containers, da mehrere Pods und mehrere Container innerhalb des Service laufen. Die Zuordnung eines Hauptprozesses ist damit nicht so eindeutig wie bei einem .container-Quadlet.

$ systemctl --user daemon-reload
$ systemctl --user start guestbook.service
$ podman ps --format "{{.Names}}"
33297cc21b2e-service
6d229813d528-infra
guestbook-backend
guestbook-frontend

Sobald der Service startet und die laufenden Container inspiziert, wird ein Service-Container (33297cc21b2e-service) gelistet. Dieser – respektive der assoziierte conmon-Prozess des Containers – agiert als Hauptprozess des systemd-Service. Der Service-Container wird vor allen anderen Containern gestartet und als letzter gestoppt. Er kümmert sich darüber hinaus um die Kommunikation mit dem Notify-Socket und dient als Notify-Proxy für laufende Container, was insbesondere bei Rollbacks zum Einsatz kommt.

Der kombinierte Einsatz von Quadlet und Podman trägt zu einem hohen Grad an Robustheit und Zuverlässigkeit bei. Sowohl Podman als auch systemd kümmern sich darum, einen sauberen Betrieb der Container sicherzustellen und diese bei Fehlern oder Abstürzen gegebenenfalls neu zu starten. Nach einer gewissen Betriebsdauer fallen im Zuge der Day-2-Operations erste Updates an, die zu den typischen Aufgaben zählen, die nach einem erfolgreichen Deployment "ab dem zweiten Tag" abzuarbeiten sind. Insbesondere im Edge-Computing müssen solche Updates automatisch erfolgen. Die betroffenen Maschinen müssen sie selbstständig laden und einspielen, da ein manuelles Update häufig nicht möglich ist. Zum einen können Tausende solcher Maschinen parallel betrieben werden. Zum anderen sind diese oft nur temporär mit dem Netzwerk verbunden.

Podman beherrscht das automatische Updaten von Containern seit Version 2.0. Dazu müssen die Container in einer systemd-Unit laufen – idealerweise mittels Quadlet – und passend konfiguriert sein. Ein .container-Quadlet könnte folgendermaßen aussehen:

[Container]
Image=registry.access.redhat.com/ubi9:9.2
ContainerName=heise-container
Exec=/usr/bin/heise
AutoUpdate=registry

Die Auto-Update-Policy ist hier auf registry konfiguriert. Sofern ein neues Container-Image in der Registry zur Verfügung steht, lädt Podman das Image herunter und startet den systemd-Service neu. Das Update erfolgt damit transparent durch den Neustart der systemd-Unit. Hierfür ist das podman-auto-update-Kommando zuständig, für das auch ein eigener systemd-Service (podman-auto-update.service) zur Verfügung steht, der sich über einen gleichnamigen Timer auch zeitgesteuert anwerfen lässt. Zu beachten ist dabei, dass ein Update immer mit demselben Image-Namen erfolgt. Im Beispiel muss also ein neues registry.access.redhat.com/ubi9:9.2-Image in der Registry zur Verfügung stehen, etwa nach einem Update von Version 9.2.0 auf 9.2.1. Image Tags und semantische Versionierung kommen hier zum Tragen.

Ab Podman Version 4.6 lässt sich das im Beispiel verwendete Feld AutoUpdate Quadlet setzen. Ältere Versionen müssen das Auto-Update-Label wie folgt setzen: Label=io.containers.autoupdate=registry. Neben der "registry"- gibt es eine "local"-Policy, die nach einem neuen Image im lokalen Container-Speicher sucht. Die "local"-Policy integriert sich damit in lokale Build-Pipelines und in "Air-Gapped Environments", also Umgebungen, die in einem physikalisch oder virtuell getrennten und gesicherten Netzwerk laufen.

Die Auto-Update-Funktion von Podman gewährleistet einen hohen Grad an Automatisierung. Die Workloads laufen robust und zuverlässig in systemd-Services, die zu bestimmten Zeitpunkten automatisch Updates erhalten können. Das gesamte System bleibt unter Kontrolle: Updates können zeitgesteuert über den systemd-Timer – zum Beispiel täglich um 10 Uhr vormittags – oder Event-basiert über das Kommando podman-auto-update und den systemd-Service erfolgen.

Trotz des hohen Grads an Automatisierung kann das Auto-Update dennoch Zuverlässigkeitsprobleme verursachen: Was ist zu tun, wenn ein Update fehlschlägt? Gründe dafür könnten sein, dass ein neues Image eventuell in bestimmten Umgebungen nicht korrekt funktioniert oder versehentlich ein falsches beziehungsweise fehlerhaftes Image hochgeladen wurde. In diesen Fällen bietet Podman mit der Rollback-Funktion Hilfe an. Ist das Update eines systemd-Service fehlgeschlagen, lässt sich das neue Image verwerfen und via Rollback das zuvor verwendete funktionierende Image wiederherstellen, um den Service neu zu starten. Damit ist sichergestellt, dass die Workloads wieder zuverlässig laufen – und das vollständig automatisiert.

Rollbacks sind in Podman standardmäßig eingeschaltet, lassen sich auf Wunsch aber per --rollback=false ausschalten. Podman erachtet ein Update als misslungen, wenn der Neustart eines systemd-Service fehlschlägt. Dabei sind jedoch ein paar Fallstricke zu beachten, denn ein Update gilt als erfolgreich, sobald der Service ohne Fehler gestartet ist, auch wenn er kurz darauf mit einem Fehler endet.

Um solche nach dem Start auftretenden Fehler zu erkennen, muss die Notify-Policy des .container-Quadlet konfiguriert sein. Sie legt fest, von wem und wann die "READY"-Nachricht an systemd geschickt wird, um den Service als aktiv zu markieren – und damit das Update als erfolgreich einzustufen. Standardmäßig setzt Podman die Notify-Policy auf "conmon"; direkt nach dem Start wird "READY" geschickt. Für komplexere Updates lässt sich die "container"-Policy verwenden. In diesem Fall ist der Container dafür verantwortlich, die "READY"-Nachricht zu schicken. Der NOTIFY_SOCKET von systemd wird hierfür über einen Proxy in conmon in den Container gemountet und ist damit direkt ansprechbar. Das Kommando für das Senden der "READY"-Nachricht in einem Quadlet könnte wie folgt aussehen:

Exec=sh -c "/usr/bin/Initialisierung; systemd-notify --ready; /usr/bin/Arbeit"

Nach erfolgreicher Initialisierung des Beispiel-Workloads wird über systemd-notify die "READY"-Nachricht geschickt und danach die eigentliche Arbeit verrichtet. Der systemd-Service gilt erst nach Erhalt der Nachricht als aktiv und damit das Auto-Update als erfolgreich. Bei einem Fehler während der Initialisierung würde das Auto-Update als fehlerhaft eingestuft. Dann löst Podman automatisch ein Rollback auf das zuletzt funktionierende Image sowie einen anschließenden Neustart aus.

Rollbacks gewährleisten zuverlässige und robuste Auto-Updates. Fehlgeschlagene Updates lassen sich noch während der Initialisierungsphase der Workloads erkennen. Doch wie lassen sich Fehler erkennen, die erst nach der Initialisierung auftreten? Schließlich ist kein Programm perfekt und Bugs gehören zum Alltag in der IT.

Podman stellt sich dieser Herausforderung mit den in Version 4.3 eingeführten Container-Healthchecks und deren Healthcheck-Actions. Healthchecks dienen dazu, die Funktionstüchtigkeit der Container-Workloads zu überwachen, um bei einem Datenbank-Container beispielsweise zu testen, ob sich die Verbindung zur Datenbank herstellen lässt.

Das Konzept der Healthchecks hat Podman von Docker übernommen. Ursprünglich beschränkte sich die Funktion darauf, festzustellen, ob ein Container "healthy" oder "unhealthy" ist. Externe Werkzeuge mussten dann diese Information verarbeiten und gegebenenfalls die kranken (unhealthy) Container neu starten. Die Healthcheck-Actions in Podman ermöglichen es, Gesundheitsprüfungen individuell für jeden Container zu konfigurieren. Insgesamt stehen vier Actions zur Verfügung:

  • Restart: Der Container wird neu gestartet.
  • Stop: Der Container wird gestoppt.
  • Kill: Der Container wird mit dem SIGKILL-Signal gestoppt.
  • None: Keine Aktion (default).

Die Restart-Action eignet sich für den Standardbetrieb von Podman. Sobald der Container anfängt zu kränkeln, startet Podman ihn neu. Läuft Podman innerhalb von systemd, empfehlen sich die Stop- oder die Kill-Action. In ersterem Fall beendet sich der Container (und conmon) mit einem Exit-Code ungleich 0 und die Restart-Policy von systemd startet den kompletten Service neu. Bei Kill wird der Container mit SIGKILL gestoppt und direkt beendet. Das kann jedoch bei Anwendungen zu Problemen führen, die mit einem Abbruch nicht umgehen können oder bei denen Datenpersistenz kritisch ist, wie etwa Datenbanken. In solchen Fällen ist Stop zu bevorzugen, da diese Action den Anwendungen etwas mehr Zeit für eine normale Abschaltung gewährt. Die Konfigurationsfelder für Healthchecks sind ausführlich in der Quadlet-Dokumentation beschrieben.