Intermediaire 2 min de lecture · 360 mots

K3s sur Proxmox : Kubernetes léger pour le homelab (cluster 3 nœuds)

Après six mois à faire tourner mes services dans des LXC Docker sur Proxmox, je voulais comprendre concrètement comment fonctionne Kubernetes sans passer trois semaines à monter un cluster kubeadm complet. Un cluster full K8s avec etcd en haute disponibilité, c’est facilement 12 GB de RAM consommés avant même de déployer quoi que ce soit. Sur un MS-01 avec 32 GB, ça reste faisable, mais il ne reste plus grand-chose pour les workloads réels. K3s m’a semblé être le bon compromis : c’est du Kubernetes certifié CNCF, juste allégé des composants qui ne me servent pas.

Architecture cible et état avant

Avant K3s, mes services (Vaultwarden, Nextcloud, Gitea, Uptime Kuma) tournaient dans des LXC Docker gérés à la main. Chaque LXC avait ses propres docker-compose.yml et ses propres volumes. La mise à jour d’un service se faisait en SSH sur le bon LXC, docker compose pull && docker compose up -d, et on espérait que rien ne cassait. Pas de rollback automatique. Pas de health check centralisé. Le monitoring Prometheus + Loki sur hubble.arewel.com voyait les métriques, mais pas l’état de santé des déploiements au sens Kubernetes.

Le plan : trois VMs sur heighliner (Proxmox VE 8.3, MS-01 i9-13900H, 32 GB DDR5), toutes en Ubuntu 24.04 LTS.

VM ID Proxmox Rôle vCPU RAM IP
k3s-master 204 Control plane 4 4 GB 10.10.30.204
k3s-worker-1 205 Worker 2 2 GB 10.10.30.205
k3s-worker-2 206 Worker 2 2 GB 10.10.30.206

Les trois VMs sont sur le VLAN Serveurs (30), trunked sur le TL-SG108-M2. Le stockage des disques VM est sur le NVMe 1 TB local de heighliner. K3s en configuration SQLite (pas etcd) : c’est le défaut pour un nœud master unique, et c’est suffisant pour un homelab. etcd n’a de sens qu’à partir de 3 masters en HA.

Préparation des VMs

Même opération sur les trois nœuds. Je commence par le master.

Désactiver le swap et le rendre persistant :

sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab

Activer le forwarding IP et charger les modules kernel requis :

cat <

Fixer les IPs en statique dans Netplan (/etc/netplan/00-installer-config.yaml) et configurer /etc/hosts sur chaque nœud pour résoudre les autres membres du cluster :

10.10.30.204  k3s-master
10.10.30.205  k3s-worker-1
10.10.30.206  k3s-worker-2

Sans entrée /etc/hosts, les nœuds workers ont du mal à s'annoncer au master pendant les phases de join si le DNS homelab n'est pas encore en place.

Installation de K3s sur le master

Avant de lancer le script d'installation, je vérifie le checksum. Le site officiel expose un fichier sha256sum à côté du binaire :

curl -sfLO https://github.com/k3s-io/k3s/releases/download/v1.31.4+k3s1/k3s
curl -sfLO https://github.com/k3s-io/k3s/releases/download/v1.31.4+k3s1/sha256sum-amd64.txt
sha256sum -c sha256sum-amd64.txt --ignore-missing

Si la somme correspond, on lance l'installation via le script officiel. Le script télécharge la bonne version du binaire et configure le service systemd :

curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="v1.31.4+k3s1" sh -

Le script get.k3s.io télécharge et exécute du code depuis Internet. Vérifie toujours la somme du binaire final indépendamment du script d'installation, ou télécharge le binaire manuellement si ton niveau de risque l'exige.

K3s démarre en tant que service systemd k3s.service. On vérifie :

sudo systemctl status k3s
sudo k3s kubectl get nodes

Le master apparaît en Ready. Traefik (ingress controller) et CoreDNS démarrent automatiquement dans le namespace kube-system - c'est une des valeurs ajoutées de K3s par rapport à kubeadm pur.

Récupérer le token qui servira aux workers :

sudo cat /var/lib/rancher/k3s/server/node-token

Note ce token. Il sera nécessaire pour chaque worker.

Jointure des workers

Sur k3s-worker-1 (même chose sur k3s-worker-2, en changeant uniquement le hostname) :

