Buildah Archives - credativ®

Nach unseren Beiträgen zu Podman und Buildah widmen wir uns heute dem dritten Tool aus der Sammlung von RedHat-Container Tools: Skopeo.

Bei Skopeo handelt es sich um ein vergleichsweise kleines Tool, das hauptsächlich Anwendung in der Container-Erstellung und -Nutzung findet und damit prädestiniert für folgende Aufgaben und Bereiche ist:

Hierbei ist noch zu erwähnen, dass Skopeo auch einen lokalen Docker-Daemon wie eine Registry behandelt und dies als Quelle genutzt werden kann. Auch wird, wenn nicht anders angegeben, immer auch für die Images von der aktuell für Skopeo genutzten Architektur ausgegangen. Dies lässt sich jedoch durch Parameter ändern bzw. erweitern.

Entwicklung

Die Skopeo Version 1.0 wurde am 18.05.2020 veröffentlicht und wird aktuell, wie auch die anderen RHEL-Container-Tools, unter der Apache 2.0 Lizenz angeboten. Es wird ebenfalls in Golang entwickelt, hat keinen festen Release-Zyklus und wird von der containers-group betreut. Der Code kann auf Github eingesehen werden.

Installation

Die Installation von Skopeo ist mittlerweile auf fast allen Linux-Distributionen recht trivial über eine einfache Paketinstallation erledigt, da alle gängigen Distributionen das Paket mittlerweile in den Repositories hinterlegt haben. Sollte eine bestimmte Version benötigt werden, oder es nicht möglich sein Pakete zu installieren, so wird Skopeo auch als dedizierter Container zur Verfügung gestellt.

Auch ist hier wie üblich zu beachten, dass nicht jede Distribution auch die aktuellste Version bereit stellt und damit ggf. Dritt-Repositories hinzugefügt werden müssen.

Eine detaillierte Dokumentation dazu findet sich auf Github.

Konfiguration

Skopeo nutzt hier, ebenso wie podman und buildah, folgende Dateien als Standard:

Arbeiten mit Skopeo

In den hier verwendeten Beispielen nutzen wir die aktuell unter CentOS 8 Streams verfügbare Version 1.5.0.

Inspizieren eines Images

Mittels skopeo inspect können die Details zu einem Image bzw. dem Repository eingesehen werden ohne sich die Daten vorher herunterladen zu müssen. Es werden unter anderem alle verfügbaren Tags des Repositorys sowie verschiedene Details zum angefragten Image beziehungsweise Image-Tag.

Es werden hierbei noch verschiedene Parameter angeboten um bestimmte Details anzufragen oder die Ausgabe anzupassen.
Folgend ein paar Beispiele:

Alle Optionen für inspect sowie diverse Beispiele finden sich in der Dokumentation.

Hierzu ein Beispiel für ein generisches skopeo inspect:

$ skopeo inspect docker://docker.io/library/postgres:latest
{
    "Name": "docker.io/library/postgres",
    "Digest": "sha256:f91f537eb66b6f80217bb6921cd3dd4035b81a5bd1291e02cfa17ed55b7b9d28",
    "RepoTags": [
        "10",
        ...
        "bullseye",
        "buster",
        "latest"
    ],
    "Created": "2022-01-04T01:19:59.244463885Z",
    "DockerVersion": "20.10.7",
    "Labels": null,
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": [
        "sha256:a2abf6c4d29d43a4bf9fbb769f524d0fb36a2edab49819c1bf3e76f409f953ea",
        ...
    ],
    "Env": [
        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/14/bin",
        "GOSU_VERSION=1.14",
        "LANG=en_US.utf8",
        "PG_MAJOR=14",
        "PG_VERSION=14.1-1.pgdg110+1",
        "PGDATA=/var/lib/postgresql/data"
    ]
}

Und ein Bespiel für ein skopeo inspect --config:

$ skopeo inspect --config docker://docker.io/library/postgres:latest

{
    "created": "2022-01-04T01:19:59.244463885Z",
    "architecture": "amd64",
    "os": "linux",
    "config": {
        "ExposedPorts": {
            "5432/tcp": {}
        },
        "Env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/14/bin",
            "GOSU_VERSION=1.14",
            "LANG=en_US.utf8",
            "PG_MAJOR=14",
            "PG_VERSION=14.1-1.pgdg110+1",
            "PGDATA=/var/lib/postgresql/data"
        ],
        "Entrypoint": [
            "docker-entrypoint.sh"
        ],
        "Cmd": [
            "postgres"
        ],
        "Volumes": {
            "/var/lib/postgresql/data": {}
        },
        "StopSignal": "SIGINT"
    },
    "rootfs": {
        "type": "layers",
        "diff_ids": [
            "sha256:2edcec3590a4ec7f40cf0743c15d78fb39d8326bc029073b41ef9727da6c851f",
        ...
        ]
    },
    "history": [
        {
            "created": "2021-12-21T01:22:43.418913408Z",
            "created_by": "/bin/sh -c #(nop) ADD file:09675d11695f65c55efdc393ff0cd32f30194cd7d0fbef4631eebfed4414ac97 in / "
        },
        ...
        {
            "created": "2022-01-04T01:19:59.244463885Z",
            "created_by": "/bin/sh -c #(nop)  CMD [\"postgres\"]",
            "empty_layer": true
        }
    ]
}

Um eine dedizierte Liste aller im Repository verfügbaren Image-Tags zu bekommen eignet sich der Befehl skopeo list-tags. Skopeo braucht nur die URL zum Repository, zum Beispiel docker://docker.io/library/postgres, und kann so die ganze Liste aller Image-Tags auslesen und verfügbar machen.

Die Dokumentation ist selbst-erklärend und fällt entsprechend kompakt aus.

Kopieren von Images

Mittels skopeo copy können Images entweder zwischen verschiedenen Registries kopiert werden, oder auch zum Beispiel von einer Registry in ein lokales Verzeichnis.

Hierbei stehen vielfältige Parameter zur Verfügung, wobei hier wieder nur ein paar Beispiele genannt werden sollen:

Hierbei kann es natürlich nötig sein Login-Informationen anzugeben. Details dazu werden weiter unten beschrieben.

Generelle Dokumentation zu den Copy-Möglichkeiten findet sich in der Dokumentation.

Löschen von Images

Mit Skopeo ist es ebenfalls möglich Images einer Registry remote als “gelöscht” zu markieren. Hierbei ist zu beachten, dass das Image erst dann tatsächlich im Dateisystem entfernt wird, wenn die Löschroutine / Garbage-Collection des Registry ihren Lauf hatte. Neben der Authentifizierung sind hier keine weiteren Optionen möglich.

Alle Informationen finden sich inkl. Beispielen in der offiziellen Dokumentation.

Synchronisieren von Images

Skopeo bietet die Möglichkeit alle Images eines Repositories entweder mit einem anderen Repository oder einem lokalen Ordner zu synchronisieren. Hierbei werden alle Images des Quellpfades in den Zielpfad übernommen.

