Il ne savait pas que c’était complètement stupide, alors il l’a fait
Quel mensonge… Bien sûr, je sais très bien que c’est débile. Raison pour laquelle cette idée est donc irrésistible.
Si vous suivez le blog, vous savez que j’utilise beaucoup Proxmox VE (les articles qui attirent le plus sur le blog sont d’ailleurs des articles sur cette technologie de virtualisation).
Proxmox VE, c’est très cool ; on peut faire des VMs (QEMU) avec, mais aussi, si on n’a pas de VT-x ou qu’on veut des “lightweight VMs” (terme dont on a abusé à outrance), on peut installer des OS complets dans des containers avec LXC.
Mais au delà de ces deux solutions, les devs du projet Proxmox VE sont assez rigides (et pas que là dessus). C’est d’ailleurs pour ça que j’ai posté quelques “hacks” pour contourner les limitations de Proxmox, notamment cet article où j’explique comment lancer des containers Docker (via LXC) dans Proxmox VE (normalement pas possible).
Et si on allait encore plus loin dans le hack idiot ?
Si, non seulement on exécutait des containers Docker dans Proxmox VE, mais qu’EN PLUS, on transformait nos hôtes Proxmox VE en Nodes Kubernetes ???
Tu pousses le bouchon un peu loin Maurice !
En théorie c’est possible. Kubernetes permet beaucoup de choses, notamment le fait que de nombreux “container runtimes” existent. Peu importe la technologie de containérisation (whatever that means), du moment que vous avez un brique logicielle qui permet au kubelet (l’agent de Kubernetes) de discuter avec le runtime de manière standardisée, c’est OK.
C’est d’ailleurs pour ça qu’on peut faire du Kubernetes avec des VM libvirt (kubevirt), des microVMs (firecracker, kata containers), et tout un tas de container runtimes plus classiques (docker, containerd, …).
Du coup, Proxmox VE supportant 2 technos de virtualisation différentes (qemu et LXC), il faut choisir. Et comme j’aime bien LXC et que j’ai déjà expérimenté sur le hack Docker => LXC sur Proxmox VE, la question était vite répondue.
On s’y prend comment ?
Toute la difficulté de l’exercice est de trouver comment rendre LXC “CRI-compatible”. Et on a de la chance, Linux Containers, la fondation qui chapeaute LXC, Incus (ex LXD, “refermé” par Canonical), a écrit il y a 4 ans un CRI pour LXC appelé lxcri. Et bien sûr, comme c’est un projet à l’utilité discutable, le truc n’est plus maintenu depuis 2021.
J’avais d’ailleurs essayé de l’utiliser, passé pas mal de temps dessus en février 2024 (j’ai essuyé bug sur bug, suis passé par des forks, …) pour échouer lamentablement sur un problème de compatibilité avec ma version de LXC (5+ sous Proxmox 8, 6 en Proxmox 9), entre autres bugs.
OCI compliance
With liblxc starting from 4.0.9 it passes all sonobuoy conformance tests.
C’est parti
Déjà, je vais avoir plein d’autres prérequis (je ne vous spoil pas). On va installer un paquet de trucs, qui vont nous servir pour la suite :
sudo apt update
sudo apt install curl git meson pkg-config cmake libdbus-1-dev docbook2x
OK. Donc, avant d’installer lxcri (ou même lxc), on lit le README du projet :
lxcri is a wrapper around LXC which can be used as a drop-in container runtime replacement for use by CRI-O.
lxcri s’appuie donc sur cri_o, le container runtime de Redhat. On commence donc par installer cri-o :
Note : mon serveur en Proxmox VE 8 de l’époque utilisait Debian 12 comme OS de base. On devrait donc se baser la dessus pour la variable $OS (comme l’indique la doc). Cependant, à l’heure où j’ai testé la première fois, les dépôts de CRI-O étaient en cours de migration, avec une documentation pas à jour et des releases manquantes. Le répo https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o/ n’avait pas le PATH Debian_12, ni la version 1.29 de Kubernetes… J’avais BIEN ragé.
- https://github.com/cri-o/cri-o/issues/7657
- https://kubernetes.io/blog/2023/10/10/cri-o-community-package-infrastructure/
On ne devrait plus avoir de soucis maintenant :
export KUBERNETES_VERSION=v1.32
export CRIO_VERSION=v1.32
# créer un répertoire pour les keyrings (il n'existe pas toujours sur une install fraiche)
sudo mkdir -p /usr/share/keyrings
# répo de kubernetes
curl -fsSL https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/deb/ /" |
sudo tee /etc/apt/sources.list.d/kubernetes.list
# répo de crio
curl -fsSL https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/deb/Release.key |
sudo gpg --dearmor -o /etc/apt/keyrings/cri-o-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/cri-o-apt-keyring.gpg] https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/deb/ /" |
sudo tee /etc/apt/sources.list.d/cri-o.list
sudo apt update
sudo apt install cri-o
A partir de là, on a cri-o. On pourrait le configurer mais si on fait ça, il utilisera runc et on n’utiliserait pas LXC en tant que runtime.
Ce n’est pas le but de ce hack !
C’est donc à ce moment là qu’on rebascule sur la documentation de lxcri :
Ya pas de release
Et oui… il faut préalablement builder le binaire lxcri et le déposer dans le /usr/local de notre serveur Proxmox VE. Il n’y a pas de binaire précompilé dans le projet, c’est une issue ouverte juste avant qu’il ne soit abandonné 😬😬.
Et pour que ce soit encore plus fun, le processus de build passer par un Dockerfile, ce qui est rigolo puisqu’on a pas Docker sur notre node proxmox…
J’ai donc essayé de builder lxcri depuis une machine avec docker, en suivant la commande indiquée sur le Github. Patatra. (Déjà, il manque un “.” en fin de commande docker build dans la doc) ça fail misérablement à la compilation…
Note : je n’ai plus l’erreur en question, probablement un problème de dépendances. C’est relou parce qu’on va devoir faire plein de choses à la main… Mais ne vous embêtez pas à git clone, on va partir sur un fork (d’un fork).
En allant jeter un oeil au Dockerfile, on se rend vite compte qu’on ne fait que lancer un script (install.sh)… Voilà les fonctions qui nous intéressent :
add_lxcri() {
local repo=$LXCRI_GIT_REPO
local version=$LXCRI_GIT_VERSION
local tmpdir=${TMPDIR}/lxcri
git_clone $tmpdir $repo $version
# lxc installed from source with default installation prefix is prefered
export PKG_CONFIG_PATH=${INSTALL_PREFIX}/lib/pkgconfig
make install
cd
rm -rf $tmpdir
}
install_runtime_noclean() {
setup $PKGS_BUILD $PKGS_RUNTIME
add_lxc
add_lxcri
}
install_runtime() {
install_runtime_noclean
clean
}
Quand on appelle docker avec le buildarg installcmd=install_runtime, on lance la fonction install_runtime, qui appelle install_runtime_noclean, qui lance add_lxc puis add_lxcri.
Je veux juste compiler lxc (pour les bindings) et lxcri. Pour ça, il faut golang 1.16 (ça date…). Il y a pas mal de code pété un peu partout et plusieurs issues ouvertes dans lesquelles les mainteneurs conseillent de partir sur un fork :
J’ai passé pas mal de temps à le débug, et au final j’ai fait mon propre fork (https://github.com/zwindler/lxcri), qui nécessite golang 1.22+ et qui ajoute un gros paquet de fixes.
Prérequis pour builder lxcri
On installe golang si on l’a pas :
wget https://go.dev/dl/go1.25.4.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.25.4.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version
go version go1.25.4 linux/amd64
Donc je vais aussi récupérer et compiler lxc. Pendant un moment, j’étais bloqué sur la version 4.0.12 (très précisément) car la 4.0.6 qu’on trouve sur Debian 11 ne fonctionne pas avec le code de lxcri (j’ai mangé plein de bugs).
Mais la bonne nouvelle c’est que comme je suis un try-harder de l’espace, à force de fixes sur mon fork, mon fork, lui, fonctionne avec la dernière version de lxc (6.x).
git clone https://github.com/lxc/lxc
cd lxc
meson setup -Dprefix=/usr -Dsystemd-unitdir=PATH build
Build targets in project: 30
lxc 6.0.0
User defined options
prefix : /usr
systemd-unitdir: PATH
Found ninja-1.12.1 at /usr/bin/ninja
meson compile -C build
INFO: autodetecting backend as ninja
INFO: calculating backend command to run: /usr/bin/ninja -C /root/lxc/build
ninja: Entering directory `/root/lxc/build'
[544/544] Linking target src/lxc/tools/lxc-monitor
Ca a buildé plein de trucs, c’est cool. Mais les fichiers qui m’intéressent sont ici :
find . -name "lxc.pc"
./build/meson-private/lxc.pc
ls build/*lxc*
build/liblxc.so build/liblxc.so.1 build/liblxc.so.1.8.0 build/lxc.spec
build/liblxc.so.1.8.0.p:
liblxc.so.1.8.0.symbols
Je met ça aux bons endroits dans mon Proxmox VE avec un :
sudo make install
sudo ldconfig
Builder comme jamais
Ok, c’est parti pour jouer avec mon fork :
git clone https://github.com/zwindler/lxcri.git lxcri.zwindler
cd lxcri.zwindler
make build
go build -ldflags '-X main.version=8805687-dirty -X github.com/lxc/lxcri.defaultLibexecDir=/usr/local/libexec/lxcri' -o lxcri ./cmd/lxcri
cc -Werror -Wpedantic -o lxcri-start cmd/lxcri-start/lxcri-start.c $(pkg-config --libs --cflags lxc)
CGO_ENABLED=0 go build -o lxcri-init ./cmd/lxcri-init
# this is paranoia - but ensure it is statically compiled
! ldd lxcri-init 2>/dev/null
go build -o lxcri-hook ./cmd/lxcri-hook
go build -o lxcri-hook-builtin ./cmd/lxcri-hook-builtin
ls -alrt
total 15288
[...]
-rwxr-xr-x 1 debian debian 7108288 Nov 16 20:06 lxcri
-rwxr-xr-x 1 debian debian 17520 Nov 16 20:06 lxcri-start
-rwxr-xr-x 1 debian debian 2942584 Nov 16 20:06 lxcri-init
-rwxr-xr-x 1 debian debian 2834743 Nov 16 20:06 lxcri-hook
-rwxr-xr-x 1 debian debian 2519097 Nov 16 20:06 lxcri-hook-builtin
Si on était sur une machine de dev, on pourrait envoyer les binaires sur le serveur Proxmox VE (lxcri dans /usr/local/bin, le reste dans /usr/local/libexec/lxcri)
Dans mon cas, je suis directement sur la machine qui build et qui run, je fais donc
$ sudo make install
mkdir -p /usr/local/bin
cp -v lxcri /usr/local/bin
'lxcri' -> '/usr/local/bin/lxcri'
mkdir -p /usr/local/libexec/lxcri
cp -v lxcri-start lxcri-init lxcri-hook lxcri-hook-builtin /usr/local/libexec/lxcri
'lxcri-start' -> '/usr/local/libexec/lxcri/lxcri-start'
'lxcri-init' -> '/usr/local/libexec/lxcri/lxcri-init'
'lxcri-hook' -> '/usr/local/libexec/lxcri/lxcri-hook'
'lxcri-hook-builtin' -> '/usr/local/libexec/lxcri/lxcri-hook-builtin'
et on peut continuer :
$ /usr/local/bin/lxcri help
NAME:
lxcri - lxcri is a OCI compliant runtime wrapper for lxc
USAGE:
lxcri [global options] command [command options] [arguments...]
VERSION:
8805687
[...]
On reprend crio
Ok, on a buildé lxcri et on a toutes les dépendances pour l’utiliser. On peut donc repartir sur la doc officielle de lxcri “setup.md” dans l’idée de configurer CRI-O, pour qu’il n’utilise pas runc, uniquement lxcri.
sudo tee /etc/crio/crio.conf.d/10-crio.conf > /dev/null <<'EOF'
[crio.image]
signature_policy = "/etc/crio/policy.json"
[crio.runtime]
default_runtime = "lxcri"
[crio.runtime.runtimes.lxcri]
runtime_path = "/usr/local/bin/lxcri"
runtime_type = "oci"
runtime_root = "/var/lib/lxc" #proxmox lxc folder
inherit_default_runtime = false
runtime_config_path = ""
container_min_memory = ""
monitor_path = "/usr/libexec/crio/conmon"
monitor_cgroup = "system.slice"
monitor_exec_cgroup = ""
privileged_without_host_devices = false
EOF
A noter, la doc d’installation setup.md nous dit générer une conf propre avec le binaire crio et la commande config, mais ça ne marche pas vraiment et on se retrouve à lancer runc ou crun sans le vouloir. J’écrase tout, c’est plus simple.
Et maintenant, on peut lancer crio :
sudo systemctl enable crio && sudo systemctl start crio
A partir de là, on a un serveur disposant d’un CRI a priori fonctionnel.
systemctl status crio
● crio.service - Container Runtime Interface for OCI (CRI-O)
Loaded: loaded (/lib/systemd/system/crio.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2025-11-17 12:02:37 UTC; 12s ago
journalctl -u crio
Nov 17 20:29:43 instance-2025-11-16-15-31-55 systemd[1]: Starting Container Runtime Interface for OCI (CRI-O)...
[...]
Nov 17 20:29:43 instance-2025-11-16-15-31-55 crio[11906]: time="2025-11-17T20:29:43.61758425Z" level=info msg="Using runtime handler lxcri version 8805687"
Yeeees :D
On peut donc installer kubelet, puis l’ajouter à un cluster Kubernetes existant
Fast forward
J’avais la fleme de monter un cluster propre avec kubeadm ou autre, et d’enrôler ensuite un node à la main avec le token+sha. J’aurais aussi pu rejouer avec mon PoC rigolo des workers “linux” de Clever cloud pour créer un control plane chez Clever, puis enrôler à la main avec le node bootstrap token (j’ai fait les choses plutôt bien dans ce PoC).
J’ai donc rejoué à l’arrache avec mon projet demystifions-kubernetes, qui permet de monter un cluster mono node à la main en lançant juste des binaires.
Une fois le control plane bootstrapé (etcd, api-server, controller-manager, scheduler), on s’arrête AVANT la partie containerd (on a déjà configuré cri-o) et on lance le kubelet à la main :
sudo bin/kubelet --kubeconfig admin.conf --container-runtime-endpoint --container-runtime-endpoint=unix:///var/run/crio/crio.sock --fail-swap-on=false --cgroup-driver="systemd"
[...]
I1117 13:41:58.741561 88434 kubelet_node_status.go:78] "Successfully registered node" node="instance-2025-11-16-15-31-55"
On progresse ! Essayons de voir l’état de santé du cluster et de déployer un pod :
$ export KUBECONFIG=admin.conf
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
instance-2025-11-16-15-31-55 Ready <none> 13m v1.34.2
$ kubectl create deployment web --image=zwindler/vhelloworld
deployment.apps/web created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-6c8cc48c68-cdbtj 1/1 Running 0 4m17s
$ kubectl logs web-6c8cc48c68-cdbtj
[Vweb] Running app on http://localhost:8081/
[Vweb] We have 3 workers
Great success!
Conclusion
A partir de là, CRI-O partage le moteur LXC de Proxmox VE de manière fonctionnelle.
Pour s’en convaincre, on peut lancer la commande lxc-ls, qui nous affichera un mix de “vrais” containers LXC (créés avec proxmox, les 151 et 200) et de containers de pods (les STOPPED sont les init containers de cilium)
$ lxc-ls -f
NAME STATE AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED
151 STOPPED 0 - - - true
200 STOPPED 0 - - - true
3045d1f3abcc069b1009acc869a27b09cf9531d0701a3f9d5a760213d57c7b20 STOPPED 0 - - - false
78057100e5d0613944c2be859dfd09f790eeba88bceebea0e04970841c9bd950 RUNNING 0 - 10.0.0.90 - false
8e5506beea9a50214c46f31fa7fa6d04397963cb912aa397261359865d40a0cd RUNNING 0 - 10.0.0.167, 10.244.0.1, 203.0.113.42, 192.168.1.10 2001:db8::1 false
abdcd5be0a27417aa9e11cc2b27882bc86a63d94f547f909332ca7470a5e075a STOPPED 0 - - - false
aeaed9c2f56328bb6196ede4a78f06c4bb2d3edf5dca7912cf38407b9bf6610a STOPPED 0 - - - false
ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d RUNNING 0 - 10.0.0.167, 10.244.0.1, 203.0.113.42, 192.168.1.10 2001:db8::1 false
edb9d42e35a4029cfec5bed5597746bdf67fbe438f21446230252265ed1c849d STOPPED 0 - - - false
$ ps -ef |grep ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d
root 2674943 1 0 22:32 ? 00:00:00 /usr/libexec/crio/conmon -b /run/containers/storage/overlay-containers/ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d/userdata -c ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d --exit-dir /var/run/crio/exits -l /var/log/pods/kube-system_cilium-operator-56d6cd6767-dps78_6082ea2d-331e-4262-b8b3-d3f88eb4e446/cilium-operator/1.log --log-level info -n k8s_cilium-operator_cilium-operator-56d6cd6767-dps78_kube-system_6082ea2d-331e-4262-b8b3-d3f88eb4e446_1 -P /run/containers/storage/overlay-containers/ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d/userdata/conmon-pidfile -p /run/containers/storage/overlay-containers/ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d/userdata/pidfile --persist-dir /var/lib/containers/storage/overlay-containers/ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d/userdata -r /usr/local/bin/lxcri --runtime-arg --root=/var/lib/lxc --socket-dir-path /var/run/crio --syslog -u ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d -s
root 2674951 2674943 0 22:32 ? 00:00:00 /usr/local/libexec/lxcri/lxcri-start ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d /var/lib/lxc /var/lib/lxc/ed9aaf5a0fd4b11be121296290472d6d71d9016e4c6bb0d05fb2e4a7f4b7a85d/config
Rien ne nous empêcherait ensuite de pousser de vice, et faire un petit script qui récupère toutes les informations des containers, et créé les fichiers /etc/pve/lxc/xxx.conf associés de manière à les afficher dans l’UI, comme j’avais fait dans l’article :
Mais j’avoue qu’après probablement une grosse dizaine d’heure de debug, de golang, de compilation C, réparties sur 2 ans, j’ai un peu été au bout de ma patience pour cette “blague”.
Ok c’était débile, maintenant que j’ai réussi, dodo X_X.
