Schlagwort-Archive: podman

Container mit podman-auto-update automatisch aktualisieren

In diesem Tutorial zeige ich euch, wie ihr eine automatische Aktualisierung für Container in rootless-Podman-Umgebungen konfigurieren und diese Container als systemd-Services verwalten könnt.

Das Tutorial gliedert sich in folgende Abschnitte:

  1. Anwendungsfälle
  2. Voraussetzungen
  3. Umgebung und verwendetes Container-Image
  4. Konfiguration des systemd-Service mit Auto-Update-Funktion
  5. Container (automatisch) aktualisieren

Wer sich nicht für die möglichen Anwendungsfälle interessiert und lieber gleich starten möchte, kann den ersten Abschnitt überspringen. Die übrigen Abschnitte sollten in der angegebenen Reihenfolge gelesen werden.

Anwendungsfälle

  • Container werden auf einem Single-Container-Host ausgeführt und nicht in K8s-Umgebungen
  • Man vertraut dem Anbieter, dass dieser stabile und nutzbare Container-Images bereitstellt
  • Es soll regelmäßig geprüft werden, ob aktualisierte Container-Images vorhanden sind
  • Sind aktuellere Images vorhanden, sollen laufende Container entfernt und unter Verwendung der aktuellen Images neu erstellt werden

Voraussetzungen

Um diesem Tutorial folgen zu können, benötigt ihr einen Host mit einer rootless-Podman-Umgebung. Podman muss dabei in der Version >= 3.3 verfügbar sein. In der folgenden Liste findet ihr einige Links, die euch helfen, eine solche Umgebung aufzusetzen.

Darüber hinaus solltet ihr Manpages lesen können.

Umgebung und verwendetes Container-Image

Das Betriebssystem spielt eine untergeordnete Rolle, da wir durch die Verwendung von Containern die Anwendung vom Betriebssystem entkoppeln. Alle zur Ausführung der Anwendung notwendigen Abhängigkeiten sind im Container-Image enthalten.

Uptime Kuma ist eine schlanke und schnelle Monitoring-Anwendung, welche unter anderem als Container-Image bereitgestellt wird. Ich habe die Anwendung als Beispiel für dieses Tutorial ausgewählt, da ich die Anwendung selbst nutzen möchte und so Synergieeffekte nutzen kann.

Wer ein anderes Container-Image nutzen möchte, muss in den folgenden Beispielen louislam/uptime-kuma:latest durch den fully qualified container name des zu nutzenden Images ersetzen.

Für die Konfiguration werden die auf dem System verfügbaren Podman-Manpages benutzt.

Konfiguration des Systemd-Service mit Auto-Update-Funktion

Bei den folgenden Schritten habe ich mich am Beispiel aus podman-auto-update(1) orientiert. Ich zeige zuerst den jeweils auszuführenden Befehl in einem Code-Block, gefolgt von einer Erläuterung der genutzten Optionen.

Podman-Volume erzeugen

$ podman volume create uptime-kuma
uptime-kuma

Um Daten persistent speichern zu können, müssen diese außerhalb des Containers abgelegt werden. Der dargestellte Befehl erzeugt ein Podman-Volume mit dem Namen „uptime-kuma“.

Mit dem folgenden Befehl lassen sich detaillierte Informationen zum gerade erstellten Volume anzeigen:

$ podman volume inspect uptime-kuma
[
     {
          "Name": "uptime-kuma",
          "Driver": "local",
          "Mountpoint": "/home/tronde/.local/share/containers/storage/volumes/uptime-kuma/_data",
          "CreatedAt": "2023-08-22T20:52:06.477341481+02:00",
          "Labels": {},
          "Scope": "local",
          "Options": {},
          "MountCount": 0,
          "NeedsCopyUp": true,
          "NeedsChown": true
     }
]

Der Schlüssel Mountpoint enthält den Pfad im lokalen Dateisystem, in dem das Volume erstellt wurde.

Manpages zum Nachschlagen:

  • podman-volume(1)
  • podman-volume-create(1)
  • podman-volume-inspect(1)

Container starten

$ podman run --label "io.containers.autoupdate=registry" -d -p 3001:3001 -v uptime-kuma:/app/data:Z --name=uptime-kuma docker.io/louislam/uptime-kuma:latest
Trying to pull docker.io/louislam/uptime-kuma:latest...
Getting image source signatures
Copying blob d7ca72974892 done  
Copying blob 475646a04a73 done  
Copying blob 1d496c24aec8 done  
Copying blob 6a3727d8681d done  
Copying blob b00c91ba9805 done  
Copying blob ddade83992f9 done  
Copying blob b8454ed537a7 done  
Copying blob 849e609eff67 done  
Copying blob f861188db6a1 done  
Copying blob 5f3c3e6d7f1c done  
Copying blob 4f4fb700ef54 skipped: already exists  
Copying blob 68b7bcf7c878 done  
Copying config fb3a3565b2 done  
Writing manifest to image destination
Storing signatures
ad7b049d9b84962311f5bafb5329f59961d8a031e54a571f079b8243ea8059ee
  • podman run ist das Kommando, mit dem ein neuer Container gestartet wird
  • Die Option --label "io.containers.autoupdate=registry" gibt an, dass Podman die Remote-Registry prüft, ob dort ein aktualisiertes Image vorhanden ist; dieses Label ist Voraussetzung, um die Auto-Update-Funktion nutzen zu können
  • Mit der Option -d wird der Container im Hintergrund gestartet und die Container-ID auf STDOUT ausgegeben
  • Durch -p 3001:3001 wird der Host-Port 3001 mit dem Port der Anwendung (ebenfalls 3001) im Container verbunden
  • Die Option -v uptime-kuma:/app/data:Z hängt das im vorhergehenden Schritt erstellte Podman-Volume in das Verzeichnis /app/data innerhalb des Containers ein; :Z sorgt dafür, dass der SELinux-Kontext korrekt gesetzt wird
  • --name=uptime-kuma spezifiziert den Namen des Containers; dieser ist etwas leichter zu merken als die Container-ID
  • Der Befehl endet mit dem fully qualified container name docker.io/louslam/uptime-kuma:latest
  • Die letzte Zeile des Code-Blocks enthält die Container-ID

Manpages zum Nachschlagen:

  • podman-run(1)
  • podman-auto-update(1)

Systemd-Service-Unit mit podman-generate-systemd erstellen

$ podman generate systemd --name --new uptime-kuma
# container-uptime-kuma.service
# autogenerated by Podman 4.4.1
# Tue Aug 22 21:29:46 CEST 2023

[Unit]
Description=Podman container-uptime-kuma.service
Documentation=man:podman-generate-systemd(1)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
TimeoutStopSec=70
ExecStart=/usr/bin/podman run \
	--cidfile=%t/%n.ctr-id \
	--cgroups=no-conmon \
	--rm \
	--sdnotify=conmon \
	--replace \
	--label io.containers.autoupdate=registry \
	-d \
	-p 3001:3001 \
	-v uptime-kuma:/app/data:Z \
	--name=uptime-kuma docker.io/louislam/uptime-kuma:latest
ExecStop=/usr/bin/podman stop \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm \
	-f \
	--ignore -t 10 \
	--cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target
  • Der Befehl gibt den Inhalt der generierten Service-Unit auf STDOUT aus
  • Die Option --name verwendet den Namen des Containers anstelle der Container-ID im Dateinamen der Service-Unit (hier: container-uptime-kuma.service)
  • Wichtig ist die Option --new, um Container von aktualisierten Images erstellen zu können; ohne diese Option können Systemd-Units Container nur unter Verwendung des ursprünglichen Images starten und stoppen und ein Auto-Update ist nicht möglich
  • Der folgende Code-Block fügt dem Befehl die Option --files hinzu, um eine Service-Unit-Datei zu erstellen
$ podman generate systemd --name --new --files uptime-kuma
/home/tronde/container-uptime-kuma.service

Manpages zum Nachschlagen:

  • podman-auto-update(1)
  • podman-generate-systemd(1)

Die erstellte Systemd-Unit aktivieren und starten

$ mv -Z container-uptime-kuma.service ~/.config/systemd/user/container-uptime-kuma.service
$ systemctl --user daemon-reload
  • Der erste Befehl verschiebt die Service-Unit in einen Pfad, wo systemd sie findet und einlesen kann
  • Die Option -Z stellt sicher, dass die Datei den SELinux-Kontext des Zielverzeichnisses zugewiesen bekommt, andernfalls kann systemd die Datei ggf. nicht verarbeiten
  • Durch den zweiten Befehl wird die Unit-Datei systemd bekannt gemacht
  • An dieser Stelle ist der neue Systemd-Service geladen, jedoch inaktiv
