Vale sempre la pena sapere cosa sta succedendo dietro le quinte. Diamo un'occhiata a cosa succede sotto il cofano dei contenitori Podman senza radici. Spiegheremo ogni componente e poi analizzeremo tutti i passaggi coinvolti.
L'esempio
Nel nostro esempio, tenteremo di eseguire un contenitore che sta già eseguendo Buildah per creare un'immagine del contenitore. Innanzitutto, creiamo un semplice Dockerfile chiamato Containerfile
che estrae un'immagine ubi8 ed esegue un comando che ti dice che stai eseguendo in un contenitore:
$ mkdir containers
$ cat > ~/Containerfile << _EOF
FROM ubi8
RUN echo “in buildah container”
_EOF
Quindi, esegui il contenitore con il seguente comando Podman:
$ podman run --device /dev/fuse -v ~/Containerfile:/Containerfile:Z \
-v ~/containers:/var/lib/containers:Z buildah buildah bud /
Questo comando aggiunge il dispositivo aggiuntivo /dev/fuse
, necessario per eseguire Buildah all'interno del contenitore. Montiamo il volume in Containerfile
in modo che Buildah possa trovarlo e utilizzare il flag SELinux :Z
per dire a Podman di ribattezzarlo. Per gestire lo stoccaggio del container di Buildah al di fuori del container, montiamo anche i containers
locali directory che ho creato sopra. E infine, eseguiamo il comando Buildah.
Ecco l'output effettivo che vedo durante l'esecuzione di questo comando:
$ 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
Ora, scaviamo in profondità ciò che sta effettivamente accadendo all'interno del comando Podman.
Configurazione dell'utente e mount degli spazi dei nomi
Quando si impostano gli spazi dei nomi utente e di montaggio, Podman verifica prima se esiste già uno spazio dei nomi utente configurato. Questo viene fatto vedendo se c'è un processo di pausa in esecuzione per l'utente. Il ruolo del processo di pausa è di mantenere attivo lo spazio dei nomi utente, poiché tutti i contenitori senza root devono essere eseguiti nello stesso spazio dei nomi utente. In caso contrario, alcune cose (come condividere lo spazio dei nomi di rete da un altro container) sarebbero impossibili.
È necessario uno spazio dei nomi utente per consentire senza root di montare determinati tipi di filesystem e accedere a più di un UID e GID.
Se il processo di pausa esiste, il relativo spazio dei nomi utente viene unito. Questa azione viene eseguita molto presto nella sua esecuzione prima dell'avvio del runtime Go perché un programma multithread non può modificare il proprio spazio dei nomi utente. Tuttavia, se il processo di pausa non esiste, quindi Podman legge il /etc/subuid
e /etc/subgid
file, cercando il nome utente o l'UID dell'utente che esegue il comando Podman. Una volta che Podman trova la voce, utilizza i contenuti e l'UID/GID corrente dell'utente per generare uno spazio dei nomi utente per loro.
Ad esempio, se l'utente è in esecuzione come UID 1000 e ha una voce di USER:100000:65536
, Podman esegue le app setuid e setgid, /usr/bin/newuidmap
e /usr/bin/newgidmap
, per configurare lo spazio dei nomi utente. Lo spazio dei nomi utente ottiene quindi la seguente mappatura:
0 3267 1
1 100000 65536
Nota che puoi vedere lo spazio dei nomi utente eseguendo:
$ podman unshare cat /proc/self/uid_map
Successivamente, Podman crea un processo di pausa per mantenere vivo lo spazio dei nomi, in modo che tutti i contenitori possano essere eseguiti dallo stesso contesto e vedere gli stessi mount. Il prossimo processo Podman si unirà direttamente allo spazio dei nomi senza che sia necessario crearlo prima. Tuttavia, se non è stato possibile creare lo spazio utente, Podman verifica se il comando può ancora essere eseguito senza uno spazio dei nomi utente. Alcuni comandi come podman version
non ne ho bisogno. In ogni altro caso, un comando senza spazio dei nomi utente avrà esito negativo.
Quindi, Podman elabora le opzioni della riga di comando, verificando che siano corrette. Puoi usare podman-help
e podman run --help
per elencare le opzioni disponibili e utilizzare le pagine man per ulteriori descrizioni.
Infine, Podman crea uno spazio dei nomi di montaggio per montare lo storage del contenitore.
Estrazione dell'immagine
Quando estrae l'immagine, Podman controlla se l'immagine del contenitore buildah/stable
esiste nell'archiviazione container locale. In tal caso, Podman configura la rete (vedere la sezione successiva). Tuttavia, se l'immagine del contenitore non esiste, Podman crea un elenco di immagini candidate da estrarre utilizzando i registri di ricerca definiti in /etc/containers/registries.conf
.
I containers/image
la libreria verrà utilizzata per estrarre queste immagini candidate una alla volta, in un ordine definito da registries.conf
. Verrà utilizzata la prima immagine da estrarre correttamente.
- I
containers/image
script utilizza il DNS per trovare l'indirizzo IP per il registro. - Questo script TCP si connette all'indirizzo IP tramite
httpd
porta (80). - Il
container/image
invia una richiesta HTTP per il manifest di
immagine contenitore./buildah/stable:latest - Se lo script non riesce a trovare l'immagine, utilizza il registro successivo come sostituto e torna al passaggio 1. Tuttavia, se l'immagine è trovato, inizia a estrarre ogni livello dell'immagine usando i
containers/image
biblioteca.
In questo esempio, buildah/stable
è stato trovato su quay.io/buildah/stable
. I containers/image
script rileva che ci sono sette livelli in quay.io/buildah/stable
e inizia a copiarli tutti contemporaneamente dal registro contenitori all'host. Copiarli contemporaneamente è efficiente.
Poiché ogni livello viene copiato nell'host Podman chiama containers/storage
biblioteca. I containers/storage
script riassembla i livelli in ordine e per ogni livello. Crea un punto di montaggio in sovrapposizione in ~/.local/share/containers/storage
sopra il livello precedente. Se non esiste un livello precedente, crea il livello iniziale.
Nota: In Podman senza root, utilizziamo effettivamente un fuse-overlayfs
eseguibile per creare il livello. Rootfull usa overlayfs
del kernel autista. Attualmente, il kernel non consente agli utenti rootless di montare filesystem sovrapposti, ma possono montare filesystem FUSE.
Successivamente, containers/storage
decomprime il contenuto del livello nel nuovo livello di archiviazione. Poiché i livelli sono privi di tar, containers/storage
chown l'UID/GID dei file nel tarball nella home directory. Si noti che questo processo può non riuscire se l'UID o il GID specificato nel file tar non è stato mappato nello spazio dei nomi utente. Vedi Perché Podman senza root non può estrarre la mia immagine?
Creazione del container
Ora è il momento per Podman di creare un nuovo contenitore basato sull'immagine. Per fare ciò, Podman aggiunge il container al database, quindi chiede a containers/storage
libreria per creare e montare un nuovo contenitore in c/storage
. Il nuovo livello contenitore funge da livello di lettura/scrittura finale ed è montato sopra l'immagine.
Configurazione della rete
Successivamente, dobbiamo configurare la rete. Per fare ciò, Podman trova ed esegue /usr/bin/slirp4netns
per configurare una rete di contenitori. In Podman senza root, non è possibile creare una rete completa e separata per i contenitori, poiché questa funzione non è consentita per gli utenti non root. In Podman senza root, utilizziamo slirp4netns
per configurare la rete host e simulare una VPN per il container.
Nota: Nei container root, Podman utilizza i plugin CNI per configurare un bridge.
Se l'utente ha specificato una mappatura delle porte come -p 8080:80
, slirpnetns
ascolterebbe sulla rete host alla porta 8080 e consentirebbe al processo del contenitore di collegarsi alla porta 80. Il slirp4netns
Il comando crea un dispositivo tap che viene iniettato all'interno del nuovo spazio dei nomi di rete, in cui risiede il contenitore. Ogni pacchetto viene riletto da slirp4netns
ed emula uno stack TCP/IP nello spazio utente. Ogni connessione al di fuori dello spazio dei nomi della rete container viene convertita in un'operazione socket che l'utente senza privilegi può eseguire nello spazio dei nomi della rete host.
Gestione dei volumi
Per gestire i volumi, Podman legge tutta la memoria del contenitore. Raccoglie le etichette SELinux utilizzate e crea una nuova etichetta non utilizzata per eseguire il contenitore utilizzando opencontainers/selinux
biblioteca.
Poiché l'utente ha specificato due volumi da montare nel contenitore e ha chiesto a Podman di rietichettare il contenuto, Podman utilizza opencontainers/selinux
per applicare ricorsivamente l'etichetta SELinux ai file/directory di origine dei volumi. Podman utilizza quindi opencontainers/runtime-tools
libreria per assemblare una specifica di runtime Open Containers Initiative (OCI):
- Podman dice a
runtime-tools
per aggiungere le sue impostazioni predefinite hardcoded per cose come capacità, ambiente e spazi dei nomi alle specifiche. - Podman utilizza le specifiche dell'immagine OCI estratte da
buildah/stable
image per impostare il contenuto nelle specifiche, come la directory di lavoro, il punto di ingresso e variabili di ambiente aggiuntive. - Podman prende l'input dell'utente e usa gli
runtime-tools
library per aggiungere campi nelle specifiche per ciascuno dei volumi e imposta il comando per il contenitore subuildah bud /
.
Nel nostro esempio, l'utente ha detto a Podman che voleva utilizzare il dispositivo /dev/fuse
all'interno del contenitore. Su un container rootato, Podman direbbe al runtime OCI di creare un /dev/fuse
dispositivo all'interno del contenitore, ma con gli utenti Podman senza root non sono autorizzati a creare dispositivi, quindi Podman dice invece alle specifiche OCI di associare mount /dev/fuse
dall'host al container.
Avvio del monitor container conmon
Una volta che i volumi sono stati gestiti, Podman trova ed esegue il conmon
predefinito per il contenitore /usr/bin/conmon
. Queste informazioni vengono lette da /usr/share/containers/libpod.conf
. Podman poi dice a conmon
eseguibile per utilizzare il runtime OCI elencato anche in libpod.conf
; di solito, /usr/bin/runc
o /usr/bin/crun
. Podman dice anche a conmon
per eseguire podman container cleanup $CTRID
per il container quando il container esce.
Conmon esegue le seguenti operazioni durante il monitoraggio del contenitore:
- Conmon esegue il runtime OCI, consegnandogli il percorso del file delle specifiche OCI e puntando al punto di montaggio del livello del contenitore in
containers/storage
. Questo punto di montaggio è chiamato rootfs. - Conmon monitora il container finché non esce e restituisce il codice di uscita.
- Conmon gestisce quando l'utente si collega al container, fornendo un socket per lo streaming di STDOUT e STDERR del container.
- Anche STDOUT e STDERR vengono registrati in un file per i
podman logs
.
Dopo aver eseguito conmon
, ma prima dell'avvio del runtime OCI, Podman si collega al socket "attach" perché il contenitore non è stato eseguito con -d
. Dobbiamo farlo prima di eseguire il contenitore, altrimenti rischiamo di perdere tutto ciò che il contenitore ha scritto nei suoi flussi standard prima di collegarlo. Farlo prima che il container si avvii ci dà tutto.
Avvio del runtime OCI
Il runtime OCI legge il file delle specifiche OCI e configura il kernel per eseguire il contenitore. Esso:
- Imposta gli spazi dei nomi aggiuntivi per il contenitore.
- Configura i cgroup se il contenitore è in esecuzione su cgroups V2 (cgroups V1 non supporta cgroup senza root).
- Imposta l'etichetta SELinux per l'esecuzione del contenitore.
- Legge il
seccomp.json
file (predefinito su/usr/share/containers/seccomp.json
) e imposta le regole seccomp. - Imposta le variabili d'ambiente.
- Bind monta i due volumi specificati sui percorsi in rootfs. Se il percorso di destinazione non esiste in rootfs, il runtime OCI crea la directory di destinazione.
- Passa da root a rootfs (rende il rootfs
/
all'interno del contenitore). - Fork il processo del contenitore.
- Esegue qualsiasi programma hook OCI, passando loro i rootf e il PID 1 del contenitore.
- Esegue il comando specificato dall'utente
buildah bud /
con il PID 1 del contenitore. - Esce dal runtime OCI, lasciando
conmon
per monitorare il contenitore.
E infine, conmon
riporta il successo a Podman.
Esecuzione di buildah
processo principale del contenitore
Ora per l'ultimo gruppo di passaggi. Inizia quando il contenitore avvia il processo Buildah iniziale. (Perché nel nostro esempio abbiamo usato Buildah.) Buildah condivide i containers/image
sottostanti e containers/storage
librerie con Podman, quindi in realtà segue la maggior parte dei passaggi sopra definiti che Podman ha utilizzato per estrarre le sue immagini e generare i suoi contenitori.
Podman si collega al conmon
socket e continua a leggere/scrivere STDOUT su conmon
. Nota che se l'utente avesse specificato -d
di Podman flag, Podman sarebbe uscito, ma il conmon
continuerebbe a monitorare il container.
Quando il processo del contenitore termina, il kernel invia un SIGCHLD a conmon
processi. A sua volta, conmon
:
- Registra il codice di uscita del contenitore.
- Chiude il file di registro del contenitore.
- Chiude STDOUT/STDERR del comando Podman.
- Esegue la
podman container cleanup $CTRID
comando.
La pulizia del contenitore Podman rimuove quindi slirp4netns
rete e dice a containers/storage
per smontare tutti i punti di montaggio del contenitore. Se l'utente ha specificato --rm
quindi il contenitore viene invece completamente rimosso. Il livello del contenitore viene rimosso da containers/storage
, e la definizione del contenitore viene rimossa dal DB.
Poiché il comando Podman originale era in esecuzione in primo piano, Podman attende conmon
per uscire, ottiene il codice di uscita dal contenitore, quindi esce con il codice di uscita del contenitore.
Conclusione
Si spera che questa spiegazione ti aiuti a capire tutta la magia ciò accade sotto le coperte quando si esegue il comando Podman senza root.
Nuovo per i container? Scarica Containers Primer e impara le basi dei container Linux.