Anwendung findet diese Synchronisierungsmöglichkeit zum Beispiel bei lokalen Entwicklungsumgebung deren Images in einem Repository liegen und dort automatisch synchronisiert werden können. Auch der umgekehrte Weg, die Synchronisierung aller Images aus einem internen Repository auf eine lokale Umgebung ist hiermit einfach umzusetzen.

Es ist sogar möglich als Quelle eine yaml-Datei anzugeben in welcher als Liste verschiedene Images aus verschiedenen Repositories hinterlegt sind und diese dann einmal auf einen bestimmten Host oder eine andere Registry zu kopieren. Hierbei ist zu beachten, dass in der YAML-Version keine lokalen Pfade als Quelle erlaubt sind. Natürlich können in der yaml-Datei auch verschiedene zusätzliche Parameter wie zum Beispiel Logindaten hinterlegt werden.

Eine volle Übersicht der Parameter und verschiedene Beispiele finden sich wie gewohnt in der Dokumentation.

Authentifizierung

In fast allen Fällen ist es möglich die Parameter für die Authentifizierung direkt im skopeo-Aufruf mit anzugeben.
Bei Befehlen welche Quelle und Ziel beinhalten gibt es die Möglichkeit für --src-creds sowie --dest-creds. Beim Arbeiten an einer einzelnen Registry wird meist nur --creds genutzt. Gleichermaßen gibt es auch diverse Einzelparameter für username und password.

Alternativ kann ein Login auch mittels skopeo login manuell gestartet werden. Dieser Aufruf erfragt die Logindaten interaktiv und erstellt ein Auth-file (Im Standard ${XDG_RUNTIME_DIR}/containers/auth.json), welches dann ebenfalls als Parameter an die einzelnen Skopeo-Aufrufe weitergereicht werden kann. So müssen Authentifizierungsdaten nicht zwangsläufig im Code selbst hinterlegt werden sondern können, angepasst an die Vorliebe des Nutzers, interaktiv erfragt werden. Diese Auth-Datei teilt sich Skopeo auch mit Podman und Buildah und reiht sich damit nahtlos in die Sammlung von RedHat-Container Tools ein.

Um Datensätze wieder zu entfernen kann skopeo logout genutzt werden.

Fazit

Skopeo ist eine sinnvolle Ergänzung zu den beiden anderen Container-Tools und bietet einige Features bei denen sich der Autor schon länger fragt warum diese nicht auch an andere Stelle zur Verfügung stehen.

Besonders die inspect und copy Funktionen von Skopeo können viele verschiedene andere Schritte und auch eine Menge Zeit einsparen wenn man viel mit verschiedenen Repositories arbeitet. Allein sich die verschiedenen Tags eines Images und deren Details anzuschauen ist durchaus eine große Hilfe in der alltäglichen Arbeit und erspart diverse Suchanfragen und Recherchen über UIs.

Auch das einfache Kopieren eines Images von einer Registry (z.B. Docker-Hub) in eine interne Registry ist gerade nach den Limitierung von Dockerhub eine willkommene Funktion, auch wenn die nach und nach Einzug haltende Funktion des Cache-Proxies bei einigen Registries wie zum Beispiel Harbor dies auch ablösen kann.

Wir unterstützen Sie gerne

Mit über 22+ Jahren an Entwicklungs- und Dienstleistungserfahrung im Open Source Bereich, kann die credativ GmbH Sie mit einem beispiellosen und individuell konfigurierbaren Support professionell Begleiten und Sie in allen Fragen bei Ihrer Open Source Infrastruktur voll und ganz unterstützen.

Die credativ GmbH ist ein herstellerunabhängiges Beratungs- und Dienstleistungs- unternehmen mit Standort in Mönchengladbach. Seit dem erfolgreichen Merger mit Instaclustr 2021 ist die credativ GmbH das europäische Hauptquartier der Instaclustr Gruppe.

Die Instaclustr Gruppe hilft Unternehmen bei der Realisierung eigener Applikationen im großen Umfang dank Managed-Plattform-Solutions für Open Source Technologien wie zum Beispiel Apache Cassandra®, Apache Kafka®, Apache Spark™, Redis™, OpenSearch™, Apache ZooKeeper™, und PostgreSQL®.
Instaclustr kombiniert eine komplette Dateninfrastruktur-Umgebung mit praktischer Expertise, Support und Consulting um eine kontinuierliche Leistung und Optimierung zu gewährleisten. Durch Beseitigung der Konplexität der Infrastruktur, wird es Unternehmen ermöglicht, ihre internen Entwicklungs- und Betriebsressourcen auf die Entwicklung innovativer kundenorientierter Anwendungen zu geringeren Kosten zu konzentrieren. Zu den Kunden von Instaclustr gehören einige der größten und innovativsten Fortune-500-Unternehmen.

In unserem vorhergehenden Artikel über Buildah haben wir erläutert wie man Container auch ohne Docker und root-Berechtigungen erstellt.

In diesem Artikel soll es darum gehen wie man eben jene Container auch ohne erweiterte Rechte nutzen kann.

Podman gehört neben dem bereits erwähnten Buildah und Skopeo zu den RedHat Container Tools und ist, kurz Zusammengefasst, eine daemonlose Laufzeitumgebung für Container. Es ist dabei, wie der Docker-Daemon, für den Betrieb eines einzelnen Hosts ausgelegt und bietet auch keine Clusterfunktionalität.

Entwicklung

Podman wurde in der Version 1.0 am 11.01.2019 released und steht ebenfalls unter der Apache 2.0 Lizenz.
Die Implementierung erfolgt in Golang und wird von der „containers organisation“ in erster Instanz vorangetrieben. Diese beinhaltet sowohl RedHat-Mitarbeiter als auch externe Entwickler.
Der Code kann auf Github eingesehen werden. Die Entwicklung erfolgt dabei in keinem festen Releasezyklus. So können sowohl Monate wie auch Wochen zwischen den Versionen liegen, je nachdem wenn Entschieden wird, dass genügend neue Features für einen Release implementiert wurden.

Podman selbst baut komplett auf die libpod auf, bzw. könnte man auch sagen, dass es das Tool zur libpod ist. Daher ist der Name des Repositorys auch libpod und nicht podman.

Container ohne root-Berechtigungen

Eine zentrale Komponente von Buildah und auch Podman ist hier die libpod welche es ermöglicht Container nur mit Nutzerberechtigungen zu starten und Images zu erstellen.
Diese setzt auf slirp4netns, fuse-overlayfs und die /etc/sub(u|g)id.

Dieser Bereich wurde bereits ausführlich im Artikel zu Buildah behandelt, weswegen hier nur auf diesen verwiesen wird, um Wiederholungen zu vermeiden.

Installation

Podman ist bei den gängigen RedHat-Distributionen direkt in den Repositories verfügbar.
Diese können dort je nach Version via dnf install podman oder yum install podman installiert werden.
Zu beachten ist, dass die Pakete in den CentOS-Distributionen nicht unbedingt aktuell sind. Daher bietet es sich hier an ebenfalls auf Kubic auszuweichen.

