Funk-lab/admin/install/kubernetes.md
Hermes Bot 00e53fc0b8 docs(hermes): sections état vérifié — inspection cluster 2026-06-05
Ajout d'une section '## État vérifié' à chaque doc admin/ basée
sur l'inspection read-only du cluster (systemctl, kubectl, nft, etc.).
Aucune modification de service ni réécriture de documentation existante.

Co-Authored-By: Hermes <hermes@funk.lab>
2026-06-05 12:32:57 +02:00

44 KiB

Installation complète du cluster Kubernetes Funk

Guide d'installation de A à Z — écrit après avoir effectué l'installation réelle sur les 3 ThinkCentre. Toutes les erreurs rencontrées sont documentées dans la section Pièges et erreurs en bas.


Sommaire

  1. Contexte et architecture
  2. Prérequis réseau — Ansible (dnsmasq + nftables)
  3. Installer les outils sur le poste perso
  4. Installer les outils sur storage-01
  5. Configurer SOPS + age (chiffrement des secrets)
  6. Écrire la configuration Talos
  7. Générer les secrets et les configs nœuds
  8. Préparer la clé USB Talos
  9. Installer chaque nœud
  10. Bootstrap etcd
  11. Récupérer le kubeconfig
  12. Vérifier que les nœuds sont Ready
  13. Installer MetalLB
  14. Installer Traefik
  15. Configurer le DNS wildcard *.lab.local
  16. Stockage persistant — NFS provisioner
  17. Vérification finale
  18. Administration courante
  19. Pièges et erreurs rencontrés

1. Contexte et architecture

Matériel

Trois Lenovo ThinkCentre M715q (AMD A10-9700E, 2.4 GHz, 4 cœurs) récupérés et réutilisés en cluster.

Machine IP fixe Rôle Disque RAM
compute-01 192.168.10.11 control-plane NVMe 256 GB 16 GB
compute-02 192.168.10.12 worker NVMe 256 GB 8 GB
compute-03 192.168.10.13 worker NVMe 256 GB 8 GB

Les machines sont dans un LAN dédié 192.168.10.0/24, isolé du LAN domestique (192.168.1.0/24) par storage-01 qui fait passerelle + NAT.

Stack logicielle

Composant Version Rôle
Talos Linux v1.13.0 OS immuable pour k8s — géré entièrement via API
Kubernetes v1.33.1 Orchestration des workloads
talhelper latest Génère les configs Talos depuis talconfig.yaml
SOPS + age v3.9.4 / latest Chiffrement des secrets dans git
Flannel intégré Talos CNI (réseau pods) — CIDR 10.42.0.0/16
MetalLB latest (Helm) LoadBalancer bare-metal — pool 192.168.10.200-230
Traefik v3.x (Helm) Ingress controller — IP fixe 192.168.10.200
Helm v3.x Gestionnaire de charts k8s

Pourquoi Talos Linux ?

Talos est un OS immuable conçu exclusivement pour faire tourner Kubernetes. Il n'a pas de shell, pas de SSH, pas de package manager. Tout se fait via son API (talosctl). Ça force des bonnes pratiques : configuration déclarative, secrets chiffrés dans git, zéro dérive manuelle possible.

Schéma réseau

Internet
    │
Freebox (192.168.1.254)
    │
    ├── LAN domestique 192.168.1.0/24
    │       └── Poste perso (192.168.1.10)
    │
    └── storage-01 WAN (192.168.1.200)
            │
         NAT + routage
            │
        storage-01 LAN (192.168.10.1)
            │
         LAN cluster 192.168.10.0/24
            ├── compute-01  192.168.10.11
            ├── compute-02  192.168.10.12
            ├── compute-03  192.168.10.13
            └── gpu-01      192.168.10.20

2. Prérequis réseau — Ansible (dnsmasq + nftables)

Avant de toucher aux nœuds Talos, storage-01 doit être opérationnel avec :

  • dnsmasq : sert le DHCP sur le LAN cluster (les nœuds en maintenance mode se prennent une IP DHCP) + DNS
  • nftables : le port 67 (DHCP) doit être ouvert côté LAN

Ces deux rôles sont gérés par Ansible. Si ce n'est pas encore fait :

cd ansible/
ansible-playbook -i inventory.yml playbooks/site.yml --tags dnsmasq,gateway

Ce que dnsmasq doit avoir dans sa config

Fichier ansible/roles/dnsmasq/defaults/main.yml :

dhcp_range_start: 192.168.10.50
dhcp_range_end: 192.168.10.99
dhcp_lease_time: 12h

dhcp_static_hosts:
  - mac: "6c:4b:90:82:8e:47"
    name: compute-01
    ip: 192.168.10.11
  - mac: "6c:4b:90:cf:7f:c5"
    name: compute-02
    ip: 192.168.10.12
  - mac: "6c:4b:90:b6:49:20"
    name: compute-03
    ip: 192.168.10.13

Si les baux statiques ne sont pas encore configurés au moment de l'installation, les nœuds prendront une IP dynamique dans 192.168.10.50-99. C'est normal — noter l'IP affiché sur l'écran du ThinkCentre ou dans les logs dnsmasq.

Ce que nftables doit avoir

Le port 67 (DHCP) doit être ouvert :

udp dport 67 iif {{ lan_interface }} accept

Sans cette règle, les requêtes DHCP des nœuds sont silencieusement droppées — les nœuds restent sans IP et talosctl apply-config ne peut pas les atteindre.


3. Installer les outils sur le poste perso

Tout cela s'installe dans /usr/local/bin.

# talhelper — génère les configs Talos depuis talconfig.yaml
curl -sL https://github.com/budimanjojo/talhelper/releases/latest/download/talhelper_linux_amd64.tar.gz \
  | sudo tar xz -C /usr/local/bin talhelper

