# 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 ``` 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 \ --file talos/clusterconfig/funk-compute-02.yaml # compute-03 talosctl apply-config --insecure --nodes \ --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 ``` --- ## 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 4m v1.33.1 192.168.10.12 compute-03 Ready 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 `` 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 `.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-/`. #### 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 1h v1.33.1 compute-03 Ready 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 kubectl logs -n -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 -n # 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 -n # → 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 - < -n # 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 ssh storage-01 "sudo rm -rf /srv/data/nfs/k8s/" ``` #### 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 # 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 ` 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: ``` --- ### ❌ 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 -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.