Retour au cours

infra / linux-bash

Conteneurs from scratch : namespaces et cgroups sans Docker

Leçon 271 exercice

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 run en 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écanismeCe qu'il isoleCommande de base
NamespacesLa vue (PID, réseau, hostname...)unshare
cgroupsLes ressources (CPU, mémoire, nombre de process)écriture dans /sys/fs/cgroup/...
chroot / pivot_rootLe système de fichiers visiblechroot

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.

bash
# --- 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-conteneur

Résumé

  • Un conteneur = namespaces (isolation de la vue) + cgroups (limitation des ressources) + chroot/pivot_root (isolation du filesystem).
  • unshare --fork --pid --mount-proc reproduit l'essentiel de l'isolation process qu'utilise docker run.
  • Écrire directement dans /sys/fs/cgroup/.../cgroup.procs attache un PID à un cgroup sans aucun outil tiers.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →