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 ?
Et si, non seulement on exécutait des containers OCI dans Proxmox VE, mais qu’EN PLUS, on transformait nos hôtes Proxmox VE en ✨️ Nodes Kubernetes ✨️ ???
On s’y prend comment ?
Le but n’est évidemment pas d’installer Kubernetes à côté de Proxmox VE, mais réutiliser le plus de choses possibles. Typiquement, je vais réutiliser la techno de “virtu”.
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.
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 et Incus (ex LXD, “refermé” par Canonical), a écrit il y a quelques années 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.
lxcri is a wrapper around LXC which can be used as a drop-in container runtime replacement for use by CRI-O.
On va donc quand même avoir besoin de 3 trucs en plus sur notre serveur Proxmox pour que mon idée débile fonctionne :
- lxcri comme container runtime de base niveau
- cri-o comme container runtime de haut niveau
- kubelet pour piloter le runtime et communiquer avec le control plane
C’est parti
OK. lxcri s’appuie donc sur cri-o, le container runtime de Red Hat. On commence donc par installer cri-o :
Note : mon serveur Proxmox VE 8 de l’époque utilisait Debian 12 comme OS de base. On devrait donc se baser là-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 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 passe par un Dockerfile, ce qui est rigolo puisqu’on n’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 œil au Dockerfile, on se rend vite compte qu’on ne fait que lancer un script (install.sh)…
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. Je vais donc le faire à la main. 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ébugger, 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 va installer un paquet de trucs :
sudo apt update
sudo apt install curl git meson pkg-config cmake libdbus-1-dev docbook2x
On installe Golang si on ne l’a pas (peu de chance que vous ayez Golang sur un node Proxmox VE) :
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
Je vais aussi devoir 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, celui-ci fonctionne avec la dernière version de lxc (6.x), ce qui tombe bien parce que c’est la version sur mon Proxmox VE 9 à jour.
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
Ça 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 mets ç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 flemme 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 le vice, et de faire un petit script qui récupère toutes les informations des containers, et crée les fichiers /etc/pve/lxc/xxx.conf associés de manière à les afficher dans l’UI, comme je l’avais fait dans l’article :
En théorie, si cette étape avait du sens, je pourrais créer les releases moi-même sur mon fork pour faciliter l’installation.
Enfin, je pourrais essayer de contribuer mes modifications pour que tout soit mergé sur le projet principal, à ceci près que les mainteneurs refusent les PRs car le projet n’est plus maintenu.
Mais j’avoue qu’après probablement une quinzaine d’heures 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.
