Featured image of post Transformer Proxmox VE en node Kubernetes avec LXC et lxcri

Transformer Proxmox VE en node Kubernetes avec LXC et lxcri

Ecrit par ~ zwindler ~

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é.

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.

Licensed under CC BY-SA 4.0

Vous aimez ce blog ou cet article ? Partagez-le avec vos amis !   Twitter Linkedin email Facebook

Vous pouvez également vous abonner à la mailing list des articles ici

L'intégralité du contenu appartenant à Denis Germain (alias zwindler) présent sur ce blog, incluant les textes, le code, les images, les schémas et les supports de talks de conf, sont distribués sous la licence CC BY-SA 4.0.

Les autres contenus (thème du blog, police de caractères, logos d'entreprises, articles invités...) restent soumis à leur propre licence ou à défaut, au droit d'auteur. Plus d'informations dans les Mentions Légales

Généré avec Hugo
Thème Stack conçu par Jimmy