Construire un « conteneur » à la main
Étape 1 : isoler la vue avec les namespaces (1 h 15)
1.1 Le namespace UTS : un hostname à soi
Le plus simple pour commencer. On crée un namespace user (indispensable en rootless pour obtenir les droits de créer les autres namespaces) et un namespace UTS :
unshare --user --map-root-user --uts sh
# Vous êtes dans un nouveau shell. Changez le hostname :
hostname conteneur-jouet
hostname # conteneur-jouet
exit
# De retour sur l'hôte :
hostname # INCHANGÉ : le namespace UTS a isolé le hostname
Consignez : le hostname changé à l'intérieur n'a pas affecté l'hôte. Vous venez d'isoler une ressource globale du noyau. C'est exactement ce que fait podman run --hostname.
1.2 Le namespace PID : devenir PID 1
Le plus spectaculaire. On isole la table des processus :
unshare --user --map-root-user --pid --fork --mount-proc sh
# Dans le nouveau shell :
echo $$ # affiche 1 : vous êtes PID 1 de votre propre monde
ps -e # vous ne voyez QUE vos processus, pas ceux de l'hôte
exit
Points à comprendre et noter (ch. 15, §2.1) :
--forkest nécessaire : c'est le processus fils qui devient PID 1 du nouveau namespace.--mount-procremonte un/procpropre au namespace, sans quoipsmontrerait encore les processus de l'hôte (/procreflète le namespace PID).- Vous êtes PID 1 : dans un vrai conteneur, cela a des conséquences (adoption des orphelins, gestion des signaux) qui justifient les init minimalistes comme
tini.
Point de contrôle n° 1 : deux vues du même processus
Ouvrez deux terminaux. Dans le premier, lancez un processus isolé qui dure :
unshare --user --map-root-user --pid --fork --mount-proc sh -c 'sleep 999'
Dans le second (sur l'hôte), retrouvez ce sleep :
ps aux | grep 'sleep 999' # il a un PID "normal" côté hôte (ex. 34712)
Le même processus est PID 1 dans son namespace et PID 34712 vu de l'hôte. Notez-le : le conteneur n'est pas caché à l'hôte, chacun a simplement sa numérotation. C'est la clé pour déboguer un conteneur depuis l'hôte.
1.3 Le namespace user : la magie du rootless
Regardez la correspondance d'identifiants qui rend tout cela possible sans privilèges. Podman expose la même primitive :
podman unshare cat /proc/self/uid_map
# exemple de sortie :
# 0 1000 1
# 1 100000 65536
Lecture (ch. 15, §2.2) : l'UID 0 (root) dans le namespace correspond à l'UID 1000 (vous) sur l'hôte ; puis une plage (UID 1 à 65536 dans le conteneur) correspond à 100000+ sur l'hôte (votre allocation /etc/subuid). Autrement dit : root dans le conteneur = vous, sans le moindre privilège réel. C'est pour cela qu'aucun sudo n'est nécessaire de tout ce TP, et pourquoi Podman est sûr en salle de TP.
Étape 2 : limiter les ressources avec un cgroup (1 h 15)
2.1 Repérer la hiérarchie cgroup v2
# Votre cgroup courant :
cat /proc/self/cgroup
# La racine de la hiérarchie v2 :
ls /sys/fs/cgroup
# Vos contrôleurs délégués en rootless (via systemd user) :
cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/cgroup.controllers
En rootless, systemd vous délègue un sous-arbre cgroup où vous pouvez créer vos propres groupes. C'est là que Podman crée les cgroups de vos conteneurs.
2.2 Créer un cgroup et limiter la mémoire
Le plus simple et robuste en rootless est de passer par systemd-run, qui crée un cgroup transitoire et y applique des limites :
# Lancer une commande dans un cgroup limité à 50 Mo de mémoire :
systemd-run --user --scope -p MemoryMax=50M \
python3 -c "print('avant'); b = bytearray(200*1024*1024); print('après')"
La commande tente d'allouer 200 Mo alors que le cgroup en autorise 50 : le noyau invoque l'OOM killer (ch. 15, §3.3), le processus est tué avant d'afficher « après ». Consignez le comportement et le message (souvent Killed ou un code de sortie 137). Ce 137 n'est pas arbitraire : c'est 128 + 9 (tué par le signal SIGKILL). La convention « lire un code de sortie » est détaillée dans l'encadré du chapitre 15, §3.3 ; gardez-la, elle resservira à chaque diagnostic.
Point de contrôle n° 2 : comparer avec Podman
Faites la même expérience avec Podman et constatez le comportement identique :
podman run --rm --memory=50m python:3.12-slim \
python3 -c "b = bytearray(200*1024*1024); print('jamais atteint')"
echo "code de sortie : $?" # 137 = 128 + SIGKILL(9) : OOMKilled
Podman n'a rien fait de plus que vous à l'étape 2.1 : il a créé un cgroup et fixé memory.max. Notez cette équivalence, c'est le cœur du TP.
Étape 3 : assembler un « conteneur » minimal (1 h)
Combinons namespaces + un changement de racine pour approcher ce qu'est un conteneur. On utilise une petite image extraite comme système de fichiers racine :
# 1. Récupérer un système de fichiers racine minimal (via podman, sans le "lancer")
mkdir -p ~/rootfs-jouet
podman export "$(podman create docker.io/library/alpine:3.20)" | tar -C ~/rootfs-jouet -xf -
ls ~/rootfs-jouet # bin, etc, usr... une mini-distribution
# 2. Entrer dans un environnement isolé, avec CE rootfs comme racine.
# podman unshare crée déjà les namespaces (dont user) ; on change la racine.
# NB : on utilise "$HOME" et non "~" car ~ ne se développe pas ici, et on
# n'efface PAS l'environnement (pas de `env -i`, sinon HOME serait vide).
podman unshare sh -c 'cd "$HOME/rootfs-jouet" && exec chroot . /bin/sh'
# Dans le shell obtenu :
cat /etc/os-release # Alpine ! ...alors que votre hôte est Ubuntu
ls / # l'arborescence d'Alpine, pas la vôtre
exit
Vous avez isolé la vue (namespaces), changé la racine (le rootfs de l'image), et obtenu... un conteneur rudimentaire. Il ne manque que les cgroups (étape 2) et un peu d'ergonomie pour être exactement ce que fait podman run. C'est tout ce qu'un moteur de conteneurs fait : le résumé de la séquence podman run du chapitre 15, §5, que vous venez de reconstituer à la main.
Point de contrôle final
- Namespace UTS : hostname isolé, hôte inchangé (consigné)
- Namespace PID : PID 1 dans le namespace, même processus vu avec un autre PID depuis l'hôte
-
uid_maplu et interprété (root dans le conteneur = votre UID sans privilège) - cgroup mémoire : OOM kill provoqué et observé (code 137), comparé au
podman run --memory - « Conteneur » minimal assemblé (rootfs Alpine + chroot dans des namespaces)
- Runbook : pour chaque manipulation, la primitive du ch. 15 correspondante nommée
Pour aller plus loin (bonus)
- Namespace réseau :
unshare --user --map-root-user --net sh, puisip addr: vous n'avez qu'une interfaceloisolée. Comment lui donner accès au réseau ? (Indice :veth, ce que Netavark fait pour Podman.) podman inspect: lancez un conteneur (podman run -d --name t alpine sleep 999), puispodman inspect t: retrouvez, dans le JSON, les namespaces et les limites cgroup. Podman ne fait que remplir ce que vous avez fait à la main.- Le problème du PID 1 : lancez
podman run --rm alpine sh -c 'sleep 5 & wait'avec et sans--init. Documentez-vous sur les processus zombies et le rôle detini.
Questions de compréhension (à préparer pour le TD et l'examen)
- Expliquez pourquoi, en rootless, il faut créer un namespace user avant de pouvoir créer un namespace PID ou réseau. Que change le namespace user aux yeux du noyau quant aux privilèges ?
- Un collègue affirme : « un conteneur, c'est isolé comme une VM ». Réfutez précisément, en vous appuyant sur ce que vous avez vu (le processus visible depuis l'hôte, le noyau partagé).
- Reliez les trois abstractions
systemd-run -p MemoryMax,podman run --memory, et (à venir)resources.limits.memoryde Kubernetes. Quelle est la primitive commune, et où vit-elle ? - Vous avez assemblé un conteneur avec namespaces + chroot + (potentiellement) cgroups. Qu'est-ce qu'un vrai moteur (Podman) ajoute que votre bricolage n'a pas ? (Pensez : gestion des images en couches, réseau, cycle de vie, sécurité seccomp.)