# age + age-keygen — chiffrement asymétrique des secrets
AGE_VERSION=$(curl -s https://api.github.com/repos/FiloSottile/age/releases/latest | grep tag_name | cut -d'"' -f4)
curl -sL "https://github.com/FiloSottile/age/releases/download/${AGE_VERSION}/age-${AGE_VERSION}-linux-amd64.tar.gz" \
  | sudo tar xz -C /tmp
sudo mv /tmp/age/age /usr/local/bin/
sudo mv /tmp/age/age-keygen /usr/local/bin/

# sops — orchestrateur de chiffrement (utilise age en backend)
sudo curl -sL "https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64" \
  -o /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops

# talosctl — CLI pour administrer les nœuds Talos
curl -sL https://talos.dev/install | sh

# kubectl — CLI Kubernetes standard
curl -sL "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
  -o /tmp/kubectl
sudo install /tmp/kubectl /usr/local/bin/kubectl

# helm — gestionnaire de charts k8s
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

Vérifier :

talhelper --version
age-keygen --version
sops --version
talosctl version --client
kubectl version --client
helm version

4. Installer les outils sur storage-01

Depuis le poste perso, se connecter en SSH puis rejouer les mêmes commandes. storage-01 doit pouvoir administrer le cluster indépendamment (backup etcd, urgences...).

ssh storage-01

# talhelper
curl -sL https://github.com/budimanjojo/talhelper/releases/latest/download/talhelper_linux_amd64.tar.gz \
  | sudo tar xz -C /usr/local/bin talhelper

# age
AGE_VERSION=$(curl -s https://api.github.com/repos/FiloSottile/age/releases/latest | grep tag_name | cut -d'"' -f4)
curl -sL "https://github.com/FiloSottile/age/releases/download/${AGE_VERSION}/age-${AGE_VERSION}-linux-amd64.tar.gz" \
  | sudo tar xz -C /tmp
sudo mv /tmp/age/age /usr/local/bin/
sudo mv /tmp/age/age-keygen /usr/local/bin/

# sops
sudo curl -sL "https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64" \
  -o /usr/local/bin/sops
sudo chmod +x /usr/local/bin/sops

# talosctl
curl -sL https://talos.dev/install | sh

# kubectl
curl -sL "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
  -o /tmp/kubectl
sudo install /tmp/kubectl /usr/local/bin/kubectl

# helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

5. Configurer SOPS + age (chiffrement des secrets)

Les secrets Talos (clés PKI du cluster) doivent être dans git mais ne peuvent pas être en clair. SOPS les chiffre avec age.

Générer la clé age (une fois par poste d'administration)

Sur le poste perso :

mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt

La sortie affiche la clé publique, exemple :

Public key: age16rg9hnvnu89rll5schg8d2q5sccxlp7cxms9jmflguxapmtjp3lq0j4393

La clé privée est dans ~/.config/sops/age/keys.txt. Ne jamais la committer. Elle est dans .gitignore.

Sur storage-01 (si administration depuis storage-01 souhaitée) :

Option A — copier la même clé privée (plus simple) :

scp ~/.config/sops/age/keys.txt storage-01:~/.config/sops/age/keys.txt

Option B — générer une clé séparée et l'ajouter dans .sops.yaml (plus sécurisé, mais nécessite de recréer talsecret.sops.yaml).

Créer le fichier .sops.yaml à la racine du repo

creation_rules:
  - path_regex: \.sops\.yaml$
    age: age16rg9hnvnu89rll5schg8d2q5sccxlp7cxms9jmflguxapmtjp3lq0j4393

La path_regex doit matcher le fichier talsecret.sops.yaml. Une regex trop restrictive (ex: talos/.*\.sops\.yaml$) peut ne pas matcher si SOPS calcule le chemin relatif différemment. La regex \.sops\.yaml$ est plus robuste.


6. Écrire la configuration Talos

talos/talconfig.yaml — source de vérité du cluster

clusterName: funk
talosVersion: v1.13.0
kubernetesVersion: v1.33.1
endpoint: https://192.168.10.11:6443
domain: cluster.local
allowSchedulingOnControlPlanes: false

clusterPodNets:
  - 10.42.0.0/16
clusterSvcNets:
  - 10.43.0.0/16

cniConfig:
  name: flannel

patches:
  - "@./patches/all.yaml"

nodes:
  - hostname: compute-01
    ipAddress: 192.168.10.11
    installDisk: /dev/nvme0n1
    controlPlane: true
    networkInterfaces:
      - deviceSelector:
          driver: r8169
        dhcp: false
        addresses:
          - 192.168.10.11/24
        routes:
          - network: 0.0.0.0/0
            gateway: 192.168.10.1

  - hostname: compute-02
    ipAddress: 192.168.10.12
    installDisk: /dev/nvme0n1
    controlPlane: false
    patches:
      - "@./patches/workers.yaml"
    networkInterfaces:
      - deviceSelector:
          driver: r8169
        dhcp: false
        addresses:
          - 192.168.10.12/24
        routes:
          - network: 0.0.0.0/0
            gateway: 192.168.10.1

  - hostname: compute-03
    ipAddress: 192.168.10.13
    installDisk: /dev/nvme0n1
    controlPlane: false
    patches:
      - "@./patches/workers.yaml"
    networkInterfaces:
      - deviceSelector:
          driver: r8169
        dhcp: false
        addresses:
          - 192.168.10.13/24
        routes:
          - network: 0.0.0.0/0
            gateway: 192.168.10.1

Points critiques :

  • installDisk: /dev/nvme0n1 — les ThinkCentre ont un NVMe, pas /dev/sda (qui est la clé USB)
  • endpoint: https://192.168.10.11:6443 — IP directe du control-plane, pas de VIP
  • Pas de VIP configuré — un VIP = même IP que le nœud = casse etcd au bootstrap
  • cniConfig: name: flannel — Talos gère Flannel nativement, ne pas appliquer de manifest externe
  • deviceSelector: driver: r8169 — carte réseau des ThinkCentre M715q

talos/patches/all.yaml — appliqué à tous les nœuds

machine:
  time:
    servers:
      - 192.168.10.1
      - time.cloudflare.com
  network:
    nameservers:
      - 192.168.10.1

NTP et DNS pointent vers storage-01 (192.168.10.1) qui est la passerelle + dnsmasq du LAN cluster.

talos/patches/workers.yaml — appliqué aux workers seulement

machine:
  kubelet:
    extraArgs:
      system-reserved: cpu=500m,memory=1Gi
      kube-reserved: cpu=500m,memory=1Gi

compute-02 et compute-03 n'ont que 8 GB de RAM. Ces réservations protègent l'OS et kubelet de l'OOM killer quand les workloads chargent.


7. Générer les secrets et les configs nœuds

cd talos/

# Générer les secrets du cluster (une seule fois — contient les PKI, tokens, etc.)
talhelper gensecret | sops --filename-override talsecret.sops.yaml --encrypt /dev/stdin > talsecret.sops.yaml

# Vérifier que le fichier est bien chiffré (doit commencer par "sops:")
head -5 talsecret.sops.yaml

# Générer les configs par nœud + talosconfig dans clusterconfig/
talhelper genconfig

Le dossier talos/clusterconfig/ est dans .gitignore — il est généré localement à partir des secrets déchiffrés. Ne pas committer.

ls talos/clusterconfig/
# funk-compute-01.yaml
# funk-compute-02.yaml
# funk-compute-03.yaml
# talosconfig

Committer les fichiers sources (pas le clusterconfig/) :

git add talos/talconfig.yaml talos/talsecret.sops.yaml talos/patches/ .sops.yaml
git commit -m "feat: talos cluster config funk"

8. Préparer la clé USB Talos

Télécharger l'ISO Talos v1.13.0 (metal AMD64) depuis https://github.com/siderolabs/talos/releases.

# Identifier le device de la clé USB
lsblk

# Flasher — remplacer /dev/sdX par le bon device
sudo dd if=/chemin/vers/metal-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync
sync

Utiliser dd direct, pas Ventoy. Talos doit être le seul système bootable sur la clé. Ventoy ajoute ses propres entrées EFI qui peuvent interférer.

La même clé peut être utilisée pour les 3 nœuds successivement.


9. Installer chaque nœud

Le processus est identique pour les 3 machines. Les étapes pour chaque nœud :

Étape A — Démarrer en maintenance mode

  1. Brancher la clé USB dans le ThinkCentre
  2. Allumer ou redémarrer
  3. Si le BIOS démarre sur un autre OS : appuyer sur F12 au boot pour choisir la clé USB
  4. Talos démarre en "maintenance mode" (pas encore d'OS installé)
  5. Le nœud obtient une IP DHCP de dnsmasq dans 192.168.10.50-99

Étape B — Identifier l'IP DHCP attribuée

# Option 1 — regarder les logs dnsmasq sur storage-01
ssh storage-01 "journalctl -u dnsmasq -n 20 | grep DHCP"

# Option 2 — scanner le réseau
nmap -sn 192.168.10.50-99

Étape C — Vérifier le disque cible (IMPORTANT)

Avant d'appliquer la config, confirmer que nvme0n1 existe bien :

export TALOSCONFIG=talos/clusterconfig/talosconfig
talosctl get disks --insecure --nodes <IP-DHCP>

Exemple de sortie attendue :

NODE            NAMESPACE   TYPE   ID        SIZE      TRANSPORT   ROTATIONAL
192.168.10.78   runtime     Disk   nvme0n1   238 GB    nvme        false
192.168.10.78   runtime     Disk   sda       16 GB     usb         false

nvme0n1 = le SSD NVMe interne → cible de l'installation. sda = la clé USB → ne PAS mettre ça dans installDisk.

Étape D — Appliquer la config et déclencher l'installation

# compute-01 (remplacer l'IP DHCP réelle)
talosctl apply-config --insecure --nodes 192.168.10.78 \
  --file talos/clusterconfig/funk-compute-01.yaml

# compute-02
talosctl apply-config --insecure --nodes <IP-DHCP-compute-02> \
  --file talos/clusterconfig/funk-compute-02.yaml

# compute-03
talosctl apply-config --insecure --nodes <IP-DHCP-compute-03> \
  --file talos/clusterconfig/funk-compute-03.yaml

Après l'application de la config :

  1. Talos installe automatiquement sur /dev/nvme0n1
  2. Il efface les entrées EFI existantes sur le NVMe
  3. Il reboot
  4. Après reboot, le nœud démarre sur son IP fixe configurée dans talconfig.yaml

Vérifier que le nœud est disponible sur son IP fixe :

talosctl --nodes 192.168.10.11 get members
# ou juste un ping
ping 192.168.10.11

La clé USB n'est plus nécessaire après la première installation réussie.

Si le nœud reboot sur un ancien OS

Si le ThinkCentre avait déjà un OS (Windows, Linux...), ses entrées EFI peuvent prendre la priorité.

Solution A — Retirer la clé USB pendant que Talos est en train de s'installer. Le reboot se fait directement sur le NVMe fraîchement installé.

Solution B — Via BIOS : entrer dans le BIOS (F1 sur ThinkCentre), aller dans Boot Order, mettre le NVMe en premier.

Solution C — Force via talosctl upgrade (si Talos démarre mais depuis la clé, pas le NVMe) :

talosctl upgrade --image ghcr.io/siderolabs/installer:v1.13.0 --nodes <IP>

10. Bootstrap etcd

Une fois les 3 nœuds sur leurs IPs fixes, initialiser etcd sur le control-plane.

talosctl bootstrap --nodes 192.168.10.11

Cette commande ne se lance qu'une seule fois, jamais sur un cluster existant. Relancer bootstrap casse etcd irrémédiablement — il faudrait tout réinstaller.

Attendre environ 2 minutes que etcd démarre et que Kubernetes s'initialise :

talosctl --nodes 192.168.10.11 service etcd
# Attendre que l'état passe à "Running"

11. Récupérer le kubeconfig

# Sur le poste perso
talosctl kubeconfig --nodes 192.168.10.11 ~/.kube/config

# Sur storage-01 (depuis storage-01 après avoir cloné le repo et régénéré clusterconfig/)
talosctl kubeconfig --nodes 192.168.10.11 ~/.kube/config

storage-01 doit avoir son propre clusterconfig/ généré localement (après avoir copié la clé age privée et talsecret.sops.yaml). Il faut faire talhelper genconfig sur storage-01 également.


12. Vérifier que les nœuds sont Ready

kubectl get nodes -o wide

Sortie attendue :

NAME         STATUS   ROLES           AGE   VERSION   INTERNAL-IP
compute-01   Ready    control-plane   5m    v1.33.1   192.168.10.11
compute-02   Ready    <none>          4m    v1.33.1   192.168.10.12
compute-03   Ready    <none>          4m    v1.33.1   192.168.10.13

Si les nœuds sont NotReady, c'est normal dans les premières minutes — Flannel intégré à Talos démarre automatiquement. Patienter 1-2 minutes.

Ne pas appliquer le manifest Flannel externe (kubectl apply -f kube-flannel.yml). Talos v1.13 inclut Flannel nativement dans kube-system avec le bon CIDR 10.42.0.0/16. Appliquer le manifest externe crée deux DaemonSets Flannel en conflit.


13. Installer MetalLB

MetalLB permet d'assigner des IPs réelles (du LAN cluster) aux services Kubernetes de type LoadBalancer. Sans MetalLB, les services LoadBalancer restent en <pending> indéfiniment.

Installation via Helm

helm repo add metallb https://metallb.github.io/metallb
helm repo update

kubectl create namespace metallb-system

helm install metallb metallb/metallb --namespace metallb-system

# Attendre que tous les pods soient Running (controller + 3 speakers)
kubectl get pods -n metallb-system -w

Sortie attendue :

NAME                          READY   STATUS    RESTARTS   AGE
controller-7dd9f6d5f9-xxxxx   1/1     Running   0          2m
speaker-xxxxx                 1/1     Running   0          2m
speaker-xxxxx                 1/1     Running   0          2m
speaker-xxxxx                 1/1     Running   0          2m

Configuration du pool d'IPs

cat <<'EOF' | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: funk-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.10.200-192.168.10.230
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: funk-l2
  namespace: metallb-system
spec:
  ipAddressPools:
  - funk-pool
EOF

Si l'application échoue avec une erreur de webhook (connection refused vers metallb-webhook-service.metallb-system.svc:443), c'est un signe que le réseau pod est instable. Vérifier d'abord que tous les pods Flannel sont 1/1 Running.


14. Installer Traefik

Traefik est l'ingress controller — il reçoit tout le trafic HTTP/HTTPS entrant sur 192.168.10.200 et le route vers les bons services en fonction des IngressRoute.

helm repo add traefik https://traefik.github.io/charts
helm repo update

kubectl create namespace infra

helm install traefik traefik/traefik \
  --namespace infra \
  --set service.type=LoadBalancer \
  --set service.loadBalancerIP=192.168.10.200 \
  --set ports.web.port=80 \
  --set ports.websecure.port=443 \
  --set ingressRoute.dashboard.enabled=true \
  --set logs.general.level=INFO

Vérifier que MetalLB a bien assigné l'IP :

kubectl get svc -n infra traefik

Sortie attendue :

NAME      TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)
traefik   LoadBalancer   10.43.192.29   192.168.10.200   80:30507/TCP,443:30480/TCP

Accéder au dashboard Traefik

Le dashboard est disponible sur le port 9000 via un IngressRoute interne :

kubectl port-forward -n infra svc/traefik 9000:9000
# Ouvrir http://localhost:9000/dashboard/

15. Configurer le DNS wildcard *.lab.local

Le rôle Ansible dnsmasq gère ça via une ligne dans la config dnsmasq :

address=/.lab.local/192.168.10.200

Elle est générée automatiquement si la variable traefik_ip est définie dans ansible/roles/dnsmasq/defaults/main.yml :

traefik_ip: 192.168.10.200

Et dans le template dnsmasq.conf.j2 :

address=/.{{ dns_domain }}/{{ traefik_ip }}

Appliquer :

ansible-playbook -i ansible/inventory.yml ansible/playbooks/site.yml --tags dnsmasq

Tester depuis le poste perso ou storage-01 :

dig @192.168.10.1 monservice.lab.local +short
# → 192.168.10.200

Avec ça, n'importe quel <service>.lab.local sera résolu vers Traefik, qui se charge du routage vers le bon service k8s.


16. Stockage persistant — NFS provisioner

Sans stockage persistant, les pods comme Grafana ou Prometheus perdent toutes leurs données à chaque redémarrage. Le cluster Funk utilise le RAID5 de storage-01 comme backend via NFS.

Architecture

Pod (Grafana, Prometheus, etc.)
  └── PersistentVolumeClaim (PVC)
        └── StorageClass "nfs"
              └── nfs-subdir-external-provisioner (Deployment, namespace nfs-provisioner)
                    └── NFS export → /srv/data/nfs/k8s sur storage-01 (RAID5)

Quand un pod demande du stockage (PersistentVolumeClaim), le provisioner :

  1. Reçoit la demande via l'API Kubernetes
  2. Crée automatiquement un sous-répertoire dans /srv/data/nfs/k8s/ sur storage-01
  3. Crée un PersistentVolume pointant vers ce sous-répertoire
  4. Bind le PV au PVC — le pod peut monter son volume

Prérequis sur storage-01

Le répertoire NFS doit exister et être exporté avant de déployer le provisioner :

# Sur storage-01 — vérifier l'export
sudo exportfs -v | grep k8s
# → /srv/data/nfs/k8s  192.168.10.0/24(rw,sync,no_subtree_check,no_root_squash)

# Si le répertoire n'existe pas encore
sudo mkdir -p /srv/data/nfs/k8s
sudo chown nobody:nobody /srv/data/nfs/k8s
sudo chmod 0777 /srv/data/nfs/k8s

# Recharger les exports
sudo exportfs -ra
sudo systemctl status nfs-server

no_root_squash est requis : le provisioner tourne en root dans le conteneur et doit pouvoir créer des sous-répertoires sur le partage.

Fichiers de configuration

k8s/apps-of-apps/apps/nfs-provisioner.yaml — Application ArgoCD parente :

spec:
  source:
    repoURL: git@github.com:Alkatrazz24/Funk-lab.git
    path: k8s/infra/nfs-provisioner

k8s/infra/nfs-provisioner/helmrelease.yaml — Application ArgoCD enfant (multi-source) :

spec:
  sources:
    - repoURL: https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
      chart: nfs-subdir-external-provisioner
      targetRevision: 4.0.18
      helm:
        valueFiles:
          - $values/k8s/infra/nfs-provisioner/values.yaml
    - repoURL: git@github.com:Alkatrazz24/Funk-lab.git
      targetRevision: main
      ref: values

k8s/infra/nfs-provisioner/values.yaml — paramètres Helm :

nfs:
  server: 192.168.10.1         # IP de storage-01
  path: /srv/data/nfs/k8s      # Répertoire exporté

storageClass:
  name: nfs
  defaultClass: true           # Classe par défaut du cluster
  reclaimPolicy: Retain        # Conserver les données si le PVC est supprimé
  archiveOnDelete: false       # Ne pas archiver à la suppression

reclaimPolicy: Retain signifie que si tu supprimes un PVC, le sous-répertoire NFS reste. Les données sont protégées. Il faut supprimer manuellement le répertoire si souhaité.

Pattern App-of-Apps pour ce déploiement

root Application (watches apps-of-apps/apps/)
  └── nfs-provisioner Application (watches k8s/infra/nfs-provisioner/)
        └── nfs-subdir-external-provisioner Application (multi-source Helm)
              ├── Deployment nfs-subdir-external-provisioner (namespace nfs-provisioner)
              └── StorageClass "nfs"

Important : le helmrelease.yaml dans k8s/infra/nfs-provisioner/ doit avoir un nom différent de son Application parente. Ici nfs-subdir-external-provisionernfs-provisioner. Un même nom créerait une boucle de gestion ArgoCD (SharedResourceWarning).

Déploiement initial (déjà fait via ArgoCD)

# ArgoCD déploie automatiquement via Git. Pour forcer manuellement :
kubectl -n argocd annotate application nfs-provisioner \
  argocd.argoproj.io/refresh=hard --overwrite

# Vérifier que tout est Synced
kubectl get applications -n argocd | grep nfs
# nfs-provisioner                   Synced   Healthy
# nfs-subdir-external-provisioner   Synced   Healthy

Utiliser le stockage NFS dans un pod

Créer un PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mon-app-data
  namespace: mon-namespace
spec:
  storageClassName: nfs      # Doit correspondre à la StorageClass
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

ReadWriteOnce (RWO) : monté par un seul pod à la fois. Suffisant pour des bases de données, Prometheus, Grafana. ReadWriteMany (RWX) : monté par plusieurs pods simultanément. NFS le supporte — utile pour du contenu statique partagé.

Monter le PVC dans un Deployment

spec:
  template:
    spec:
      containers:
        - name: mon-app
          image: mon-image
          volumeMounts:
            - name: data
              mountPath: /data          # Chemin dans le conteneur
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: mon-app-data    # Nom du PVC ci-dessus

Comment Grafana utilise le NFS

Grafana stocke dans son volume NFS :

  • Sa base de données SQLite (grafana.db) — dashboards, utilisateurs, alertes, préférences
  • Le cache des images PNG/PDF exportées depuis les dashboards
  • Les plugins installés à chaud

Dans k8s/infra/monitoring/values.yaml :

grafana:
  persistence:
    enabled: true
    storageClassName: nfs
    size: 2Gi

Le chart Grafana crée automatiquement un PVC nommé kube-prometheus-stack-grafana. Le provisioner NFS provisionne un répertoire dans /srv/data/nfs/k8s/monitoring-kube-prometheus-stack-grafana-pvc-<uid>/.

Comment Prometheus utilise le NFS

Prometheus utilise un StatefulSet avec un volumeClaimTemplate dans le CRD Prometheus :

prometheus:
  prometheusSpec:
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: nfs
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 20Gi

Le PVC créé s'appelle prometheus-kube-prometheus-stack-prometheus-db-prometheus-kube-prometheus-stack-prometheus-0 et est lié à un répertoire dans /srv/data/nfs/k8s/.

Attention : Les volumeClaimTemplate de StatefulSet ne peuvent pas être mis à jour une fois créés. Si tu changes la taille ou la storageClass, tu dois :

  1. Patcher le CRD Prometheus pour retirer storageSpec
  2. Supprimer le StatefulSet (sans --cascade=foreground pour ne pas tuer les pods)
  3. Supprimer le PVC
  4. Remettre storageSpec dans values.yaml
  5. Git push → ArgoCD recrée tout

17. Vérification finale

# État des nœuds
kubectl get nodes -o wide

# Tous les pods (rien ne doit être en erreur)
kubectl get pods -A

# MetalLB
kubectl get pods -n metallb-system

# Traefik avec son IP
kubectl get svc -n infra traefik

# Flannel (doit être 3/3 Running)
kubectl get pods -n kube-system | grep flannel

# Test DNS wildcard depuis storage-01
ssh storage-01 "dig @192.168.10.1 test.lab.local +short"

Sortie finale attendue :

NAME         STATUS   ROLES           AGE   VERSION
compute-01   Ready    control-plane   1h    v1.33.1
compute-02   Ready    <none>          1h    v1.33.1
compute-03   Ready    <none>          1h    v1.33.1

18. Administration courante

Variables d'environnement (à mettre dans ~/.bashrc)

export TALOSCONFIG=~/Projets/lab/talos/clusterconfig/talosconfig
export KUBECONFIG=~/.kube/config

Commandes du quotidien

# État global
kubectl get nodes -o wide
kubectl get pods -A
kubectl get events -A --sort-by='.lastTimestamp'

# État Talos (services internes)
talosctl --nodes 192.168.10.11,192.168.10.12,192.168.10.13 service

# Logs d'un service Talos
talosctl --nodes 192.168.10.11 dmesg | tail -30
talosctl --nodes 192.168.10.11 service etcd

# Logs d'un pod k8s
kubectl logs -n <namespace> <pod>
kubectl logs -n <namespace> <pod> -f    # follow

# Reboot propre d'un nœud
talosctl --nodes 192.168.10.12 reboot

# Shutdown d'un nœud
talosctl --nodes 192.168.10.12 shutdown

# Tous les workers d'un coup
talosctl --nodes 192.168.10.12,192.168.10.13 reboot

Mise à jour de la config d'un nœud

cd talos/

# 1. Modifier talconfig.yaml
vim talconfig.yaml

# 2. Régénérer les configs
talhelper genconfig

# 3. Appliquer sur le nœud (sans --insecure cette fois, on connaît déjà le nœud)
talosctl --nodes 192.168.10.11 apply-config \
  --file clusterconfig/funk-compute-01.yaml

Backup etcd (critique — single control-plane)

talosctl --nodes 192.168.10.11 etcd snapshot /tmp/etcd-backup-$(date +%Y%m%d).db
scp /tmp/etcd-backup-*.db storage-01:/srv/data/backups/etcd/

À planifier en cron hebdomadaire. Sur un single control-plane, la perte de etcd sans backup = reinstallation complète du cluster.

Régénérer les configs depuis un nouveau poste

Si tu changes de poste ou récupères le repo sur storage-01 :

# 1. Copier la clé age privée
mkdir -p ~/.config/sops/age
# Copier keys.txt depuis l'ancien poste

# 2. Cloner le repo
git clone git@github.com:Alkatrazz24/Funk-lab.git

# 3. Régénérer clusterconfig/ (déchiffre talsecret.sops.yaml avec la clé age)
cd Funk-lab/talos
talhelper genconfig

# 4. Récupérer le kubeconfig
talosctl kubeconfig --nodes 192.168.10.11 ~/.kube/config

Administrer le stockage NFS

# État du provisioner
kubectl get pods -n nfs-provisioner
kubectl get deployment -n nfs-provisioner

# État de la StorageClass
kubectl get storageclass

# Tous les PVCs du cluster (toutes namespaces)
kubectl get pvc -A

# Tous les PVs (volumes physiques côté cluster)
kubectl get pv

# Détail d'un PVC (binding, capacité, storageClass)
kubectl describe pvc <nom-pvc> -n <namespace>

# Vérifier l'espace utilisé côté NFS (storage-01)
ssh storage-01 "du -sh /srv/data/nfs/k8s/*"
ssh storage-01 "df -h /srv/data"

# Logs du provisioner (utile si un PVC reste en Pending)
kubectl logs -n nfs-provisioner deployment/nfs-subdir-external-provisioner --tail=30

Pourquoi un PVC reste-t-il en Pending ?

Un PVC reste Pending quand le provisioner ne peut pas créer le volume. Causes courantes :

Cause Diagnostic Fix
StorageClass nfs inexistante kubectl get sc → vide Forcer sync ArgoCD nfs-provisioner
Provisioner pod en erreur kubectl get pods -n nfs-provisioner Lire les logs, redéployer
storage-01 inaccessible ping 192.168.10.1 depuis un pod Vérifier réseau/NFS
Répertoire NFS non exporté ssh storage-01 "exportfs -v" Relancer le playbook Ansible
Permissions insuffisantes Logs provisioner : permission denied Vérifier no_root_squash dans /etc/exports
# Diagnostic complet pour un PVC bloqué
kubectl describe pvc <pvc-name> -n <namespace>
# → Section "Events" montre la raison du blocage

# Test de connectivité NFS depuis un node k8s
kubectl run nfs-test --image=busybox --rm -it --restart=Never -- \
  mount -t nfs 192.168.10.1:/srv/data/nfs/k8s /mnt

Créer un PVC manuellement (hors ArgoCD)

kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-nfs
  namespace: default
spec:
  storageClassName: nfs
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi
EOF

# Vérifier que le PVC passe en Bound
kubectl get pvc test-nfs -w

# Vérifier le répertoire créé sur storage-01
ssh storage-01 "ls /srv/data/nfs/k8s/ | grep test-nfs"

# Nettoyage
kubectl delete pvc test-nfs
# → Le répertoire NFS reste (reclaimPolicy: Retain) — supprimer manuellement si souhaité

Supprimer un PVC et ses données

# 1. Supprimer le PVC
kubectl delete pvc <pvc-name> -n <namespace>

# 2. Le PV passe en "Released" (pas automatiquement supprimé car reclaimPolicy=Retain)
kubectl get pv  # chercher le PV en Released

# 3. Si tu veux libérer l'espace, supprimer aussi le PV et le répertoire NFS
kubectl delete pv <pv-name>
ssh storage-01 "sudo rm -rf /srv/data/nfs/k8s/<répertoire-correspondant>"

Forcer la re-synchronisation ArgoCD du provisioner

Le provisioner peut être purgé par ArgoCD si l'Application parente est OutOfSync. Pattern de récupération :

# 1. Forcer refresh root pour recréer les applications enfants
kubectl -n argocd annotate application root \
  argocd.argoproj.io/refresh=hard --overwrite

# 2. Forcer sync nfs-provisioner spécifiquement
kubectl -n argocd annotate application nfs-provisioner \
  argocd.argoproj.io/refresh=hard --overwrite

# 3. Attendre que les deux apps soient Synced/Healthy
kubectl get applications -n argocd | grep nfs
# nfs-provisioner                   Synced   Healthy
# nfs-subdir-external-provisioner   Synced   Healthy

# 4. Vérifier la StorageClass et le pod
kubectl get sc && kubectl get pods -n nfs-provisioner

Si des PVCs étaient en Pending pendant l'absence du provisioner, elles se resolveront automatiquement dès que la StorageClass et le provisioner sont de nouveau présents.


19. Pièges et erreurs rencontrés

Tout ce qui a planté lors de l'installation réelle, pour ne pas y retomber.


installDisk = /dev/sda → Talos installe sur la clé USB

Symptôme : Le nœud reboot mais reste en maintenance mode ou redémarre en boucle.

Cause : installDisk: /dev/sda dans talconfig.yaml. Sur les ThinkCentre M715q, /dev/sda est la clé USB. Le NVMe interne est /dev/nvme0n1.

Vérification avant d'appliquer :

talosctl get disks --insecure --nodes <IP-DHCP>
# Chercher le disk avec TRANSPORT=nvme

Fix : Corriger talconfig.yaml, régénérer avec talhelper genconfig, reflasher la clé USB avec le bon ISO, redémarrer le nœud en maintenance mode, réappliquer.


VIP = même IP que le nœud → etcd bloque sur "Waiting for etcd spec"

Symptôme : Après talosctl bootstrap, etcd ne démarre jamais. talosctl service etcd montre Waiting for etcd spec.

Cause : Une VIP (Virtual IP) avait été configurée dans talconfig.yaml avec la même adresse que le nœud (192.168.10.11). Talos attend que la VIP soit différente du nœud pour certaines opérations réseau internes, et ça crée un deadlock.

Fix : Supprimer complètement la section VIP dans talconfig.yaml. On n'en a pas besoin — sur un single control-plane, l'endpoint est l'IP directe du nœud.


Nœud reboot sur l'ancien OS (entrées EFI résiduelles)

Symptôme : Après talosctl apply-config, le nœud reboot mais démarre sur Windows ou un Linux précédent.

Cause : L'ancien OS avait des entrées dans le firmware EFI (NVRAM) avec une priorité plus haute que le nouveau bootloader Talos.

Fix A : Retirer la clé USB pendant que Talos est en train d'installer sur le NVMe — le reboot suivant n'a plus que l'entrée NVMe disponible.

Fix B : Via BIOS (F1) → Boot Priority → mettre le NVMe en premier.


DHCP ne fonctionne pas — nœuds sans IP

Symptôme : Le nœud démarre en maintenance mode mais reste sans IP. talosctl apply-config --insecure --nodes <IP> ne trouve rien.

Cause : nftables sur storage-01 ne laissait pas passer le port 67 (DHCP). Les broadcast DHCP des nœuds étaient silencieusement droppés.

Fix : Ajouter la règle dans ansible/roles/gateway/templates/nftables.conf.j2 :

udp dport 67 iif {{ lan_interface }} accept

Puis rejouer le playbook gateway :

ansible-playbook -i inventory.yml playbooks/site.yml --tags gateway

SOPS "no matching creation rules found"

Symptôme : talhelper gensecret | sops --filename-override talsecret.sops.yaml --encrypt /dev/stdin échoue.

Cause : La path_regex dans .sops.yaml ne matchait pas le chemin relatif calculé par SOPS. Exemple : talos/.*\.sops\.yaml$ ne matche pas si SOPS calcule le chemin depuis la racine du repo.

Fix : Utiliser une regex plus simple et robuste :

creation_rules:
  - path_regex: \.sops\.yaml$
    age: <clé-publique>

Flannel CrashLoopBackOff sur 2 pods sur 3

Symptôme :

kube-flannel-frw6t   0/1   CrashLoopBackOff
kube-flannel-zl6ds   0/1   CrashLoopBackOff

Logs :

Failed to create SubnetManager: error retrieving pod spec for 'kube-system/kube-flannel-frw6t':
pods "kube-flannel-frw6t" is forbidden: User "system:serviceaccount:kube-system:flannel"
cannot get resource "pods" in API group "" in the namespace "kube-system"

Cause : Le manifest Flannel externe avait été appliqué (kubectl apply -f kube-flannel.yml), créant une ClusterRoleBinding qui pointait vers le service account flannel dans le namespace kube-flannel. Quand on a supprimé ce namespace (conflit avec le Flannel intégré de Talos), la ClusterRoleBinding a perdu sa cible mais est restée — sauf qu'elle pointait toujours vers kube-flannel, pas vers kube-system où tournent les pods Flannel natifs de Talos.

Fix :

# 1. Corriger le namespace dans la ClusterRoleBinding
kubectl patch clusterrolebinding flannel --type='json' \
  -p='[{"op": "replace", "path": "/subjects/0/namespace", "value": "kube-system"}]'

# 2. Supprimer les pods en erreur pour forcer leur redémarrage
kubectl delete pod -n kube-system kube-flannel-frw6t kube-flannel-zl6ds

Leçon : Avec Talos v1.13+, ne jamais appliquer le manifest Flannel externe. Flannel est déjà là, dans kube-system, avec le bon CIDR.


MetalLB webhook timeout

Symptôme : kubectl apply de l'IPAddressPool échoue avec :

Internal error occurred: failed calling webhook "ipaddresspoolvalidationwebhook.metallb.io":
Post "https://metallb-webhook-service.metallb-system.svc:443/validate-metallb-io-v1beta1-ipaddresspool"
dial tcp 10.42.2.2:9443: connect: connection refused

Cause : Le réseau pod était instable à cause des deux pods Flannel en CrashLoopBackOff. L'appel webhook du kube-apiserver vers le pod MetalLB ne passait pas.

Fix : Résoudre d'abord le problème Flannel (voir ci-dessus), attendre que les 3 pods Flannel soient 1/1 Running, puis réappliquer la config MetalLB.



PVC reste en Pending — StorageClass "nfs" not found

Symptôme :

Warning  ProvisioningFailed  persistentvolume-controller  storageclass.storage.k8s.io "nfs" not found

Cause : L'Application ArgoCD nfs-provisioner (ou nfs-subdir-external-provisioner) a été purgée ou est OutOfSync. La StorageClass disparaît du cluster.

Fix :

kubectl -n argocd annotate application root \
  argocd.argoproj.io/refresh=hard --overwrite
kubectl -n argocd annotate application nfs-provisioner \
  argocd.argoproj.io/refresh=hard --overwrite
# Attendre ~60s, puis vérifier :
kubectl get sc && kubectl get pods -n nfs-provisioner

Les PVCs en Pending se resolvent automatiquement dès que la StorageClass est recréée.


helmrelease.yaml — même nom que l'Application parente (SharedResourceWarning)

Symptôme :

Application/nfs-provisioner is part of applications argocd/nfs-provisioner and root
ComparisonError: Failed to load target state: permission denied

Cause : Le helmrelease.yaml dans k8s/infra/nfs-provisioner/ était une Application ArgoCD nommée nfs-provisioner — même nom que l'Application parente qui le contient. ArgoCD essayait de gérer une ressource qui portait son propre nom → boucle.

Fix : Renommer l'Application dans helmrelease.yaml avec un nom différent (nfs-subdir-external-provisioner). Toujours suivre le pattern monitoring : l'Application enfant doit avoir un nom distinct de son parent.


Prometheus StatefulSet bloqué — impossible de mettre à jour volumeClaimTemplate

Symptôme : ArgoCD signale l'Application kube-prometheus-stack en erreur après modification de storageSpec dans values.yaml :

cannot patch "prometheus-kube-prometheus-stack-prometheus" with kind StatefulSet:
StatefulSet.apps "..." is invalid: spec.volumeClaimTemplates: Forbidden: updates to statefulset

Cause : Kubernetes n'autorise pas la modification des volumeClaimTemplates d'un StatefulSet existant.

Fix :

# 1. Retirer storageSpec du CRD Prometheus pour que l'operator le détache
kubectl patch prometheus kube-prometheus-stack-prometheus -n monitoring \
  --type=merge -p '{"spec":{"storage":null}}'

# 2. Supprimer le StatefulSet sans cascade (les pods continuent)
kubectl delete statefulset prometheus-kube-prometheus-stack-prometheus-0 \
  -n monitoring --cascade=orphan

# 3. Supprimer le PVC
kubectl delete pvc <pvc-name> -n monitoring

# 4. Remettre storageSpec dans values.yaml → git push → ArgoCD recrée tout

node-exporter bloqué par PodSecurity (baseline → privileged requis)

Symptôme : Les pods node-exporter restent en Pending ou sont rejetés :

pods "kube-prometheus-stack-node-exporter-xxx" is forbidden:
violates PodSecurity "baseline:latest": host namespaces (hostNetwork=true, hostPID=true),
hostPath volumes

Cause : Talos applique par défaut la politique baseline:latest sur les namespaces. node-exporter a besoin de hostNetwork, hostPID, et de montages hostPath pour lire les métriques système — ce qui est refusé par baseline.

Fix : Labeler le namespace monitoring en privileged dans k8s/infra/monitoring/namespace.yaml :

metadata:
  name: monitoring
  labels:
    pod-security.kubernetes.io/enforce: privileged
    pod-security.kubernetes.io/audit: privileged
    pod-security.kubernetes.io/warn: privileged

Résumé des points d'attention

Sujet Règle
installDisk Toujours /dev/nvme0n1 sur les ThinkCentre M715q
USB ISO dd direct, pas Ventoy
Bootstrap etcd Une seule fois sur compute-01, jamais relancer
VIP Ne pas configurer si VIP = IP du nœud
Flannel externe Ne pas appliquer kube-flannel.yml — Talos le gère nativement
MetalLB webhook Vérifier que Flannel est 100% healthy avant d'appliquer IPAddressPool
SOPS path_regex Utiliser \.sops\.yaml$ (simple) plutôt qu'un chemin relatif complet
etcd backup Critique sur single control-plane — cron hebdomadaire minimum
RAM workers compute-02/03 = 8 GB — toujours mettre resources.requests/limits
clusterconfig/ Généré par talhelper — ne pas committer, dans .gitignore
NFS helmrelease Nom de l'Application enfant ≠ nom du parent ArgoCD
StatefulSet storage volumeClaimTemplate immutable — delete StatefulSet + PVC pour changer
PodSecurity monitoring Namespace monitoring doit être privileged pour node-exporter

État vérifié — 2026-06-05

Tous les nœuds du cluster sont actifs et en état Ready. Les versions Kubernetes (v1.33.1) et les systèmes d'exploitation (Talos v1.13.0) sont conformes aux spécifications. Aucune divergence notable avec la documentation. Un pod dans l'espace sacrifice (sacrifice-assign-renfort-29676600-wcjnr) a terminé son execution, ce qui pourrait indiquer une tâche ponctuelle ou un test réussi.