LXC ou comment avoir un petit lab léger

LXC ou comment avoir un petit lab léger

Cet article est le fruit d'une question : comment faire pour pouvoir tester des applications sans pourrir les sites et applications déjà en place sur mon serveur ?

Contexte

Pour faire un rapide état des lieux, le serveur c'est 3000 paquets installés, des mises à jour constantes à faire, des bouts d'applications installés par ci et par là, des utilisateurs créés mais plus utilisés... Un énorme bordel finalement. Tellement un bordel qu'un de mes containers LXC était limité en mémoire sans que j'eusse à faire quoique ce soit... Pour finir découvrir que l'ancien moi avait limité un ancien utilisateur. Sachant aussi que ce serveur a été installé en Debian 8, il y a donc beaucoup de legacy.

Mais alors comment répondre à cette question ? (Bon, vous avez déjà lu la réponse, mais je vais faire comme si ce n'était pas le cas).

Il y a plusieurs technologies qui permettraient de répondre à ce besoin. Toutes partent du principe qu'il va falloir séparer la production des tests et donc également compartimenter ces applications. Pour y répondre il y a la virtualisation mais qui consomme beaucoup de ressources que n'a pas ma machine. Il y a aussi la containérisation qui consomme moins de ressources par rapport à la virtualisation pour une moins bonne isolation. Pour mon besoin et vu l'état actuel du serveur, l'isolation ne sera que meilleure avec la containérisation et le peu de ressources nécessaires (en RAM et en temps de calcul) demandées par cette technologie me la fait choisir.

Du coup la containerisation fut mon choix. Mais pour une technologie, il y a 10 000 logiciels... LXC, OpenVZ, Linux-VServer, Docker, Systemd-nspawn...

Il va falloir choisir. Bon, l'envie de réinstaller le serveur et donc gérer les migrations et tout ça ne m'enchante pas vraiment, ça retire donc OpenVZ, Linux-VServer, Proxmox. Il reste LXC, Systemd-nspawn, Docker / Podman.

Docker c'est un gros gros non, entre le fait que chaque dĂ©veloppeur gĂšre sa propre image Docker, pas forcĂ©ment Ă  jour, pas forcĂ©ment unifiĂ©e. Qui dit Ă©galement pas forcĂ©ment unifiĂ©e dit Ă©galement qu'il y a diffĂ©rent couche, donc pas d'unification de celle-ci. Si je souhaite unifier tout ça et bien gĂ©rer tout ça, je me retrouve Ă  devoir dĂ©velopper trouzmille images, gĂ©rer le build rĂ©guliĂšrement (notamment pour Ă©viter les CVE sauvages et tout). Au final, la simplicitĂ© de dĂ©ploiement va ĂȘtre gĂąchĂ©e par une complexitĂ© pour faire les choses correctement (et puis tout ne tourne pas sous Docker). On m'a conseillĂ© Podman, mais ça ressemble beaucoup Ă  Docker 😅

Systemd-nspawn, j'ai déjà tenté, Debian utilise SystemD et finalement LXC se repose également sur SystemD (mais pas que) donc autant directement utiliser SystemD... Bon, la documentation est excessivement inexistante (c'est un peu mieux maintenant mais bon). J'ai déjà tenté d'utiliser cet outil... Mais finalement non.

Il me reste LXC. Ça se repose sur les cgroup (gĂ©rĂ© par SystemD Ă  partir de Debian 11), il y a un peu de doc KOF.

Mise en place

Étape 1 : SURPRISE ! 🎊

LXC, c'est une technologie principalement maintenue par Canonical, livrée avec certaines commandes telles que lxc-create, lxc-start... Mais propose un logiciel externe nommé LXD qui se tape lxc... Déjà ça casse toutes les recherches sur internet, car lxc c'est en fait LXD. Et LXD ne sera pas disponible sous Debian 13 aprÚs un changement de licence de la part de canonical.

C'est un bordel

Je n'ai pas vraiment envie d'implĂ©menter une technologie avec un logiciel qui ne va pas tarder Ă  faire faux bond. MĂȘme si Incus sera proposĂ© Ă  la place. RIP la documentation alors. Il y en a peu. Mais mon choix est dĂ©jĂ  fait, donc je vais me casser le crĂąne lĂ -dessus. Vous aurez donc cet article comme documentation 😃 (ainsi que la documentation officiel et celle de Debian qui sont... complĂ©mentaires ?)

Étape 1 (la vraie Ă©tape 1) : PrĂ©paration du terrain

LXC vient par dĂ©faut avec un serveur DHCP sur une interface virtuelle spĂ©cifiĂ© dans /etc/default/lxc-net. Une IP alĂ©atoire va ĂȘtre problĂ©matique car j'ai un reverse proxy. Et on peut fixer une adresse IP sur le serveur DHCP mais il faut connaitre les IPs dĂ©jĂ  en place.
Par pur envie d'automatisation je souhaite utiliser un IPAM (Netbox 👋) qui va me permettre de choisir et d'assigner une IP Ă  chaque machine pendant toute la durĂ©e de vie de la machine sans que j'aie Ă  faire quoique ce soit (crĂ©er une IP, vĂ©rifier si elle n'est pas dĂ©jĂ  prise, si c'est le cas recommencer...).
Petite difficultĂ©, le container va ĂȘtre "non privilĂ©giĂ©". Ça va rajouter un peu de fun Ă  tout ça (car c'est trĂšs peu documentĂ©).

⚠
L'article a été écrit à partir d'une machine sous Debian 12. Tout autre OS peut avoir ses spécificités.

Pour utiliser LXC, il va déjà falloir l'installer. Pour ça, rien de plus simple :

apt-get install lxc

Ensuite, on va désactiver le serveur DHCP en écrivant dans le fichier /etc/default/lxc-net

# https://wiki.archlinux.org/title/Linux_Containers (la doc est sympa)
# Leave USE_LXC_BRIDGE as "true" if you want to use lxcbr0 for your
# containers.  Set to "false" if you'll use virbr0 or another existing
# bridge, or mavlan to your host's NIC.
USE_LXC_BRIDGE="false"

Puis on redémarre lxc-net.service et on supprime l'interface lxcbr0.
On valide au passage que l'ipv4 forwarding est activée sur la machine.

systemctl restart lxc-net.service
ip link delete lxcbr0
echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/90-ipv4.conf
đŸ˜¶
Je n'utilise pas DNSmasq pour tout un tas de raison, deux en fait.
PremiĂšrement j'ai envie que les IPs soient fixes mĂȘme en cas de reconstruction des machines (donc changement d'adresse mac).
Secondement j'aime bien Netbox et c'est moins fun de configurer DNSmasq (oui, le kiff passe avant l'utile parfois 😛)

On crée l'interface réseau qui sera le bridge entre les containers LXC et l'interface physique du serveur. Je l'ai appelé lxcnet. Juste avant de créer le fichier, on crée une adresse mac pour cette interface avec la commande suivante : hexdump -n6 -e '6/1 ":%02x"' /dev/urandom | awk ' { sub(/^:../, "02"); print } '

vim /etc/network/interfaces.d/lxcnet
auto lxcnet
iface lxcnet inet static
        address 10.10.10.1/24
        # mark to create bridge w/o
        bridge_ports none
        hwaddress ether 02:96:c0:8e:fd:d8  

iface lxcnet inet6 static
        address 2001:db8:41f:ea7::1/64
        bridge_ports none

Petit bonus, ma Freebox délÚgue la gestion de pool d'IPv6. Par défaut, la Freebox gÚre un /63 et utilise le premier /64 et nous laisse ainsi 6 autres /64. J'ai donc délégué la gestion d'un /64 au serveur. En configurant l'interface réseau, le DAD (Duplicate address detection) partait en timeout. Vu qu'on est sur un réseau intégralement maitrisé, j'ai donc désactivé le DAD

echo net.ipv6.conf.lxcnet.accept_dad=0" > /etc/sysctl.d/90-ipv6-dad.conf

Puis on lance l'interface

ifup lxcnet

Normalement ici tout s'est bien passé, une nouvelle interface est présente sur votre serveur. Vous pouvez constater ça avec la commande

ip a show dev lxcnet

Pour info, l'IPv6 présentée ici est un range réservé à la documentation internet.

Pour l'IPv6, pas besoin de plus de configuration, pour l'IPv4 cependant, sauf si vous avez un routeur configurable, il va falloir faire du NAT. J'ai rajouté dans le fichier nftables.conf :

table inet nat {
        include "/etc/nftables.d/nat-postrouting.nft"
}

Et dans le fichier /etc/nftables.d/nat-postrouting.nft

chain postrouting {

        # 000 policy
        type nat hook postrouting priority 100;
        ip saddr 10.10.10.0/24 oif eno1 masquerade

}

Et voilĂ , le serveur est prĂȘt ! On va pouvoir accueillir la toute premiĂšre machine qui portera Netbox.

💡
Je vais rendre à César ce qui est à César, j'utilise un playbook ansible confectionné par l'institut de physique de Rennes pour gérer ma configuration nftables.

Étape 2 : Notre premier container

Juste avant de créer le container, on va créer un utilisateur. Il aura pour but d'isoler le container et d'éviter une élévation de privilÚge en sortant du container. Pour ça, on va créer un utilisateur mais aussi utiliser subuid pour que l'utilisateur puisse créer des UID non mappés sur l'hÎte. Comme ça, chaque application dans le container pourra avoir un utilisateur et une élévation de privilÚge ne finira que dans un utilisateur sur l'hÎte.

Donc on crée l'utilisateur :

useradd -d /var/lib/lxc/ipam -m -s /bin/bash lxc_ipam

Bien sûr, vous pouvez utiliser le home que vous voulez.
/var/lib/lxc n'existe pas par défaut. Un coup de mkdir /var/lib/lxc résout le problÚme.

La création de cet utilisateur va générer des entrées dans /etc/subuid et /etc/subgid

On va les récupérer et les garder bien au chaud pour plus tard

root@mul ~ # grep lxc_ipam /etc/subuid
lxc_ipam:755360:65536
root@mul ~ # grep lxc_ipam /etc/subgid
lxc_ipam:755360:65536

Et on va créer le fichier de configuration du container LXC dans /var/lib/lxc/ipam/.config/lxc/default.conf


lxc.arch = amd64 # arm64 si vous ĂȘtes sur un Raspberry Pi
lxc.uts.name = ipam

lxc.net.0.type = veth
lxc.net.0.link = lxcnet
lxc.net.0.flags = up 
lxc.net.0.name = eth0 
lxc.net.0.ipv4.address = 10.10.10.2 # Une adresse que vous fixez
lxc.net.0.ipv4.gateway = 10.10.10.1 # L'adresse de l'interface créé plus tÎt

lxc.net.0.ipv6.address = 2001:db8:41f:ea7::2
lxc.net.0.ipv6.gateway = 2001:db8:41f:ea7::1


lxc.apparmor.profile = unconfined

# Les identifiants récupéré plus haut
lxc.idmap = u 0 755360 65536
lxc.idmap = g 0 755360 65536

# -- En cas d'autostart -- #
lxc.start.auto = 1
lxc.start.order = 0
lxc.start.delay = 0
lxc.group = onboot

# -- #


lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.autodev = 1 #

Allez, on n'oublie pas l'autostart. Il se situe dans le .config Ă©galement /var/lib/lxc/ipam/.config/systemd/user/lxc-autostart.service :

[Unit]
Description="Lxc-autostart for lxc user"

[Service] 
Type=oneshot
ExecStart=/usr/bin/lxc-autostart
ExecStop=/usr/bin/lxc-autostart -s
RemainAfterExit=1

[Install]
WantedBy=default.target

Puis en tant qu'utilisateur il va falloir taper la commande suivante :

systemctl daemon-reload --user
systemctl enable --user lxc-autostart.service

Cependant, SystemD ne dĂ©marre pas l'environnement utilisateur si celui-ci n'a pas de session active et l'arrĂȘte dĂšs que la session est terminĂ©e. "Lingering" permet de crĂ©er l'environnement au dĂ©marrage du systĂšme sans avoir besoin de session active. Cela va nous ĂȘtre utile pour laisser un container tourner.

loginctl enable-linger lxc_ipam

Maintenant, le container !

En tant qu'utilisateur, on tape la commande suivante :

lxc-create --name ipam --quiet --template download --bdev dir --logfile /var/lib/lxc/ipam/lxc-ipam.log --logpriority INFO -- --dist debian --release bookworm --arch amd64 --variant cloud

Avant de lĂącher le micro, je vais expliquer les arguments :

  • --logfile va Ă©crire les logs de la machine. Tout ce qui se passe dans la console sera Ă©crit dans ce fichier. Vous pouvez le retirer, mais j'aime bien avoir des logs quand ça plante.
  • --bdev il s'agit ici d'une option permettant de choisir le type de stockage pour accueillir le container. J'ai choisi dir car je n'ai pas de zpool, ni de LVM et encore moins de Ceph pouvant accueillir mon container. (L'explication en anglais de cette directive (alias le man))
  • --template download va se baser sur un template nommĂ© download. Celle-ci va tĂ©lĂ©charger l'image en se basant sur les autres arguments que vous avez utilisĂ©s. Si vous avez crĂ©Ă© votre propre template, vous remplacez download par votre template. Les arguments sont transmis aprĂšs les deux tirets vides. L'image `lxc-download rĂ©cupĂšre ses images sur le site de linuxcontainers
  • --dist c'est la distribution souhaitĂ©e. Ici Debian, mais vous pouvez faire tourner n'importe quelle machine, tant que l'architecture de votre machine est respectĂ©e.
  • --release la version de la distribution souhaitĂ©e. Bookworm pour moi
  • --arch l'architecture souhaitĂ©e, ça doit correspondre Ă  votre machine (la mienne amd64)
  • --variant la variante pour l'image. Des images cloud ou default sont gĂ©nĂ©rĂ©es, les images cloud sont censĂ©es ĂȘtre plus lĂ©gĂšre, mais embarquent surtout cloud-init (que je n'ai pas utilisĂ© au final).

AprÚs sa création, un fichier de configuration sera généré ici /var/lib/lxc/ipam.local/share/lxc/ipam/config. Lorsqu'une modification de la configuration sera à apporter (par exemple l'IP), il faudra le faire ici.

Avec Debian, le package lxc vient avec deux petites commandes en plus lxc-unpriv-start et lxc-unpriv-attach. Il s'agit de wrapper bash qui permet de lancer les containers en non privilégié avec SystemD sans trifouiller comme indiqué dans la documentation LXC.

Maintenant, on va lancer le container.

lxc_ipam@mul:~$ lxc-ls -f
NAME  STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED 
ipam  STOPPED 1         onboot -    -    true
lxc_ipam@mul:~$ lxc-unpriv-start -d --logfile lxc-{{ item.name }}.log --logpriority INFO --name ipam


lxc_ipam@mul:~$ lxc-ls -f
NAME  STATE   AUTOSTART GROUPS IPV4       IPV6                 UNPRIVILEGED 
ipam  RUNNING 1         onboot 10.10.10.2 2001:db8:41f:ea7::2  true

Le container est démarré mais il n'est pas accessible en SSH. On va devoir utiliser l'autre wrapper pour installer et configurer la machine (SSH, DNS). J'ai nommé lxc-unpriv-attach :

      lxc-unpriv-attach -n ipam -- /bin/sh -c "mkdir -p /root/.ssh/ /etc/systemd/resolved.conf.d/"
      lxc-unpriv-attach -n ipam -- /bin/sh -c "cat <<EOF > /etc/systemd/resolved.conf.d/dns.conf
      [Resolve]
      DNS=10.10.10.1
      Domains=~.
      FallbackDNS=
      EOF"
      lxc-unpriv-attach -n ipam -- systemctl restart systemd-resolved
      lxc-unpriv-attach -n ipam -- locale-gen fr_FR.UTF-8
      lxc-unpriv-attach -n ipam -- mkdir -p /root/.ssh/
      lxc-unpriv-attach -n ipam -- /bin/sh -c "echo 'VOTRECLESSHPUBLIQUE' > /root/.ssh/authorized_keys"
      lxc-unpriv-attach -n ipam -- apt-get update
      lxc-unpriv-attach -n ipam -- apt-get upgrade -y openssh-server

J'ai mon propre resolver DNS (qui Ă©coute sur l'interface en 10.10.10.1), si vous n'en avez pas, utilisez ceux de votre FAI, de la FDN, de Quad Nine ou mettez en place le votre : ArrĂȘtez (de conseiller) d'utiliser Google Public DNS.

Et voilà, la machine est maintenant fonctionnelle, démarre et vous pouvez vous y connectez dessus en SSH.

Étape 3 : Limitez les ressources

LXC par défaut propose à tous ses containers toute la RAM de la machine ainsi que tout le CPU. Et ça fonctionne trÚs bien, vous pouvez trÚs bien laisser tous vos containers avec toute la RAM de la machine. Les ressources étant partagées, vos applications se partageront les ressources comme actuellement sans isolation.

Mais c'est sympa aussi d'avoir un peu de contrîle là-dessus 😉 .

Plus haut, j'ai dit que LXC se basait sur les Cgroup pour l'isolation. Sur Debian 12, c'est mĂȘme les CgroupV2 gĂ©rĂ© par SystemD. Du coup, on va devoir Ă©diter le service user@.service. Pour ça il faut rĂ©cupĂ©rer l'uid de l'utilisateur :

root@mul ~ # id -u lxc_ipam
2000

Puis, on Ă©dite le service

systemctl edit user@2000.service

[Service]
MemoryMax=1.2
MemoryHigh=1G
CPUQuota=50%

Ça va crĂ©er un fichier (et le dossier parent) /etc/systemd/system/user@2000.service.d/override.conf.
Si vous souhaitez automatiser ça, crĂ©er le dossier /etc/systemd/system/user@2000.service.d/ puis le fichier override.conf avec le mĂȘme contenu fonctionne aussi. Il faudra cependant Ă  la fin faire un systemctl daemon-reload pour que ces valeurs soient appliquĂ©es.
En plus c'est applicable à chaud, pas besoin de redémarrer les sessions utilisateurs ni rien.

Une petite explication quand mĂȘme :

  • MemoryMax va dĂ©finir une limite maximale de la mĂ©moire vive consommable. Au delĂ  de cette limite, les processus sont tuĂ©s. La documentation recommande de l'utiliser en tant que derniĂšre ligne de dĂ©fense
  • MemoryHigh va dĂ©finir la limite haute de la mĂ©moire vive consommable. Au delĂ , le systĂšme va limiter la consommation et la mĂ©moire requise en plus sera excessivement lente
  • CPUQuota permet de dĂ©finir une limitation du CPU. Il est prĂ©fĂ©rable d'utiliser CPUQuota plutĂŽt que de pin les CPU afin de conserver l'usage du multithreading mais Ă©galement d'Ă©viter un goulot d'Ă©tranglement si toutes les applications utilisent le CPU 1.

La documentation sur les controles des ressources est trÚs complÚte. Vous pouvez vraiment tuner le container jusqu'à l'écriture disque. Je suis personnellement resté sur un truc assez basique, mon but principal était l'isolation. La gestion poussée de la mémoire pour tout ça m'importe aujourd'hui peu.

Partie 4 : Les points de montage

Les points de montage, dans mon cas, permettent de mapper facilement les dossiers du container sur l'hÎte. Au lieu de chercher le /opt dans /var/lib/lxc/ipam/.local/share/lxc/ipam/rootfs/opt je l'ai directement dans /var/lib/lxc/mountpoints/opt et pour borgmatic (un wrapper se basant sur borg pour les backups), j'ai une directive pour sauvegarder toutes les données de mes containers d'un coup (parce que c'est pas utile de sauvegarder tout le /rootfs d'un container) :

location:
    source_directories:
        - /var/lib/lxc/*/mountpoints/

Du coup, l'idée c'est de créer un dossier nommé mountpoints qui contient tous les dossiers qui serviront de point de montage.

Il suffit de mettre la ligne suivante dans le fichier de configuration /var/lib/lxc/ipam/.local/share/lxc/ipam/config

lxc.mount.entry = /var/lib/lxc/ipam/mountpoints/opt/ opt/ none bind,rw,create=dir 0 0

create=dir permet de créer le dossier dans le container avant de monter le dossier. (La documentation de lxc.mount.entry).


Et voilĂ  un container prĂȘt Ă  accueillir vos applications. Il s'appelle IPAM parce que j'ai installĂ© Netbox dessus. Une fois Netbox installĂ© et configurĂ©, je peux lancer la crĂ©ation de mes autres containers sans me soucier de l'IP. Netbox me file une IP, je l'attribue automatiquement Ă  une machine...
Je n'ai pas confectionné de script bash tout beau, PAR CONTRE j'ai créé un playbook Ansible qui le fait tout seul.
On dĂ©fini la machine, son OS, ses points de montage (si on veut), ses ressources (si on veut), on lance le playbook et une nouvelle machine est prĂȘte !

Sur ce, portez-vous bien.

Photo de Florian Krumm