curl -sfL https://get.k3s.io | 
  INSTALL_K3S_VERSION="v1.31.4+k3s1" 
  K3S_URL="https://10.10.30.204:6443" 
  K3S_TOKEN="" 
  sh -

Le service s'appelle k3s-agent.service sur les workers. Vérifier depuis le master :

sudo k3s kubectl get nodes -o wide

Les trois nœuds doivent apparaître en Ready. La jointure prend environ 30 secondes par worker.

Kubeconfig local

Pour piloter le cluster depuis ma machine de développement sans avoir à SSH sur le master à chaque fois, je copie le kubeconfig :

scp ubuntu@10.10.30.204:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s-heighliner.yaml

Il faut ensuite modifier l'URL du serveur dans ce fichier, car K3s écrit 127.0.0.1 par défaut :

sed -i 's/127.0.0.1/10.10.30.204/' ~/.kube/k3s-heighliner.yaml
export KUBECONFIG=~/.kube/k3s-heighliner.yaml
kubectl get nodes

Tu peux merger plusieurs kubeconfig avec KUBECONFIG=~/.kube/config:~/.kube/k3s-heighliner.yaml kubectl config view --merge --flatten > ~/.kube/merged.yaml et basculer de contexte avec kubectl config use-context.

Premiers objets : Nginx, Service, Ingress

Un déploiement minimal pour valider que le scheduling fonctionne sur les workers :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-test
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-test
  template:
    metadata:
      labels:
        app: nginx-test
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-test
  namespace: default
spec:
  selector:
    app: nginx-test
  ports:
    - port: 80
      targetPort: 80
  type: ClusterIP
kubectl apply -f nginx-test.yaml
kubectl get pods -o wide

Les deux réplicas doivent se répartir sur k3s-worker-1 et k3s-worker-2. Pour exposer nginx via l'Ingress Traefik (déjà installé) :

# nginx-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-test
  namespace: default
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
  rules:
    - host: nginx-test.homelab.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-test
                port:
                  number: 80
kubectl apply -f nginx-ingress.yaml

Traefik écoute sur le port 80 de l'IP du master (10.10.30.204). En ajoutant nginx-test.homelab.local à Pi-hole qui pointe vers 10.10.30.204, la page nginx s'affiche correctement depuis n'importe quel client du réseau LAN.

Stockage persistant avec Longhorn

K3s vient avec un provisionner de stockage local (local-path), mais c'est du stockage node-local : si un pod est replanifié sur un autre worker, il perd l'accès à ses données. Pour du stockage distribué, Longhorn réplique les volumes entre les workers.

Installation de Helm 3

curl -sfL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
# version.BuildInfo{Version:"v3.16.4", ...}

Ajouter le repo Longhorn :

helm repo add longhorn https://charts.longhorn.io
helm repo update

Prérequis Longhorn

Longhorn a besoin de open-iscsi sur chaque nœud :

# Sur les trois nœuds (master + workers)
sudo apt-get install -y open-iscsi
sudo systemctl enable --now iscsid

Sans ça, Longhorn s'installe mais les volumes restent en Unknown. C'est la première chose à vérifier si un volume refuse de se monter.

Installation Longhorn via Helm

kubectl create namespace longhorn-system

helm install longhorn longhorn/longhorn 
  --namespace longhorn-system 
  --version 1.7.2 
  --set defaultSettings.defaultReplicaCount=2

Avec defaultReplicaCount=2, chaque volume est répliqué sur deux workers. Avec seulement deux workers, ça couvre la perte d'un nœud.

L'installation prend 3 à 5 minutes. On suit la progression :

kubectl -n longhorn-system get pods --watch

Une fois tous les pods en Running, l'interface web Longhorn est accessible via port-forward :

kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80

L'UI montre l'espace disponible par nœud, les volumes créés, et l'état de santé des réplicas. Avec les deux workers à 2 GB RAM chacun et les disques VM sur le NVMe heighliner, Longhorn affiche environ 180 GB d'espace utilisable par nœud (les VMs ont des disques de 40 GB chacune, donc le total Longhorn est plafonné par le plus petit disque dans le pool).

Pour définir Longhorn comme StorageClass par défaut :

kubectl patch storageclass local-path 
  -p '{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "false"}}}'

kubectl patch storageclass longhorn 
  -p '{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "true"}}}'