Für Debian und Derivate sowie Suse stehen analog zu Buildah Pakete in Kubic bereit.

Weitere Informationen dazu finden sich in der Dokumentation

[podmanager@buildah ~]$ podman -v
podman version 1.8.2

Konfiguration

Die Konfigurationsdatei für Podman ist analog zu Builder unter /etc/containers/libpod.conf für die globale und unter ~/.config/containers/libpod.conf für die benutzerspezifische Konfiguration zu finden.
Die Vorlage mit den Default-Werten findet sich unter /usr/share/containers/libpod.conf. Diese sollte jedoch nicht direkt, sondern durch die beiden Alternativen angepasst werden.
In der Datei lassen sich verschiedene Parameter für Podman konfigurieren; welches CNI-Plugin genutzt werden soll, welche Container-Runtime oder auch wo die Volumes der Container zu finden sind.

Ein Online-Beispiel findet sich auf Github

Für einen ersten Testbetrieb sind hier jedoch keine Änderungen erforderlich, es dient lediglich dazu ggf. podman an die eigenen Wünsche anzupassen.

Arbeiten mit Podman

Podman wurde als Drop-In-Replacement für Docker entworfen und daher funktionieren die meisten Befehle wie ps, rm, inspect, logs oder exec analog zu docker und sollen hier wenn dann nur kurz erwähnt werden. Die Funktionalität ist dabei nicht nur auf den Betrieb von Container beschränkt, es ist im begrenzten Maße auch möglich Container zu erstellen. Im Hintergrund setzt Podman auf die Funktionalität von Buildah, kann jedoch Container nur aus einem Containerfile heraus erstellen.
Details dazu finden sich in der Dokumentation

Auch ein podman top $ContainerID funktioniert ebenso wie das Erstellen, Migrieren und Restore eines Checkpoints.

[user@system1 ~]$ podman container checkpoint <container_id> -e /tmp/checkpoint.tar.gz
[user@system1 ~]$ scp /tmp/checkpoint.tar.gz <destination_system>:/tmp

[user@system2 ~]$ podman container restore -i /tmp/checkpoint.tar.gz

In den folgenden Bereichen soll daher in erster Linie auf die Unterschiede in der Containerhandhabe zwischen Docker und Podman eingegangen werden.

Einen Container starten

Um einen Container der Wahl (hier postgres) zu starten, pullen wir das Image und starten dies anschließend.

[podmanager@buildah ~]$ podman pull postgres
...
Copying config 73119b8892 done  
Writing manifest to image destination
Storing signatures
73119b8892f9cda38bb0f125b1638c7c0e71f4abe9a5cded9129c3f28a6d35c3

[podmanager@buildah ~]$ podman inspect postgres | grep "PG_VERSION="
    "PG_VERSION=12.2-2.pgdg100+1",
    "created_by": "/bin/sh -c #(nop)  ENV PG_VERSION=12.2-2.pgdg100+1",

[podmanager@buildah ~]$ podman run -d -e POSTGRES_PASSWORD=SuperDB --name=postgres_dev postgres
c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7
[podmanager@buildah ~]$ podman ps
CONTAINER ID  IMAGE                              COMMAND   CREATED        STATUS            PORTS  NAMES
c8b9732b6ad2  docker.io/library/postgres:latest  postgres  5 seconds ago  Up 4 seconds ago         postgres_dev

Es läuft nun ein PostgreSQL®-Container mit dem Namen „postgres_dev“. Dies unterscheidet sich soweit nicht von Docker.

Die Besonderheit von podman ist erst in der Prozessliste ersichtlich:

podmana+  2209     1  0 13:11 ?        Ssl    0:00 /usr/bin/conmon --api-version 1 -c c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7 -u c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7 -r /usr/bin/runc -b

232070    2219  2209  0 13:11 ?        Ss     0:00  \_ postgres

Der PostgreSQL®-Prozess läuft nicht als Kind eines Daemon-Prozesses, sondern der Komponente „conmon“.
Diese überwacht den Zustand des Containers nach dem Start. Außerdem stellt es den Socket für die Kommunikation bereit und den Stream für den Output, welche
in das von podman konfigurierte Log geschrieben werden.
Weiterführende Informationen zu conmon finden sich auf Github

Starten wir jetzt einen zweiten Container (postgres_prod) via podman wird ein weiterer conmon-Prozess gestartet:

[podmanager@buildah ~]$ podman run -d -e POSTGRES_PASSWORD=SuperDB --name=postgres_prod postgres
6581a25c82620c725fe1cfb6546479edac856228ecb3c11ad63ab95a453c1b64

[podmanager@buildah ~]$ podman ps
CONTAINER ID  IMAGE                              COMMAND   CREATED         STATUS             PORTS  NAMES
6581a25c8262  docker.io/library/postgres:latest  postgres  15 seconds ago  Up 15 seconds ago         postgres_prod
c8b9732b6ad2  docker.io/library/postgres:latest  postgres  7 minutes ago   Up 7 minutes ago          postgres_dev
podmana+  2209     1  0 13:11 ?        Ssl    0:00 /usr/bin/conmon --api-version 1 -c c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7 -u c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7 -r /usr/bin/runc -b
232070    2219  2209  0 13:11 ?        Ss     0:00  \_ postgres
...
podmana+  2337     1  0 13:19 ?        Ssl    0:00 /usr/bin/conmon --api-version 1 -c 6581a25c82620c725fe1cfb6546479edac856228ecb3c11ad63ab95a453c1b64 -u 6581a25c82620c725fe1cfb6546479edac856228ecb3c11ad63ab95a453c1b64 -r /usr/bin/runc -b
232070    2348  2337  0 13:19 ?        Ss     0:00  \_ postgres
...

Hier finden sich im Prozess jeweils die UUIDs der Container wieder.
Der cmdline des Prozesses ist natürlich sehr viel länger als hier dargestellt. Folgend noch ein komplettes Beispiel manuell formatiert:

[podmanager@buildah ~]$ ps -ef f  | grep conmon
...
podmana+  2209     1  0 13:11 ?        Ssl    0:00 /usr/bin/conmon
--api-version 1
-c c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7
-u c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7
-r /usr/bin/runc
-b /home/podmanager/.local/share/containers/storage/overlay-containers/c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7/userdata
-p /var/tmp/run-1002/containers/overlay-containers/c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7/userdata/pidfile
-l k8s-file:/home/podmanager/.local/share/containers/storage/overlay-containers/c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7/userdata/ctr.log
--exit-dir /var/tmp/run-1002/libpod/tmp/exits
--socket-dir-path /var/tmp/run-1002/libpod/tmp/socket
--log-level error
--runtime-arg --log-format=json
--runtime-arg --log
--runtime-arg=/var/tmp/run-1002/containers/overlay-containers/c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7/userdata/oci-log
--conmon-pidfile /var/tmp/run-1002/containers/overlay-containers/c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7/userdata/conmon.pid
--exit-command /usr/bin/podman
--exit-command-arg --root 
--exit-command-arg /home/podmanager/.local/share/containers/storage 
--exit-command-arg --runroot 
--exit-command-arg /var/tmp/run-1002/containers 
--exit-command-arg --log-level 
--exit-command-arg error 
--exit-command-arg --cgroup-manager 
--exit-command-arg cgroupfs 
--exit-command-arg --tmpdir 
--exit-command-arg /var/tmp/run-1002/libpod/tmp 
--exit-command-arg --runtime 
--exit-command-arg runc 
--exit-command-arg --storage-driver 
--exit-command-arg overlay 
--exit-command-arg --storage-opt 
--exit-command-arg overlay.mount_program=/usr/bin/fuse-overlayfs 
--exit-command-arg --storage-opt 
--exit-command-arg overlay.mount_program=/usr/bin/fuse-overlayfs 
--exit-command-arg --events-backend 
--exit-command-arg file 
--exit-command-arg container 
--exit-command-arg cleanup 
--exit-command-arg c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7
...

Durch die Formatierung ist sehr schön zu sehen, wie die Übergabe von Parametern zwischen podman und conmon über die *args funktioniert.

Zusätzlich zu conmon wird natürlich auch jeweils eine Instanz von slirp4netns und fuse-overlayfs pro container gestartet um Netzwerk und Storage ohne root-Berechtigungen bereitzustellen.

podmana+  2201     1  0 13:11 ?        Ss     0:00 /usr/bin/fuse-overlayfs -o lowerdir=/home/podmanager/.local/share/containers/storage/overlay/l/FX4RZGGJ5HSNVMGVFG6K3I7PIL:/home/podmanager/.local/share/containers/storage/overlay/l/AIHUOS

podmana+  2206     1  0 13:11 pts/0    S      0:00 /usr/bin/slirp4netns --disable-host-loopback --mtu 65520 --enable-sandbox -c -e 3 -r 4 --netns-type=path /tmp/run-1002/netns/cni-18902a12-5b1b-15d3-0c31-138efe1d66ba tap0

podmana+  2209     1  0 13:11 ?        Ssl    0:00 /usr/bin/conmon --api-version 1 -c c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7 -u c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7 -r /usr/bin/runc -b

232070    2219  2209  0 13:11 ?        Ss     0:00  \_ postgres

Erstellen eines Systemd-service-files

Da die Container ohne Daemon laufen und einzeln angestartet werden können, bietet es sich natürlich auch an diese nicht via Docker, sondern via Systemd zu steuern.
Das Schreiben von Service-files ist jedoch im allgemeinen mühsam, weswegen podman dafür direkt eine Funktion eingebaut hat.

Folgend ein Beispiel für unsere postgres_dev

[podmanager@buildah ~]$ podman generate systemd postgres_dev
# container-c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7.service
# autogenerated by Podman 1.8.2
# Tue Mar 24 13:47:11 CET 2020

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

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=on-failure
ExecStart=/usr/bin/podman start c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7
ExecStop=/usr/bin/podman stop -t 10 c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7
PIDFile=/var/tmp/run-1002/containers/overlay-containers/c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7/userdata/conmon.pid
KillMode=none
Type=forking

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

Einen Fehler gibt es hier jedoch noch. Es muss im Servicefile der User ergänzt werden unter dem der Container gestartet werden soll, insofern dies nicht als root passieren soll. Dazu muss in der Sektion [Service] nur User=podmanager ergänzt werden (bzw. der Username auf ihrem System).

Um den Container als Service zu hinterlegen wären unter CentOS 8 folgende Schritte durchzuführen:

[podmanager@buildah ~]$ podman generate systemd --files --name postgres_dev
/home/podmanager/container-postgres_dev.service
# User= im servicefile ergänzen
[podmanager@buildah ~]$ sudo cp /home/podmanager/container-postgres_dev.service /etc/systemd/system/
[podmanager@buildah ~]$ sudo systemctl daemon-reload
[podmanager@buildah ~]$ sudo systemctl start container-postgres_dev.service
[podmanager@buildah ~]$ systemctl status container-postgres_dev.service 
● container-postgres_dev.service - Podman container-postgres_dev.service
   Loaded: loaded (/etc/systemd/system/container-postgres_dev.service; disabled; vendor preset: disabled)
   Active: active (running) since Tue 2020-03-24 14:04:14 CET; 1s ago
     Docs: man:podman-generate-systemd(1)
  Process: 7691 ExecStart=/usr/bin/podman start postgres_dev (code=exited, status=0/SUCCESS)
 Main PID: 7717 (conmon)
    Tasks: 11 (limit: 25028)
   Memory: 46.7M
   CGroup: /system.slice/container-postgres_dev.service
           ├─7710 /usr/bin/fuse-overlayfs -o lowerdir=/home/podmanager/.local/share/containers/storage/overlay/l/FX4RZGGJ5HSNVMGVFG6K3I7PIL:/home/podmanager/.local/share/containers/storage/overlay/l/AIHUOSIVGT5DN5GCUR7PRELVKK:/home/podma>
           ├─7714 /usr/bin/slirp4netns --disable-host-loopback --mtu 65520 --enable-sandbox -c -e 3 -r 4 --netns-type=path /tmp/run-1002/netns/cni-a0ee9d78-2f8c-a563-1947-92d0766a43b7 tap0
           ├─7717 /usr/bin/conmon --api-version 1 -c c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7 -u c8b9732b6ad253710ae6e75f934a74e8469e61bc5b5d88c2fa92c7257d00d2e7 -r /usr/bin/runc -b /home/podmanager/.local/share/c>
           ├─7727 postgres
           ├─7758 postgres: checkpointer
           ├─7759 postgres: background writer
           ├─7760 postgres: walwriter
           ├─7761 postgres: autovacuum launcher
           ├─7762 postgres: stats collector
           └─7763 postgres: logical replication launcher

Mär 24 14:04:13 buildah.localdomain systemd[1]: Starting Podman container-postgres_dev.service...
Mär 24 14:04:14 buildah.localdomain podman[7691]: postgres_dev
Mär 24 14:04:14 buildah.localdomain systemd[1]: Started Podman container-postgres_dev.service.

Wichtig zu erwähnen ist noch, dass man den über Systemd gestarteten Container natürlich auch via podman administrieren / erreichen kann, zumindest das Starten und Stoppen aber dem Service überlassen sollte.

Erstellen eines Pods

Wie der Name vermuten lässt ist es mit Podman nicht nur möglich Container zu erstellen, sondern diese auch in Pods zu organisieren.
Ein Pod stellt dabei analog zu Kubernetes einen organisatorischen Verbund von Containern dar die sich auch bestimmte Namespaces wie pids, network o.ä. teilen können.
Diese werden dann via podman pod $cmd administriert.

Im Folgenden laden wir das Image des postgres-exporters für prometheus und erstellen daraus einen Pod mit der Bezeichnung postgres-prod-pod.

