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.
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Ă©).
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
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.
Ă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 choisidir
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éfenseMemoryHigh
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 lenteCPUQuota
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