infra / linux-bash
Conteneurs from scratch : namespaces et cgroups sans Docker
Explication
Ce que vous allez apprendre
- Comprendre qu'un conteneur Docker n'est qu'un processus Linux ordinaire, pas une mini-machine virtuelle
- Isoler la vue d'un processus (PID, réseau, hostname) à la main avec
unshare - Limiter les ressources d'un processus avec les cgroups v2, sans passer par Docker
- Isoler le système de fichiers visible par un processus avec
chroot - Relier ces trois briques pour comprendre exactement ce que fait
docker runen interne
Dans quel contexte ?
Un recruteur technique te demande en entretien : "Qu'est-ce qu'un conteneur, concrètement, sans citer Docker ?" Cette leçon construit la réponse à la main, brique par brique, sans lancer un seul docker run.
| Mécanisme | Ce qu'il isole | Commande de base |
|---|---|---|
| Namespaces | La vue (PID, réseau, hostname...) | unshare |
| cgroups | Les ressources (CPU, mémoire, nombre de process) | écriture dans /sys/fs/cgroup/... |
| chroot / pivot_root | Le système de fichiers visible | chroot |
D'abord, une idée reçue à corriger
Il est tentant de croire qu'un conteneur Docker est une sorte de mini-machine virtuelle. En réalité, un conteneur n'est qu'un processus Linux ordinaire, auquel on a appliqué deux mécanismes du noyau qui existent indépendamment de Docker : les namespaces et les cgroups.
Piège classique
Confondre isolation et sécurité totale : un namespace isole la vue qu'a un processus du système, pas les vulnérabilités du noyau qui reste, lui, partagé avec l'hôte. Un conteneur reste structurellement moins étanche qu'une vraie machine virtuelle.
Voyons d'abord ce que fait un namespace
Un namespace isole une vue du système pour un processus donné : sa propre liste de processus, son propre réseau, son propre nom de machine. La commande sudo unshare --mount --uts --ipc --net --pid --fork --mount-proc=/proc bash lance un shell qui, une fois dedans, ne voit que lui-même avec ps aux — il croit être seul sur la machine, alors qu'il tourne sur le même noyau que tout le reste.
Une fois isolé côté vue, il reste un problème : les ressources ne sont pas limitées
Ce processus isolé pourrait, sans limite, consommer tout le CPU ou toute la mémoire de la machine hôte. C'est exactement ce que corrigent les cgroups (control groups) : écrire echo "256M" | sudo tee /sys/fs/cgroup/mon-conteneur/memory.max plafonne la mémoire disponible pour ce groupe de processus précis.
Maintenant, comment isoler le système de fichiers vu par ce processus
sudo chroot /mnt/rootfs /bin/bash change la racine / visible par ce processus vers un dossier précis, /mnt/rootfs, préparé au préalable avec debootstrap. Le processus partage toujours le même noyau que l'hôte, mais ne voit plus les fichiers du système réel.
Un dernier morceau : isoler le réseau avec une interface virtuelle
Une paire veth créée avec sudo ip link add veth0 type veth peer name veth1 relie un namespace réseau isolé à la machine hôte, exactement comme Docker le fait en interne pour donner une IP à chaque conteneur.
Le lien avec la suite
Namespaces (vue) + cgroups (limites de ressources) + chroot (isolation du système de fichiers), c'est la totalité de ce qu'est un conteneur : Docker n'ajoute qu'une couche pratique par-dessus ces trois briques que tu viens de manipuler toi-même à la main. La prochaine leçon descend encore d'un niveau, vers les modules que le noyau charge à chaud.
Commandes & code
Conteneurs from scratch : namespaces et cgroups sans Docker
Docker n'est qu'une couche d'orchestration au-dessus de primitives noyau. Voici comment isoler un process à la main.
# --- Namespaces : isolent la VUE qu'un process a du système ---
# Types : mnt (points de montage), pid, net, uts (hostname), ipc, user, cgroup, time
# unshare : crée un nouveau process dans de nouveaux namespaces
sudo unshare --mount --uts --ipc --net --pid --fork --mount-proc=/proc bash
# --fork est nécessaire avec --pid pour que bash devienne le PID 1 du nouveau namespace
# Dans ce shell isolé :
hostname conteneur-test # change SEULEMENT le hostname vu depuis ce namespace uts
hostname # confirme, sans affecter la machine hôte
ps aux # ne voit que les process du nouveau namespace pid (grâce à mount-proc)
ip addr show # namespace net vide : aucune interface sauf lo
# --- pivot_root / chroot : change la racine du filesystem visible ---
sudo debootstrap stable /mnt/rootfs http://deb.debian.org/debian # prépare un mini rootfs Debian
sudo chroot /mnt/rootfs /bin/bash # change de racine, mais partage encore le noyau hôte
# pivot_root est préféré en prod (permet de démonter proprement l'ancienne racine) :
# cd /mnt/rootfs && pivot_root . old_root
# --- Namespace réseau isolé avec une paire veth (interface virtuelle) ---
sudo ip netns add ns1 # crée un namespace réseau nommé
sudo ip link add veth0 type veth peer name veth1 # paire d'interfaces virtuelles reliées entre elles
sudo ip link set veth1 netns ns1 # place une extrémité dans le namespace ns1
sudo ip addr add 10.200.0.1/24 dev veth0
sudo ip netns exec ns1 ip addr add 10.200.0.2/24 dev veth1
sudo ip link set veth0 up
sudo ip netns exec ns1 ip link set veth1 up
sudo ip netns exec ns1 ip link set lo up
sudo ip netns exec ns1 ping -c2 10.200.0.1 # le namespace isolé joint l'hôte via la veth
# --- cgroups v2 : limiter les ressources du process isolé (sans Docker) ---
cat /sys/fs/cgroup/cgroup.controllers # contrôleurs disponibles (cpu, memory, io, pids...)
sudo mkdir /sys/fs/cgroup/mon-conteneur
echo "+cpu +memory +pids" | sudo tee /sys/fs/cgroup/cgroup.subtree_control
echo "50000 100000" | sudo tee /sys/fs/cgroup/mon-conteneur/cpu.max # 50% d'un coeur (50ms/100ms)
echo "256M" | sudo tee /sys/fs/cgroup/mon-conteneur/memory.max
echo "100" | sudo tee /sys/fs/cgroup/mon-conteneur/pids.max
# Attacher le process isolé au cgroup (place son PID dans cgroup.procs)
echo $$ | sudo tee /sys/fs/cgroup/mon-conteneur/cgroup.procs
# Vérifier la consommation réelle du cgroup
cat /sys/fs/cgroup/mon-conteneur/memory.current
cat /sys/fs/cgroup/mon-conteneur/cpu.stat
# --- Nettoyage ---
sudo ip netns del ns1
sudo rmdir /sys/fs/cgroup/mon-conteneurRésumé
- Un conteneur = namespaces (isolation de la vue) + cgroups (limitation des ressources) + chroot/pivot_root (isolation du filesystem).
unshare --fork --pid --mount-procreproduit l'essentiel de l'isolation process qu'utilisedocker run.- Écrire directement dans
/sys/fs/cgroup/.../cgroup.procsattache un PID à un cgroup sans aucun outil tiers.
Exercices pratiques
Mission : expliquer un conteneur sans citer Docker
Objectif : Construire à la main les briques d'isolation (namespaces, cgroups, chroot) et en cerner les limites de sécurité.
Contexte
Un recruteur technique te demande en entretien : "Qu'est-ce qu'un conteneur, concrètement, sans citer Docker ?" Tu dois construire la réponse brique par brique, à la main, sans lancer un seul docker run.