Ce qui a coincé : le DNS CoreDNS

Après l'installation du cluster et le déploiement de Longhorn, j'ai voulu déployer Vaultwarden avec une référence à gitea.homelab.local (mon Gitea auto-hébergé sur LXC 104). Les pods démarraient mais les requêtes DNS vers les noms de domaine locaux échouaient avec NXDOMAIN.

CoreDNS, dans K3s, utilise par défaut les serveurs DNS du nœud master (lus dans /etc/resolv.conf au démarrage). Le problème : sur Ubuntu 24.04, systemd-resolved est actif et /etc/resolv.conf pointe vers 127.0.0.53 (le stub resolver local). CoreDNS voit 127.0.0.53 comme upstream, mais ce stub resolver n'est pas accessible depuis les pods (qui tournent dans un réseau overlay différent).

La solution : configurer CoreDNS pour forwarder vers Pi-hole directement. Pi-hole tourne sur LXC 105, IP 10.10.20.5.

kubectl -n kube-system edit configmap coredns

La section forward du Corefile par défaut ressemble à :

forward . /etc/resolv.conf

Je la remplace par :

forward . 10.10.20.5

Le ConfigMap complet après modification :

.:53 {
    errors
    health
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
      pods insecure
      fallthrough in-addr.arpa ip6.arpa
    }
    hosts /etc/coredns/NodeHosts {
      ttl 60
      reload 15s
      fallthrough
    }
    prometheus :9153
    forward . 10.10.20.5
    cache 30
    loop
    reload
    loadbalance
}

CoreDNS se recharge automatiquement dans les 30 secondes. Pour forcer :

kubectl -n kube-system rollout restart deployment coredns

Si tu as plusieurs serveurs DNS (Pi-hole primaire + secondaire), tu peux écrire forward . 10.10.20.5 10.10.20.6 - CoreDNS fait du round-robin entre les upstreams listés.

Après le redémarrage, les pods résolvent correctement les noms locaux. kubectl exec dans un pod de test pour vérifier :

kubectl run -it --rm dns-test --image=busybox:1.36 --restart=Never -- nslookup gitea.homelab.local

La réponse revient avec l'IP correcte du LXC Gitea.

Résultats mesurés

Consommation RAM en idle une heure après l'installation complète (Longhorn inclus), mesurée via kubectl top nodes avec metrics-server installé :

helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm install metrics-server metrics-server/metrics-server 
  --namespace kube-system 
  --set args={--kubelet-insecure-tls}
Nœud CPU (millicores) RAM utilisée
k3s-master ~120m ~1 480 MB
k3s-worker-1 ~45m ~790 MB
k3s-worker-2 ~42m ~785 MB

Le master consomme environ 1.5 GB : K3s server, CoreDNS, Traefik, Longhorn manager, et SQLite. Les workers tournent à ~800 MB en idle. Sur 32 GB dans heighliner, ces trois VMs (4 + 2 + 2 = 8 GB alloués) représentent 25% de la RAM totale. Il reste largement de la marge pour les workloads applicatifs et les autres LXC existants.

Grafana sur hubble.arewel.com collecte les métriques K3s via le job kubernetes-pods dans Prometheus. L'alerte de nœud en NotReady est configurée avec un seuil de 2 minutes.

Ce que je fais maintenant

Le cluster tourne depuis trois semaines sans redémarrage. Prochaine étape : migrer Vaultwarden et Uptime Kuma depuis leurs LXC Docker vers des déploiements K3s gérés par ArgoCD. L'article suivant couvre la mise en place du GitOps avec ArgoCD sur ce même cluster.

Ce que je n'ai pas fait dans cet article : la mise en HA du control plane (3 masters + etcd). Pour un homelab mono-nœud physique, ça n'a pas beaucoup de sens - si heighliner tombe, tout tombe de toute façon. La HA K3s multi-masters ne s'amortit que si on a plusieurs machines physiques.


slug: k3s-proxmox-kubernetes-homelab
meta: Monter un cluster K3s 3 nœuds sur Proxmox VE 8.3 avec Longhorn et la correction du DNS CoreDNS pour le réseau homelab. (~155 chars)
tags: k3s, kubernetes, proxmox, longhorn, helm, traefik, coredns, homelab

Une remarque, un retour ?

Cet article est vivant - corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.

Laisser un commentaire