mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 06:24:42 +02:00
docs: réorganisation install/ — guides séparés par machine
- admin/install/README.md — index + ordre d'installation recommandé - admin/install/storage-01.md — AlmaLinux + RAID5 + Ansible + stack IA (Hermes/LiteLLM/Qdrant/PostgreSQL) - admin/install/gpu-01.md — AlmaLinux + ROCm + llama-server (RX 6700XT, HSA_OVERRIDE) - admin/install/kubernetes.md — ex admin/installe.md (Talos + MetalLB + Traefik) - admin/README.md — séparation install/ vs référence opérationnelle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0bdfe49d75
commit
0ce6be04bb
5 changed files with 1125 additions and 1 deletions
22
admin/install/README.md
Normal file
22
admin/install/README.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Guides d'installation Funk
|
||||
|
||||
Guides d'installation complets, écrits après avoir réalisé chaque étape en réel.
|
||||
Chaque fichier couvre une machine ou une couche du projet de A à Z.
|
||||
|
||||
| Guide | Machine | OS | Rôle |
|
||||
|---|---|---|---|
|
||||
| [storage-01.md](storage-01.md) | storage-01 | AlmaLinux 9.7 | Passerelle + RAID5 + services IA (Hermes, LiteLLM, Qdrant, PostgreSQL) |
|
||||
| [gpu-01.md](gpu-01.md) | gpu-01 | AlmaLinux 9.7 | Inférence LLM (ROCm + llama-server, RX 6700XT) |
|
||||
| [kubernetes.md](kubernetes.md) | compute-01/02/03 | Talos v1.13 | Cluster Kubernetes (MetalLB, Traefik, DNS wildcard) |
|
||||
|
||||
## Ordre d'installation recommandé
|
||||
|
||||
```
|
||||
1. storage-01 ← indépendant, point d'entrée de tout le lab
|
||||
2. gpu-01 ← nécessite storage-01 (DNS, NAT, NFS)
|
||||
3. kubernetes ← nécessite storage-01 (DHCP, DNS, NFS, NAT)
|
||||
```
|
||||
|
||||
storage-01 est la pièce centrale : sans lui, rien d'autre ne fonctionne
|
||||
(pas de DHCP pour les nœuds Talos, pas de NAT pour sortir sur internet,
|
||||
pas de DNS `lab.local`, pas de NFS pour les modèles LLM).
|
||||
365
admin/install/gpu-01.md
Normal file
365
admin/install/gpu-01.md
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
# Installation gpu-01
|
||||
|
||||
Guide d'installation complet de gpu-01 — de l'ISO AlmaLinux 9.7 à llama-server
|
||||
en production avec ROCm sur RX 6700XT. Écrit après installation réelle.
|
||||
|
||||
---
|
||||
|
||||
## Sommaire
|
||||
|
||||
1. [Rôle et architecture](#1-rôle-et-architecture)
|
||||
2. [Installation AlmaLinux 9.7 (base OS)](#2-installation-almalinux-97-base-os)
|
||||
3. [Configuration réseau initiale (manuelle)](#3-configuration-réseau-initiale-manuelle)
|
||||
4. [Prérequis : storage-01 opérationnel](#4-prérequis--storage-01-opérationnel)
|
||||
5. [Jouer le playbook Ansible](#5-jouer-le-playbook-ansible)
|
||||
6. [Vérifications post-install](#6-vérifications-post-install)
|
||||
7. [Pièges et incidents rencontrés](#7-pièges-et-incidents-rencontrés)
|
||||
|
||||
---
|
||||
|
||||
## 1. Rôle et architecture
|
||||
|
||||
gpu-01 est le serveur d'inférence LLM du lab. Il n'est pas dans le cluster Kubernetes.
|
||||
Il est consommé par storage-01 (LiteLLM le pointe directement via HTTP).
|
||||
|
||||
### Ce qu'il fait
|
||||
|
||||
| Fonction | Détail |
|
||||
|---|---|
|
||||
| Inférence LLM | llama-server (llama.cpp) via ROCm 7.x sur RX 6700XT 12 GB VRAM |
|
||||
| Embeddings | `/v1/embeddings` OpenAI-compatible (`--embeddings --pooling mean`) |
|
||||
| NFS client | Monte `/srv/data/models` depuis storage-01 → symlink pour llama-server |
|
||||
|
||||
### Matériel
|
||||
|
||||
| Composant | Détail |
|
||||
|---|---|
|
||||
| CPU | AMD Ryzen 5 3600 (6c/12t) |
|
||||
| RAM | 32 GB DDR4 |
|
||||
| GPU | AMD RX 6700XT — 12 GB VRAM, architecture RDNA2 (gfx1031) |
|
||||
| OS | AlmaLinux 9.7 |
|
||||
| Disque | SSD SATA 500 GB (OS + binaires) |
|
||||
| Stockage modèles | NFS via storage-01 `/srv/data/models` → monté sur `/mnt/models` |
|
||||
|
||||
### Contrainte critique — RX 6700XT (gfx1031)
|
||||
|
||||
La RX 6700XT n'est **pas officiellement supportée** par ROCm. Son GFX ID est `gfx1031`.
|
||||
ROCm ne reconnaît que jusqu'à `gfx1030` dans la plupart des versions.
|
||||
|
||||
**Workaround permanent** dans tout service qui utilise le GPU :
|
||||
```
|
||||
HSA_OVERRIDE_GFX_VERSION=10.3.0
|
||||
```
|
||||
|
||||
Sans cette variable, ROCm ne voit pas le GPU et l'inférence tombe sur le CPU.
|
||||
|
||||
---
|
||||
|
||||
## 2. Installation AlmaLinux 9.7 (base OS)
|
||||
|
||||
Même ISO que storage-01 : AlmaLinux 9.7 minimal.
|
||||
|
||||
```bash
|
||||
sudo dd if=AlmaLinux-9.7-x86_64-minimal.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
||||
```
|
||||
|
||||
### Paramètres d'installation (Anaconda)
|
||||
|
||||
**Réseau** : gpu-01 est dans le LAN cluster — il n'a pas d'accès direct au LAN domestique.
|
||||
Pour accéder à internet pendant l'install (télécharger les packages ROCm), il faut que
|
||||
storage-01 soit déjà en ligne et fasse le NAT.
|
||||
|
||||
Configurer l'interface réseau :
|
||||
- IP statique : `192.168.10.20/24`
|
||||
- Gateway : `192.168.10.1` (storage-01)
|
||||
- DNS : `192.168.10.1`
|
||||
- Hostname : `gpu-01`
|
||||
|
||||
**Disque** : sélectionner le SSD SATA (pas le NVMe si présent — celui-ci peut être
|
||||
réservé à autre chose). Partitionnement automatique LVM convient.
|
||||
|
||||
**Utilisateur** : root avec mot de passe temporaire.
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration réseau initiale (manuelle)
|
||||
|
||||
Avant Ansible, vérifier que gpu-01 est joignable via storage-01 :
|
||||
|
||||
```bash
|
||||
# Depuis le poste perso via jump
|
||||
ssh -J ansible@192.168.1.200 root@192.168.10.20
|
||||
```
|
||||
|
||||
Depuis storage-01 :
|
||||
```bash
|
||||
ping 192.168.10.20 # doit répondre
|
||||
```
|
||||
|
||||
Si storage-01 n'a pas encore `192.168.10.20 gpu-01` dans `/etc/hosts` :
|
||||
```bash
|
||||
ssh s01 "echo '192.168.10.20 gpu-01' | sudo tee -a /etc/hosts"
|
||||
```
|
||||
|
||||
Vérifier que gpu-01 a accès à internet via storage-01 (NAT) :
|
||||
```bash
|
||||
ssh -J s01 root@192.168.10.20 "ping -c3 8.8.8.8"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Prérequis : storage-01 opérationnel
|
||||
|
||||
gpu-01 dépend de storage-01 pour :
|
||||
- **DNS** : `192.168.10.1` doit résoudre `lab.local`
|
||||
- **NAT** : pour télécharger les packages ROCm (~10 GB de téléchargements)
|
||||
- **NFS** : `/srv/data/models` doit être exporté pour monter les modèles
|
||||
|
||||
Vérifier avant de continuer :
|
||||
```bash
|
||||
ssh s01 "sudo systemctl is-active dnsmasq nftables nfs-server"
|
||||
# → active / active / active
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Jouer le playbook Ansible
|
||||
|
||||
Tout se lance depuis le poste perso à la racine du repo.
|
||||
|
||||
### Test de connectivité
|
||||
|
||||
```bash
|
||||
make ping
|
||||
# gpu-01 doit maintenant répondre
|
||||
```
|
||||
|
||||
Si gpu-01 n'est pas encore dans l'inventaire, il l'est déjà (`ansible/inventory.yml`) :
|
||||
```yaml
|
||||
gpu_hosts:
|
||||
hosts:
|
||||
gpu-01:
|
||||
ansible_host: 192.168.10.20
|
||||
ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o ProxyJump=root@192.168.1.200'
|
||||
```
|
||||
|
||||
La première exécution se fait en root. Après le rôle `common`, ce sera le compte `ansible`.
|
||||
|
||||
### Phase 1 — Base OS
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/gpu-01.yml --tags common
|
||||
```
|
||||
|
||||
Même chose que storage-01 : hostname, timezone, EPEL, user ansible, SSH durci.
|
||||
|
||||
### Phase 2 — ROCm
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/gpu-01.yml --tags rocm
|
||||
```
|
||||
|
||||
Le rôle `rocm` fait :
|
||||
- Ajoute le repo AMD officiel (`amdgpu-install`)
|
||||
- Installe ROCm 7.x (`rocm-hip-sdk`, `rocm-opencl-runtime`, `rocminfo`, `clinfo`)
|
||||
- Ajoute l'user `ansible` aux groupes `video` et `render` (accès GPU sans root)
|
||||
- Déploie `/etc/environment` et `/etc/profile.d/rocm.sh` avec `HSA_OVERRIDE_GFX_VERSION=10.3.0`
|
||||
- Reload udev pour les permissions GPU
|
||||
|
||||
> L'installation ROCm pèse environ 8-10 GB — prévoir 15-20 minutes selon la connexion.
|
||||
> gpu-01 télécharge via le NAT de storage-01.
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
ssh g01 "HSA_OVERRIDE_GFX_VERSION=10.3.0 rocminfo | grep -A3 'Agent 2'"
|
||||
# doit lister : AMD Radeon RX 6700 XT, gfx1031
|
||||
```
|
||||
|
||||
### Phase 3 — NFS client (modèles)
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/gpu-01.yml --tags nfs_client
|
||||
```
|
||||
|
||||
Le rôle `nfs_client` fait :
|
||||
- Installe `nfs-utils`, `autofs`
|
||||
- Configure les montages dans `/etc/fstab` (ou automount) :
|
||||
- `/mnt/nfs` ← `192.168.10.1:/srv/data/nfs/k8s`
|
||||
- `/mnt/models` ← `192.168.10.1:/srv/data/models`
|
||||
- Options : `nfsvers=4,soft,timeo=30,retrans=3,x-systemd.automount`
|
||||
- Crée le symlink `/opt/lmstudio/.lmstudio/models` → `/mnt/models`
|
||||
|
||||
> Les montages sont `soft` avec timeout : si storage-01 est éteint, gpu-01 boot
|
||||
> normalement et ne reste pas bloqué à attendre le NFS.
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
ssh g01 "df -h /mnt/models"
|
||||
ssh g01 "ls /mnt/models/"
|
||||
# → bartowski/Qwen3-8B-GGUF/ (ou autre modèle présent sur le RAID)
|
||||
```
|
||||
|
||||
### Phase 4 — llama-server (inférence ROCm)
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/gpu-01.yml --tags llama_server
|
||||
```
|
||||
|
||||
Le rôle `llama_server` fait :
|
||||
- Télécharge le binaire `llama-server` pré-compilé pour ROCm (release llama.cpp GitHub)
|
||||
- Déploie `/etc/systemd/system/llama-server.service` :
|
||||
- `HSA_OVERRIDE_GFX_VERSION=10.3.0` dans l'environnement
|
||||
- `--model /mnt/models/bartowski/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf`
|
||||
- `--alias qwen3-8b`
|
||||
- `--ctx-size 32768`
|
||||
- `--host 0.0.0.0 --port 1234`
|
||||
- `--embeddings --pooling mean` (pour `/v1/embeddings` OAI-compatible)
|
||||
- Active et démarre `llama-server.service`
|
||||
|
||||
Variables dans `host_vars/gpu-01/vars.yml` :
|
||||
```yaml
|
||||
llama_model_path: "/mnt/models/bartowski/Qwen3-8B-GGUF/Qwen3-8B-Q4_K_M.gguf"
|
||||
llama_model_alias: "qwen3-8b"
|
||||
llama_ctx_size: 32768
|
||||
```
|
||||
|
||||
> Avec un contexte de 32768 tokens, l'empreinte mémoire est :
|
||||
> ~5 GB modèle + ~3 GB KV cache = ~8 GB sur 12 GB VRAM.
|
||||
> Ajuster `llama_ctx_size` si un modèle plus lourd est utilisé.
|
||||
|
||||
---
|
||||
|
||||
## 6. Vérifications post-install
|
||||
|
||||
### Checklist
|
||||
|
||||
```bash
|
||||
# 1. GPU détecté par ROCm
|
||||
ssh g01 "HSA_OVERRIDE_GFX_VERSION=10.3.0 rocminfo | grep -E 'Agent|gfx'"
|
||||
|
||||
# 2. NFS monté
|
||||
ssh g01 "df -h /mnt/models && ls /mnt/models/"
|
||||
|
||||
# 3. llama-server actif
|
||||
ssh g01 "sudo systemctl is-active llama-server"
|
||||
ssh g01 "curl -s http://localhost:1234/v1/models | python3 -m json.tool"
|
||||
|
||||
# 4. Inférence fonctionnelle (test depuis storage-01)
|
||||
ssh s01 "curl -s http://192.168.10.20:1234/v1/chat/completions \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer lm-studio' \
|
||||
-d '{\"model\":\"qwen3-8b\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":10}'"
|
||||
|
||||
# 5. Embeddings fonctionnels
|
||||
ssh s01 "curl -s http://192.168.10.20:1234/v1/embeddings \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer lm-studio' \
|
||||
-d '{\"model\":\"qwen3-8b\",\"input\":\"test\"}' | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d[\"data\"][0][\"embedding\"]), \"dimensions\")'"
|
||||
# → 4096 dimensions
|
||||
```
|
||||
|
||||
### VRAM en cours d'inférence
|
||||
|
||||
```bash
|
||||
ssh g01 "watch -n 2 'cat /sys/class/drm/card*/device/mem_info_vram_used'"
|
||||
# Affiche la VRAM utilisée en octets — diviser par 1073741824 pour avoir des GB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Pièges et incidents rencontrés
|
||||
|
||||
---
|
||||
|
||||
### ❌ ROCm ne voit pas le GPU — HSA_OVERRIDE absent
|
||||
|
||||
**Symptôme** : `rocminfo` liste uniquement le CPU, pas le GPU RX 6700XT.
|
||||
llama-server démarre mais tourne sur le CPU → ~2 tok/s au lieu de ~35 tok/s.
|
||||
|
||||
**Cause** : La RX 6700XT est `gfx1031`, non officiel dans ROCm. Sans `HSA_OVERRIDE_GFX_VERSION=10.3.0`,
|
||||
ROCm ignore ce GPU.
|
||||
|
||||
**Fix** : La variable doit être présente dans :
|
||||
1. `/etc/environment` (sessions interactives)
|
||||
2. Le `[Service]` de `llama-server.service` (service systemd)
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
Environment=HSA_OVERRIDE_GFX_VERSION=10.3.0
|
||||
```
|
||||
|
||||
Géré par le rôle Ansible `llama_server`. Vérifier :
|
||||
```bash
|
||||
ssh g01 "sudo systemctl cat llama-server | grep HSA"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ❌ Gemma 4 — crash ROCm au-delà de 800 tokens
|
||||
|
||||
**Symptôme** : llama-server crash ou freeze dès que le contexte dépasse ~800 tokens
|
||||
avec les modèles Gemma 4.
|
||||
|
||||
**Cause** : Bug de compatibilité entre l'architecture Gemma 4 et le backend ROCm
|
||||
sur cette combinaison ROCm 7.x + gfx1031 (non officiel). Non reproduit avec Qwen3.
|
||||
|
||||
**Fix** : Utiliser Qwen3-8B ou Qwen2.5-14B. Ne pas charger de modèles Gemma 4 sur gpu-01.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Embeddings — erreur si --pooling absent
|
||||
|
||||
**Symptôme** : Requêtes vers `/v1/embeddings` retournent une erreur 500 ou des vecteurs vides.
|
||||
|
||||
**Cause** : llama-server nécessite `--embeddings` ET `--pooling mean` pour activer
|
||||
les embeddings OAI-compatible. `--embeddings` seul active le endpoint mais sans
|
||||
pooling les résultats sont incorrects.
|
||||
|
||||
**Fix** : Le service doit avoir les deux flags :
|
||||
```
|
||||
--embeddings --pooling mean
|
||||
```
|
||||
|
||||
Géré par le rôle Ansible `llama_server`.
|
||||
|
||||
---
|
||||
|
||||
### ❌ NFS bloquant au boot si storage-01 est éteint
|
||||
|
||||
**Symptôme** : gpu-01 boot très lentement (plusieurs minutes) ou reste bloqué en
|
||||
attendant les montages NFS si storage-01 n'est pas allumé.
|
||||
|
||||
**Cause** : Montages NFS en `hard` (comportement par défaut) — le client attend
|
||||
indéfiniment que le serveur réponde.
|
||||
|
||||
**Fix** : Options `soft,timeo=30,retrans=3` dans fstab/automount + `x-systemd.automount`.
|
||||
Avec `soft`, si storage-01 ne répond pas en 30 × timeo ms, le montage échoue proprement
|
||||
et gpu-01 continue à démarrer. Le montage sera réessayé au premier accès.
|
||||
|
||||
Géré par le rôle Ansible `nfs_client`.
|
||||
|
||||
---
|
||||
|
||||
### ❌ llama-server redémarre pas si modèle indisponible au boot
|
||||
|
||||
**Symptôme** : `llama-server.service` en `failed` au démarrage car `/mnt/models/...`
|
||||
n'est pas encore monté (NFS automount).
|
||||
|
||||
**Cause** : Conflit d'ordre de démarrage — llama-server démarre avant que
|
||||
le montage NFS soit déclenché.
|
||||
|
||||
**Fix** : `After=mnt-models.automount` dans le `.service`. Géré par Ansible.
|
||||
Ou simplement `sudo systemctl start llama-server` après boot.
|
||||
|
||||
---
|
||||
|
||||
### Résumé des points d'attention
|
||||
|
||||
| Sujet | Règle |
|
||||
|---|---|
|
||||
| HSA_OVERRIDE | `HSA_OVERRIDE_GFX_VERSION=10.3.0` dans tout service GPU et dans `/etc/environment` |
|
||||
| Gemma 4 | Interdit sur gpu-01 — crash ROCm >800 tokens |
|
||||
| Embeddings | `--embeddings --pooling mean` obligatoires ensemble |
|
||||
| Contexte 32k | ~8 GB VRAM sur 12 GB — ajuster si modèle plus lourd |
|
||||
| NFS boot | Options `soft,timeo=30` — jamais `hard` |
|
||||
| ROCm install | ~10 GB de téléchargement — prévoir le NAT storage-01 opérationnel |
|
||||
935
admin/install/kubernetes.md
Normal file
935
admin/install/kubernetes.md
Normal file
|
|
@ -0,0 +1,935 @@
|
|||
# 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` |
|
||||
728
admin/install/storage-01.md
Normal file
728
admin/install/storage-01.md
Normal file
|
|
@ -0,0 +1,728 @@
|
|||
# Installation storage-01
|
||||
|
||||
Guide d'installation complet de storage-01 — de l'ISO AlmaLinux à la stack IA
|
||||
complète (Hermes + LiteLLM + Qdrant + PostgreSQL). Écrit après installation réelle.
|
||||
|
||||
---
|
||||
|
||||
## Sommaire
|
||||
|
||||
1. [Rôle et architecture](#1-rôle-et-architecture)
|
||||
2. [Installation AlmaLinux 9.7 (base OS)](#2-installation-almalinux-97-base-os)
|
||||
3. [Configuration réseau initiale (manuelle)](#3-configuration-réseau-initiale-manuelle)
|
||||
4. [Préparer le poste perso pour Ansible](#4-préparer-le-poste-perso-pour-ansible)
|
||||
5. [Cloner le repo et initialiser Ansible](#5-cloner-le-repo-et-initialiser-ansible)
|
||||
6. [Configurer le vault Ansible (secrets)](#6-configurer-le-vault-ansible-secrets)
|
||||
7. [Jouer le playbook — phase par phase](#7-jouer-le-playbook--phase-par-phase)
|
||||
8. [Vérifications post-install](#8-vérifications-post-install)
|
||||
9. [Pièges et incidents rencontrés](#9-pièges-et-incidents-rencontrés)
|
||||
|
||||
---
|
||||
|
||||
## 1. Rôle et architecture
|
||||
|
||||
storage-01 est la machine centrale du lab. Elle n'est **pas dans le cluster Kubernetes**
|
||||
— elle est la fondation sur laquelle tout le reste repose.
|
||||
|
||||
### Ce qu'elle fait
|
||||
|
||||
| Fonction | Détail |
|
||||
|---|---|
|
||||
| Passerelle NAT | Forward LAN cluster → internet, LAN domestique → LAN cluster |
|
||||
| DHCP + DNS | dnsmasq pour le LAN cluster (`192.168.10.0/24`) et le wildcard `*.lab.local` |
|
||||
| Stockage RAID5 | 4 disques SATA 1 TB → 2,7 TB utiles, montés sur `/srv/data` |
|
||||
| NFS | Exports vers gpu-01 et le cluster k8s (`/srv/data/nfs/k8s`, `/srv/data/models`) |
|
||||
| Hermes Agent | Agent IA autonome avec mémoire vectorielle |
|
||||
| LiteLLM Proxy | Routage LLM : Qwen local (gpu-01) ↔ Claude API (Anthropic) |
|
||||
| Qdrant | Base vectorielle pour la mémoire de Hermes (`hermes_memory`) |
|
||||
| PostgreSQL 16 | Bases pour Hermes et LiteLLM |
|
||||
|
||||
### Interfaces réseau
|
||||
|
||||
| Interface | IP | Rôle |
|
||||
|---|---|---|
|
||||
| `enp4s0` | `192.168.1.200` | WAN — vers Freebox (LAN domestique `192.168.1.0/24`) |
|
||||
| `enp6s0f3u2c2` | `192.168.10.1` | LAN cluster (`192.168.10.0/24`) — carte USB-Ethernet |
|
||||
|
||||
> `enp6s0f3u2c2` est une carte USB-Ethernet branchée en permanence — les noms d'interface sont contre-intuitifs.
|
||||
|
||||
### RAID5
|
||||
|
||||
4 disques SATA 1 TB (Seagate/WD) assemblés en RAID5 software Linux (`md0` ou `md127`).
|
||||
- Capacité utile : ~2,7 TB
|
||||
- Tolérance : 1 disque mort
|
||||
- Point de montage : `/srv/data`
|
||||
- ⚠️ `sdb` a généré des erreurs I/O après une coupure de courant — surveiller avec `smartctl`
|
||||
|
||||
---
|
||||
|
||||
## 2. Installation AlmaLinux 9.7 (base OS)
|
||||
|
||||
### Télécharger l'ISO
|
||||
|
||||
AlmaLinux 9.7 minimal (pas de desktop) :
|
||||
`https://repo.almalinux.org/almalinux/9/isos/x86_64/`
|
||||
|
||||
Flasher sur clé USB :
|
||||
```bash
|
||||
sudo dd if=AlmaLinux-9.7-x86_64-minimal.iso of=/dev/sdX bs=4M status=progress conv=fsync
|
||||
```
|
||||
|
||||
### Paramètres d'installation (Anaconda)
|
||||
|
||||
Dans l'installeur graphique Anaconda :
|
||||
|
||||
**Langue / clavier** : Français ou English — peu importe, Ansible configure le reste.
|
||||
|
||||
**Disque système** : sélectionner le NVMe interne uniquement.
|
||||
- Ne pas sélectionner les disques SATA (RAID5) — ils sont gérés manuellement.
|
||||
- Partitionnement automatique convient (LVM sur NVMe).
|
||||
|
||||
**Réseau** : configurer uniquement l'interface WAN (`enp4s0`) pendant l'install :
|
||||
- IP statique : `192.168.1.200/24`
|
||||
- Gateway : `192.168.1.254` (Freebox)
|
||||
- DNS : `1.1.1.1`
|
||||
- Hostname : `storage-01`
|
||||
|
||||
L'interface LAN (`enp6s0f3u2c2`) sera configurée par Ansible.
|
||||
|
||||
**Utilisateurs** :
|
||||
- Root : définir un mot de passe temporaire (sera désactivé par Ansible)
|
||||
- Créer un premier utilisateur `ansible` (ou laisser Ansible le créer — dans ce cas se connecter en root pour la première exécution)
|
||||
|
||||
### Premier boot
|
||||
|
||||
Après l'installation, vérifier que la machine est joignable depuis le poste perso :
|
||||
```bash
|
||||
ping 192.168.1.200
|
||||
ssh root@192.168.1.200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration réseau initiale (manuelle)
|
||||
|
||||
Avant de lancer Ansible, il faut que storage-01 soit SSH-accessible et que les deux
|
||||
interfaces réseau soient opérationnelles. Ces opérations sont faites une seule fois
|
||||
depuis la console ou root SSH.
|
||||
|
||||
### Configurer l'interface LAN (USB-Ethernet)
|
||||
|
||||
```bash
|
||||
# Se connecter en root
|
||||
ssh root@192.168.1.200
|
||||
|
||||
# Identifier la carte USB-Ethernet (généralement enp6s0f3u2c2 ou similaire)
|
||||
nmcli device status
|
||||
ip link show
|
||||
|
||||
# Créer la connexion NetworkManager
|
||||
nmcli connection add type ethernet \
|
||||
ifname enp6s0f3u2c2 \
|
||||
con-name lan-cluster \
|
||||
ipv4.method manual \
|
||||
ipv4.addresses 192.168.10.1/24 \
|
||||
ipv4.never-default yes \
|
||||
connection.autoconnect yes
|
||||
|
||||
nmcli connection up lan-cluster
|
||||
```
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
ip addr show enp6s0f3u2c2
|
||||
# → inet 192.168.10.1/24
|
||||
```
|
||||
|
||||
### Activer l'IP forwarding
|
||||
|
||||
```bash
|
||||
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.d/99-forwarding.conf
|
||||
sysctl -p /etc/sysctl.d/99-forwarding.conf
|
||||
```
|
||||
|
||||
> Ansible gère aussi l'IP forwarding, mais l'activer maintenant permet aux nœuds
|
||||
> Talos de sortir sur internet dès que dnsmasq et nftables seront en place.
|
||||
|
||||
### Ajouter gpu-01 dans /etc/hosts (si gpu-01 sera installé avant dnsmasq)
|
||||
|
||||
```bash
|
||||
echo '192.168.10.20 gpu-01' >> /etc/hosts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Préparer le poste perso pour Ansible
|
||||
|
||||
Tout Ansible se lance depuis le **poste perso** (ou storage-01 une fois qu'il est
|
||||
opérationnel — voir section suivante).
|
||||
|
||||
### Installer Python et créer le venv
|
||||
|
||||
```bash
|
||||
# Python 3 doit être disponible
|
||||
python3 --version
|
||||
|
||||
# Créer le venv à la racine du repo
|
||||
cd /mnt/nvme0n1/nvme0n1p3/Projets/lab
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# Installer ansible-core
|
||||
pip install -r requirements.txt
|
||||
# ansible-core>=2.17,<2.18
|
||||
```
|
||||
|
||||
### Installer les collections Ansible
|
||||
|
||||
```bash
|
||||
.venv/bin/ansible-galaxy collection install -r ansible/requirements.yml
|
||||
```
|
||||
|
||||
### Générer une clé SSH pour Ansible
|
||||
|
||||
```bash
|
||||
# Si pas encore de clé ed25519
|
||||
ssh-keygen -t ed25519 -C "ansible@poste-perso" -f ~/.ssh/id_ed25519
|
||||
|
||||
# Copier la clé publique vers storage-01
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@192.168.1.200
|
||||
```
|
||||
|
||||
Ansible utilisera le compte `ansible` (créé par le rôle `common`) — mais pour la
|
||||
première exécution, il se connecte en root.
|
||||
|
||||
### Configurer ~/.ssh/config
|
||||
|
||||
```
|
||||
Host s01
|
||||
HostName 192.168.1.200
|
||||
User ansible
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
|
||||
Host g01
|
||||
HostName 192.168.10.20
|
||||
User ansible
|
||||
ProxyJump s01
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
StrictHostKeyChecking no
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Cloner le repo et initialiser Ansible
|
||||
|
||||
```bash
|
||||
git clone git@github.com:Alkatrazz24/Funk-lab.git
|
||||
cd Funk-lab
|
||||
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
.venv/bin/ansible-galaxy collection install -r ansible/requirements.yml
|
||||
```
|
||||
|
||||
### Structure Ansible du repo
|
||||
|
||||
```
|
||||
ansible/
|
||||
├── inventory.yml # hosts + groupes
|
||||
├── playbooks/
|
||||
│ ├── site.yml # import storage-01.yml + gpu-01.yml
|
||||
│ ├── storage-01.yml # playbook storage-01
|
||||
│ └── gpu-01.yml # playbook gpu-01
|
||||
├── group_vars/
|
||||
│ ├── all/
|
||||
│ │ ├── vars.yml # variables globales (dns_domain, cluster_network...)
|
||||
│ │ └── vault.yml # secrets chiffrés (Ansible Vault)
|
||||
│ └── gateway/
|
||||
│ └── vars.yml # interfaces réseau storage-01
|
||||
├── host_vars/
|
||||
│ ├── storage-01/
|
||||
│ │ └── vars.yml # RAID uuid, NFS exports, chemins services
|
||||
│ └── gpu-01/
|
||||
│ └── vars.yml # modèle llama-server, montages NFS
|
||||
└── roles/
|
||||
├── common/ # hostname, timezone, EPEL, user ansible, SSH hardenin
|
||||
├── gateway/ # nftables + NAT + IP forwarding
|
||||
├── dnsmasq/ # DNS lab.local + DHCP cluster
|
||||
├── nfs_server/ # RAID5 fstab + exports NFS
|
||||
├── postgresql/ # PostgreSQL 16 sur RAID5
|
||||
├── qdrant/ # Qdrant vectordb sur RAID5
|
||||
├── litellm/ # LiteLLM proxy + hermes-switch
|
||||
├── hermes_agent/ # Hermes Agent + Dashboard
|
||||
├── rocm/ # ROCm AMD pour gpu-01
|
||||
├── llama_server/ # llama-server sur gpu-01
|
||||
└── nfs_client/ # montages NFS automatiques (gpu-01)
|
||||
```
|
||||
|
||||
### Variables globales (`group_vars/all/vars.yml`)
|
||||
|
||||
```yaml
|
||||
dns_domain: lab.local
|
||||
ntp_servers:
|
||||
- 0.fr.pool.ntp.org
|
||||
- 1.fr.pool.ntp.org
|
||||
cluster_network: 192.168.10.0/24
|
||||
wan_gateway: 192.168.1.254
|
||||
```
|
||||
|
||||
### Variables gateway (`group_vars/gateway/vars.yml`)
|
||||
|
||||
```yaml
|
||||
wan_interface: enp4s0 # NIC PCIe — vers Freebox
|
||||
lan_interface: enp6s0f3u2c2 # NIC USB — vers LAN cluster
|
||||
wan_ip: 192.168.1.200
|
||||
lan_ip: 192.168.10.1
|
||||
```
|
||||
|
||||
### Variables storage-01 (`host_vars/storage-01/vars.yml`)
|
||||
|
||||
```yaml
|
||||
ansible_host: 192.168.1.200
|
||||
raid_uuid: "3add8360-fa01-47eb-8aa1-0e84bacbbc15"
|
||||
raid_mount: /srv/data
|
||||
nfs_exports:
|
||||
- path: /srv/data/nfs/k8s
|
||||
network: "{{ cluster_network }}"
|
||||
options: "rw,sync,no_subtree_check,no_root_squash"
|
||||
- path: /srv/data/models
|
||||
network: "{{ cluster_network }}"
|
||||
options: "rw,sync,no_subtree_check,no_root_squash"
|
||||
hermes_data_dir: /srv/data/hermes
|
||||
postgres_data_dir: /srv/data/postgres
|
||||
qdrant_data_dir: /srv/data/qdrant
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Configurer le vault Ansible (secrets)
|
||||
|
||||
Le vault contient les mots de passe et l'API key Anthropic. Il est chiffré dans git.
|
||||
|
||||
### Fichier mot de passe vault
|
||||
|
||||
```bash
|
||||
# Créer le fichier de mot de passe (ne jamais le committer — dans .gitignore)
|
||||
echo "ton-mot-de-passe-vault" > .vault_pass
|
||||
chmod 600 .vault_pass
|
||||
```
|
||||
|
||||
`ansible.cfg` pointe automatiquement dessus :
|
||||
```ini
|
||||
[defaults]
|
||||
vault_password_file = .vault_pass
|
||||
```
|
||||
|
||||
### Créer le vault avec les secrets
|
||||
|
||||
```bash
|
||||
cd ansible/
|
||||
../.venv/bin/ansible-vault create group_vars/all/vault.yml
|
||||
```
|
||||
|
||||
Contenu du vault :
|
||||
```yaml
|
||||
vault_anthropic_api_key: "sk-ant-..."
|
||||
vault_pg_hermes_password: "mot-de-passe-fort"
|
||||
vault_pg_litellm_password: "mot-de-passe-fort"
|
||||
```
|
||||
|
||||
Pour modifier plus tard :
|
||||
```bash
|
||||
make vault-edit
|
||||
# ou
|
||||
cd ansible && ../.venv/bin/ansible-vault edit group_vars/all/vault.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Jouer le playbook — phase par phase
|
||||
|
||||
Il est recommandé d'appliquer les rôles dans l'ordre ci-dessous, par petits
|
||||
blocs, pour valider chaque couche avant de passer à la suivante.
|
||||
|
||||
### Test de connectivité
|
||||
|
||||
```bash
|
||||
make ping
|
||||
# ou
|
||||
cd ansible && ../.venv/bin/ansible all -m ping
|
||||
```
|
||||
|
||||
Sortie attendue :
|
||||
```
|
||||
storage-01 | SUCCESS => {"ping": "pong"}
|
||||
```
|
||||
|
||||
> gpu-01 échouera si pas encore installé — normal.
|
||||
|
||||
### Phase 1 — Base OS
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags common
|
||||
```
|
||||
|
||||
Le rôle `common` fait :
|
||||
- Hostname → `storage-01`
|
||||
- Timezone → Europe/Paris
|
||||
- EPEL activé
|
||||
- `dnf update -y` (tous les packages à jour)
|
||||
- Packages de base : `vim`, `curl`, `wget`, `git`, `htop`, `tmux`, `rsync`, `lsof`, `net-tools`, `bind-utils`
|
||||
- Création user `ansible` avec sudo NOPASSWD
|
||||
- Clé SSH publique du poste perso déposée dans `~/.ssh/authorized_keys`
|
||||
- SSH durci : `PermitRootLogin no`, `PasswordAuthentication no`, `X11Forwarding no`
|
||||
|
||||
Après cette étape, les connexions root ne fonctionnent plus — utiliser `ssh s01`.
|
||||
|
||||
### Phase 2 — Passerelle (nftables + NAT)
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags gateway
|
||||
```
|
||||
|
||||
Le rôle `gateway` fait :
|
||||
- Déploie `/etc/nftables.conf` depuis le template (flush + rebuild complet)
|
||||
- Active IP forwarding (`sysctl net.ipv4.ip_forward=1`)
|
||||
- Active et redémarre `nftables`
|
||||
|
||||
Vérifier que le NAT fonctionne depuis le LAN cluster :
|
||||
```bash
|
||||
# Depuis une machine du cluster (si déjà installée)
|
||||
ssh 192.168.10.11 "curl -s https://ifconfig.me"
|
||||
```
|
||||
|
||||
### Phase 3 — DNS + DHCP (dnsmasq)
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags dnsmasq
|
||||
```
|
||||
|
||||
Le rôle `dnsmasq` fait :
|
||||
- Installe `dnsmasq`
|
||||
- Déploie la config depuis template : DNS `lab.local`, DHCP `192.168.10.50-99`, baux statiques MACs
|
||||
- Wildcard `*.lab.local → 192.168.10.200` (Traefik)
|
||||
- Override systemd : dnsmasq attend que l'interface USB-Ethernet soit prête avant de démarrer
|
||||
|
||||
Tester depuis le poste perso :
|
||||
```bash
|
||||
dig @192.168.10.1 storage-01.lab.local +short
|
||||
# → 192.168.10.1
|
||||
|
||||
dig @192.168.10.1 test.lab.local +short
|
||||
# → 192.168.10.200 (wildcard)
|
||||
```
|
||||
|
||||
> Sans l'override systemd, dnsmasq démarre avant que `enp6s0f3u2c2` (USB-Ethernet)
|
||||
> soit disponible → `bind(): Address not available` → service en échec au boot.
|
||||
|
||||
### Phase 4 — NFS + RAID5
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags nfs_server
|
||||
```
|
||||
|
||||
Le rôle `nfs_server` fait :
|
||||
- Installe `nfs-utils` et `mdadm`
|
||||
- Persiste la config RAID dans `/etc/mdadm.conf` (pour survie aux reboots)
|
||||
- Ajoute `/srv/data` dans `/etc/fstab` avec `_netdev 0 0`
|
||||
- Crée les répertoires d'export (`/srv/data/nfs/k8s`, `/srv/data/models`)
|
||||
- Déploie `/etc/exports` et recharge `nfs-server`
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
ssh s01 "cat /proc/mdstat" # RAID doit être [4/4] [UUUU]
|
||||
ssh s01 "sudo exportfs -v" # doit lister les deux exports
|
||||
ssh s01 "df -h /srv/data" # doit afficher ~2.7 TB
|
||||
```
|
||||
|
||||
> **UUID du RAID** : récupérer avec `sudo blkid | grep md` sur storage-01 et
|
||||
> mettre à jour `host_vars/storage-01/vars.yml` avant de jouer ce rôle.
|
||||
|
||||
### Phase 5 — PostgreSQL 16
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags postgresql
|
||||
```
|
||||
|
||||
Le rôle `postgresql` fait :
|
||||
- Installe le repo officiel PGDG (EL9)
|
||||
- Désactive le module AppStream par défaut (`dnf module disable postgresql`)
|
||||
- Installe `postgresql16-server` et `python3-psycopg2`
|
||||
- Crée `/srv/data/postgres` sur le RAID5 et le configure comme `PGDATA`
|
||||
- Override systemd pour pointer sur le bon `PGDATA`
|
||||
- `initdb` si la base n'existe pas encore
|
||||
- Crée les bases `hermes` et `litellm` et leurs users avec les mots de passe du vault
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
ssh s01 "sudo -u postgres psql -c '\l'"
|
||||
# doit lister : hermes, litellm, postgres, template0, template1
|
||||
```
|
||||
|
||||
### Phase 6 — Qdrant
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags qdrant
|
||||
```
|
||||
|
||||
Le rôle `qdrant` fait :
|
||||
- Télécharge le binaire Qdrant (release GitHub)
|
||||
- Crée un user système `qdrant`
|
||||
- Configure le stockage dans `/srv/data/qdrant`
|
||||
- Déploie `qdrant.service` (bind `0.0.0.0:6333`, gRPC `6334`)
|
||||
- Active et démarre le service
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
ssh s01 "curl -s http://localhost:6333/healthz"
|
||||
# → {"result":"ok","status":"ok","time":0.00...}
|
||||
```
|
||||
|
||||
### Phase 7 — LiteLLM Proxy
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags litellm
|
||||
```
|
||||
|
||||
Le rôle `litellm` fait :
|
||||
- Installe `litellm` via pip dans un venv dédié
|
||||
- Déploie `/etc/litellm/config.yaml` avec les modèles (Qwen local + Claude)
|
||||
- Configure `master_key: lm-studio` (aligné avec `LM_API_KEY=lm-studio` dans Hermes)
|
||||
- Déploie `litellm.service` (bind `127.0.0.1:4000` — jamais exposé sur le réseau)
|
||||
- Déploie le script `hermes-switch` dans `/usr/local/bin/`
|
||||
|
||||
Config LiteLLM (résumé) :
|
||||
```yaml
|
||||
model_list:
|
||||
- model_name: hermes-default
|
||||
litellm_params:
|
||||
model: openai/qwen3-8b
|
||||
api_base: http://192.168.10.20:1234/v1
|
||||
api_key: lm-studio
|
||||
|
||||
- model_name: claude-sonnet-4-6
|
||||
litellm_params:
|
||||
model: anthropic/claude-sonnet-4-6
|
||||
api_key: "{{ vault_anthropic_api_key }}"
|
||||
|
||||
general_settings:
|
||||
master_key: lm-studio
|
||||
```
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
ssh s01 "curl -s http://localhost:4000/health -H 'Authorization: Bearer lm-studio'"
|
||||
```
|
||||
|
||||
### Phase 8 — Hermes Agent
|
||||
|
||||
```bash
|
||||
cd ansible && ../.venv/bin/ansible-playbook playbooks/storage-01.yml --tags hermes_agent
|
||||
```
|
||||
|
||||
Le rôle `hermes_agent` fait :
|
||||
- Crée un user système `hermes` avec home sur le RAID5 (`/srv/data/hermes`)
|
||||
- Installe Hermes via son installer officiel dans `/opt/hermes`
|
||||
- Crée le venv Python (`/srv/data/hermes/hermes-agent/venv/`) avec `qdrant-client`
|
||||
- Déploie `~hermes/.config/hermes/config.yaml` :
|
||||
- `model.provider: lmstudio`
|
||||
- `model.base_url: http://127.0.0.1:4000/v1`
|
||||
- `model.name: hermes-default`
|
||||
- Déploie `hermes-agent.service` et `hermes-dashboard.service`
|
||||
- Déploie `/usr/local/bin/hermes-tui` (wrapper pour TUI interactif)
|
||||
- Permissions `/opt/hermes` → `711` (traversable, non listable)
|
||||
|
||||
Vérifier :
|
||||
```bash
|
||||
ssh s01 "sudo systemctl status hermes-agent hermes-dashboard"
|
||||
```
|
||||
|
||||
Dashboard accessible depuis le poste perso (restreint à `192.168.1.10`) :
|
||||
```
|
||||
http://192.168.1.200:9119
|
||||
```
|
||||
|
||||
TUI interactif (doit être lancé sous le compte hermes) :
|
||||
```bash
|
||||
ssh storage-01
|
||||
sudo -i -u hermes
|
||||
cd /srv/data/hermes
|
||||
hermes --tui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Vérifications post-install
|
||||
|
||||
### Checklist complète
|
||||
|
||||
```bash
|
||||
# 1. Réseau
|
||||
ping 192.168.10.20 # gpu-01 joignable
|
||||
ssh g01 "curl -s https://ifconfig.me" # NAT fonctionnel
|
||||
dig @192.168.10.1 storage-01.lab.local +short # DNS lab.local
|
||||
|
||||
# 2. RAID
|
||||
ssh s01 "cat /proc/mdstat" # [4/4] [UUUU]
|
||||
ssh s01 "df -h /srv/data" # ~2.7 TB monté
|
||||
|
||||
# 3. NFS
|
||||
ssh s01 "sudo exportfs -v" # exports actifs
|
||||
|
||||
# 4. PostgreSQL
|
||||
ssh s01 "sudo -u postgres psql -c '\l'" # bases hermes + litellm présentes
|
||||
|
||||
# 5. Qdrant
|
||||
ssh s01 "curl -s http://localhost:6333/healthz" | python3 -m json.tool
|
||||
|
||||
# 6. LiteLLM
|
||||
ssh s01 "curl -s http://localhost:4000/health -H 'Authorization: Bearer lm-studio'"
|
||||
|
||||
# 7. Hermes
|
||||
ssh s01 "sudo systemctl is-active hermes-agent hermes-dashboard"
|
||||
|
||||
# 8. funk-cluster (script de gestion global)
|
||||
ssh s01 "sudo /usr/local/bin/funk-cluster status"
|
||||
```
|
||||
|
||||
### Services actifs attendus
|
||||
|
||||
```bash
|
||||
ssh s01 "sudo systemctl list-units --type=service --state=running | grep -E 'dnsmasq|nfs|postgres|qdrant|litellm|hermes|nftables'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Pièges et incidents rencontrés
|
||||
|
||||
---
|
||||
|
||||
### ❌ dnsmasq ne démarre pas au boot — "Address not available"
|
||||
|
||||
**Symptôme** : `systemctl status dnsmasq` → `bind(): Address not available`.
|
||||
Storage-01 rebooté → dnsmasq en `failed`, plus de DHCP pour le cluster.
|
||||
|
||||
**Cause** : dnsmasq tentait de binder sur `192.168.10.1` (`enp6s0f3u2c2`, USB-Ethernet)
|
||||
avant que cette interface soit disponible. L'USB-Ethernet est détectée plus tard que
|
||||
les NIC PCIe dans la séquence de boot.
|
||||
|
||||
**Fix** : Un override systemd dans `/etc/systemd/system/dnsmasq.service.d/wait-lan.conf` :
|
||||
```ini
|
||||
[Unit]
|
||||
After=network-online.target sys-subsystem-net-devices-enp6s0f3u2c2.device
|
||||
Wants=network-online.target sys-subsystem-net-devices-enp6s0f3u2c2.device
|
||||
```
|
||||
|
||||
Géré par le rôle Ansible `dnsmasq` — ne pas modifier manuellement.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Boucle fsck au boot après coupure de courant
|
||||
|
||||
**Symptôme** : Boot bloqué sur `emergency mode`, impossible de saisir le mot de passe root
|
||||
(messages d'erreur défilants), le RAID `/dev/md0` ne monte plus.
|
||||
|
||||
**Cause** : Coupure de courant → corruption du filesystem XFS sur `almalinux-root` (NVMe).
|
||||
systemd tentait de monter `/srv/data` (RAID5) avant que le RAID soit assemblé, créant
|
||||
une boucle bloquante.
|
||||
|
||||
**Résolution** (procédure complète dans `admin/incidents.md`) :
|
||||
1. Shell via GRUB (`init=/bin/bash` dans la ligne kernel)
|
||||
2. `lvm vgchange -ay` pour activer le LVM
|
||||
3. `xfs_repair /dev/mapper/almalinux-root` (pas `fsck` — XFS a son propre outil)
|
||||
4. Commenter `/srv/data` dans `/etc/fstab` temporairement pour booter
|
||||
5. Après boot : corriger fstab → ajouter `_netdev 0 0` pour `/srv/data`
|
||||
|
||||
**Fix permanent dans fstab** :
|
||||
```
|
||||
UUID=3add8360-... /srv/data ext4 defaults,noatime,nodiratime,_netdev 0 0
|
||||
```
|
||||
- `_netdev` : attend que les périphériques soient prêts (RAID assemblé) avant de monter
|
||||
- `0 0` : désactive le `fsck` automatique au boot (le RAID a sa propre intégrité)
|
||||
|
||||
Géré par le rôle Ansible `nfs_server`. Vérifier que ces options sont bien présentes.
|
||||
|
||||
---
|
||||
|
||||
### ❌ hermes --tui "gateway error" si lancé sous le mauvais compte
|
||||
|
||||
**Symptôme** : `hermes --tui` lancé depuis le compte `ansible` retourne des "gateway error"
|
||||
en boucle. TUI inutilisable.
|
||||
|
||||
**Cause** : Hermes charge sa configuration depuis `$HERMES_HOME`, qui doit pointer vers
|
||||
`/srv/data/hermes`. Lancé sous `ansible`, les variables d'environnement ne sont pas
|
||||
correctes et `hermes` cherche sa config dans `~ansible/.config/hermes/` qui n'existe pas.
|
||||
|
||||
**Fix** : Toujours lancer le TUI sous le compte `hermes` :
|
||||
```bash
|
||||
ssh storage-01
|
||||
sudo -i -u hermes
|
||||
cd /srv/data/hermes
|
||||
hermes --tui
|
||||
```
|
||||
|
||||
Ou via le wrapper `/usr/local/bin/hermes-tui` qui gère automatiquement l'environnement.
|
||||
|
||||
---
|
||||
|
||||
### ❌ qdrant-client : méthode search() supprimée en v1.14+
|
||||
|
||||
**Symptôme** : Scripts Python Hermes qui appellent Qdrant échouent avec
|
||||
`AttributeError: 'AsyncQdrantClient' object has no attribute 'search'`.
|
||||
|
||||
**Cause** : `qdrant-client` v1.14+ a supprimé la méthode `search()`.
|
||||
Elle est remplacée par `query_points()`.
|
||||
|
||||
**Fix** : Mettre à jour les scripts pour utiliser `query_points()`.
|
||||
Le rôle Ansible `hermes_agent` installe la version compatible — ne pas faire
|
||||
`pip install qdrant-client` manuellement sans vérifier la version.
|
||||
|
||||
---
|
||||
|
||||
### ❌ LM_API_KEY absent → Hermes ne peut pas appeler LiteLLM
|
||||
|
||||
**Symptôme** : Hermes démarre mais toutes les requêtes vers LiteLLM échouent avec 401.
|
||||
|
||||
**Cause** : Hermes utilise `LM_API_KEY` comme Bearer token. LiteLLM attend `master_key`.
|
||||
Les deux doivent avoir la même valeur. Si `LM_API_KEY` n'est pas défini dans
|
||||
l'environnement du service systemd, Hermes envoie un token vide ou incorrect.
|
||||
|
||||
**Fix** : Le service `hermes-agent.service` doit avoir dans sa section `[Service]` :
|
||||
```ini
|
||||
Environment=LM_API_KEY=lm-studio
|
||||
```
|
||||
|
||||
La valeur `lm-studio` est une valeur magique alignée avec `master_key: lm-studio`
|
||||
dans la config LiteLLM. Géré par le rôle Ansible.
|
||||
|
||||
---
|
||||
|
||||
### ❌ hermes -z ne fonctionne pas depuis n'importe où
|
||||
|
||||
**Symptôme** : `hermes -z "question"` depuis `/home/ansible` → erreur de chemin,
|
||||
Hermes ne trouve pas son `.git`.
|
||||
|
||||
**Cause** : Hermes remonte l'arborescence à la recherche d'un `.git` pour localiser
|
||||
sa config de projet. Si lancé depuis un répertoire sans `.git` en amont, il échoue.
|
||||
|
||||
**Fix** : Toujours lancer depuis `/srv/data/hermes` :
|
||||
```bash
|
||||
cd /srv/data/hermes && hermes -z "ta question"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Résumé des points d'attention
|
||||
|
||||
| Sujet | Règle |
|
||||
|---|---|
|
||||
| dnsmasq au boot | Override systemd obligatoire pour USB-Ethernet |
|
||||
| fstab `/srv/data` | `_netdev 0 0` — jamais `0 2` |
|
||||
| TUI Hermes | Toujours sous le compte `hermes`, depuis `/srv/data/hermes` |
|
||||
| `hermes -z` | Toujours depuis `/srv/data/hermes` |
|
||||
| LM_API_KEY | Doit valoir `lm-studio` (= `master_key` LiteLLM) |
|
||||
| qdrant-client | Utiliser `query_points()` pas `search()` (supprimé en v1.14+) |
|
||||
| RAID sdb | Surveiller régulièrement avec `smartctl -a /dev/sdb` |
|
||||
| UUID RAID | Vérifier `host_vars/storage-01/vars.yml` avant de jouer `nfs_server` |
|
||||
| Vault | `.vault_pass` ne doit jamais être commité — dans `.gitignore` |
|
||||
Loading…
Add table
Add a link
Reference in a new issue