Ajout section 16 (NFS provisioner) dans le guide d'installation k8s : - Architecture du provisionnement dynamique (StorageClass → NFS RAID5) - Prérequis storage-01, fichiers de config, pattern App-of-Apps - Utilisation dans les pods (PVC, Deployment, Grafana, Prometheus) - Section admin : diagnostics, PVC manuels, cleanup, re-sync ArgoCD Ajout dans section admin courante : commandes de gestion NFS, tableau diagnostics PVC Pending, procédure test PVC manuel. Ajout dans pièges : StorageClass not found, SharedResourceWarning (boucle nom ArgoCD), StatefulSet volumeClaimTemplate immutable, PodSecurity baseline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
43 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
- Contexte et architecture
- Prérequis réseau — Ansible (dnsmasq + nftables)
- Installer les outils sur le poste perso
- Installer les outils sur storage-01
- Configurer SOPS + age (chiffrement des secrets)
- Écrire la configuration Talos
- Générer les secrets et les configs nœuds
- Préparer la clé USB Talos
- Installer chaque nœud
- Bootstrap etcd
- Récupérer le kubeconfig
- Vérifier que les nœuds sont Ready
- Installer MetalLB
- Installer Traefik
- Configurer le DNS wildcard *.lab.local
- Stockage persistant — NFS provisioner
- Vérification finale
- Administration courante
- 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-configne 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_regexdoit matcher le fichiertalsecret.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 externedeviceSelector: 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
dddirect, 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
- Brancher la clé USB dans le ThinkCentre
- Allumer ou redémarrer
- Si le BIOS démarre sur un autre OS : appuyer sur F12 au boot pour choisir la clé USB
- Talos démarre en "maintenance mode" (pas encore d'OS installé)
- 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 dansinstallDisk.
É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 :
- Talos installe automatiquement sur
/dev/nvme0n1 - Il efface les entrées EFI existantes sur le NVMe
- Il reboot
- 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 ettalsecret.sops.yaml). Il faut fairetalhelper genconfigsur 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 danskube-systemavec le bon CIDR10.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 refusedversmetallb-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 sont1/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 :
- Reçoit la demande via l'API Kubernetes
- Crée automatiquement un sous-répertoire dans
/srv/data/nfs/k8s/sur storage-01 - Crée un
PersistentVolumepointant vers ce sous-répertoire - 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_squashest 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: Retainsignifie 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.yamldansk8s/infra/nfs-provisioner/doit avoir un nom différent de son Application parente. Icinfs-subdir-external-provisioner≠nfs-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
volumeClaimTemplatede StatefulSet ne peuvent pas être mis à jour une fois créés. Si tu changes la taille ou la storageClass, tu dois :
- Patcher le CRD Prometheus pour retirer
storageSpec- Supprimer le StatefulSet (sans
--cascade=foregroundpour ne pas tuer les pods)- Supprimer le PVC
- Remettre
storageSpecdans values.yaml- 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 |