$ systemctl --user status container-uptime-kuma.service
○ container-uptime-kuma.service - Podman container-uptime-kuma.service
     Loaded: loaded (/home/tronde/.config/systemd/user/container-uptime-kuma>
     Active: inactive (dead)
       Docs: man:podman-generate-systemd(1)
$ podman stop uptime-kumauptime-kuma
$ podman rm uptime-kumauptime-kuma
$ systemctl --user start container-uptime-kuma.service
$ systemctl --user status container-uptime-kuma.service
● container-uptime-kuma.service - Podman container-uptime-kuma.service     Loaded: loaded (/home/tronde/.config/systemd/user/container-uptime-kuma>     Active: active (running) since Tue 2023-08-22 21:59:56 CEST; 14s ago
…
  • Der erste Befehl in obigen Code-Block prüft den aktuellen Status des Service
  • Der zweite und dritte Befehl stoppen und entfernen den laufenden Container, den wir weiter oben gestartet haben
  • Befehl Nummer 4 startet den Uptime-Kuma-Service
  • Befehl Nummer 5 prüft den neuen Status des Service; dieser ist nun up-and-running

Manpages zum Nachschlagen:

  • mv(1)
  • systemd.unit(5)
  • systemctl(1)

Auf neue Container-Images prüfen

$ podman auto-update --dry-run --format "{{.Image}} {{.Updated}}"
docker.io/louislam/uptime-kuma:latest false
  • Durch die Option --dry-run wird sichergestellt, dass nur auf die Verfügbarkeit neuer Images geprüft wird, es werden jedoch keine Pull-Operationen ausgeführt und keine Container neu erstellt
  • Es wird eine Liste von Container-Images ausgegeben, die mit dem Label io.containers.autoupdate=registry gestartet wurden
  • Die erste Spalte enthält den Image-Namen
  • Die zweite Splate zeigt an, ob ein Update verfügbar ist; in diesem Fall ist kein Update verfügbar (false)

Container (automatisch) aktualisieren

Wurde die Konfiguration erfolgreich abgeschlossen, können die entsprechenden Container durch folgenden Befehl manuell aktualisiert werden:

$ podman auto-update
            UNIT                           CONTAINER                   IMAGE                                  POLICY      UPDATED
            container-uptime-kuma.service  df21116f2573 (uptime-kuma)  docker.io/louislam/uptime-kuma:latest  registry    false

Leider ist aktuell kein Update verfügbar, weshalb es hier nichts zu tun gibt und der Status von Updated gleich false ist.

Podman bringt bei der Installation die beiden systemd units podman-auto-update.timer und podman-auto-update.service mit, welche zumindest unter RHEL 9 manuell aktiviert werden müssen:

$ systemctl --user enable podman-auto-update.{service,timer}
Created symlink /home/tronde/.config/systemd/user/default.target.wants/podman-auto-update.service → /usr/lib/systemd/user/podman-auto-update.service.
Created symlink /home/tronde/.config/systemd/user/timers.target.wants/podman-auto-update.timer → /usr/lib/systemd/user/podman-auto-update.timer.

$ systemctl --user start podman-auto-update.timer
$ systemctl --user status podman-auto-update.{service,timer}
○ podman-auto-update.service - Podman auto-update service
     Loaded: loaded (/usr/lib/systemd/user/podman-auto-update.service; enabled; preset: disabled)
     Active: inactive (dead)
TriggeredBy: ● podman-auto-update.timer
       Docs: man:podman-auto-update(1)

● podman-auto-update.timer - Podman auto-update timer
     Loaded: loaded (/usr/lib/systemd/user/podman-auto-update.timer; enabled; preset: disabled)
     Active: active (waiting) since Sat 2023-09-02 20:56:09 CEST; 1s ago
      Until: Sat 2023-09-02 20:56:09 CEST; 1s ago
    Trigger: Sun 2023-09-03 00:12:22 CEST; 3h 16min left
   Triggers: ● podman-auto-update.service
  • Der Timer startet jeden Tag um Mitternacht den Auto-Update-Service
  • Der Service prüft, ob aktualisierte Container-Images verfügbar sind und führt ggf. ein Update der Container durch
  • Schlägt ein Start nach Aktualisierung des Container-Images fehl, wird der Dienst automatisch von der vorherigen Image-Version gestartet; siehe --rollback in podman-auto-update(1)
  • Der folgende Code-Block zeigt den Status, nachdem ein Update durchgeführt wurde
$ systemctl --user --no-pager -l status podman-auto-update
○ podman-auto-update.service - Podman auto-update service
     Loaded: loaded (/usr/lib/systemd/user/podman-auto-update.service; enabled; preset: disabled)
     Active: inactive (dead) since Sun 2023-09-03 00:12:56 CEST; 7h ago
TriggeredBy: ● podman-auto-update.timer
       Docs: man:podman-auto-update(1)
    Process: 309875 ExecStart=/usr/bin/podman auto-update (code=exited, status=0/SUCCESS)
    Process: 310009 ExecStartPost=/usr/bin/podman image prune -f (code=exited, status=0/SUCCESS)
   Main PID: 309875 (code=exited, status=0/SUCCESS)
        CPU: 5.128s

Sep 03 00:12:50 example.com podman[309875]: Copying config sha256:d56b643e048f2d351ed536ec9a588555dfd4c70de3c8d510ed61078a499ba464
Sep 03 00:12:50 example.com podman[309875]: Writing manifest to image destination
Sep 03 00:12:50 example.com podman[309875]: Storing signatures
Sep 03 00:12:51 example.com podman[309875]: 2023-09-03 00:12:41.98296115 +0200 CEST m=+1.880671312 image pull  docker.io/louislam/uptime-kuma:latest
Sep 03 00:12:55 example.com podman[309875]:             UNIT                           CONTAINER                   IMAGE                                  POLICY      UPDATED
Sep 03 00:12:55 example.com podman[309875]:             container-uptime-kuma.service  814407c7312c (uptime-kuma)  docker.io/louislam/uptime-kuma:latest  registry    true
Sep 03 00:12:56 example.com podman[310009]: fb3a3565b2da641402e99594e09b3cdadd1b9aa84f59e7960b9961662da5ff65
Sep 03 00:12:56 example.com podman[310009]: 2023-09-03 00:12:55.421998943 +0200 CEST m=+0.020260134 image remove fb3a3565b2da641402e99594e09b3cdadd1b9aa84f59e7960b9961662da5ff65 
Sep 03 00:12:56 example.com systemd[686]: Finished Podman auto-update service.
Sep 03 00:12:56 example.com systemd[686]: podman-auto-update.service: Consumed 5.128s CPU time.

Ich hoffe, das Tutorial hat euch gefallen und konnte euch einen Eindruck vermitteln, wie man automatische Updates für Container konfigurieren kann.

Ansible: Seafile Professional Edition in Rootless-Podman-Umgebung bereitstellen

Wer diesen Blog regelmäßig liest, kann den Eindruck gewinnen, es sei mein Hobby, Ansible-Rollen zu schreiben, mit denen von mir genutzte Web-Anwendungen auf Servern bereitgestellt werden können. Dieses Mal habe ich es mit Seafile getan und möchte in diesem Beitrag darüber berichten.

Was ist Seafile?

Seafile ist eine Sync&Share- bzw. Private-Cloud-Lösung ähnlich wie Nextcloud, ownCloud oder TeamDrive. Auf mich erweckt Seafile den Eindruck, als wenn der Schwerpunkt jedoch auf der Synchronisation und dem Teilen von Dateien liegt und damit genau meinem Suchmuster entspricht.

Seafile gibt es in einer Community und einer Professional Edition. Die Professional Edition darf mit bis zu drei Benutzern kostenlos verwendet werden.

Für weiterführende Informationen wird auf die Seiten des Herstellers und den Wikipedia-Artikel verwiesen.

Was ist das Ziel?

Nun, es gibt nicht das eine Ziel. Ich habe mit diesem kleinen Wochenendprojekt folgende Ziele verfolgt:

  • Beschäftige dich mit Ansible.
  • Beschäftige dich mit der Collection Containers.Podman.
  • Beschäftige dich mit rootless Podman.
  • Deploye Seafile Professional Edition in einer rootless-Podman-Umgebung, um es als Sync&Share-Lösung nutzen zu können.

Die Vorgehensweise

Zuerst habe ich mich informiert, ob es Container-Images für Seafile gibt und ob entsprechende Installationswege in der Dokumentation beschrieben sind. Meine Recherche förderte folgende Treffer zutage:

Ansible-Rollen haben eine einheitliche Struktur. Mit dem Befehl ansible-galaxy role init ansible_role_deploy_seafile_with_rootless_podman habe ich das Grundgerüst für meine Rolle erstellt. Anschließend habe ich die notwendigen Dateien {defaults,meta,tasks,vars}/main.yml mit Inhalt gefüllt und nicht benötigte Verzeichnisse wie handlers gelöscht. Mir ist dabei wichtig, dass alle notwendigen Parameter über Variablen definiert werden, die in defaults/main.yml zu finden sind. In vars/main.yml befinden sich hingegen nur Variablen, welche intern von der Rolle verwendet werden und vom Benutzer nicht explizit gesetzt werden sollen. So lässt sich die Rolle leicht wiederverwenden, um verschiedene Seafile-Instanzen auf dem gleichen Host oder in unterschiedlichen Umgebungen zu deployen.

Bevor ich die Rolle zum ersten Mal auf meinen Server angewendet habe, habe ich sie mit yamllint und ansible-lint geprüft und vorhandene Warnungen und Fehler behoben. Allerdings lassen sich mit den Lint-Werkzeugen und der Option --syntax-check nicht alle Fehler im Vorfeld finden. Da mir ein zweites Augenpaar fehlte, habe ich die letzten Tippfehler erst durch die Verwendung des Ansible Playbook Debugger gefunden.

Das Ergebnis

Das Ergebnis findet ihr auf:

Unter allen drei URLs könnt ihr meine Rolle herunterladen. Es ist damit möglich, eine lauffähige Seafile Pro Instanz bereitzustellen. Ein Test auf Idempotenz und ob diese Rolle auch zur Aktualisierung einer bestehenden Umgebung genutzt werden kann, steht noch aus.

Ihr seid herzlich eingeladen, die Rolle bei Interesse zu testen. Ich freue mich über Rückmeldungen zur Rolle und Dokumentation (Readme.md).

Ich habe das Deployment bisher nur auf Debian Buster getestet. Daher freue ich mich besonders über Rückmeldungen, wenn ihr die Rolle erfolgreich auf anderen Plattformen angewendet habt. Dann kann ich die entsprechenden Angaben für Ansible Galaxy ergänzen.

Eure Rückmeldungen nehme ich in den Kommentaren zu diesem Beitrag, per E-Mail oder in meinem neuen Matrix-Kanal #my-it-brain:matrix.org entgegen.

Fragen an meine Leser*innen

Ich interessiere mich für Themen rund um Ansible und Podman und frage mich, wie dies bei euch aussieht. Daher freue ich mich, wenn ihr in den Kommentaren oder gern auch per E-Mail und Chat folgende Fragen beantworten mögt:

  • Verwendet ihr Ansible für Software-Deployments?
  • Kennt ihr Podman und nutzt ihr es im privaten und/oder beruflichen Umfeld?
  • Findet ihr die Nutzung von Ansible zur Bereitstellung von Software auf (rootless) Podman sinnvoll? Oder bevorzugt ihr andere Bereitstellungsverfahren?

Ich freue mich auf eure Antworten.

Nextcloud im Container – Teil 1: Der Plan

Dies ist der Beginn meines zweiten Container-Projekts. Nach Kanboard im Container möchte ich diesmal eine Nextcloud-Instanz als Container, zusammen mit einem Datenbank-Container, in einem Podman-Pod betreiben.

Da ein einzelner Artikel vermutlich zu lang wird, teile ich das Projekt in mehrere Artikel auf. Wie viele es genau werden, kann ich jetzt noch nicht sagen. Am Ende der Reihe werde ich hier eine Übersicht einführen und die einzelnen Teilen entsprechend miteinander verbinden.

In diesem ersten Teil geht es um meine Motivation, das eigentliche Ziel und den groben Plan.

Was Leser dieser Reihe erwartet

Ihr könnt mich durch diese Reihe begleiten und euch von meinen Erlebnissen und Erkenntnissen unterhalten lassen. Dabei dürft ihr nicht annehmen, dass es sich bei dem von mir beschriebenen Vorgehen um eine gute Praxis handelt. Hier gilt eher: Der Weg ist das Ziel.

Ihr seid herzlich eingeladen, die Artikel zu kommentieren und über das Vorgehen und Alternativen dazu zu diskutieren. Gern in der Kommentarsektion unter den jeweiligen Beiträgen oder als Artikel in euren eigenen Blogs.

Ich plane die Artikel im Wochenrhythmus, wenigstens monatlich, zu veröffentlichen. Bitte verzeiht, wenn es etwas unregelmäßig wird. Dies ist ein Hobby, dem nur begrenzt Zeit zur Verfügung steht.

Motivation

Bei Linux-Containern handelt es sich um eine Technologie, die gekommen ist, um zu bleiben. Sie hat bereits in vielen Branchen Fuß gefasst und immer mehr Projekte bieten ihre Anwendungen zusätzlich oder ausschließlich in Form von Containern an.

Als Sysadmin mittleren Alters werden mich Linux-Container sicher noch viele Jahre begleiten. Um praktische Erfahrungen mit dem Betrieb zu sammeln, möchte ich einige private Projekte in Containern betreiben.

Beruflich arbeite ich überwiegend mit RHEL. Red Hat engagiert sich stark in den Projekten Ansible und Podman, welche ich auch unter anderen Distributionen, wie z.B. Debian, einsetze. Ich möchte das Projekt als Chance nutzen, mein Wissen auch in diesen Werkzeugen zu festigen und auszubauen.

Ich spiele schon seit einiger Zeit mit dem Gedanken, wieder eine eigene Nextcloud-Instanz zu betreiben. Da auf dem zur Verfügung stehenden Server bereits eine Nextcloud-Instanz läuft und ich meine Anwendung von der bestehenden Instanz getrennt und möglichst losgelöst vom Betriebssystem betreiben möchte, habe ich mich entschieden, Nextcloud im Container zu betreiben.

Ziele

Ziel dieses Projekts sind das Deployment und der Betrieb einer Nextcloud-Instanz als Podman-Pod. Im Einzelnen sollen folgende Ziele erreicht werden:

  1. Entwicklung eines wiederverwendbaren Verfahrens zum Deployment einer Nextcloud im Container
  2. Persistente Speicherung von Konfigurations- und inhaltlichen Daten im Dateisystem des Hosts
  3. Konfiguration eines Reverse-Proxies (NGINX) für den Zugriff auf die Nextcloud-Instanz
  4. Konfiguration von Backup und Restore für Konfiguration und Inhalte der Nextcloud-Instanz
  5. Konfiguration und Test automatischer durch Ansible gesteuerter Updates

Umgebung

Für die Umsetzung des Projekts steht mir ein Virtual Private Server (VPS) mit genügend Ressourcen zur Verfügung. Dieser wird in einem Rechenzentrum in Deutschland betrieben. Auf diesem sind Debian Bullseye, NGINX, ein OpenSSH-Server, Podman 3.0.1 (rootless) und Python 3.9.2 installiert. Damit erfüllt dieses System die Voraussetzungen, um mit Ansible konfiguriert zu werden und Container ausführen zu können.

Ansible selbst läuft in meiner privaten Arbeitsumgebung auf meinem Debian-PC und einem Fedora-35-Notebook.

Methodik und verwendete Werkzeuge

Zu Beginn habe ich mich etwas in der Nextcloud-Dokumentation und den verfügbaren Nextcloud-Images belesen. Besagte Dokumentation sowie die der verwendeten Werkzeuge sind im folgenden Abschnitt verlinkt.

Um die oben formulierten Ziele zu erreichen, werde ich in einem Python Virtual Environment eine Ansible-Version installieren, mit der ich die Collection containers.podman nutzen kann. Hiermit werde ich eine Ansible-Rolle entwickeln, die ich wiederverwenden kann, um Nextcloud-Instanzen in einer rootless-Podman-Umgebung zu deployen. Die Ansible-Rolle wird anschließend auf meinem GitHub-Account veröffentlicht.

Die Konfiguration von NGINX und acme.sh für die TLS-Zertifikate erfolgt manuell.

In diesem Abschnitt liste ich Links zu Artikeln und Dokumentationen auf, welche ich im Vorfeld gelesen habe und deren Kenntnis ich für die Umsetzung als nützlich erachte. Zur besseren Übersicht gliedere ich diese in die Unterabschnitte Hintergrundwissen, Dokumentation und Eigene Artikel.

Die weiteren Artikel dieser Reihe

Hintergrundwissen

Dokumentation

Eigene Artikel

Migration auf ein neueres PostgreSQL-Image im Containerland

Als kleines Wochenendprojekt betreibe ich Kanboard im Container. Als ich den Pod initial deployt habe, verwendete ich für die Datenbank rhel8/postgresql-96 in der Annahme, dass hierfür der gleiche Support-Zeitraum wie für RHEL 8 gilt. Eher durch Zufall habe ich noch im letzten Jahr bemerkt, dass das von mir genutzte Image deprecated ist und somit keine Updates mehr erhält.

An dieser Stelle dokumentiere ich kurz die Migration zu rhel8/postgresql-13. Offen bleibt für mich die Frage, wie ich hätte früher von der Deprication erfahren können.

Migration und Upgrade auf die neuere PostgreSQL-Version

Ich habe mich an den englischsprachigen Artikel „How to Upgrade Your PostgreSQL Version Using Docker“ von José Postiga gehalten, welcher folgende Vorgehensweise vorsieht:

  1. Aktuelle Datenbank sichern
  2. Pod stoppen und entfernen
  3. Verzeichnis und Dateien der alten PostgreSQL-Version aus dem Podman-Volume löschen
  4. SQL-Dump-Datei ins Podman-Volume verschieben
  5. Pod mit rhel8/postgresql-13-Container starten
  6. Datenbank wiederherstellen

Dazu habe ich auf meinem System folgende Befehle ausgeführt:

# Backup DB
podman exec -t <NAME des Containers> /usr/bin/pg_dump <DB-NAME> >dump.sql

# Podman-Volume der DB leeren
podman volume inspect <Volume-NAME>
sudo rm -rf /home/tronde/.local/share/containers/storage/volumes/<Volume-NAME/_data/*

# Dump ins Podman-Volume verschieben
mv dump.sql /home/tronde/.local/share/containers/storage/volumes/<Volume-Name>/_data/

# Datenbank im Container wiederherstellen
podman exec -it <Container-ID> bash
bash-4.4$ psql -U <USER-Name> -d <DB-Name> < /var/lib/pgsql/data/dump.sql

Fazit

Wenn man einmal weiß, wie es geht, ist die Migration auf ein neues DB-Release nicht schwer. Die hier dokumentierte Vorgehensweise lässt sich in meinen Augen auch auf andere DBMS übertragen, die in Containern laufen.

Mich stört nur, dass ich quasi nur zufällig davon erfahren habe, dass das von mir eingesetzte Container-Image im Status deprecated gelandet ist. Wie informiert ihr euch über Statusänderungen bei den von euch verwendeten Container-Images?

Aktualisierung eines Kanboard-Pods

Dies ist der letzte Artikel in meiner kleinen Reihe, über meinen Ausflug ins Container-Land, zur Bereitstellung der Anwendung Kanboard.

Wie meine Reise ins Container-Land begonnen hat, kann in „Kanboard im Container…“ nachgelesen werden. Wie man einen Reverse-Proxy vor einem Pod betreibt sowie mit dem Thema Backup und Restore habe ich mich inzwischen ebenso beschäftigt. Letzteres habe ich zum Glück implementiert und getestet, bevor ich mit der Dokumentation Datenverlust erlitten habe. Damit die Anwendung nach einem Neustart automatisch startet und für ein bisschen Komfort, habe ich Systemd-Service-Units generiert. In diesem Teil geht es nun um die Aktualisierung dieser Umgebung.

Umgebung

Aktuell läuft Kanboard 1.2.18 auf einer RHEL 8 VM. Zur Ausführung sind folgende Werkzeuge und Container-Images beteiligt.

$ podman --version
podman version 2.2.1
$ podman images
REPOSITORY                              TAG      IMAGE ID      CREATED        SIZE
registry.redhat.io/rhel8/postgresql-96  latest   4ce7daa6dc1d  7 weeks ago    451 MB
docker.io/kanboard/kanboard             v1.2.18  e7ee6403944b  3 months ago   58.6 MB
k8s.gcr.io/pause                        3.2      80d28bedfe5d  14 months ago  688 kB

Um mir die Erstellung eines Pods und das Einhängen von Podman-Volumes zu erleichtern, nutze ich folgendes kleines Skript:

#!/bin/bash
podman run -d --pod new:kanboardpod --name kanboard --privileged -p 127.0.0.1:8080:80 -v kanboard_datadir:/var/www/app/data:Z -v kanboard_pluginsdir:/var/www/app/plugins:Z kanboard/kanboard:v1.2.18

podman run -d --pod kanboardpod --name pgsql_db -e POSTGRESQL_USER=<USERNAME> -e POSTGRESQL_PASSWORD=<Verrate ich nicht> -e POSTGRESQL_DATABASE=kanboard -v pgsql_dbdir:/var/lib/pgsql/data:Z rhel8/postgresql-96:1-107

Mein Pod und die drei dazugehörigen Container werden während der folgenden Schritte noch normal ausgeführt.

Die Container selbst sind dabei zustandslos. Die persistent zu speichernden Daten werden in Podman-Volumes im lokalen Dateisystem der VM abgelegt.

Vorgehensweise

Ich verzichte in diesem Fall bewusst auf podman-auto-update(1), da ich mir erstmal einen Überblick verschaffen möchte, was denn generell zu tun ist und wie die einzelnen Schritte aussehen können. Die grundsätzliche Reihenfolge für ein Update sieht dabei wie folgt aus:

  1. Aktuelle Container-Images aus einer Registry holen (podman-pull(1))
  2. Laufende Pod-Instanz stoppen und entfernen (podman-pod(1))
  3. Neue Pod-Instanz mit angepasstem Wrapper-Skript erstellen
  4. Systemd-Service-Units erneut generieren (podman-generate-systemd(1))
  5. Pod-Instanz stoppen
  6. Generierte Systemd-Service-Unit starten

An dieser Stelle möchte ich vorweg nehmen, dass es genau so einfach war, wie es sich anhört. Die neuen Container-Images habe ich mit folgenden Kommandos heruntergeladen.

$ podman pull docker.io/kanboard/kanboard:v1.2.19
Trying to pull docker.io/kanboard/kanboard:v1.2.19...
Getting image source signatures
Copying blob 0c2b98bb5f7e done
Copying blob ca3cd42a7c95 done
Copying blob e994ab432c32 done
Copying blob 7b30337f40d2 done
Copying blob f58d66ecc40b done
Copying config 2cb48121b7 done
Writing manifest to image destination
Storing signatures
2cb48121b7437ba15cd984472754b300395026c3e09e7c659b4f9b62e5b5b4dd

$ podman pull registry.redhat.io/rhel8/postgresql-96:1-127
Trying to pull registry.redhat.io/rhel8/postgresql-96:1-127...
Getting image source signatures
Copying blob 64607cc74f9c done
Copying blob 320ae7fa06a7 done
Copying blob 13897c84ca57 done
Copying blob b3b2bbe848df done
Copying config 9c6ab01c14 done
Writing manifest to image destination
Storing signatures
9c6ab01c14748f7ff79483759122cb28e0f2c8b0310e5c8d9b5af8383e91f163

$ podman images
REPOSITORY                              TAG      IMAGE ID      CREATED        SIZE
docker.io/kanboard/kanboard             v1.2.19  2cb48121b743  4 days ago     61.3 MB
registry.redhat.io/rhel8/postgresql-96  1-127    9c6ab01c1474  2 weeks ago    449 MB
registry.redhat.io/rhel8/postgresql-96  latest   4ce7daa6dc1d  7 weeks ago    451 MB
docker.io/kanboard/kanboard             v1.2.18  e7ee6403944b  3 months ago   58.6 MB
k8s.gcr.io/pause                        3.2      80d28bedfe5d  14 months ago  688 kB

Mit den folgenden Befehlen werden Informationen zum laufenden Pod angezeigt, der Service wird gestoppt und der Pod inkl. seiner Container entfernt.

$ podman pod ls
POD ID        NAME         STATUS   CREATED      INFRA ID      # OF CONTAINERS
2f3aa7d07e6e  kanboardpod  Running  4 weeks ago  34e8479a2847  3

$ systemctl --user stop pod-kanboardpod.service

$ podman pod ls
POD ID        NAME         STATUS  CREATED      INFRA ID      # OF CONTAINERS
2f3aa7d07e6e  kanboardpod  Exited  4 weeks ago  34e8479a2847  3

$ podman pod rm kanboardpod
2f3aa7d07e6eb7d4c3a0c9927dac222be52b8992f95929c12af8ce4afafd4eb1

In mein Wrapper-Skript (siehe Abschnitt Umgebung) trage ich die neuen Versionsnummern bei den entsprechenden Aufrufen ein:

#!/bin/bash
podman run -d --pod new:kanboardpod --name kanboard --privileged -p 127.0.0.1:8080:80 -v kanboard_datadir:/var/www/app/data:Z -v kanboard_pluginsdir:/var/www/app/plugins:Z kanboard/kanboard:v1.2.19

podman run -d --pod kanboardpod --name pgsql_db -e POSTGRESQL_USER=<USERNAME> -e POSTGRESQL_PASSWORD=<Verrate ich nicht> -e POSTGRESQL_DATABASE=kanboard -v pgsql_dbdir:/var/lib/pgsql/data:Z rhel8/postgresql-96:1-127

Nachdem das obige Wrapper-Skript ausgeführt wurde, prüfe ich, ob die neue Pod-Instanz läuft, erstelle die Service-Units und aktiviere diese:

$ podman pod ls
POD ID        NAME         STATUS   CREATED             INFRA ID      # OF CONTAINERS
85273ee9bb82  kanboardpod  Running  About a minute ago  82f45a722dff  3

$ podman container ls
CONTAINER ID  IMAGE                                         COMMAND         CREATED             STATUS                 PORTS                   NAMES
6becc68a9c20  registry.redhat.io/rhel8/postgresql-96:1-127  run-postgresql  About a minute ago  Up About a minute ago  127.0.0.1:8080->80/tcp  pgsql_db
82f45a722dff  k8s.gcr.io/pause:3.2                                          About a minute ago  Up About a minute ago  127.0.0.1:8080->80/tcp  85273ee9bb82-infra
e72ca46110be  docker.io/kanboard/kanboard:v1.2.19                           About a minute ago  Up About a minute ago  127.0.0.1:8080->80/tcp  kanboard

$ podman generate systemd --files --name kanboardpod
/home/bob/pod-kanboardpod.service
/home/bob/container-kanboard.service
/home/bob/container-pgsql_db.service

$ mv *.service .config/systemd/user/

$ podman pod stop kanboardpod
85273ee9bb82e49e236ae37d9320fd95af1eb186d7d965d72b9e2a270ca5cedf

$ systemctl --user daemon-reload
$ systemctl --user start pod-kanboardpod.service

Fazit

Das war einfacher als gedacht. Oder anders formuliert, es hat tatsächlich so funktioniert, wie ich es erwartet habe.

Mein kleines Wochenend-Projekt skaliert sicher nicht gut und ist als Beispiel für Produktionsumgebungen vermutlich weniger geeignet. Doch um Software als rootless-Container auszuführen und in kleinem Umfang zu testen, scheint dieser Weg durchaus geeignet zu sein.

Vielleicht schiebe ich in Zukunft noch einen Artikel unter Verwendung von podman-auto-update(1) nach.

Podman kann auch Systemd-Service-Units generieren

Mit „Kanboard im Container…“ hat mein Ausflug ins Containerland begonnen. Mittlerweile läuft mein Pod bereits eine Weile und ich nutze die Anwendung regelmäßig. Jedoch musste ich nach jedem Neustart des Hosts den Pod kanboardpod manuell starten. Ich hatte mir daher vorgenommen, hierfür eine Systemd-Service-Unit zu erstellen, welche diesen Task automatisiert. Doch habe ich mit Freude festgestellt, dass podman-generate-systemd(1) mir diese Arbeit abnehmen kann.

Also starte ich einen ersten Versuch und erzeuge entsprechende Service-Unit-Dateien in meinem HOME-Verzeichnis. Die Option „--name“ sorgt dafür, dass in den Service-Unit-Dateien die Namen des Pods bzw. der Container verwendet werden, anstatt der UUIDs. Die Option „--files“ sorgt dafür, dass die Ausgabe in Dateien und nicht auf die Standard-Ausgabe geschrieben wird.

$ podman generate systemd --name --files kanboardpod
/home/alice/pod-kanboardpod.service
/home/alice/container-kanboard.service
/home/alice/container-pgsql_db.service

$ cat pod-kanboardpod.service
# pod-kanboardpod.service
# autogenerated by Podman 2.0.5
# Mon Jan 25 13:19:51 CET 2021

[Unit]
Description=Podman pod-kanboardpod.service
Documentation=man:podman-generate-systemd(1)
Wants=network.target
After=network-online.target
Requires=container-kanboard.service container-pgsql_db.service
Before=container-kanboard.service container-pgsql_db.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
ExecStart=/usr/bin/podman start 62cdd29105a4-infra
ExecStop=/usr/bin/podman stop -t 10 62cdd29105a4-infra
ExecStopPost=/usr/bin/podman stop -t 10 62cdd29105a4-infra
PIDFile=/run/user/1000/containers/overlay-containers/b3c9bd9754cdc999108d0f4aad7d808c007cc34eee34faefd41ee39c3e1ca18b/userdata/conmon.pid       
KillMode=none
Type=forking

[Install]
WantedBy=multi-user.target default.target

$ cat container-kanboard.service 
# container-kanboard.service
# autogenerated by Podman 2.0.5
# Mon Jan 25 13:19:51 CET 2021

[Unit]
Description=Podman container-kanboard.service
Documentation=man:podman-generate-systemd(1)
Wants=network.target
After=network-online.target
BindsTo=pod-kanboardpod.service
After=pod-kanboardpod.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
ExecStart=/usr/bin/podman start kanboard
ExecStop=/usr/bin/podman stop -t 10 kanboard
ExecStopPost=/usr/bin/podman stop -t 10 kanboard
PIDFile=/run/user/1000/containers/overlay-containers/99d386a42b186efb3347d909cea265b990469dc33e1889a3006425a71956699b/userdata/conmon.pid
KillMode=none
Type=forking

[Install]
WantedBy=multi-user.target default.target

$ cat container-pgsql_db.service 
# container-pgsql_db.service
# autogenerated by Podman 2.0.5
# Mon Jan 25 13:19:51 CET 2021

[Unit]
Description=Podman container-pgsql_db.service
Documentation=man:podman-generate-systemd(1)
Wants=network.target
After=network-online.target
BindsTo=pod-kanboardpod.service
After=pod-kanboardpod.service

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
ExecStart=/usr/bin/podman start pgsql_db
ExecStop=/usr/bin/podman stop -t 10 pgsql_db
ExecStopPost=/usr/bin/podman stop -t 10 pgsql_db
PIDFile=/run/user/1000/containers/overlay-containers/fe757283f0662220fee23a563053ea7f30dbdf6d9fbb492c010a01dd7598fc9b/userdata/conmon.pid
KillMode=none
Type=forking

[Install]
WantedBy=multi-user.target default.target

Um die generierten Service-Units zu installieren und zukünftig als derjenige User auszuführen, welcher den Pod und die Container erzeugt hat, verschiebe ich sie in das zu erstellende Verzeichnis ~/.config/systemd/user. Anschließend werden die neuen Units in die Systemd-Konfiguration eingelesen und aktiviert.

$ mkdir -p .config/systemd/user
$ mv *.service .config/systemd/user/
$ systemctl --user enable pod-kanboardpod.service
$ podman pod stop kanboardpod
$ systemctl --user start pod-kanboardpod.service

Nachdem ich die Service-Units an die richtige Stelle verschoben und aktiviert hatte, habe ich meine laufende Pod-Instanz gestoppt und über die entsprechende Service-Unit gestartet.

Ich wähnte mich nun bereits am Ziel. Doch nach einem Neustart des Hosts war die Anwendung wieder nicht verfügbar. Die Service-Unit war nicht gestartet worden. Podman kann hier nichts dafür, denn es ist systemd, welcher dafür sorgt, dass im User-Kontext laufende Services beendet werden, wenn sich der entsprechende User ausloggt und diese erst startet, wenn der User sich einloggt.

Valentin Rothberg von Red Hat gab mir den entscheidenden Tipp, um dieses Problem zu lösen. Die Lösung versteckt sich in der Manpage zu podman-generate-systemd(1):

The systemd user instance is killed after the last session for the user is closed. The systemd user instance can be kept running ever after the user logs out by enabling lingering using

$ loginctl enable-linger <username>

Manual page podman-generate-systemd(1)

@Valentin: Thanks a lot, that solved my issue!

Fazit

Ich bin ehrlich gesagt hoch erfreut, dass die Entwickler hier eine Möglichkeit vorgesehen haben, um aus bestehenden Pods bzw. Container-Instanzen Systemd-Service-Units generieren zu können. Dies ermöglicht es, Container-Instanzen und Pods mit den gewohnten Werkzeugen zu starten, zu stoppen und deren Status zu kontrollieren.

So besteht die Möglichkeit, die rootless-Podman-Container auch als unprivilegierte Dienste laufen zu lassen. Das gefällt mir.

Mit Dokumentation zum Datenverlust

Wie ihr sicher gemerkt habt, beschäftige ich mich im Rahmen eines Wochenend-Projekts mit „Kanboard im Container…“ im Speziellen und Linux-Containern im Allgemeinen. Die Einrichtung von „Backup und Restore im Kanboard-Container-Land“ liegt bereits hinter mir. Und das ist gut so, habe ich doch nun den ersten Datenverlust erlitten und musste meine Daten wiederherstellen.

Die etwas unglückliche Verkettung von Umständen, welche zum Datenverlust führten, möchte ich in diesem Artikel festhalten, so dass euch diese Erfahrung erspart bleiben kann.

Die Vorgeschichte

Da Container zustandslose Gebilde sind, nutze ich podman volumes, um die anfallenden Daten persistent zu speichern.

Als Einsteiger in die Thematik habe ich mich an der offiziellen Container-Dokumentation von Red Hat entlang gehangelt und bin den Anweisungen in Kapitel 3.4. Sharing files between two containers (die Dokumentation wurde überarbeitet; das Kapitel existiert so nicht mehr) gefolgt. Dort wird beschrieben, wie man den Volume-Pfad einer Variablen zuweist, welche später verwendet wird, um das Volume über den Pfad in den Container einzuhängen.

Da ich es nicht besser wusste, bin ich der Anleitung Schritt-für-Schritt gefolgt. Dies führte zu einer funktionierenden Konfiguration, in der meine Daten persistent gespeichert wurden.

Kommando ‚podman volume prune‘ und die Daten waren weg

Am Ende meiner Spielerei wollte ich das Spielfeld bereinigen. Dabei bin ich auf das Kommando podman volume prune gestoßen, welches laut podman-volume-prune(1) alle Volumens entfernt, die sich nicht in Verwendung befinden. Dies klang nach genau dem Befehl, nach dem ich gesucht habe.

TL;DR: Nach der Ausführung des Kommandos waren meine Volumes weg. Auch jene, die aktiv in laufenden Container-Instanzen eingehängt waren.

Die Analyse

Nach ein paar Tests und einer Internetrecherche stand die Ursache für das Verhalten fest. Diese ist im GitHub Issue #7862 dokumentiert und besagt, dass podman volume prune in Verwendung befindliche Volumes löscht, wenn diese über ihren Pfad und nicht über ihren Namen eingehängt wurden. Da ich wie oben beschrieben der Dokumentation von Red Hat strikt gefolgt bin, welche aber genau den Pfad und eben nicht den Namen verwendet, waren Ursache und Erklärung für den Datenverlust gefunden.

Die Folge

In Folge meiner Erfahrungen habe ich zwei Anfragen zur Produktverbesserung (englisch: Request For Enhancement oder kurz RFE) gestellt:

  1. Bug 1914096 – Needs improvement: Building, running, and managing containers: 3.4. Sharing files between two containers
  2. RFE: Let `podman volume prune` show the volumes that are going to be removed

Die erste Anfrage ist an Red Hat adressiert, mit der Bitte, in der Dokumentation den Volume-Namen an Stelle des in einer Variablen gespeicherten Volume-Pfades zu benutzen. Damit sollte verhindert werden, dass andere, die der Dokumentation folgen, die gleichen Erfahrungen wie ich machen müssen.

Als Ziel wird die Veröffentlichung von RHEL 8.4 anvisiert. Dieses Release sollte im Mai bzw. Juni 2021 erscheinen. Ich bin gespannt. Ich würde mich über eine frühere Aktualisierung der Dokumentation freuen. Update 2021-01-25: Bereits am 20. Januar wurde eine neue Version der Dokumentation veröffentlicht. In dieser war nur noch ein kleiner Tippfehler enthalten. Der Bug wurde mit dem heutigen Datum (25.01.2021) geschlossen. So ist sichergestellt, dass hier niemand mehr in die Falle tappt. Vielen Dank ans RHEL-Docs-Team im Allgemeinen und Gabriela im Speziellen.

Die zweite Anfrage richtet sich an das Upstream-Projekt. Sie beinhaltet den Vorschlag, podman volume prune (um eine Option) zu erweitern, so dass die Liste der zu löschenden Volumes angezeigt wird, bevor man die Entfernung bestätigt. Stand 17.01.2021 existiert bereits ein Pull-Request, welcher dieses Thema adressiert.

Meinen Artikel „Kanboard im Container…“ habe ich entsprechend angepasst, so dass auch dort die Volumen-Namen zum Einhängen verwendet werden und nicht die Volume-Pfade.

Alte Erkenntnis bestätigt

Dieses Beispiel zeigt wieder einmal sehr deutlich, wie wichtig eine funktionierende Datensicherung ist. Denn sie ist die zwingende Voraussetzung, um im Fehlerfall Daten auch wiederherstellen zu können. Daher kann ich nur jedem raten, ein entsprechendes Datensicherungs- und Wiederherstellungs-Konzept zu implementieren, bevor man Daten in eine Anwendung tut, die einem am Herzen liegen oder von denen die Zukunft des Unternehmens abhängt.

Zum Stöbern führe ich im Folgenden einige Artikel aus diesem Blog auf, welche sich mit dem Thema Backup befassen:

Update 2021-11-09

Eine Lösung für den Upstream-Issue „RFE: Let `podman volume prune` show the volumes that are going to be removed“ wurde bereits am 27.01.2021 gemerged. Unter dem gleichen Namen hatte ich am 15.02.2021 einen RFE im Red Hat Bugzilla unter der Nummer 1928936 geöffnet. Dieser wechselte heute in den Status „Release Pending“ und kündigt an, dass der Fix in der Paketversion podman-3.3.0-0.4.el8 für RHEL 8 enthalten sein wird.

Ich erwarte das Paket im kommenden Release von RHEL 8.5.

Kanboard im Container…

… aber das sind ja gleich zwei Dinge auf einmal. Richtig. Denn hier versuche ich, etwas nützliches (Kanboard) mit der Möglichkeit, etwas zu lernen (Container), zu kombinieren.

Inspiriert durch Dirks Artikel und einem darauf folgenden, regen E-Mail-Verkehr, widme ich mich mal wieder dem Thema Linux-Container. Zuletzt hatte ich mich ca. 2016/2017 damit befasst und es mit der Erkenntnis zu den Akten gelegt, dass es noch ein Hype und für den produktiven Einsatz wenig geeignet war. Mittlerweile hat sich die Lage etwas geändert. Einige Unternehmen haben sich des Themas angenommen und arbeiten daran, Linux-Container Enterprise-Ready zu gestalten. So nehme ich wahr, dass in meinem beruflichen Netzwerk seit ca. Anfang 2019 OpenShift-Cluster wie Pilze aus dem Boden schießen. Man könnte den Eindruck gewinnen, dass Red Hat diese Subskriptionen wie geschnitten Brot verkauft. Andere Hersteller scheinen den relativ jungen Markt nicht allein den roten Hüten überlassen zu wollen. So hat VMware mit vSphere 7 und Tanzu hier ebenfalls eine Lösung im Portfolio und auch SUSE scheint sich mit dem Kauf von Rancher in diesem Segment stärker zu engagieren.

Ich selbst möchte mein Wissen rund um dieses Thema auffrischen und habe mir daher folgendes Wochenendprojekt überlegt. Um Projekte, Aufgaben oder schlicht den Alltag besser zu organisieren, möchte ich zukünftig die Anwendung Kanboard nutzen. Diese Anwendung unterstützt die Aufgaben- bzw. Projekt-Organisation nach der Kanban-Methode. Sie macht einen minimalistischen Eindruck, kommt ohne viel Schnick-Schnack daher und scheint daher gut zu mir zu passen. Um gleichzeitig praktische Erfahrungen im Umgang mit Linux-Containern zu sammeln, werde ich Kanboard mit einer Postgresql-Datenbank mit Hilfe von zwei Containern betreiben.

In meinen Augen wird Docker in den nächsten Jahren sowohl als Firma wie auch als Werkzeug stetig an Bedeutung verlieren. Daher setze ich bei der Umsetzung meines Wochenend-Projekts auf die Werkzeuge podman, skopeo und buildah.

Ich gehe in diesem Text nicht auf die Konzepte, die Architektur, sowie die Vor- und Nachteile von Linux-Containern ein. Hierzu wurde in den letzten Jahren bereits genug an anderer Stelle geschrieben. Informationen zu diesen Themen finden sich in der — im Aufbau befindlichen — Linksammlung und am Ende dieses Artikels.

Umfeld

Als Basis für dieses Projekt dient mir eine virtuelle Maschine in meinem heimischen Labor. Als Betriebssystem nutze ich ein aktuelles RHEL 8 mit der kostenlosen Developer-Subskription. Diese VM dient mir als Host zum Ausführen diverser Linux-Container. Um die Container aus dem Netzwerk erreichbar zu machen, installiere ich NGINX aus den Paketquellen von RHEL 8. Dieser kümmert sich als Reverse-Proxy um die Portweiterleitung zu den Containern.

Ziele

Mit diesem Wochenendprojekt möchte ich folgende Ziele erreichen:

  1. Bereitstellung der Anwendung Kanboard mittels Linux-Container
  2. Nutzung von Postgresql mittels Container als Kanboard-Datenbank-Backend
  3. Persistente Speicherung der Kanboard-Inhalte im Dateisystem des Hosts
  4. Erreichbarkeit und Nutzbarkeit von Kanboard über den NGINX-Reverse-Proxy
  5. Einrichtung Backup und Restore
  6. Updates

Schritt 1: rootless-Container-Umgebung einrichten

Während Entwickler viel Schweiß und Tränen investiert haben, damit Dienste wie Apache oder NGINX nach ihrem Start die root-Rechte ablegen können, liefen die ersten Linux-Container durchgängig mit root-Rechten. Dies ist aus Sicht der IT-Sicherheit nicht wünschenswert. Daher ist es in meinen Augen erfreulich, dass es mittlerweile auch ohne root-Rechte geht; Kernel User Namespaces sei Dank.

Ich folge der Red Hat Dokumentation (Kapitel 1.4, [1]), um den User Alice für die Nutzung von rootless-Containern einzurichten.

# echo "alice:165537:65536" >> /etc/subuid
[root@podhost-r8-1 ~]# echo "alice:165537:65536" >> /etc/subgid
[root@podhost-r8-1 ~]# echo "user.max_user_namespaces=65636" > /etc/sysctl.d/userns.conf
[root@podhost-r8-1 ~]# sysctl -p /etc/sysctl.d/userns.conf
user.max_user_namespaces = 65636

Anschließend installiere ich wie in Kap. 1.3 [1] beschrieben die Container-Tools.

# yum module install -y container-tools
$ podman --version
podman version 2.0.5

Der Werkzeugkasten ist bestückt. Weiter zu Schritt 2.

Schritt 2: Container-Images suchen, inspizieren und herunterladen

Mit dem Kommando podman mache ich mich auf die Suche nach Containern für Kanboard.

$ podman search kanboard
INDEX       NAME                                            DESCRIPTION                                       STARS   OFFICIAL   AUTOMATED
docker.io   docker.io/kanboard/kanboard                     Official Docker image for Kanboard                34
docker.io   docker.io/webhippie/kanboard                    Docker images for Kanboard                        2                  [OK]
docker.io   docker.io/larueli/kanboard-nonroot              Safe image for Kanboard as Non Root / Suitab...   0
docker.io   docker.io/masker/kanboard                       use alpine linux build kanboard server            0
docker.io   docker.io/xoxys/kanboard                        Deprecated                                        0
docker.io   docker.io/dotriver/kanboard                     Kanboard on Alpine Linux + S6 Overlay             0
docker.io   docker.io/thegeeklab/kanboard                   Custom image for Kanboard Kanban project man...   0
docker.io   docker.io/jonats/kanboard-pi                    Raspberry Pi image for Kanboard                   0
docker.io   docker.io/bastilian/kanboard                                                                      0
docker.io   docker.io/oriaks/kanboard                       Kanboard                                          0                  [OK]
docker.io   docker.io/kanboard/tests                                                                          0
docker.io   docker.io/blufor/kanboard                       Kanboard with Postgres, SMTP and GitLab inte...   0                  [OK]
docker.io   docker.io/boomer/kanboard                       Kanboard is a simple visual task board web a...   0
docker.io   docker.io/joshuacox/kanboard-redmine            kanboard redmine importer                         0                  [OK]
docker.io   docker.io/janost/kanboard-unit                  Kanboard + nginx unit, running rootless with...   0
docker.io   docker.io/benoit/kanboard                                                                         0                  [OK]
docker.io   docker.io/lidstah/kanboard                      Kanboard armv71 debian (nginx/php7-fpm) base...   0
docker.io   docker.io/doc75/kanboard                                                                          0
docker.io   docker.io/witsec/kanboard                       Kanboard, with the option to filter (hide) s...   0                  [OK]
docker.io   docker.io/ionutalexandru97/kanboard-openshift   Kanboard ready to be deployed on OpenShift        0
docker.io   docker.io/hihouhou/kanboard                     simple kanboard                                   0                  [OK]
docker.io   docker.io/alxsdhm/kanboard                      kanboard image                                    0
docker.io   docker.io/papango/kanboard                                                                        0
docker.io   docker.io/mrtheduke/kanboard                    kanboard                                          0
docker.io   docker.io/kvorobyev/kanboard_app

Herzlichen Glückwunsch. Die Trefferliste stellt für mich als SysAdmin einen Alptraum dar. Sämtliche Treffer stammen vom Docker-Hub, einem riesigen Misthaufen für Software (welcher durchaus ein paar Perlen enthalten kann). Von den 26 Treffern ist keiner als OFFICIAL markiert, lediglich die Anzahl STARS bietet einen Anhaltspunkt, welcher Container den meisten Zuspruch findet. In einer Produktiv-Umgebung sollte man sich jedoch nicht allein auf diese Sterne verlassen. Ich inspiziere das Container-Image mit den meisten Sternen mit skopeo:

$ skopeo inspect docker://docker.io/kanboard/kanboard | less

Die vollständige Ausgabe spare ich hier aus. Sie ist wenig hilfreich. Mit ein wenig Internet-Recherche ([2], [3] und [4]) bin ich hinreichend sicher, das „offizielle“ Container-Image des Projekts gefunden zu haben.

Als nächstes mache ich mich auf die Suche nach Postgresql:

$ podman search postgresql | wc -l
64

Naja, zumindest an Auswahl scheint es auch diesmal nicht zu mangeln. Hier komme ich so jedoch nicht weiter. Also nehme ich einen Webbrowser zur Hand und recherchiere ein geeignetes Container-Image unter der URL: https://catalog.redhat.com/software/containers/explore

Da ich Red Hat bereits zutraue, eine stabile und hinreichend sichere Enterprise Linux Distribution zu bauen, traue ich ihnen auch zu, ordentliche Container-Images für Postgresql zu bauen. Daher fasse ich folgende drei Kandidaten ins Auge:

  1. rhel8/postgresql-96
  2. rhel8/postgresql-10
  3. rhel8/postgresql-12

Zu diesem Zeitpunkt (2020-12-27) fehlt Nr. 3 eine ordentliche Beschreibung. Dafür kommt dieses Image mit 6 offenen Sicherheitslücken daher. Nr. 2 besitzt nur 3 Schwachstellen und eine deutliche bessere Dokumentation zu dessen Verwendung. Und Nr. 1 ist zwar das Älteste, jedoch auch das mit einer guten Dokumentation und ohne Schwachstellen.

Kanboard erfordert Postgresql >= 9.4. Damit ist Nummer 1 mein Gewinner. Mit den beiden folgenden Kommandos hole ich mir die Kanboard- und Postgresql-Container-Images auf meinen Host.

$ podman pull docker.io/kanboard/kanboard
Trying to pull docker.io/kanboard/kanboard...
Getting image source signatures
Copying blob df20fa9351a1 done  
Copying blob 3108c8300796 done  
Copying blob b190b4dd9bb5 done  
Copying blob bb1f52abd628 done  
Copying blob e37ffd2cbe7b done  
Copying config c355188e0c done  
Writing manifest to image destination
Storing signatures
c355188e0c187bc891826d282cc850cbe0907ccd7df28d4487d024d831c4f9af

$ podman login --username=Joerg-Dev registry.redhat.io
Password: 
Login Succeeded!
$ podman pull registry.redhat.io/rhel8/postgresql-96
Trying to pull registry.redhat.io/rhel8/postgresql-96...
Getting image source signatures
Copying blob cca21acb641a done  
Copying blob 620696f92fec done  
Copying blob fca753c96be9 done  
Copying blob d9e72d058dc5 done  
Copying config f7266b012d done  
Writing manifest to image destination
Storing signatures
f7266b012db03478b858eba6af4264829b99ce9ac67d6bc8a7c273b5fc5c8e9a

Damit ist dieser Schritt abgeschlossen. In Schritt drei erstelle ich sogenannte Volumes, um Daten außerhalb der Container persistent im Dateisystem des Hosts speichern zu können.

Schritt 3: Persistenten Speicher für Container erzeugen

Nach dem Container-Mantra haben diese zustandslos zu sein. Dies bedeutet, dass in ihnen gespeicherte Daten verloren gehen, wenn der Container entfernt wird. Nun hat es die Elektronische Datenverarbeitung (EDV) so an sich, dass das Ergebnis der Verarbeitung häufig persistent zu speichern ist. Dies kann im Container-Universum mit sogenannten Volumes erledigt werden. Hierbei wird ein Verzeichnis vom Host in den Container eingehängt.

Für mein Projekt erstelle ich nach Kapitel 3.4 [1] folgende Volumes:

  • kanboard_data
  • kanboard_plugins
  • kanboard_ssl
  • pgsql_db
$ podman volume create VOLUMENAME

Um im Folgenden etwas leichter mit diesen Volumes arbeiten zu können, speichere ich den Einhängepfad in Variablen à la:

$ mntPoint=$(podman volume inspect VOLUMENAME --format {{.Mountpoint}})

Die obige Streichung erfolgte, da dieser Schritt nicht notwendig ist und im weiteren Artikel nicht mit entsprechenden Variablen gearbeitet wird.

Schritt 4: Kanboard konfigurieren

Um eine angepasste, persistente config.php-Datei für den Kanboard-Container zu schreiben, ist etwas Vorarbeit notwendig. Der Kanboard-Container wird gestartet und das Volume „kanboard_data“ wird dabei in den Pfad /var/www/app/data gemountet. Anschließend starte ich eine Shell im Container und kopiere die Datei /var/www/app/config.default.php nach /var/www/app/data/config.php.

$ podman run -d --name kanboard -v kanboard_data:/var/www/app/data:Z  kanboard/kanboard
93e6d7e3847fb94639b8fce89ddb93a3879a80522f95ed13dff91f6558594ac6
$ podman ps
CONTAINER ID  IMAGE                               COMMAND  CREATED        STATUS            PORTS   NAMES
93e6d7e3847f  docker.io/kanboard/kanboard:latest           5 seconds ago  Up 5 seconds ago          kanboard
$ podman exec -it 93e6d7e3847f /bin/bash
bash-5.0# cp /var/www/app/config.default.php /var/www/app/data/config.php
bash-5.0# exit
exit
$ podman stop 93e6d7e3847f && podman rm 93e6d7e3847f
$ vi $kanboard_data/config.php

Um Postgresql als Datenbank-Backend zu nutzen, werden folgende Werte in der config.php gesetzt:

// Run automatically database migrations
// If set to false, you will have to run manually the SQL migrations from the CLI during the next Kanboard upgrade
// Do not run the migrations from multiple processes at the same time (example: web page + background worker)
define('DB_RUN_MIGRATIONS', true);

// Database driver: sqlite, mysql or postgres (sqlite by default)
define('DB_DRIVER', 'postgres');

// Mysql/Postgres username
define('DB_USERNAME', 'root');

// Mysql/Postgres password
define('DB_PASSWORD', 'SuperSicheresPasswort');

// Mysql/Postgres hostname
define('DB_HOSTNAME', 'localhost');

// Mysql/Postgres database name
define('DB_NAME', 'kanboard');

// Mysql/Postgres custom port (null = default port)
define('DB_PORT', null);

Normalerweise würde man eine Anwendung niemals mit dem Datenbank-Root-User auf eine Datenbank zugreifen lassen. In dieser Umgebung ist es hingegen verschmerzbar, da nur die Daten des Kanboards in diesem Postgresql-Container gespeichert werden. Im Falle einer Kompromittierung verliere ich nur die zur Anwendung gehörende Datenbank.

Schritt 5: Pod erstellen und Container hinzufügen

Mit diesem Schritt habe ich etwas Mühe. Zuerst wollte ich einen Pod erstellen, den Kanboard- und Postgresql-Container zu diesem hinzufügen, um sie stets gemeinsam starten und stoppen zu können. Dies ist laut [1] und [7] der einfachste Weg. Allerdings habe ich dann in [5] und [7] gelesen, dass sich die Container eines Pods dessen IP, MAC und Port-Bindings teilen. Dies bedeutet, dass Portfreigaben für Kanboard (80 und 443 TCP) auch für den Postgresql-Container gültig sind. Dies möchte ich eigentlich nicht. Doch ist mir bisher nichts besseres eingefallen. Falls ihr Anregungen oder konkrete Beschreibungen habt, wie ich dies besser umsetzen kann, immer her damit.

Frickelpit hat mich in seinem Kommentar darauf hingewiesen, dass man den Zugriff auf den Port des Pods noch weiter beschränken kann, indem man diesen an 127.0.0.1 bindet. Ich habe unten stehenden Code-Block entsprechend aktualisiert.

Ich erstelle nun gemäß [7] einen neuen Pod, welcher den Kanboard-Container beinhaltet und für diesen Port-Bindings besitzt:

$ podman run -d --pod new:kanboardpod --name kanboard -p 127.0.0.1:8080:80 -v kanboard_data:/var/www/app/data:Z -v kanboard_plugins:/var/www/app/plugins:Z kanboard/kanboard
e62c7fa2ecf771f4085e788e9f0f7d24b7f87d487e9951a403847d8a7a2a6471

$ podman pod ps
POD ID        NAME         STATUS   CREATED        # OF CONTAINERS  INFRA ID
d7afa6821382  kanboardpod  Running  8 seconds ago  2                6b065fe7ecc7

$ podman ps
CONTAINER ID  IMAGE                               COMMAND  CREATED         STATUS             PORTS                 NAMES
6b065fe7ecc7  k8s.gcr.io/pause:3.2                         10 seconds ago  Up 10 seconds ago  0.0.0.0:8080->80/tcp  d7afa6821382-infra
e62c7fa2ecf7  docker.io/kanboard/kanboard:latest           10 seconds ago  Up 10 seconds ago  0.0.0.0:8080->80/tcp  kanboard

Im zweiten Schritt füge ich den Postgresql-Container hinzu:

$ podman run -d --pod kanboardpod --name pgsql_db -e POSTGRESQL_USER=root -e POSTGRESQL_PASSWORD=SuperGeheimesPasswort -e POSTGRESQL_DATABASE=kanboard -v pgsql_data:/var/lib/pgsql/data:Z rhel8/postgresql-96
c242a4b9b57d53a822585c9eb83d081d5abbd40cb2b5952aee4457fee041e128

$ podman ps
CONTAINER ID  IMAGE                                          COMMAND         CREATED        STATUS            PORTS                 NAMES
6b065fe7ecc7  k8s.gcr.io/pause:3.2                                           2 minutes ago  Up 2 minutes ago  0.0.0.0:8080->80/tcp  d7afa6821382-infra
c242a4b9b57d  registry.redhat.io/rhel8/postgresql-96:latest  run-postgresql  3 seconds ago  Up 3 seconds ago  0.0.0.0:8080->80/tcp  pgsql_db
e62c7fa2ecf7  docker.io/kanboard/kanboard:latest                             2 minutes ago  Up 2 minutes ago  0.0.0.0:8080->80/tcp  kanboard

Nun läuft ein Pod mit drei Containern (Infra-, Kanboard- und Postgresql-Container). Rufe ich http://IP-DES-HOSTS:8080 in einem Webbrowser auf, begrüßt mich bereits die Kanboard-Anmeldemaske (Bild 1).

Bild 1: Kanboard-Anmeldemaske

Schritt 6: Nacharbeiten

Zwar konnte ich mich bereits an der Kanboard-Anwendung anmelden, neue Nutzer erstellen und Profileinstellungen verwalten, doch beim Dateiupload klemmte es noch. Hier musste ich in der config.php-Datei des Kanboard-Containers den folgenen Standardwert, wie im Codeblock gezeigt, angepassen:

// Folder for uploaded files (must be writeable by the web server user)
// Folgende Zeilen wurden auskommentiert
// define('FILES_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'files');

// Folgender Eintrag wurde hinzugefuegt
define('FILES_DIR', 'data/files');

Abschließend habe ich den Kanboard-Container mittels podman restart kanboard neugestartet.

Fazit

Bei der Internet-Recherche nach guter Dokumentation und der Arbeit mit einigen Container-Registries erinnere ich mich an ein Zitat:

Das Internet ist ein großer Misthaufen, in dem man allerdings auch kleine Schätze und Perlen finden kann.

Joseph Weizenbaum, Vortrag in Hamburg am 2. Mai 2001, heise.de

Bisher wurden die Ziele 1-3 erreicht. Die dabei verwendeten Befehlszeilen besitzen eine beachtliche Länge. Hier bietet es sich an, die Befehle in kurze Shell-Wrapper zu verpacken.

Die Ziele 4 und 5 werde ich in einem Folgeartikel, an einem anderen Wochenende, in Angriff nehmen. Und auch der Frage, wie man diesen Verhau am besten aktualisiert und Updates implementiert, werde ich noch nachgehen.

Ihr habt bis hierhin durchgehalten? Dann danke ich euch für euer Interesse. Was haltet ihr von diesem Wochend-Projekt? Wieviel Sinn bzw. Unsinn steckt darin? Bitte lasst es mich in den Kommentaren oder auch gern per E-Mail wissen.

Quellen und weiterführende Links

  1. Building, running, and managing Linux containers on Red Hat Enterprise Linux 8
  2. Kanban Project Management Software — Kanboard
  3. Running Kanboard with Docker
  4. Kaboard Releases
  5. https://podman.io/getting-started/network
  6. Podman: Managing pods and containers in a local container runtime; Brent Baude; January 15, 2019