[podmanager@buildah ~]$ podman pull docker.io/wrouesnel/postgres_exporter

[podmanager@buildah ~]$ podman pod create --name postgres-prod-pod
727e7544515e0e683525e555934e02a341a42009a9c49fb2fd53094187a1e97c

[podmanager@buildah ~]$ podman run -d --pod postgres-prod-pod -e POSTGRES_PASSWORD=SuperDB postgres:latest
8f313260973ef6eb6fa84d2893875213cee89b48c93d08de7642b0a8b03c4a88

[podmanager@buildah ~]$ podman run -d --pod postgres-prod-pod -e DATA_SOURCE_NAME="postgresql://postgres:password@localhost:5432/postgres?sslmode=disable" postgres_exporter
fee22f24ff9b2ace599831fa022fb1261ef836846e0ba938c7b469d8dfb8a48a

[podmanager@buildah ~]$ podman pod ps
POD ID         NAME                STATUS    CREATED          # OF CONTAINERS   INFRA ID
727e7544515e   postgres-prod-pod   Running   48 seconds ago   3                 6edc862441f1

[podmanager@buildah ~]$ podman pod inspect postgres-prod-pod
{
     "Config": {
          "id": "727e7544515e0e683525e555934e02a341a42009a9c49fb2fd53094187a1e97c",
          "name": "postgres-prod-pod",
          "hostname": "postgres-prod-pod",
          "labels": {

          },
          "cgroupParent": "/libpod_parent",
          "sharesCgroup": true,
          "sharesIpc": true,
          "sharesNet": true,
          "sharesUts": true,
          "infraConfig": {
               "makeInfraContainer": true,
               "infraPortBindings": null
          },
          "created": "2020-03-24T14:44:27.01249721+01:00",
          "lockID": 0
     },
     "State": {
          "cgroupPath": "/libpod_parent/727e7544515e0e683525e555934e02a341a42009a9c49fb2fd53094187a1e97c",
          "infraContainerID": "6edc862441f18234f0c61693f11d946f601973a71b85fa9d777273feed68ed3c",
          "status": "Running"
     },
     "Containers": [
          {
               "id": "6edc862441f18234f0c61693f11d946f601973a71b85fa9d777273feed68ed3c",
               "state": "running"
          },
          {
               "id": "8f313260973ef6eb6fa84d2893875213cee89b48c93d08de7642b0a8b03c4a88",
               "state": "running"
          },
          {
               "id": "fee22f24ff9b2ace599831fa022fb1261ef836846e0ba938c7b469d8dfb8a48a",
               "state": "running"
          }
     ]
}

Hier ist nun zu sehen, dass der Pod die beiden Container postgres und postgres-exporter beinhaltet (Vergleich UUIDs).
Der dritte Container ist der Infra-Container für das Bootstrapping und soll hier nicht weiter Thema sein.

In der Prozessliste zeigt sich das Ganze dann wie folgt:

podmana+  6279     1  0 14:44 ?        Ss     0:00 /usr/bin/fuse-overlayfs -o lowerdir=/home/podmanager/.local/share/containers/storage/overlay/l/
podmana+  6281     1  0 14:44 pts/2    S      0:00 /usr/bin/slirp4netns --disable-host-loopback --mtu 65520 --enable-sandbox -c -e 3 -r 4 --netns-type=path /tmp/run-1002/netns/cni-5fe4356e-77c0-8127-f72b-7335c2ed05c4 tap0
podmana+  6284     1  0 14:44 ?        Ssl    0:00 /usr/bin/conmon --api-version 1 -c 6edc862441f18234f0c61693f11d946f601973a71b85fa9d777273feed68ed3c -u 6edc862441f18234f0c61693f11d946f601973a71b85fa9d777273feed68ed3c -r /usr/bin/runc -b
podmana+  6296  6284  0 14:44 ?        Ss     0:00  \_ /pause
podmana+  6308     1  0 14:44 ?        Ss     0:00 /usr/bin/fuse-overlayfs -o lowerdir=/home/podmanager/.local/share/containers/storage/overlay/l/FX4RZGGJ5HSNVMGVFG6K3I7PIL:/home/podmanager/.local/share/containers/storage/overlay/l/AIHUOS
podmana+  6312     1  0 14:44 ?        Ssl    0:00 /usr/bin/conmon --api-version 1 -c 8f313260973ef6eb6fa84d2893875213cee89b48c93d08de7642b0a8b03c4a88 -u 8f313260973ef6eb6fa84d2893875213cee89b48c93d08de7642b0a8b03c4a88 -r /usr/bin/runc -b
232070    6322  6312  0 14:44 ?        Ss     0:00  \_ postgres
...
232070    6549  6322  0 14:44 ?        Ss     0:00      \_ postgres: postgres postgres ::1(51290) idle
podmana+  6520     1  0 14:44 ?        Ss     0:00 /usr/bin/fuse-overlayfs -o lowerdir=/home/podmanager/.local/share/containers/storage/overlay/l/P5NJW4TB6JUFOBBIN2MOHW7272:/home/podmanager/.local/share/containers/storage/overlay/l/KVC54Z
podmana+  6523     1  0 14:44 ?        Ssl    0:00 /usr/bin/conmon --api-version 1 -c fee22f24ff9b2ace599831fa022fb1261ef836846e0ba938c7b469d8dfb8a48a -u fee22f24ff9b2ace599831fa022fb1261ef836846e0ba938c7b469d8dfb8a48a -r /usr/bin/runc -b
251072    6534  6523  0 14:44 ?        Ssl    0:00  \_ /postgres_exporter

Hier ist zu sehen, dass es zwar mehrere conmon und overlayfs-Prozesse für die Container gibt, jedoch nur einen slirp4netns, da die Container sich diesen teilen und darüber auch via localhost kommunizieren können.
Auch ist zu sehen dass die PostgreSQL®-Datenbank eine Verbindung von localhost hat (PID 6549) wobei es sich um den Exporter handelt.

Im Normalfall werden bei der Erstellung eines Pods via podman pod create folgende Namespaces für die Container zusammengefasst: net,ipc,uts und user.
Somit hat auch jeder Container trotz der Zusammenfassung im Pod noch seinen eigenen PID-namespace.
Sollte dies jedoch gewünscht sein, so kann mittels dem Parameter --share bei der Erstellung angegeben werden was alles geteilt werden soll.

So sieht z.B. die Prozessliste des Pods ohne gemeinsamen pid-namespace aus. Jeder Container hat seine eigene Prozesstruktur.

[podmanager@buildah ~]$ podman pod top postgres-prod-pod
USER                PID   PPID   %CPU    ELAPSED            TTY   TIME   COMMAND
0                   1     0      0.000   11m45.845291911s   ?     0s     /pause 
postgres            1     0      0.000   11m45.854330176s   ?     0s     postgres 
postgres            25    1      0.000   11m45.854387876s   ?     0s     postgres: checkpointer    
postgres            26    1      0.000   11m45.854441615s   ?     0s     postgres: background writer    
postgres            27    1      0.000   11m45.854483844s   ?     0s     postgres: walwriter    
postgres            28    1      0.000   11m45.854525645s   ?     0s     postgres: autovacuum launcher    
postgres            29    1      0.000   11m45.854567082s   ?     0s     postgres: stats collector    
postgres            30    1      0.000   11m45.854613262s   ?     0s     postgres: logical replication launcher    
postgres            31    1      0.000   11m45.854653703s   ?     0s     postgres: postgres postgres ::1(51292) idle 
postgres_exporter   1     0      0.000   11m45.859505449s   ?     0s     /postgres_exporter

Als Alternative die Ausgabe bei der Erstellung des Pods mittels podman pod create --name postgres-prod-pod --share=pid,net,ipc,uts,user

[podmanager@buildah ~]$ podman pod top postgres-prod-pod
USER       PID   PPID   %CPU    ELAPSED         TTY     TIME   COMMAND
root       1     0      0.000   20.396867487s   ?       0s     /pause 
postgres   6     0      0.000   20.396936905s   pts/0   0s     postgres 
postgres   60    6      0.000   18.396997034s   ?       0s     postgres: checkpointer    
postgres   61    6      0.000   18.397086198s   ?       0s     postgres: background writer    
postgres   62    6      0.000   18.39713465s    ?       0s     postgres: walwriter    
postgres   63    6      0.000   18.39718056s    ?       0s     postgres: autovacuum launcher    
postgres   64    6      0.000   18.397229737s   ?       0s     postgres: stats collector    
postgres   65    6      0.000   18.397279102s   ?       0s     postgres: logical replication launcher    
20001      66    0      0.000   16.397325377s   pts/0   0s     /postgres_exporter

Zur Auswahl stehen bei --share ipc, net, pid, user und uts.

Der gesamte pod kann nun auch via podman pod stop/start gestartet und gestoppt werden.
Ebenso können für Pods wie Container systemd-service-files generiert werden.

Podman und Kubernetes

Podman bietet einige Unterstützung im Bereich Kubernetes-YAML.

So ist es z.B. möglich aus den Pods, welche man mit podman erstellt hat, kubernes-Pod YAML zu generieren.

[podmanager@buildah ~]$ podman generate kube postgres-prod-pod -f postgres-prod-pod.yaml
[podmanager@buildah ~]$ cat postgres-prod-pod.yaml
# Generation of Kubernetes YAML is still under development!
#
# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-1.8.2
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2020-03-24T14:08:14Z"
  labels:
    app: postgres-prod-pod
  name: postgres-prod-pod
spec:
  containers:
  - command:
    - postgres
    env:
    ...
    image: docker.io/library/postgres:latest
    name: reverentptolemy
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities: {}
      privileged: false
      readOnlyRootFilesystem: false
      seLinuxOptions: {}
    workingDir: /
  - env:
    ...
    image: docker.io/wrouesnel/postgres_exporter:latest
    name: friendlyshirley
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities: {}
      privileged: false
      readOnlyRootFilesystem: false
      runAsGroup: 20001
      runAsUser: 20001
      seLinuxOptions: {}
    workingDir: /
status: {}

Mit der Option -s wird sogar noch ein Service mit ggf. published-Ports generiert:

---
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2020-03-24T14:10:42Z"
  labels:
    app: postgres-prod-pod
  name: postgres-prod-pod
spec:
  selector:
    app: postgres-prod-pod
  type: NodePort
status:
  loadBalancer: {}

Die generate-yaml Funktionalität greift dabei für Pods wie auch für Services und auch in beide Richtungen.
Mit podman play ist es möglich Pod und Container-Definitionen in podman zu testen.

Fazit

Dies war ein grober Einblick in die Möglichkeiten Container ohne Daemon und root-Berechtigungen auf einem Host zu betreiben.
Natürlich ist mit podman noch viel mehr möglich, jedoch würde die Erläuterung jeder Option hier den Rahmen sprengen.

Zu erwähnen ist vielleicht noch, dass Podman mit aktiviertem cgroupsV2 auch z.B. Resourcennutzung in Pods auswerten kann.
Dies ist jedoch aktuell nur unter Fedora31 standardmäßig aktiviert.

Lange Zeit war Docker das Standard-Tool zum Erstellen und Betreiben von Containern.

Durch die starke Verbreitung der Technologie gibt es mittlerweile deutlich mehr Tools, die einzelne Teilbereiche des Gespannes aus Docker-Daemon und Docker-CLI übernehmen. Auch im Hinblick auf den Aspekt der Sicherheit ist der root-Rechte benötigende Docker-Daemon für viele Nutzer nicht unbedingt die erste Wahl.

Buildah ist neben Podman und Skopeo Teil der RedHat-Containertools und übernimmt den Abschnitt für Erstellung und Modifizierung von Containern und Images.

Die Tools wurden dabei nach dem bekannten Unix-Prinzip entwickelt, so dass es kleinere Programme beinhaltet, die jedoch gut zusammenarbeiten und jeweils einen eigenen Teilbereich abdecken.

Entwicklung

Buildah wurde in der Version 1.0 am 07.05.2018 veröffentlicht und ist unter Apache 2.0 lizenziert. Die Implementierung erfolgt in Golang und wird in erster Instanz von der „containers organisation“ vorangetrieben. Diese beinhaltet sowohl RedHat-Mitarbeiter, wie auch externe Entwickler.

Der Code kann auf Github eingesehen werden. Die Entwicklung erfolgt dabei in keinem festen Releasezyklus. So können sowohl Monate wie auch Wochen zwischen den Versionen liegen, je nachdem ob genügend neue Features für einen Release implementiert wurden.

Container im Userspace

Eine zentrale Komponente von Buildah und auch Podman ist die libpod, die es ermöglicht Container im Userspace zu starten und Images zu erstellen. Diese setzt auf slirp4netns, fuse-overlayfs und die /etc/sub(u|g)id auf.

slirp4netns

Seit Kernelversion 3.8 sind network- bzw. usernamespaces nutzbar. Jedoch werden für die Erstellung eines Interfaces, das diese nutzen kann, immer noch root-Rechte benötigt.

slirp4netns umgeht diese Problematik, in dem es ein TAP-Device innerhalb des neuen Namespaces mit dem usermode TCP-IP-Stack verbindet. Details dazu sind im Projekt auf Github zu finden.

fuse-overlayfs

fuse-overlayfs stellt eine Implementierung von overlay+shiftfs in FUSE (Filesystem in Userspace) bereit. Dieses ermöglicht Nutzern Container-Dateisysteme auch ohne root-Rechte zu erstellen und anzupassen. Details dazu finden sich auf dem Projekt-Github.

/etc/sub(u|g)id

Sowohl /etc/subuid als auch /etc/subgid sind Teil der shadow-utils auf aktuellen Systemen und definieren welche uids und gids ein Nutzer des Systems beanspruchen darf.

