Es lohnt sich immer zu wissen, was hinter den Kulissen passiert. Werfen wir einen Blick darauf, was unter der Haube von wurzellosen Podman-Containern passiert. Wir erklären jede Komponente und schlüsseln dann alle beteiligten Schritte auf.
Das Beispiel
In unserem Beispiel versuchen wir, einen Container auszuführen, der bereits Buildah ausführt, um ein Container-Image zu erstellen. Zuerst erstellen wir ein einfaches Dockerfile namens Containerfile
das ein ubi8-Image zieht und einen Befehl ausführt, der Ihnen mitteilt, dass Sie in einem Container laufen:
$ mkdir containers
$ cat > ~/Containerfile << _EOF
FROM ubi8
RUN echo “in buildah container”
_EOF
Führen Sie als Nächstes den Container mit dem folgenden Podman-Befehl aus:
$ podman run --device /dev/fuse -v ~/Containerfile:/Containerfile:Z \
-v ~/containers:/var/lib/containers:Z buildah buildah bud /
Dieser Befehl fügt das zusätzliche Gerät /dev/fuse
hinzu , die erforderlich ist, um Buildah innerhalb des Containers auszuführen. Wir binden Volumes in Containerfile
ein damit Buildah es finden kann, und verwenden Sie das SELinux-Flag :Z
um Podman zu sagen, dass er es umbenennen soll. Um den Containerspeicher von Buildah außerhalb des Containers zu handhaben, mounten wir auch die lokalen containers
Verzeichnis, das ich oben erstellt habe. Und schließlich führen wir den Buildah-Befehl aus.
Hier ist die tatsächliche Ausgabe, die ich sehe, wenn ich diesen Befehl ausführe:
$ podman run -ti --device /dev/fuse -v ~/Containerfile:/Containerfile:Z -v ~/containers:/var/lib/containers:Z buildah/stable buildah bud /
Trying to pull docker.io/buildah/stable...
denied: requested access to the resource is denied
Trying to pull registry.fedoraproject.org/buildah/stable...
manifest unknown: manifest unknown
Trying to pull quay.io/buildah/stable...
Getting image source signatures
Copying blob 907e338ec93d done
Copying blob a3ed95caeb02 done
Copying blob a3ed95caeCob02 done
Copying blob a3ed95caeb02 skipped: already exists
Copying blob d318c91bf2a8 done
Copying blob e721a8015139 done
Copying blob a3ed95caeb02 done
Copying blob 8dd367492bc7 done
Writing manifest to image destination
Storing signatures
STEP 1: FROM ubi8
Getting image source signatures
Copying blob c65691897a4d done
Copying blob 641d7cc5cbc4 done
Copying config 11f9dba4d1 done
Writing manifest to image destination
Storing signatures
STEP 2: RUN echo "in buildah container"
in buildah container
STEP 3: COMMIT
Getting image source signatures
Copying blob 6866631b657e skipped: already exists
Copying blob 48905dae4010 skipped: already exists
Copying blob 5f70bf18a086 skipped: already exists
Copying config 9c54016647 done
Writing manifest to image destination
Storing signatures
9c5401664748e032b43b8674dba90e9b853d6b47b679d056cb2a1e3118f9dab7
Sehen wir uns nun genauer an, was tatsächlich im Podman-Befehl vor sich geht.
Einrichten der Benutzer- und Mount-Namespaces
Beim Einrichten von Benutzer- und Mount-Namespaces prüft Podman zunächst, ob bereits ein Benutzer-Namespace konfiguriert ist. Dies geschieht, indem geprüft wird, ob für den Benutzer ein Pausenprozess ausgeführt wird. Die Rolle des Pause-Prozesses besteht darin, den Benutzernamensraum am Leben zu erhalten, da alle Rootless-Container im selben Benutzernamensraum ausgeführt werden müssen. Wenn dies nicht der Fall ist, wären einige Dinge (wie die gemeinsame Nutzung des Netzwerk-Namensraums von einem anderen Container) unmöglich.
Ein Benutzernamensraum ist erforderlich, um rootless zu erlauben, bestimmte Arten von Dateisystemen einzuhängen und auf mehr als eine UID und GID zuzugreifen.
Wenn der Pausenprozess existiert, wird sein Benutzernamensraum hinzugefügt. Diese Aktion wird sehr früh in ihrer Ausführung ausgeführt, bevor die Go-Laufzeit gestartet wird, da ein Multithread-Programm seinen Benutzer-Namespace nicht ändern kann. Wenn der Pausenvorgang jedoch nicht existieren, dann liest Podman die /etc/subuid
und /etc/subgid
Dateien und suchen Sie nach dem Benutzernamen oder der UID des Benutzers, der den Podman-Befehl ausführt. Sobald Podman den Eintrag findet, verwendet es den Inhalt sowie die aktuelle UID/GID des Benutzers, um einen Benutzernamensraum für ihn zu generieren.
Zum Beispiel, wenn der Benutzer als UID 1000 ausgeführt wird und einen Eintrag von USER:100000:65536
hat , führt Podman die setuid- und setgid-Apps aus, /usr/bin/newuidmap
und /usr/bin/newgidmap
, um den Benutzernamensraum zu konfigurieren. Der Benutzernamensraum bekommt dann folgendes Mapping:
0 3267 1
1 100000 65536
Beachten Sie, dass Sie den Benutzernamensraum sehen können, indem Sie Folgendes ausführen:
$ podman unshare cat /proc/self/uid_map
Als Nächstes erstellt Podman einen Pausenprozess, um den Namespace am Leben zu erhalten, sodass alle Container im selben Kontext ausgeführt werden und dieselben Mounts sehen können. Der nächste Podman-Prozess tritt dem Namespace direkt bei, ohne dass er zuerst erstellt werden muss. Wenn der Benutzerbereich jedoch nicht erstellt werden konnte, prüft Podman, ob der Befehl noch ohne Benutzernamensraum ausgeführt werden kann. Einige Befehle wie podman version
brauche keine. Andernfalls schlägt ein Befehl ohne Benutzernamensraum fehl.
Anschließend verarbeitet Podman die Befehlszeilenoptionen und überprüft, ob sie korrekt sind. Sie können podman-help
verwenden und podman run --help
, um verfügbare Optionen aufzulisten und die Manpages für weitere Beschreibungen zu verwenden.
Schließlich erstellt Podman einen Mount-Namespace zum Mounten des Containerspeichers.
Bild ziehen
Beim Pullen des Images prüft Podman, ob das Container-Image buildah/stable
ist ist im lokalen Containerspeicher vorhanden. Ist dies der Fall, richtet Podman das Netzwerk ein (siehe nächster Abschnitt). Wenn das Container-Image jedoch nicht vorhanden ist, erstellt Podman eine Liste mit Kandidaten-Images, die mithilfe der in /etc/containers/registries.conf
definierten Suchregistrierungen abgerufen werden können .
Die containers/image
Bibliothek wird verwendet, um diese Kandidaten-Images einzeln in einer durch registries.conf
definierten Reihenfolge abzurufen . Das erste erfolgreich abgerufene Bild wird verwendet.
- Die
containers/image
-Skript verwendet DNS, um die IP-Adresse für die Registrierung zu finden. - Dieses Skript TCP verbindet sich mit der IP-Adresse über
httpd
Anschluss (80). - Der
containers/image
sendet eine HTTP-Anforderung für das Manifest von
Container-Image./buildah/stable:latest - Wenn das Skript das Bild nicht finden kann, verwendet es die nächste Registrierung als Ersatz und kehrt zu Schritt 1 zurück. Wenn das Bild jedoch ist gefunden, beginnt es mit dem Pullen jeder Ebene des Bildes unter Verwendung von
containers/image
Bibliothek.
In diesem Beispiel buildah/stable
wurde unter quay.io/buildah/stable
gefunden . Die containers/image
Das Skript findet, dass es sieben Ebenen in quay.io/buildah/stable
gibt und beginnt damit, alle gleichzeitig aus der Containerregistrierung auf den Host zu kopieren. Sie gleichzeitig zu kopieren ist effizient.
Während jede Schicht auf den Host kopiert wird, ruft Podman containers/storage
auf Bücherei. Die containers/storage
Das Skript setzt die Ebenen der Reihe nach und für jede Ebene wieder zusammen. Es erstellt einen Overlay-Einhängepunkt in ~/.local/share/containers/storage
auf der vorherigen Schicht. Wenn es keine vorherige Ebene gibt, wird die erste Ebene erstellt.
Hinweis: In rootless Podman verwenden wir tatsächlich ein fuse-overlayfs
ausführbar, um die Ebene zu erstellen. Rootfull verwendet die overlayfs
des Kernels Treiber. Derzeit erlaubt der Kernel rootlosen Benutzern nicht, Overlay-Dateisysteme zu mounten, aber sie können FUSE-Dateisysteme mounten.
Als nächstes containers/storage
entpackt den Inhalt der Ebene in die neue Speicherebene. Da die Schichten nicht tariert sind, containers/storage
chowns die UID/GIDs von Dateien im Tarball in das Home-Verzeichnis. Beachten Sie, dass dieser Vorgang fehlschlagen kann, wenn die in der TAR-Datei angegebene UID oder GID nicht dem Benutzernamensraum zugeordnet wurde. Siehe Warum kann Podman mein Image nicht ohne Root ziehen?
Container erstellen
Jetzt ist es für Podman an der Zeit, einen neuen Container basierend auf dem Image zu erstellen. Um dies zu erreichen, fügt Podman den Container zur Datenbank hinzu und fragt dann containers/storage
ab Bibliothek zum Erstellen und Mounten eines neuen Containers in c/storage
. Die neue Containerebene fungiert als letzte Lese-/Schreibebene und wird über dem Image bereitgestellt.
Netzwerk einrichten
Als nächstes müssen wir das Netzwerk einrichten. Um dies zu erreichen, findet und führt Podman /usr/bin/slirp4netns
aus Containernetzwerke einzurichten. In rootless Podman können wir kein vollständiges, separates Netzwerk für Container erstellen, da diese Funktion für Nicht-Root-Benutzer nicht zulässig ist. In rootless Podman verwenden wir slirp4netns
um das Hostnetzwerk zu konfigurieren und ein VPN für den Container zu simulieren.
Hinweis: In Rootful-Containern verwendet Podman die CNI-Plug-ins, um eine Bridge zu konfigurieren.
Wenn der Benutzer eine Portzuordnung wie -p 8080:80
angegeben hat , slirpnetns
würde auf dem Host-Netzwerk an Port 8080 lauschen und dem Container-Prozess erlauben, sich an Port 80 zu binden. Der slirp4netns
Der Befehl erstellt ein Tap-Gerät, das in den neuen Netzwerk-Namespace eingefügt wird, in dem sich der Container befindet. Jedes Paket wird von slirp4netns
zurückgelesen und emuliert einen TCP/IP-Stack im Userspace. Jede Verbindung außerhalb des Namespace des Container-Netzwerks wird in eine Socket-Operation konvertiert, die der nicht privilegierte Benutzer im Namespace des Host-Netzwerks ausführen kann.
Umgang mit Volumen
Um Volumes handhaben zu können, liest Podman den gesamten Containerspeicher. Es sammelt die verwendeten SELinux-Labels und erstellt ein neues, unbenutztes Label, um den Container mit opencontainers/selinux
auszuführen Bibliothek.
Da der Benutzer zwei Volumes angegeben hat, die in den Container gemountet werden sollen, und Podman gebeten hat, den Inhalt umzubenennen, verwendet Podman opencontainers/selinux
um das SELinux-Label rekursiv auf die Quelldateien/-verzeichnisse der Volumes anzuwenden. Podman verwendet dann die opencontainers/runtime-tools
Bibliothek zum Zusammenstellen einer Laufzeitspezifikation der Open Containers Initiative (OCI):
- Podman teilt
runtime-tools
mit seine fest codierten Standardwerte für Dinge wie Fähigkeiten, Umgebung und Namespaces zur Spezifikation hinzuzufügen. - Podman verwendet die OCI-Image-Spezifikation, die aus
buildah/stable
gezogen wurde image, um Inhalte in der Spezifikation festzulegen, wie das Arbeitsverzeichnis, den Einstiegspunkt und zusätzliche Umgebungsvariablen. - Podman nimmt die Eingaben des Benutzers und verwendet die
runtime-tools
Bibliothek, um Felder in der Spezifikation für jedes der Volumes hinzuzufügen, und es setzt den Befehl für den Container aufbuildah bud /
.
In unserem Beispiel hat der Benutzer Podman mitgeteilt, dass er das Gerät /dev/fuse
verwenden möchte Innenseite des Behälters. Auf einem Root-Container würde Podman die OCI-Laufzeit anweisen, ein /dev/fuse
zu erstellen Gerät innerhalb des Containers, aber mit wurzellosem Podman dürfen Benutzer keine Geräte erstellen, also weist Podman stattdessen die OCI-Spezifikation an, mount /dev/fuse
zu binden vom Host in den Container.
Starten des Containermonitors conmon
Sobald die Volumes bearbeitet sind, findet Podman den Standard conmon
und führt ihn aus für den Container /usr/bin/conmon
. Diese Informationen werden aus /usr/share/containers/libpod.conf
gelesen . Podman teilt es dann dem conmon
mit ausführbar, um die OCI-Laufzeit zu verwenden, die auch in libpod.conf
aufgeführt ist; normalerweise /usr/bin/runc
oder /usr/bin/crun
. Podman teilt dies auch conmon
mit um podman container cleanup $CTRID
auszuführen für den Container, wenn der Container beendet wird.
Conmon geht beim Überwachen des Containers wie folgt vor:
- Conmon führt die OCI-Laufzeit aus, übergibt ihr den Pfad zur OCI-Spezifikationsdatei und zeigt auf den Einhängepunkt der Containerschicht in
containers/storage
. Dieser Einhängepunkt heißt rootfs. - Conmon überwacht den Container, bis er beendet wird, und meldet seinen Beendigungscode zurück.
- Conmon handhabt, wenn der Benutzer eine Verbindung zum Container herstellt, und stellt einen Socket bereit, um STDOUT und STDERR des Containers zu streamen.
- STDOUT und STDERR werden auch in einer Datei für
podman logs
protokolliert .
Nach dem Ausführen von conmon
, aber bevor die OCI-Laufzeit gestartet wird, hängt Podman an den „attach“-Socket an, da der Container nicht mit -d
ausgeführt wurde . Wir müssen dies tun, bevor wir den Container ausführen, da wir sonst riskieren, alles zu verlieren, was der Container vor dem Anhängen in seine Standard-Streams geschrieben hat. Dies zu tun, bevor der Container startet, gibt uns alles.
Starten der OCI-Laufzeit
Die OCI-Laufzeit liest die OCI-Spezifikationsdatei und konfiguriert den Kernel zum Ausführen des Containers. Es:
- Stellt die zusätzlichen Namespaces für den Container ein.
- Konfiguriert die Cgroups wenn der Container auf Cgroups V2 ausgeführt wird (Cgroups V1 unterstützt keine Rootless-Cgroups).
- Stellt das SELinux-Label zum Ausführen des Containers ein.
- Liest die
seccomp.json
Datei (standardmäßig/usr/share/containers/seccomp.json
) und richtet seccomp-Regeln ein. - Setzt die Umgebungsvariablen.
- Bind hängt die beiden angegebenen Volumes auf die Pfade im rootfs. Wenn der Zielpfad nicht im rootfs existiert, erstellt die OCI-Laufzeit das Zielverzeichnis.
- Schaltet root auf das rootfs um (macht das rootfs
/
im Behälter). - Verzweigt den Containerprozess.
- Führt alle OCI-Hook-Programme aus und übergibt ihnen die rootfs sowie die PID 1 des Containers.
- Führt den vom Benutzer
buildah bud /
angegebenen Befehl aus mit der PID 1 des Containers. - Beendet die OCI-Laufzeit und verlässt
conmon
um den Container zu überwachen.
Und schließlich conmon
meldet den Erfolg an Podman zurück.
Ausführen von buildah
Primärprozess des Containers
Nun zur letzten Gruppe von Schritten. Es beginnt, wenn der Container den anfänglichen Buildah-Prozess startet. (Weil wir in unserem Beispiel Buildah verwendet haben.) Buildah teilt die zugrunde liegenden containers/image
und containers/storage
Bibliotheken mit Podman, so dass es tatsächlich den meisten der oben definierten Schritte folgt, die Podman zum Abrufen seiner Bilder und zum Generieren seiner Container verwendet hat.
Podman hängt an conmon
Socket und liest/schreibt weiterhin STDOUT in conmon
. Beachten Sie, dass, wenn der Benutzer Podmans -d
angegeben hätte Flag, würde Podman beenden, aber der conmon
würde den Container weiterhin überwachen.
Wenn der Containerprozess beendet wird, sendet der Kernel ein SIGCHLD an conmon
Prozess. Im Gegenzug conmon
:
- Zeichnet den Exit-Code des Containers auf.
- Schließt die Logdatei des Containers.
- Schließt STDOUT/STDERR des Podman-Befehls.
- Führt die
podman container cleanup $CTRID
aus Befehl.
Podman-Container-Bereinigung beendet dann slirp4netns
network und teilt containers/storage
mit um alle Mount-Punkte des Containers zu unmounten. Wenn der Benutzer --rm
angegeben hat dann wird stattdessen der Behälter vollständig entfernt. Die Containerschicht wird aus containers/storage
entfernt , und die Containerdefinition wird aus der DB entfernt.
Da der ursprüngliche Podman-Befehl im Vordergrund ausgeführt wurde, wartet Podman auf conmon
zu beenden, ruft den Exit-Code aus dem Container ab und wird dann mit dem Exit-Code des Containers beendet.
Abschluss
Hoffentlich hilft Ihnen diese Erklärung, die ganze Magie zu verstehen das passiert unter der Decke, wenn der Rootless-Podman-Befehl ausgeführt wird.
Neu bei Containern? Laden Sie den Containers Primer herunter und lernen Sie die Grundlagen von Linux-Containern kennen.