# 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](#pièges-et-erreurs-rencontrés) en bas.

---

## Sommaire

1. [Contexte et architecture](#1-contexte-et-architecture)
2. [Prérequis réseau — Ansible (dnsmasq + nftables)](#2-prérequis-réseau--ansible-dnsmasq--nftables)
3. [Installer les outils sur le poste perso](#3-installer-les-outils-sur-le-poste-perso)
4. [Installer les outils sur storage-01](#4-installer-les-outils-sur-storage-01)
5. [Configurer SOPS + age (chiffrement des secrets)](#5-configurer-sops--age-chiffrement-des-secrets)
6. [Écrire la configuration Talos](#6-écrire-la-configuration-talos)
7. [Générer les secrets et les configs nœuds](#7-générer-les-secrets-et-les-configs-nœuds)
8. [Préparer la clé USB Talos](#8-préparer-la-clé-usb-talos)
9. [Installer chaque nœud](#9-installer-chaque-nœud)
10. [Bootstrap etcd](#10-bootstrap-etcd)
11. [Récupérer le kubeconfig](#11-récupérer-le-kubeconfig)
12. [Vérifier que les nœuds sont Ready](#12-vérifier-que-les-nœuds-sont-ready)
13. [Installer MetalLB](#13-installer-metalLB)
14. [Installer Traefik](#14-installer-traefik)
15. [Configurer le DNS wildcard *.lab.local](#15-configurer-le-dns-wildcard-lablocal)
16. [Stockage persistant — NFS provisioner](#16-stockage-persistant--nfs-provisioner)
17. [Vérification finale](#17-vérification-finale)
18. [Administration courante](#18-administration-courante)
19. [Pièges et erreurs rencontrés](#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 :

```bash
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` :
```yaml
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`.

```bash
# 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 :
```bash
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...).

```bash
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 :**
```bash
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) :
```bash
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

```yaml
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

```yaml
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

```yaml
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

```yaml
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

```bash
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.

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

Committer les fichiers sources (pas le `clusterconfig/`) :
```bash
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`.

```bash
# 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

```bash
# 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 :

```bash
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

```bash
# 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 :
```bash
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) :
```bash
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.

```bash
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 :
```bash
talosctl --nodes 192.168.10.11 service etcd
# Attendre que l'état passe à "Running"
```

---

## 11. Récupérer le kubeconfig

```bash
# 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

```bash
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

```bash
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

```bash
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.

```bash
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 :
```bash
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 :
```bash
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` :

```yaml
traefik_ip: 192.168.10.200
```

Et dans le template `dnsmasq.conf.j2` :
```jinja2
address=/.{{ dns_domain }}/{{ traefik_ip }}
```

Appliquer :
```bash
ansible-playbook -i ansible/inventory.yml ansible/playbooks/site.yml --tags dnsmasq
```

Tester depuis le poste perso ou storage-01 :
```bash
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 :

```bash
# 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 :
```yaml
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) :
```yaml
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 :
```yaml
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-provisioner` ≠ `nfs-provisioner`. Un même nom créerait une boucle de gestion ArgoCD (SharedResourceWarning).

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

```bash
# 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

```yaml
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

```yaml
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` :
```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` :

```yaml
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

```bash
# É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)

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

### Commandes du quotidien

```bash
# É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

```bash
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)

```bash
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 :

```bash
# 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

```bash
# É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` |

```bash
# 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)

```bash
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

```bash
# 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 :

```bash
# 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** :
```bash
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` :
```bash
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 :
```yaml
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** :
```bash
# 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** :
```bash
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** :
```bash
# 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` :
```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.