Als Standard bekommt jeder neu angelegte Nutzer eines Systems 65536 ids zugeteilt (sowohl uid, wie auch gid). Es sind jedoch auch größere oder mehrere Ranges möglich. Diese müssen allerdings manuell angepasst werden und es ist darauf zu achten, dass es keine Überschneidungen gibt. Auch ist es möglich, dass beiden Dateien auf älteren Systemen manuell angelegt / angepasst werden müssen. Mindestens seit Fedora 30 sind diese Teil der Standardinstallation.

Diese IDs werden beim Betrieb von libpod für die uid/gid innerhalb von Containern genutzt.

Folgend ein Beispiel:

[builder@buildah ~]$ cat /etc/subuid
builder:100000:65536
wracker:165536:65536

[builder@buildah ~]$ cat /etc/subgid
builder:100000:65536
wracker:165536:65536

Hier ist zu sehen, dass der Nutzer builder uids/gids im Bereich von 100000 bis 165535 nutzen darf. Neue Nutzer bekommen (ohne manuelle Anpassungen) den entsprechend nächsten Block zugeteilt.

Installation

Buildah ist auf den gängigen Distributionen entweder direkt paketiert (z.B. Fedora, Archlinux, Suse, Gentoo) oder aber per Dritt-Repo verfügbar (Debian und Derivate).

Unter Fedora ist es zum Zeitpunkt der Erstellung dieses Artikel noch notwendig zusätzlich das Paket seccomp zu installieren, da es hier in der letzten Version Probleme mit den Abhängigkeiten gibt. So wäre der Aufruf sudo dnf install buildah libseccomp unter Fedora und sudo pacman -S buildah unter Archlinux.

Für die Debian-Derivate gibt es das Kubic-Repository, in dem aktuelle Pakete verfügbar sind.

Debian selbst enthält das Paket zunächst nur in den unstable-Repositories. Für Ubuntu oder Debian stable muss Kubic genutzt werden.

Selbstredend ist eine manuelle Installation über die Quellen ebenfalls möglich. Eine Komplette Liste der Installationsmöglichkeiten findet sich auf Github.

Nach der Installation von Buildah sollte der Aufruf in etwa die folgende Ausgabe liefern:

[builder@buildah ~]$ buildah
A tool that facilitates building OCI images

Usage:
  buildah [flags]
  buildah [command]
# ...

Konfiguration

Die Konfiguration findet in einzelnen Dateien im Ordner /etc/containers für die globale Konfiguration statt, oder in $HOME/.config/containers
für die Konfiguration der einzelnen Nutzer. Die Nutzerkonfiguration überschreibt dabei wie immer die globale.

Folgende Dateien sind dabei in den Ordnern, bzw. mindestens global, zu finden:

storage.conf

Hier wird definiert mit welchem Treiber und in welchen Ordner die Images bzw. der interne Storage der Container gespeichert werden soll.

Beispiel der globalen Konfiguration:

[root@buildah containers]# grep -v "^#\|^$" /etc/containers/storage.conf 
[storage]
driver = "overlay"
runroot = "/var/run/containers/storage"
graphroot = "/var/lib/containers/storage"
# ...

Beispiel einer Konfiguration eines Nutzers:

[builder@buildah containers]$ cat ~/.config/containers/storage.conf

[storage]
  driver = "overlay"
  runroot = "/var/tmp/1000"
  graphroot = "/home/builder/.local/share/containers/storage"
# ...

mounts.conf

Hier wird pro Zeile ein Mount definiert, der immer in jeden auf dem System bzw. unter dem Nutzer gestarteten Container eingebunden werden soll.

Diese Möglichkeit wird meist genutzt um häufig genutzt Credentials oder Zertifikate in einen Container zu mounten (z.B: /usr/share/rhel/secrets:/run/secrets für RHEL-Subscriptions). Hier ist ggf. darauf zu achten, dass die Mount-Wünsche nicht mit bestehenden SELinux-Konfigurationen kollidieren. Im Problemfall wäre hier das /var/log/audit/audit.log zu prüfen.

registries.conf

Konfiguration der verfügbaren Container-Registries im V1-Format. Hier können analog zu Docker-Konfiguration die genutzten Registries freigegeben oder auch ungewollte Blockiert werden. Bei Mehrfachangaben werden die Registries der Reihe nach angefragt.

Auch wenn es sich hier um „V1“ handelt und „V2“ bereits verfügbar ist, so ist dies die stabile und auch empfohlene Variante seine Repositories zu definieren.

Beispiel einer registries.conf:

[root@buildah containers]# grep -v "^#\|^$" /etc/containers/registries.conf
[registries.search]
registries = ['registry.access.redhat.com', 'registry.fedoraproject.org', 'registry.centos.org', 'docker.io']
[registries.insecure]
registries = []
[registries.block]
registries = []

registries.d

Stellt die aktuell noch in der Beta befindlichen Version 2 der Registry-Konfiguration dar. Im Ordner registries.d finden sich verschiedene Dateien,
welche jeweils Registries konfigurieren.

In dieser Version wird auf das Feature der sigstores (Signature-Stores) gesetzt, welches jedoch aktuell nur von wenigen Registries überhaupt unterstützt wird.

Es wird dabei zwischen sigstore-staging (nur schreibend) und sigstore (lesend und schreibend) unterschieden. So soll das Image z.B. im sigstore-staging nur erstellt werden um es anschließend im sigstore zu signieren und zu veröffentlichen.

Folgend eine Beispielkonfiguration:

docker:
  privateregistry.com:
  sigstore: http://privateregistry.com/sigstore/
  sigstore-staging: /mnt/nfs/privateregistry/sigstore

Da es sich bei dem Feature jedoch um ein Beta-Feature ohne breite Unterstützung handelt soll hier an dieser Stelle nicht weiter darauf eingegangen werden.

Details dazu finden sich auf Github.

Buildah – CLI

Viele Befehle der Buildah-CLI sind mit der Docker-CLI soweit kompatibel und nur um eigene Optionen ergänzt. So z.B. ps, images, pull, push, rm, usw.

Im Unterschied zu Docker kann jedoch auch die CLI genutzt werden um neue Container und Images zu erstellen. Dafür stehen dann Optionen wie run,add,copy o.ä. bereit. Dies erlaubt eine sehr gute Automatisierbarkeit bzw. Parametrisierbarkeit der Erstellung, da hier mittels einfacher Shell-Befehle statt eines Dockerfile-Images gebaut werden können.

Es werden bei der Erstellung zwei verschiedene Formate unterstützt, die sich jedoch nur im Manifest unterscheiden: DockerV2 und OCI (standard).

Erstellung eines Images mittels CLI

Der erste Schritt um ein Image zu erstellen lautet z.B. buildah from centos, welches analog zum Dockerfile FROM das Basisimage definiert. Buildah wird das Image herunterladen und einen entsprechenden Container erstellen.

