Cet article décrit la mise en œuvre de l'isolation des systèmes de fichiers rootfs entre conteneurs, garantissant que chaque instance de conteneur opère dans un environnement de fichiers distinct et indépendant.
Contexte
Précédemment, bien que l'isolation du rootfs du conteneur par rapport à l'hôte ait été établie via des techniques comme pivotRoot et overlayfs, les conteneurs partageaient une même couche de travail (writable layer) lors de leur exécution. Cette configuration entraînait des interdépendances, où des modifications effectuées par un conteneur pouvaient affecter d'autres. L'objectif est de remédier à cela en attribuant un système de fichiers rootfs unique à chaque conteneur.
Initialement, tous les conteneurs utilisaient un répertoire partagé comme /root/merged sur la machine hôte. Cette approche sera remplacée par une structure où chaque conteneur possède son propre répertoire rootfs sous /var/lib/mydocker/overlay2/{containerID}/merged, similaire à la pratique de Docker.
Implémentation Technique
Pour parvenir à cette isolation, les modifications suivantes sont nécessaires au niveau des commandes principales de l'outil mydocker :
- Mise à jour de la commande
commitpour créer des images à partir de systèmes de fichiers de conteneurs spécifiques. - Mise à jour de la commande
runpour permettre la spécification d'images et l'allocation d'un système de fichiers isolé unique pour chaque nouveau conteneur. Cela implique de décompresser le fichier tar de l'image spécifiée dans le répertoirelowerde l'overlayfs. - Mise à jour de la commande
rmpour supprimer les systèmes de fichiers associés lors de la suppression d'un conteneur.
Mise à Jour de la Commande commit
Auparavant, la commande commit compressait directement le répertoire /root/merged en un fichier tar pour en faire une image. Désormais, elle doit assembler le chemin du répertoire à compresser en utilisant le format /var/lib/mydocker/overlay2/{containerID}/merged, basé sur l'ID du conteneur.
Dans le fichier main_command.go, la fonction commandeCommit est ajustée pour accepter un ID de conteneur et un nom d'image. Elle appelle ensuite une fonction enregistrerConteneurImage.
var commandeCommit = cli.Command{
Name: "commit",
Usage: "enregistre un conteneur en tant qu'image, ex: mydocker commit <idConteneur> <nomImage>",
Action: func(contexte *cli.Context) error {
if len(contexte.Args()) < 2 {
return fmt.Errorf("id de conteneur et nom d'image manquants")
}
idConteneur := contexte.Args().Get(0)
nomImage := contexte.Args().Get(1)
return enregistrerConteneurImage(idConteneur, nomImage)
},
}
La fonction enregistrerConteneurImage est modifiée pour déterminer le chemin du point de montage (cheminMontage) en fonction de l'ID du conteneur et le chemin de destination de l'image (fichierImageTar) en fonction du nom de l'image. Elle vérifie ensuite si l'image existe déjà avant de la créer.
var ErrImageExistante = errors.New("l'image existe déjà")
func enregistrerConteneurImage(idConteneur, nomImage string) error {
cheminMontage := utilitaires.ObtenirCheminFusionne(idConteneur)
fichierImageTar := utilitaires.ObtenirCheminImage(nomImage)
existe, err := utilitaires.CheminExiste(fichierImageTar)
if err != nil {
return errors.WithMessagef(err, "vérification de l'existence de l'image [%s] dans [%s] a échoué", nomImage, fichierImageTar)
}
if existe {
return ErrImageExistante
}
log.Infof("enregistrerConteneurImage : création de l'image tar à %s", fichierImageTar)
if _, err = exec.Command("tar", "-czf", fichierImageTar, "-C", cheminMontage, ".").CombinedOutput(); err != nil {
return errors.WithMessagef(err, "échec de la compression du dossier %s", cheminMontage)
}
return nil
}
Mise à Jour de la Commande run pour l'Isolation du Système de Fichiers
La commande run subit des changements plus importants, car toutes les références aux chemins de fichiers doivent être ajustées.
Fonctionnalités ajoutées :
- Le paramètre
nomImageest ajouté à la commanderunCommandpour permettre aux utilisateurs de spécifier l'image à utiliser. - Le système de fichiers rootfs est désormais construit dynamiquement en fonction de l'
idConteneur.
var commandeExecuter = cli.Command{
Action: func(contexte *cli.Context) error {
// ... autres traitements omis
// Récupération du nom de l'image
nomImage := tableauCommandes[0]
tableauCommandes = tableauCommandes[1:]
estTty := contexte.Bool("it")
estDetache := contexte.Bool("d")
// La fonction 'Executer' inclut désormais le paramètre 'nomImage'
Executer(estTty, tableauCommandes, configRessources, volumeMnt, nomConteneur, nomImage)
return nil
},
}
La fonction Executer est mise à jour pour accepter le paramètre nomImage et l'utiliser lors de la création du procesuss parent du conteneur.
func Executer(estTty bool, commandes []string, config *subsystems.ResourceConfig, volumeMnt, nomConteneur, nomImage string) {
idConteneur := conteneur.GenererIDConteneur() // Génère un ID de conteneur de 10 caractères
// Démarrer le conteneur
parent, pipeEcriture := conteneur.CreerProcessusParent(estTty, volumeMnt, idConteneur, nomImage)
if parent == nil {
log.Errorf("Erreur lors de la création du processus parent")
return
}
if err := parent.Start(); err != nil {
log.Errorf("Erreur au démarrage du processus parent: %v", err)
return
}
// Enregistrer les informations du conteneur
err := conteneur.EnregistrerInfosConteneur(parent.Process.Pid, commandes, nomConteneur, idConteneur)
if err != nil {
log.Errorf("Erreur lors de l'enregistrement des informations du conteneur %v", err)
return
}
// Créer le gestionnaire cgroup, configurer et appliquer les limites de ressources
gestionnaireCgroups := cgroups.NouveauGestionnaireCgroup("mydocker-cgroup")
defer gestionnaireCgroups.Detruire()
_ = gestionnaireCgroups.Definir(config)
_ = gestionnaireCgroups.Appliquer(parent.Process.Pid, config)
// Envoyer les arguments via le pipe après la création du processus enfant
envoyerCommandeInit(commandes, pipeEcriture)
if estTty { // Si c'est un TTY, le processus parent attend (exécution au premier plan)
_ = parent.Wait()
conteneur.SupprimerEspaceDeTravail(idConteneur, volumeMnt)
conteneur.SupprimerInfosConteneur(idConteneur)
}
}
Ajustements des Chemins Rootfs
Les répertoires liés au rootfs sont désormais définis comme des variables et des fonctions utilitaires sont fournies pour construire dynamiquement les chemins en fonction de l'idConteneur.
// Chemins de répertoire relatifs aux conteneurs
const (
CheminImagesBase = "/var/lib/mydocker/image/"
CheminRacineConteneurs = "/var/lib/mydocker/overlay2/"
FormatDossierLower = CheminRacineConteneurs + "%s/lower"
FormatDossierUpper = CheminRacineConteneurs + "%s/upper"
FormatDossierWork = CheminRacineConteneurs + "%s/work"
FormatDossierMerged = CheminRacineConteneurs + "%s/merged"
FormatOverlayFS = "lowerdir=%s,upperdir=%s,workdir=%s"
)
func ObtenirCheminRacine(idConteneur string) string { return CheminRacineConteneurs + idConteneur }
func ObtenirCheminImage(nomImage string) string { return fmt.Sprintf("%s%s.tar", CheminImagesBase, nomImage) }
func ObtenirDossierLower(idConteneur string) string {
return fmt.Sprintf(FormatDossierLower, idConteneur)
}
func ObtenirDossierUpper(idConteneur string) string {
return fmt.Sprintf(FormatDossierUpper, idConteneur)
}
func ObtenirDossierWork(idConteneur string) string {
return fmt.Sprintf(FormatDossierWork, idConteneur)
}
func ObtenirCheminFusionne(idConteneur string) string { return fmt.Sprintf(FormatDossierMerged, idConteneur) }
func ObtenirOptionsMontageOverlayFS(dossierLower, dossierUpper, dossierWork string) string {
return fmt.Sprintf(FormatOverlayFS, dossierLower, dossierUpper, dossierWork)
}
Les fonctions CreerEspaceDeTravail et SupprimerEspaceDeTravail, ainsi que leurs méthodes internes, sont ajustées pour utiliser ces chemins dynamiques basés sur l'ID du conteneur.
// CreerEspaceDeTravail configure un système de fichiers OverlayFS pour le rootfs d'un conteneur.
/*
1) Crée la couche inférieure (lower layer) en décompressant l'image.
2) Crée les couches supérieure (upper) et de travail (work layer).
3) Crée le répertoire fusionné (merged) et y monte l'OverlayFS.
4) Si un volume est spécifié, il est également monté.
*/
func CreerEspaceDeTravail(volumeMnt, nomImage, nomConteneur string) {
err := creerCoucheInferieure(nomImage, nomConteneur)
if err != nil {
log.Errorf("échec de la création de la couche inférieure: %v", err)
return
}
err = creerCouchesSuperieureEtTravail(nomConteneur)
if err != nil {
log.Errorf("échec de la création des couches supérieure et de travail: %v", err)
return
}
err = monterOverlayFS(nomConteneur)
if err != nil {
log.Errorf("échec du montage d'OverlayFS: %v", err)
return
}
if volumeMnt != "" {
urlsVolume := extraireURLsVolume(volumeMnt)
if len(urlsVolume) == 2 && urlsVolume[0] != "" && urlsVolume[1] != "" {
err = monterVolume(nomConteneur, urlsVolume)
if err != nil {
log.Errorf("échec du montage du volume: %v", err)
return
}
} else {
log.Infof("le format du paramètre de volume est incorrect.")
}
}
}
// SupprimerEspaceDeTravail supprime le système de fichiers OverlayFS à la sortie du conteneur.
/*
Étapes inverses de la création :
1) Démonte le volume si spécifié.
2) Démonte et supprime le répertoire fusionné.
3) Supprime les couches supérieure et de travail.
*/
func SupprimerEspaceDeTravail(idConteneur, volumeMnt string) error {
// Si un volume est spécifié, il doit être démonté en premier
if volumeMnt != "" {
urlsVolume := extraireURLsVolume(volumeMnt)
if len(urlsVolume) == 2 && urlsVolume[0] != "" && urlsVolume[1] != "" {
err := demonterVolume(idConteneur, urlsVolume)
if err != nil {
return errors.Wrap(err, "échec du démontage du volume")
}
}
}
// Ensuite, démonter le point de montage global du conteneur
err := demonterOverlayFS(idConteneur)
if err != nil {
return errors.Wrap(err, "échec du démontage d'OverlayFS")
}
// Enfin, supprimer les dossiers associés
err = supprimerCouchesSuperieureEtTravail(idConteneur)
if err != nil {
return errors.Wrap(err, "échec de la suppression des couches supérieure et de travail")
}
return nil
}
Mise à Jour de la Commande rm
La commande rm est modifiée pour inclure la suppression du système de fichiers du conteneur en plus de ses informations de configuration.
func supprimerConteneur(idConteneur string, forcerSuppression bool) {
infoConteneur, err := obtenirInfosParIDConteneur(idConteneur)
if err != nil {
log.Errorf("Erreur lors de la récupération des informations du conteneur %s : %v", idConteneur, err)
return
}
switch infoConteneur.Statut {
case conteneur.ARRETTE: // Les conteneurs arrêtés peuvent être supprimés directement
// Supprimer d'abord le répertoire de configuration, puis le répertoire rootfs
if err = conteneur.SupprimerInfosConteneur(idConteneur); err != nil {
log.Errorf("Échec de la suppression de la configuration du conteneur [%s] : %v", idConteneur, err)
return
}
conteneur.SupprimerEspaceDeTravail(idConteneur, infoConteneur.Volume)
case conteneur.EN_COURS_EXECUTION: // Les conteneurs en cours d'exécution nécessitent l'option force
if !forcerSuppression {
log.Errorf("Impossible de supprimer le conteneur en cours d'exécution [%s]. Arrêtez le conteneur ou utilisez l'option de suppression forcée.", idConteneur)
return
}
log.Infof("Suppression forcée du conteneur en cours d'exécution [%s]", idConteneur)
arreterConteneur(idConteneur)
supprimerConteneur(idConteneur, forcerSuppression) // Réessayer la suppression après l'arrêt
default:
log.Errorf("Impossible de supprimer le conteneur, statut invalide : %s", infoConteneur.Statut)
return
}
}
La ligne ajoutée est : conteneur.SupprimerEspaceDeTravail(idConteneur, infoConteneur.Volume).
Tests Fonctionnels
Vérification de l'Ajustement du Rootfs
Pour tester l'isolation, nous utiliserons une image busybox.tar. Commencez par la déplacer vers le répertoire /var/lib/mydocker/image/.
root@mydocker:~# mv busybox.tar /var/lib/mydocker/image/
Démarrez ensuite un conteneur en utilisant cette image :
root@mydocker:~/refactor-isolate-rootfs/mydocker# go build .
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker run -d -name conteneur-rootfs busybox top
{"level":"info","msg":"createTty false","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"lower:/var/lib/mydocker/overlay2/5341624332/lower image.tar:/var/lib/mydocker/image/busybox.tar","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/var/lib/mydocker/overlay2/5341624332/lower,upperdir=/var/lib/mydocker/overlay2/5341624332/upper,workdir=/var/lib/mydocker/overlay2/5341624332/work /var/lib/mydocker/overlay2/5341624332/merged]","time":"2024-02-22T13:34:12+08:00"}
{"level":"info","msg":"command all is top","time":"2024-02-22T13:34:12+08:00"}
Vérifiez que le conteneur est en cours d'exécution :
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5341624332 conteneur-rootfs 219016 running top 2024-02-22 13:34:12
Inspectez le répertoire /var/lib/mydocker/overlay2 pour confirmer la création du rootfs isolé :
root@mydocker:/var/lib/mydocker/overlay2# cd /var/lib/mydocker/overlay2/5341624332
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls
lower merged upper work
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls lower
bin dev etc home proc root sys tmp usr var
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls merged/
bin dev etc home proc root sys tmp usr var
Les répertoires lower, merged, upper, et work sont présents sous /var/lib/mydocker/overlay2/{containerID}. Le contenu de lower provient de l'image décompressée, et merged est le point de montage du rootfs du conteneur.
Créez un fichier à l'intérieur du conteneur :
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker exec 5341624332 /bin/sh
{"level":"info","msg":"container pid:219016 command:/bin/sh","time":"2024-02-22T13:37:42+08:00"}
got mydocker_pid=219016
got mydocker_cmd=/bin/sh
/ # echo MonTextePersonnalise > fichier.txt
/ # cat fichier.txt
MonTextePersonnalise
Vérifiez l'existence du fichier dans le répertoire merged de l'hôte :
root@mydocker:/var/lib/mydocker/overlay2/5341624332# ls merged/
bin dev etc home proc root sys tmp usr var fichier.txt
root@mydocker:/var/lib/mydocker/overlay2/5341624332# cat merged/fichier.txt
MonTextePersonnalise
Ces étapes confirment que l'ajustement du rootfs fonctionne correctement.
Test de la Commande commit
Nous allons maintenant commiter le conteneur précédent pour en faire une nouvelle image.
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5341624332 conteneur-rootfs 219016 running top 2024-02-22 13:34:12
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker commit 5341624332 busybox-avec-personnalisation
{"level":"info","msg":"enregistrerConteneurImage : création de l'image tar à /var/lib/mydocker/image/busybox-avec-personnalisation.tar","time":"2024-02-22T13:43:33+08:00"}
Vérifiez le répertoire /var/lib/mydocker/image/ :
root@mydocker:/var/lib/mydocker/overlay2/5341624332# cd /var/lib/mydocker/image/
root@mydocker:/var/lib/mydocker/image# ls
busybox-avec-personnalisation.tar busybox.tar
L'image busybox-avec-personnalisation.tar a été créée avec succès. Démarrez un nouveau conteneur avec cette image et vérifiez son contenu :
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker run -d -name conteneur-rootfs2 busybox-avec-personnalisation top
{"level":"info","msg":"createTty false","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"resConf:\u0026{ 0 }","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"lower:/var/lib/mydocker/overlay2/8118341786/lower image.tar:/var/lib/mydocker/image/busybox-avec-personnalisation.tar","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"mount overlayfs: [/usr/bin/mount -t overlay overlay -o lowerdir=/var/lib/mydocker/overlay2/8118341786/lower,upperdir=/var/lib/mydocker/overlay2/8118341786/upper,workdir=/var/lib/mydocker/overlay2/8118341786/work /var/lib/mydocker/overlay2/8118341786/merged]","time":"2024-02-22T13:45:53+08:00"}
{"level":"info","msg":"command all is top","time":"2024-02-22T13:45:53+08:00"}
Accédez au nouveau conteneur :
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5341624332 conteneur-rootfs 219016 running top 2024-02-22 13:34:12
8118341786 conteneur-rootfs2 219109 running top 2024-02-22 13:45:53
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker exec 8118341786 /bin/sh
{"level":"info","msg":"container pid:219109 command:/bin/sh","time":"2024-02-22T13:46:14+08:00"}
got mydocker_pid=219109
got mydocker_cmd=/bin/sh
/ # cat fichier.txt
MonTextePersonnalise
Le fichier fichier.txt est présent dans la nouvelle image, confirmant le bon fonctionnement de la commande commit.
Test de la Commande rm
Enfin, testons si la commande rm supprime correctement les informations du conteneur et son répertoire rootfs.
Obtenez l'ID du conteneur :
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker ps
ID NAME PID STATUS COMMAND CREATED
5341624332 conteneur-rootfs 219016 running top 2024-02-22 13:34:12
8118341786 conteneur-rootfs2 219109 running top 2024-02-22 13:45:53
Supprimez un conteneur par son ID (avec l'option de force si nécessaire) :
root@mydocker:~/refactor-isolate-rootfs/mydocker# ./mydocker rm 5341624332 -f
{"level":"info","msg":"Suppression forcée du conteneur en cours d'exécution [5341624332]","time":"2024-02-22T13:47:36+08:00"}
{"level":"info","msg":"demonterOverlayFS,cmd:/usr/bin/umount /var/lib/mydocker/overlay2/5341624332/merged","time":"2024-02-22T13:47:36+08:00"}
Vérifiez si le répertoire rootfs du conteneur a été supprimé :
cd /var/lib/mydocker/overlay2
root@mydocker:/var/lib/mydocker/overlay2# ls
8118341786
Le répertoire correspondant à l'ID 5341624332 a été supprimé, démontrant que la commande rm fonctionne comme prévu, y compris la suppression des répertoires de système de fichiers du conteneur.