# 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. [Vérification finale](#16-vérification-finale)
17. [Administration courante](#17-administration-courante)
18. [Pièges et erreurs rencontrés](#18-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. 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
```

---

## 17. 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
```

---

## 18. 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.

---

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