Um nun Anpassungen am Basisimage vorzunehmen kann z.B. buildah run $newcontainer dnf install wget ausgeführt werden. Der run Befehl führt ebenfalls, wieder analog zu Docker, einen Befehl innerhalb des Container aus. Um ENV-Variablen wie einen Proxy o.ä. zu definieren kann buildah config
genutzt werden.

So z.B. buildah config --created-by "DaniloE" $newcontainer um den Autor zu definieren. Mit config lassen sich diverse Optionen im Container setzen wie z.B. env, cmd, author, port, user. Details dazu unter man buildah-config.

Um Dateien in einen Container zu kopieren kann copy/add genutzt werden. z.B. buildah copy/add $newcontainer ./hello /hello.

copy kopiert dabei Dateien eines lokalen Pfades in den Container während add einen Pfad oder eine URL akzeptiert.

Um aus dem angepassten Container ein neues Image zu erstellen wird nach Abschluss der Anpassungen z.B. buildah commit $newcontainer hellocontainer oder buildah commit $newcontainer docker://localhost:5000/hellocontainer genutzt. Dieses erstellt ein Image aus 2 Layern (Basis-Image + alle Änderungen).

Sollte es aus z.B. Platzgründen nötig sein dies auf einen Layer zu reduzieren kann der Parameter --squash genutzt werden. --rm löscht bei diesem Aufruf auch gleich den Zugrunde liegenden Arbeitscontainer.

Zusammenfassend sähe ein Skript um einen Container zu bauen z.B. aus wie folgt:

newcontainer=$(buildah from centos)
buildah run $newcontainer touch /test
buildah copy $newcontainer ./hello /hello
buildah config --cmd /hello $newcontainer
buildah config --created-by "DaniloE"  $newcontainer
buildah config --author "DaniloE" $newcontainer
buildah config --label name=simple-helloworld $newcontainer
buildah commit --rm $newcontainer hellocontainer

Erstellung eines Images mittels Dockerfile

Buildah hat natürlich im Zuge der Docker-Kompatibilität eine Möglichkeit Container aus bestehenden Dockerfiles zu erstellen.

Hierzu wird der Aufruf buildah bud (build-using-dockerfile) genutzt.

Es erstellt ein Image aus einem Containerfile und einer Buildcontext-URL.

Wird kein Buildcontext übergeben ist $(pwd) automatisch gesetzt und hier wird auf das Dockerfile erwartet. Es ist aber auch möglich mit Remote-Sourcen zu arbeiten.

Weitere Möglichkeiten wären z.B. als Context ein Git-Repository zu übergeben. Hier wird der Inhalt des Repositories in ein temporäres Verzeichnis ausgecheckt (default „/var/tmp“) und im Ergebnis das Dockerfile erwartet.

Alternativ funktioniert dies auch mit einer Archiv URL. Hier wird das Archiv (xz,bzip2,gzip) ebenfalls in das temporäre Verzeichnis heruntergeladen, entpackt und als Context behandelt

Mit z.B. -f files/Dockfile wird der Pfad zum Dockerfile angegeben. Dies kann sowohl ein lokales Verzeichnis sein, oder aber ein Unterverzeichnis im Git oder Archiv.

Wichtig: Bei der Übergabe eines remote-context wird der Parameter immer auf den Inhalt der Remote Sources angewandt, nicht auf einen lokalen Pfad. Es ist also nicht möglich ein lokales Dockerfile und einen Remote-Context zu verbinden.

Weitere Informationen zu Parametern und Optionen sind unter man buildah-bud ausführlich dokumentiert.

Folgend noch einige Beispielaufrufe zur Veranschaulichung:

Erstellung eines Images „from scratch“

Buildah bietet auch die Option, Images komplett neu zu erstellen ohne ein vorangegangenes Basisimage zu nutzen.

Hierfür wird ein neuer Container mit buildah from scratch erstellt.

Dieser beinhaltet keine Dateien sondern lediglich ein leeres Dateisystem, das man anschließend befüllt.

So können z.B. mit der lokalen Paketverwaltung Pakete installiert oder aber lediglich Binaries in das Dateisystem übertragen werden.

Folgend ein Beispielskript für die Erstellung eines Containers from scratch mit der CLI.

newcontainer=$(buildah from scratch)
scratchmnt=$(buildah mount ${newcontainer})
dnf install --installroot ${scratchmnt} --releasever 30 bash coreutils --setopt install_weak_deps=false -y

Es wird ein neuer, leerer Container erstellt, das Dateisystem des Containers in ein temporäres Verzeichnis gemounted und mit Bordmitteln Pakete inkl. Abhängigkeiten installiert.

Für die Nutzung als unprivilegierter User gibt es hier jedoch einen wichtigen Punkt zu beachten.

So bricht das buildah mount ohne root Rechte ab:

[builder@buildah ~]$ newcontainer=$(buildah from centos)
[builder@buildah ~]$ buildah mount $newcontainer
cannot mount using driver overlay in rootless mode. You need to run it in a `buildah unshare` session

Um diese Problematik zu umgehen muss die Containererstellung in einem separaten Namespace durchgeführt werden.

Dieser wird mittels buildah unshare erstellt und auch direkt aktiviert. Da in diesem sowohl uid wie auch gid als root gesetzt sind lässt sich das FS entsprechend mounten.

[builder@buildah ~]$ buildah unshare
[root@buildah ~]# newcontainer=$(buildah from centos)
[root@buildah ~]# buildah mount $newcontainer
/home/builder/.local/share/containers/storage/overlay/...
[root@buildah ~]# ls -l /home/builder/.local/share/containers/storage/...
insgesamt 16
-rw-r--r--.  1 root root 12076  5. Dez 2018  anaconda-post.log
lrwxrwxrwx.  1 root root     7  5. Dez 2018  bin -> usr/bin
drwxr-xr-x.  2 root root     6  5. Dez 2018  dev
drwxr-xr-x. 47 root root  4096  5. Dez 2018  etc

Dieser Umstand klingt erst einmal problematisch, ist in der Praxis jedoch nicht, sofern man die Container automatisiert erstellt.

Ein Beispielskript findet sich unter man buildah-unshare.

Vergleich Buildah und andere Lösungen

| Applikation | Punkte                                         |
| ----------- | ---------------------------------------------- |
| Buildkit    | * benötigt Daemon + CLI                        |
|             | * schneller bei großen/Komplexen Builds        |
|             | * kompatibel mit verschiedenen Imagefile       |
|             | * z.B. Docker-/Mocker-/Gopperfile              |
|             | * rootless Mode noch experimental              |
| Kaniko      | * Nur Unterstützung für Dockerfiles            |
|             | * speziell für Builds in Containern/Kubernetes |
|             | * Nur Kaniko-eigenes Image empfohlen           |
|             | * Empfohlen für Gitlab CI/CD []                |

Unterstützung

Falls Sie Unterstützung beim Einsatz von Docker oder Buildah benötigen, steht Ihnen unser Open Source Support Center zur Verfügung – Falls gewünscht auch 24 Stunden am Tag, an 365 Tagen im Jahr.

Wir freuen uns auf Ihre Kontaktaufnahme.