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