Chaque étape est une page : choisissez-la dans le menu, ou cherchez une commande, un nom de nœud ou un message d'erreur avec la barre de recherche (touche /). Les étapes se suivent dans l'ordre : chacune s'appuie sur les précédentes. Un encadré « En bref » ouvre chaque page pour expliquer à quoi sert l'étape.
ℹ️ Information utile pour comprendre ce qui se passe.
⚠️ Piège ou point d'attention : à lire avant d'exécuter la commande.
Chaque bloc de code possède un bouton Copier. Les commandes et les fichiers de configuration ont été testés : recopiez-les tels quels.
ℹ️ HAProxy tourne sur un serveur physique dédié, séparé des masters Kubernetes. Ce serveur est le point d'entrée unique pour tout le trafic entrant (applicatif, API K8s, BDD, WireGuard).
0.1 — Architecture du Load Balancer
⚠️ WireGuard (51821/UDP) non géré par HAProxy — le mode UDP n'est pas supporté par les images officielles HAProxy. WireGuard ne passe donc pas par le LB : chaque peer se connecte directement à l'IP publique du master concerné.
Internet
│
▼
Serveur HAProxy dédié
IP publique : <PUBLIC_IP_HAPROXY>
IP WG100 : 192.168.0.254
│
├── :6443/TCP → API Kubernetes (192.168.0.1/2)
├── :80/TCP → NGINX Ingress HTTP (192.168.1.1/2/3:30080)
├── :443/TCP → NGINX Ingress HTTPS (192.168.1.1/2/3:30443)
├── :3306/TCP → MySQL ProxySQL (192.168.1.1/2/3:NodePort)
├── :5432/TCP → PostgreSQL PGBouncer (192.168.1.1/2/3:NodePort)
└── :5433/TCP → PostgreSQL direct (192.168.1.1/2/3:NodePort)
0.2 — Structure des dossiers
bash
# Créer la structure# Créer les répertoires de configuration et de logs HAProxy
mkdir-p/opt/xsel-haproxy/config
mkdir-p/opt/xsel-haproxy/logs
# Arborescence finale# /opt/xsel-haproxy/# ├── docker-compose.yml# ├── config/# │ └── haproxy.cfg# └── logs/
0.2.1 — Configuration du Firewall
bash
# Ouvrir tous les ports nécessaires au LB
firewall-cmd--add-port=80/tcp--permanent# HTTP applicatif
firewall-cmd--add-port=443/tcp--permanent# HTTPS applicatif
firewall-cmd--add-port=6443/tcp--permanent# API Kubernetes
firewall-cmd--add-port=51821/udp--permanent# WireGuard VPN (futur)
firewall-cmd--add-port=3306/tcp--permanent# MySQL externe
firewall-cmd--add-port=5432/tcp--permanent# PostgreSQL PGBouncer
firewall-cmd--add-port=5433/tcp--permanent# PostgreSQL direct
firewall-cmd--add-port=8404/tcp--permanent# Stats HAProxy# Appliquer toutes les règles de firewall en persistance
firewall-cmd--reload
# Vérifier# Vérifier que tous les ports sont bien ouverts
firewall-cmd--list-ports
ℹ️ Le port 51820/udp est WireGuard WG0 (réseau OM) — déjà ouvert par l'équipe infrastructure. Le port 51821/udp est WireGuard WG100 (réseau Nodes) — à ouvrir pour les futurs clients.
0.2.2 — Ajouter l'interface WireGuard WG100 à la zone trusted
⚠️ L'interface wg100 n'est par défaut dans aucune zone firewalld. Sans cette étape, les connexions TCP depuis les nodes Kubernetes sont rejetées avec No route to host malgré un ping fonctionnel.
bash
# Ajouter wg100 à la zone trusted# Ajouter l'interface WireGuard WG100 à la zone trusted pour autoriser le trafic Kubernetes
firewall-cmd--zone=trusted--add-interface=wg100--permanent
# Appliquer toutes les règles de firewall en persistance
firewall-cmd--reload
# Vérifier# Afficher toutes les règles de la zone active
firewall-cmd--list-all
# Vérifier que wg100 est bien dans la zone trusted
firewall-cmd--zone=trusted--list-all
# Tester depuis un master# Tester la connectivité vers l'API Kubernetes via HAProxy
curl-khttps://192.168.0.254:64432>&1|head-3
# Attendu : SSL_ERROR_ZERO_RETURN (normal si cluster pas encore initialisé)# ou réponse JSON si cluster déjà up
ℹ️ SSL_ERROR_ZERO_RETURN est normal — HAProxy répond mais ferme la connexion car aucun backend (master) n'est encore disponible. C'est le comportement attendu avant l'initialisation du cluster.
0.3 — Docker Compose
bash
# Vérifier si Docker est déjà installé# Vérifier si Docker est déjà présent sur le système
docker--version2>/dev/null&&echo"✅ Docker déjà installé"||echo"❌ Docker absent"# Installer Docker si absentif!command-vdocker&>/dev/null;thendnfinstall-ydocker-cedocker-ce-clicontainerd.io
systemctlenable--nowdocker
echo"✅ Docker installé"fi# Créer le fichier docker-compose.yml
cat>/opt/xsel-haproxy/docker-compose.yml<< 'EOF'version: "3.8"services: haproxy: image: haproxy:3.0-alpine container_name: xsel-haproxy-lb restart: always network_mode: host volumes: - ./config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro - ./logs:/var/log/haproxy cap_add: - NET_ADMIN - NET_BIND_SERVICE healthcheck: test: ["CMD", "haproxy", "-c", "-f", "/usr/local/etc/haproxy/haproxy.cfg"] interval: 30s timeout: 10s retries: 3EOFecho"✅ docker-compose.yml créé"
ℹ️ network_mode: host est obligatoire pour que HAProxy puisse accéder au réseau WG100 (192.168.X.X) et écouter sur les ports 80, 443, 6443, etc. directement sur l'interface du serveur.
0.4 — Configuration HAProxy complète
bash
cat>/opt/xsel-haproxy/config/haproxy.cfg<< 'HAEOFEOF'# =============================================================# XSEL — HAProxy Configuration# Serveur dédié Load Balancer# Version : 1.0 — Mai 2026# =============================================================global # stdout obligatoire dans Docker — sinon docker logs est vide log stdout format raw local0 stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners stats timeout 30s maxconn 50000defaults log global mode tcp option tcplog option dontlognull timeout connect 5s timeout client 1h timeout server 1h# =============================================================# STATS — Interface de monitoring HAProxy# =============================================================listen stats bind *:8404 mode http stats enable stats uri /stats stats refresh 10s stats admin if TRUE stats show-legends stats show-node# =============================================================# KUBERNETES API — Accès Control Plane# TCP 6443 → Masters (via WG100)# =============================================================frontend k8s_api_frontend bind *:6443 mode tcp option tcplog default_backend k8s_api_mastersbackend k8s_api_masters mode tcp option tcp-check balance roundrobin server xsel-master-01 192.168.0.1:6443 check inter 5s fall 3 rise 2 server xsel-master-02 192.168.0.2:6443 check inter 5s fall 3 rise 2# =============================================================# vCLUSTER DEV — Trafic applicatif HTTP# TCP 80 → DevNodes NGINX Ingress NodePort 30080# =============================================================frontend dev_http_frontend bind *:80 mode tcp option tcplog default_backend dev_http_workersbackend dev_http_workers mode tcp option tcp-check balance roundrobin server xsel-devnode-01 192.168.1.1:30080 check inter 5s fall 3 rise 2 server xsel-devnode-02 192.168.1.2:30080 check inter 5s fall 3 rise 2 server xsel-devnode-03 192.168.1.3:30080 check inter 5s fall 3 rise 2# =============================================================# vCLUSTER DEV — Trafic applicatif HTTPS# TCP 443 → DevNodes NGINX Ingress NodePort 30443# =============================================================frontend dev_https_frontend bind *:443 mode tcp option tcplog default_backend dev_https_workersbackend dev_https_workers mode tcp option tcp-check balance roundrobin server xsel-devnode-01 192.168.1.1:30443 check inter 5s fall 3 rise 2 server xsel-devnode-02 192.168.1.2:30443 check inter 5s fall 3 rise 2 server xsel-devnode-03 192.168.1.3:30443 check inter 5s fall 3 rise 2# =============================================================# MySQL — Accès externe base de données# TCP 3306 → DevNodes NodePort (ProxySQL)# =============================================================frontend mysql_frontend bind *:3306 mode tcp option tcplog default_backend mysql_workersbackend mysql_workers mode tcp option tcp-check balance roundrobin server xsel-devnode-01 192.168.1.1:30432 check inter 5s fall 3 rise 2 server xsel-devnode-02 192.168.1.2:30432 check inter 5s fall 3 rise 2 server xsel-devnode-03 192.168.1.3:30432 check inter 5s fall 3 rise 2# =============================================================# PostgreSQL PGBouncer — Accès externe avec connection pooling# TCP 5432 → DevNodes NodePort (PGBouncer)# =============================================================frontend postgres_pgbouncer_frontend bind *:5432 mode tcp option tcplog default_backend postgres_pgbouncer_workersbackend postgres_pgbouncer_workers mode tcp option tcp-check balance roundrobin server xsel-devnode-01 192.168.1.1:30432 check inter 5s fall 3 rise 2 server xsel-devnode-02 192.168.1.2:30432 check inter 5s fall 3 rise 2 server xsel-devnode-03 192.168.1.3:30432 check inter 5s fall 3 rise 2# =============================================================# PostgreSQL Direct — Accès sans base obligatoire (admin)# TCP 5433 → DevNodes NodePort (PostgreSQL primaire)# =============================================================frontend postgres_direct_frontend bind *:5433 mode tcp option tcplog default_backend postgres_direct_workersbackend postgres_direct_workers mode tcp option tcp-check balance roundrobin server xsel-devnode-01 192.168.1.1:30433 check inter 5s fall 3 rise 2 server xsel-devnode-02 192.168.1.2:30433 check inter 5s fall 3 rise 2 server xsel-devnode-03 192.168.1.3:30433 check inter 5s fall 3 rise 2# =============================================================# vCLUSTER CLX (CLIENTS) — Trafic HTTP# Décommenter et adapter quand les workers clients sont ajoutés# =============================================================# frontend clx_http_frontend# bind *:8080# mode tcp# option tcplog# default_backend clx_http_workers## backend clx_http_workers# mode tcp# option tcp-check# balance roundrobin# server xsel-clxnode-01 192.168.10X.Y:30080 check inter 5s fall 3 rise 2# server xsel-clxnode-02 192.168.10X.Z:30080 check inter 5s fall 3 rise 2# =============================================================# vCLUSTER CLX (CLIENTS) — Trafic HTTPS# =============================================================# frontend clx_https_frontend# bind *:8443# mode tcp# option tcplog# default_backend clx_https_workers## backend clx_https_workers# mode tcp# option tcp-check# balance roundrobin# server xsel-clxnode-01 192.168.10X.Y:30443 check inter 5s fall 3 rise 2# server xsel-clxnode-02 192.168.10X.Z:30443 check inter 5s fall 3 rise 2HAEOFEOFecho"✅ haproxy.cfg créé"
0.5 — Démarrer HAProxy
bash
cd/opt/xsel-haproxy
# Valider la configuration avant de démarrer
dockerrun--rm-v$(pwd)/config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro\haproxy:3.0-alpinehaproxy-c-f/usr/local/etc/haproxy/haproxy.cfg
# Attendu : Configuration file is valid# Démarrer# Démarrer HAProxy en arrière-plan
dockercomposeup-d
# Vérifier# Vérifier le statut du conteneur
dockercomposeps
# Afficher les logs du conteneur HAProxy
dockerlogsxsel-haproxy-lb
0.6 — Commandes de gestion
bash
cd/opt/xsel-haproxy
# Voir les logs en temps réel# Suivre les logs en temps réel
dockerlogs-fxsel-haproxy-lb
# Recharger la config sans downtime# Recharger la configuration à chaud sans interruption de trafic
dockerkill-sHUPxsel-haproxy-lb
# Redémarrer# Redémarrer le conteneur
dockercomposerestart
# Arrêter# Arrêter et supprimer le conteneur
dockercomposedown
# Mettre à jour l'image HAProxy# Récupérer la dernière version de l'image HAProxy
dockercomposepull
# Démarrer HAProxy en arrière-plan
dockercomposeup-d
0.7 — Vérification
bash
# Vérifier le container# Vérifier l'état du conteneur HAProxy
dockercompose-f/opt/xsel-haproxy/docker-compose.ymlps
# API Kubernetes via HAProxy# Tester l'accès à l'API Kubernetes via HAProxy
curl-khttps://192.168.0.254:6443/version
# Stats HAProxy# Accéder aux statistiques HAProxy
curlhttp://192.168.0.254:8404/stats
# Ports actifs# Lister les ports TCP en écoute sur le serveur
ss-tlnp|grep-E"80|443|6443|3306|5432|5433|8404"
0.8 — Tableau des ports
Port
Protocole
Destination
Usage
6443
TCP
Masters 192.168.0.1/2
API Kubernetes
80
TCP
DevNodes :30080
HTTP applicatif
443
TCP
DevNodes :30443
HTTPS applicatif
3306
TCP
DevNodes NodePort
MySQL externe
5432
TCP
DevNodes NodePort
PostgreSQL PGBouncer
5433
TCP
DevNodes NodePort
PostgreSQL direct
8080
TCP
CLX Workers :30080
HTTP clients (désactivé)
8443
TCP
CLX Workers :30443
HTTPS clients (désactivé)
8404
HTTP
—
Stats HAProxy
⚠️ Remplacer les placeholdersPUBLIC_IP_HAPROXY, 173.249.55.153, 207.180.237.139 par les vraies IPs publiques avant de démarrer.
Cluster Kubernetes
Nettoyage — Supprimer l'ancien HAProxy des Masters
ℹ️ Si HAProxy tournait auparavant sur les masters via Docker, le supprimer avant de commencer l'installation du cluster.
bash
# À exécuter sur MASTER-01 ET MASTER-02# Vérifier si un container HAProxy tourne# Vérifier si un conteneur HAProxy est en cours d'exécution
dockerps|grephaproxy
# Arrêter et supprimer le container# Arrêter le conteneur HAProxy (silencieux si absent)
dockerstopxsel-haproxy-lb2>/dev/null||true# Supprimer le conteneur HAProxy
dockerrmxsel-haproxy-lb2>/dev/null||true# Supprimer l'image# Supprimer l'image HAProxy locale
dockerrmihaproxy2>/dev/null||true# Vérifier que les ports sont libres# S'assurer que les ports 80, 443 et 6443 sont libres
ss-tlnp|grep-E":80|:443|:6443"# Supprimer le dossier de config# Supprimer le dossier de configuration HAProxy
rm-rf/opt/xsel-haproxy
Cluster Kubernetes
Étape 1 — Préparation sur MASTER-01
⚠️ Attention aux chemins rm -rf — vérifier l'absence d'espace après le / avant d'exécuter
bash
# Reset avec containerd explicite# Réinitialiser kubeadm en précisant le socket containerd
sudokubeadmreset-f--cri-socketunix:///var/run/containerd/containerd.sock
# Nettoyage Kubernetes# Supprimer la configuration du cluster Kubernetes
sudorm-rf/etc/kubernetes/
# Supprimer les données du kubelet
sudorm-rf/var/lib/kubelet/
# Supprimer les données etcd
sudorm-rf/var/lib/etcd/
# Supprimer la configuration kubectl de l'utilisateur
sudorm-rf~/.kube/
# Nettoyer les configurations CNI (Calico)# Supprimer les configurations CNI résiduelles
sudorm-rf/etc/cni/net.d/
# Supprimer les données CNI résiduelles
sudorm-rf/var/lib/cni/
# Nettoyer iptables rules créées par Kubernetes# Vider toutes les règles iptables IPv4 créées par Kubernetes
sudoiptables-F&&sudoiptables-tnat-F&&sudoiptables-tmangle-F&&sudoiptables-X
# Vider toutes les règles iptables IPv6
sudoip6tables-F&&sudoip6tables-tnat-F&&sudoip6tables-tmangle-F&&sudoip6tables-X
# Nettoyer les interfaces réseau virtuelles de Kubernetes/Calico# Supprimer l'interface bridge CNI si elle existe
sudoiplinkdeletecni02>/dev/null||true# Supprimer l'interface Flannel si elle existe
sudoiplinkdeleteflannel.12>/dev/null||true# Supprimer toutes les interfaces veth virtuelles résiduelles
sudoiplinkshow|grepveth|awk'{print $2}'|cut-d@-f1|xargs-I{}sudoiplinkdelete{}2>/dev/null||true# Nettoyer les routes liées à Calico# Supprimer les routes BGP ajoutées par Calico
sudoiprouteflushprotobird2>/dev/null||true# Redémarrer containerd# Redémarrer containerd pour repartir d'un état propre
sudosystemctlrestartcontainerd
# Vérifier que tout est propreecho"=== Vérification post-nettoyage ==="echo"Docker containers:"
dockerps
echo""echo"HAProxy status:"# Vérifier si un conteneur HAProxy est en cours d'exécution
dockerps|grephaproxy
echo""echo"WireGuard interfaces:"
ipaddrshowwg0|grepinet
ipaddrshowwg100|grepinet
echo""echo"VIP status:"
ipaddrshow|grep-E"(10.0.0.10|192.168.0.254)"echo""echo"Kubernetes cleaned:"
ls/etc/kubernetes/2>/dev/null||echo"/etc/kubernetes/ supprimé"
ls/var/lib/kubelet/2>/dev/null||echo"/var/lib/kubelet/ supprimé"
1.1 — Créer le répertoire de travail
bash
# Se connecter à MASTER-01 via SSH
sshroot@173.249.55.153
# Créer le répertoire de travail pour les manifests Kubernetes
mkdir-p/root/k8s-xsel
# Se positionner dans le répertoire de travailcd/root/k8s-xsel
1.2 — Créer le fichier kubeadm-config.yaml
bash
vikubeadm-config.yaml
Copier le contenu du fichier kubeadm-config.yaml fourni.
# Vérifier l'état du service kubelet
systemctlstatuskubelet
# Activer kubelet au démarrage
systemctlenablekubelet.service
ls/etc/kubernetes/2>/dev/null&&echo"ERREUR: /etc/kubernetes existe encore"||echo"OK"
ls/var/lib/kubelet/2>/dev/null&&echo"ERREUR: /var/lib/kubelet existe encore"||echo"OK"# Tester la connectivité vers MASTER-02 via WG100
ping-c2192.168.0.2
# Tester la connectivité vers DEVNODE-01 via WG100
ping-c2192.168.1.1
# Tester la connectivité vers DEVNODE-02 via WG100
ping-c2192.168.1.2
# Tester l'accès à HAProxy avant l'initialisation du cluster
curl-khttps://192.168.0.254:64432>&1|head-5
Cluster Kubernetes
Étape 2 — Initialisation du Premier Master
2.1 — Initialiser le cluster
bash
# Se positionner dans le répertoire de travailcd/root/k8s-xsel
# Initialiser le cluster Kubernetes depuis la configuration et téléverser les certificats pour le second master
sudokubeadminit--configkubeadm-config.yaml--upload-certs
2.2 — Sauvegarder les commandes de join (très important)
# Créer le répertoire de configuration kubectl
mkdir-p$HOME/.kube
# Copier le kubeconfig admin généré par kubeadm
sudocp-i/etc/kubernetes/admin.conf$HOME/.kube/config
# Donner la propriété du fichier à l'utilisateur courant
sudochown$(id-u):$(id-g)$HOME/.kube/config
# Vérifier# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
# Vérifier les pods du plan de contrôle Kubernetes
kubectlgetpods-nkube-system
Cluster Kubernetes
Étape 3 — Joindre le Second Master (MASTER-02)
3.1 — Préparer MASTER-02
bash
# Se connecter à MASTER-02# Se connecter à MASTER-02 via SSH
sshroot@207.180.237.139
# Vérifier les prérequis# Vérifier l'état du service kubelet
systemctlstatuskubelet
# Devrait être "inactive (dead)"
ls/etc/kubernetes/2>/dev/null&&echo"ERREUR"||echo"OK"
ls/var/lib/kubelet/2>/dev/null&&echo"ERREUR"||echo"OK"
# Sur MASTER-02# Utiliser la commande sauvegardée à l'étape 2.2# IMPORTANT: Ajouter ces paramètres à la commande de join:
sudokubeadmjoin192.168.0.254:6443\--token<TOKEN>\--discovery-token-ca-cert-hashsha256:<HASH>\--control-plane\--certificate-key<CERT-KEY>\--cri-socketunix:///var/run/containerd/containerd.sock\--apiserver-advertise-address192.168.0.2
# Cela prend environ 2 minutes
3.3 — Configurer kubectl sur MASTER-02
bash
# Sur MASTER-02# Créer le répertoire de configuration kubectl
mkdir-p$HOME/.kube
# Copier le kubeconfig admin généré par kubeadm
sudocp-i/etc/kubernetes/admin.conf$HOME/.kube/config
# Donner la propriété du fichier à l'utilisateur courant
sudochown$(id-u):$(id-g)$HOME/.kube/config
# Vérifier — devrait montrer les 2 masters# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
Cluster Kubernetes
Étape 4 — Joindre les Workers
4.1 — Sur DEVNODE-01 (207.180.196.59)
bash
# Se connecter# Se connecter à DEVNODE-01 via SSH
sshroot@207.180.196.59
# Utiliser la commande worker sauvegardée à l'étape 2.2
sudokubeadmjoin192.168.0.254:6443\--token<TOKEN>\--discovery-token-ca-cert-hashsha256:<HASH>\--cri-socketunix:///var/run/containerd/containerd.sock
# Environ 1 minute
4.2 — Sur DEVNODE-02 (149.102.138.247)
bash
# Se connecter# Se connecter à DEVNODE-02 via SSH
sshroot@149.102.138.247
# Même commande que DEVNODE-01
sudokubeadmjoin192.168.0.254:6443\--token<TOKEN>\--discovery-token-ca-cert-hashsha256:<HASH>\--cri-socketunix:///var/run/containerd/containerd.sock
# Ir Environ 1 minute
4.3 — Vérifier depuis MASTER-01
bash
# Sur MASTER-01# Afficher les nœuds avec leur IP interne et version
kubectlgetnodes-owide
# Devrait montrer les 4 nœuds en Ready:# NAME STATUS ROLE AGE VERSION
INTERNAL-IP
# xsel-master-01 Ready control-plane 15m v1.29.15192.168.0.1
# xsel-master-02 192.168 .100 .2
Ready
control-plane
7m
v1.29.15
# xsel-devnode-01 192.168.1.1
Ready
3m
v1.29.15
# xsel-devnode-02 192.168.1.2
Ready
2m
v1.29.15
Cluster Kubernetes
Étape 5 — Installation de Calico CNI (VXLAN)
5.1 — Télécharger Calico
bash
# Se positionner dans le répertoire de travailcd/root/k8s-xsel
# Télécharger le manifeste Calico CNI v3.28.0
curlhttps://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml-O
5.2 — Modifier la configuration Calico
Trois variables sont à configurer dans calico.yaml :
Variable
Valeur
Rôle
CALICO_IPV4POOL_CIDR
172.16.0.0/16
CIDR des pods (doit correspondre au podSubnet de kubeadm)
CALICO_IPV4POOL_IPIP
Never
Désactiver IPIP (on utilise VXLAN)
CALICO_IPV4POOL_VXLAN
Always
Activer VXLAN
bash
# Étape 1 — Décommenter et corriger CALICO_IPV4POOL_CIDR# Décommenter la variable CALICO_IPV4POOL_CIDR
sed-i's/# - name: CALICO_IPV4POOL_CIDR/- name: CALICO_IPV4POOL_CIDR/'calico.yaml
# Remplacer le CIDR par défaut par le CIDR pods XSEL (172.16.0.0/16)
sed-i's/# value: "192.168.0.0\/16"/ value: "172.16.0.0\/16"/'calico.yaml
# Étape 2 — Corriger IPIP et VXLAN# (les valeurs sont parfois inversées dans le manifeste par défaut)
sed-i'/CALICO_IPV4POOL_IPIP/{n; s/value: "Always"/value: "Never"/}'calico.yaml
sed-i'/CALICO_IPV4POOL_VXLAN/{n; s/value: "Never"/value: "Always"/}'calico.yaml
# Étape 3 — Vérification globale
grep-A1"CALICO_IPV4POOL_CIDR\|CALICO_IPV4POOL_IPIP\|CALICO_IPV4POOL_VXLAN"calico.yaml|grep-v"^--$"
⚠️ Si les valeurs IPIP/VXLAN sont déjà correctes, l'étape 2 ne changera rien — les sed sont idempotents.
5.3 — Appliquer Calico
bash
# Appliquer Calico# Déployer Calico CNI dans le cluster
kubectlapply-fcalico.yaml
# Attendre que Calico démarre (2-3 minutes)echo"Attente du démarrage de Calico..."# Attendre que tous les pods calico-node soient opérationnels (max 5 min)
kubectlwait--for=condition=readypod-lk8s-app=calico-node-nkube-system--timeout=300s
# Vérifier les pods Calico# Vérifier l'état des pods Calico
kubectlgetpods-nkube-system|grepcalico
Touslespodscalico-nodedoiventêtre1/1Running
Activer l’auto-approbation des csr.
bash
#D'abord approuver les CSR pending actuels :# Lister les demandes de signature de certificat en attente
kubectlgetcsr|grepPending
# Approuver automatiquement tous les CSR en attente
kubectlgetcsr--no-headers|grepPending|awk'{print $1}'|xargskubectlcertificateapprove
cat<<EOF | kubectl apply -f -apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: auto-approve-kubelet-serving-csrssubjects:- kind: Group name: system:nodes apiGroup: rbac.authorization.k8s.ioroleRef: kind: ClusterRole name: system:certificates.k8s.io:certificatesigningrequests:selfnodeclient apiGroup: rbac.authorization.k8s.ioEOF# Vérifier le statut final des CSR
kubectlgetcsr
#Vérifier l'état global du cluster:# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
# Pods système# Lister tous les pods de tous les namespaces
kubectlgetpods-A
# Routes VXLAN (le problème initial)# Vérifier la présence des routes VXLAN Calico
iproute|grep172.16
Désactiver BGP complètement
bash
# Sur MASTER-01# Désactiver BGP (qui empêche VXLAN de fonctionner correctement)
cat<<EOF | calicoctl apply -f -apiVersion: projectcalico.org/v3kind: BGPConfigurationmetadata: name: defaultspec: nodeToNodeMeshEnabled: false asNumber: 64512EOF
Patcher IP_AUTODETECTION_METHOD
⚠️ Toujours vérifier d'abord si la variable existe déjà dans le DaemonSet avant de lancer le patch.
bash
# 1) Vérifier si la variable est déjà présente dans le DaemonSet
kubectlgetdaemonsetcalico-node-nkube-system\-ojsonpath='{range .spec.template.spec.containers[0].env[*]}{.name}{"\n"}{end}'\|grepIP_AUTODETECTION
# 2) Si ABSENTE → appliquer le patch
kubectlpatchdaemonsetcalico-node-nkube-system--type='json'-p='[ { "op": "add", "path": "/spec/template/spec/containers/0/env/-", "value": { "name": "IP_AUTODETECTION_METHOD", "value": "interface=wg100" } }]'# Vérifier que le rollout est terminé# Attendre la fin du redémarrage du DaemonSet Calico
kubectlrolloutstatusdaemonsetcalico-node-nkube-system
# 3) Si PRÉSENTE → ignorer le patch, juste redémarrer les pods Calico# Forcer le redémarrage de tous les pods Calico
kubectldeletepod-nkube-system-lk8s-app=calico-node
# Laisser le temps aux pods de redémarrer
sleep30# Vérifier l'état des pods Calico
kubectlgetpods-nkube-system|grepcalico
# Attendre que les pods Calico soient prêts (max 3 min)
kubectlwait--for=condition=readypod-lk8s-app=calico-node-nkube-system--timeout=180s
5.4 — Vérifier que MASTER-01 est Ready
bash
# Vérifier que le nœud est maintenant Ready# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
# Vérifier les pods système avec leur placement sur les nœuds
kubectlgetpods-nkube-system-owide
Cluster Kubernetes
Étape 6 — Configurer les IP Pools Calico Personnalisés
6.1 — Vérifier la version de Calico / Installer calicoctl
Exécuter: calicoctl version
si ‘command not found’ → installer calicoctl
6.1 Installer calicoctl
bash
# Sur MASTER-01# Se positionner dans le répertoire de travailcd/root/k8s-xsel
# Télécharger calicoctl# Télécharger le binaire calicoctl v3.28.0
curl-Lhttps://github.com/projectcalico/calico/releases/download/v3.28.0/calicoctl-linux-amd64-ocalicoctl
# Rendre le binaire exécutable
chmod+xcalicoctl
# Installer calicoctl globalement
sudomvcalicoctl/usr/local/bin/
# Vérifier# Vérifier que calicoctl est correctement installé
calicoctlversion
sinon, Si la version de Calico s’affiche, passer directement à la création des IP pools ci-dessous.
6.2 — Créer le fichier de configuration des IP pools calicoippool.yaml
6.3 — Ajouter la FelixConfiguration pour le routage VXLAN entre les pools
⚠️ Sans cette configuration, les pods des différents nœuds ne peuvent pas communiquer.
bash
# Sur MASTER-01# Créer la FelixConfiguration pour forcer le dataplane VXLAN
cat>/root/k8s-xsel/felixconfiguration.yaml<< 'EOF'apiVersion: projectcalico.org/v3kind: FelixConfigurationmetadata: name: defaultspec: vxlanEnabled: true vxlanPort: 4789 vxlanVNI: 4096 #vxlanMTU: 1450 # À décommenter et ajuster si problème de connectivité pod-to-podEOF# Appliquer# Appliquer la configuration FelixConfiguration pour activer VXLAN
calicoctlapply-f/root/k8s-xsel/felixconfiguration.yaml
# Vérifier# Vérifier les paramètres VXLAN dans la FelixConfiguration
kubectlgetfelixconfigurationdefault-oyaml|grep-E"vxlan|mtu"
6.3.1 — Appliquer les IP pools
bash
# Voir les pools existants# Lister les IP pools Calico existants avec les détails
calicoctlgetippool-owide
# Supprimer le pool par défaut# Supprimer le pool IP par défaut Calico
calicoctldeleteippooldefault-ipv4-ippool
# Appliquer les pools# Appliquer les IP pools personnalisés par nœud
calicoctlapply-f/root/k8s-xsel/calico-ippool.yaml
# Vérifier les pools# Lister les IP pools Calico existants avec les détails
calicoctlgetippool-owide
6.4 — Redémarrer les pods concernés
bash
# Sur MASTER-01# Redémarrer le DaemonSet Calico# Redémarrer le DaemonSet Calico pour prendre en compte les nouveaux pools
kubectlrolloutrestartdaemonsetcalico-node-nkube-system
# 1. Redémarrer CoreDNS# Redémarrer CoreDNS pour actualiser la configuration réseau
kubectlrolloutrestartdeploymentcoredns-nkube-system
# 2. Redémarrer Calico kube-controllers# Redémarrer les contrôleurs Calico
kubectlrolloutrestartdeploymentcalico-kube-controllers-nkube-system
# 3. Attendre que les nouveaux pods démarrent# Attendre que CoreDNS soit disponible
kubectlrolloutstatusdeploymentcoredns-nkube-system
# Attendre que les contrôleurs Calico soient disponibles
kubectlrolloutstatusdeploymentcalico-kube-controllers-nkube-system
# 4. Vérifier les nouvelles IPs# Vérifier les IPs assignées à CoreDNS et Calico controllers
kubectlgetpods-nkube-system-owide|grep-E"coredns|calico-kube"# Résultat attendu :# coredns: 172.16.1.x (sur master-01)# calico-kube-controllers: 172.16.1.x (sur master-01)
6.5 — Configurer le firewall et les prérequis réseau
⚠️ Ces étapes sont indispensables — sans elles les pods ne peuvent pas communiquer avec l'API server ni entre eux.
6.5.1 — Vider le conntrack après les changements
bash
# Vider la table de suivi des connexions sur DEVNODE-01 après changement réseau
sshroot@192.168.1.1"conntrack -F 2>/dev/null; echo OK"# Vider la table de suivi des connexions sur DEVNODE-02
sshroot@192.168.1.2"conntrack -F 2>/dev/null; echo OK"
6.5.2 — Vérifier la connectivité pod → API server
⚠️ Sans serverTLSBootstrap, kubectl logs et kubectl exec échouent avec tls: internal error — le certificat serveur kubelet n'est pas émis par l'API Kubernetes.
bash
# Activer serverTLSBootstrap sur tous les nodesfornode_ipin192.168.0.1192.168.0.2192.168.1.1192.168.1.2;dosshroot@$node_ip" grep -q 'serverTLSBootstrap' /var/lib/kubelet/config.yaml || \ echo 'serverTLSBootstrap: true' >> /var/lib/kubelet/config.yaml # Redémarrer kubelet après containerd systemctl restart kubelet "done# Attendre la génération des CSRs
sleep10# Lister les demandes de signature de certificat en attente
kubectlgetcsr|grepPending
# Approuver tous les CSRs en attente
kubectlgetcsr--no-headers|grepPending|awk'{print $1}'|\xargskubectlcertificateapprove
# Vérifier — aucun Pending restant# Lister les demandes de signature de certificat en attente
kubectlgetcsr|grepPending
# Tester la connectivité pod → API server
kubectlruntest-api--image=nicolaka/netshoot--restart=Never\--command--sh-c"curl -sk --max-time 5 https://10.96.0.1/healthz; echo"
sleep10# Vérifier la sortie du pod de test (attendu : "ok")
kubectllogstest-api
# Résultat attendu : ok ✅# Supprimer le pod de test temporaire
kubectldeletepodtest-api
Cluster Kubernetes
Étape 7 — Tests et Vérifications Finales
7.1 — Vérifier nœuds et pods système
bash
# Afficher les nœuds avec leur IP interne et version
kubectlgetnodes-owide
# Vérifier les pods système avec leur placement sur les nœuds
kubectlgetpods-nkube-system-owide
7.2 — Test de connectivité pod-to-pod
ℹ️ Les pods sont forcés sur des workers différents via nodeSelector pour valider la connectivité cross-node via VXLAN.
bash
# Créer test-pod-1 sur DEVNODE-01
kubectlruntest-pod-1--image=nicolaka/netshoot--overrides='{"spec": {"nodeSelector": {"kubernetes.io/hostname": "xsel-devnode-01"}}}'--command--sleep3600# Créer test-pod-2 sur DEVNODE-02
kubectlruntest-pod-2--image=nicolaka/netshoot--overrides='{"spec": {"nodeSelector": {"kubernetes.io/hostname": "xsel-devnode-02"}}}'--command--sleep3600# Attendre que les pods soient prêts
kubectlwait--for=condition=Readypod/test-pod-1--timeout=60s
kubectlwait--for=condition=Readypod/test-pod-2--timeout=60s
# Vérifier les IPs et les nodes
kubectlgetpodstest-pod-1test-pod-2-owide
# Résultat attendu :# test-pod-1 Running xsel-devnode-01 172.16.11.x# test-pod-2 Running xsel-devnode-02 172.16.12.x# Récupérer les IPs# Récupérer l'IP du pod 1 pour le test pingPOD1_IP=$(kubectlgetpodtest-pod-1-ojsonpath='{.status.podIP}')# Récupérer l'IP du pod 2POD2_IP=$(kubectlgetpodtest-pod-2-ojsonpath='{.status.podIP}')echo"Pod 1 IP (devnode-01): $POD1_IP"echo"Pod 2 IP (devnode-02): $POD2_IP"# Test ping cross-node depuis test-pod-1 → test-pod-2echo"=== Test ping DEVNODE-01 → DEVNODE-02 ==="# Tester la connectivité pod-to-pod : DEVNODE-01 vers DEVNODE-02
kubectlexectest-pod-1--ping-c5$POD2_IP# Test ping cross-node depuis test-pod-2 → test-pod-1echo"=== Test ping DEVNODE-02 → DEVNODE-01 ==="# Tester la connectivité pod-to-pod : DEVNODE-02 vers DEVNODE-01
kubectlexectest-pod-2--ping-c5$POD1_IP# Nettoyage# Supprimer les pods de test
kubectldeletepodtest-pod-1test-pod-2
ℹ️ Le Metrics Server collecte les métriques CPU et mémoire des nœuds et pods. Il est indispensable pour kubectl top, l'autoscaling HPA/VPA, et le Kubernetes Dashboard.
8.1 — Installer le Metrics Server via Helm
bash
# Sur MASTER-01# Ajouter le repo Helm (si pas encore fait)# Ajouter le repo Helm officiel du Metrics Server
helmrepoaddmetrics-serverhttps://kubernetes-sigs.github.io/metrics-server/
# Mettre à jour le cache des repos Helm
helmrepoupdate
# Installer le Metrics Server# --kubelet-insecure-tls est nécessaire car les kubelets utilisent des certificats auto-signés
helmupgrade--installmetrics-servermetrics-server/metrics-server\--namespacekube-system\--setargs[0]="--kubelet-insecure-tls"\--wait\--timeout2m
echo"✅ Metrics Server installé"
8.2 — Vérifier l'installation
bash
# Vérifier que le pod est Running# Vérifier que le pod Metrics Server est en Running
kubectlgetpods-nkube-system|grepmetrics-server
# Résultat attendu :# metrics-server-<hash> 1/1 Running 0 ...# Vérifier le service# Vérifier le service Metrics Server
kubectlgetsvc-nkube-systemmetrics-server
8.3 — Tester les métriques
⚠️ Attendre 1 à 2 minutes après l'installation avant de lancer kubectl top — le Metrics Server a besoin de collecter un premier cycle de données.
bash
# Métriques des nœuds# Afficher la consommation CPU/RAM de chaque nœud
kubectltopnodes
# Résultat attendu :# NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%# xsel-master-01 <valeur>m <x>% <valeur>Mi <x>%# xsel-master-02 <valeur>m <x>% <valeur>Mi <x>%# xsel-devnode-01 <valeur>m <x>% <valeur>Mi <x>%# xsel-devnode-02 <valeur>m <x>% <valeur>Mi <x>%# Métriques des pods (namespace kube-system)# Afficher la consommation CPU/RAM des pods système
kubectltoppods-nkube-system
8.4 — Résumé
text
✅ Metrics Server : installé dans kube-system
✅ kubectl top nodes : fonctionnel
✅ kubectl top pods : fonctionnel
✅ HPA/VPA : prêts à être utilisés
✅ K8s Dashboard : métriques disponibles (étape 11)
Services de base
Étape 9 — Installation NGINX Ingress Controller
9.1 — Télécharger le manifeste
bash
# Sur MASTER-01# Se positionner dans le répertoire de travailcd/root/k8s-xsel
# Télécharger le manifeste NGINX Ingress Controller# Télécharger le manifeste NGINX Ingress Controller pour bare-metal
curl-oingress-nginx.yamlhttps://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/baremetal/deploy.yaml
# Vérifier que le fichier est téléchargé# Vérifier que le fichier a bien été téléchargé
ls-lhingress-nginx.yaml
# Appliquer le manifeste# Déployer le contrôleur NGINX Ingress
kubectlapply-fingress-nginx.yaml
# Attendre que le controller soit prêt# Attendre que le pod NGINX Ingress soit opérationnel (max 2 min)
kubectlwait--namespaceingress-nginx--for=condition=readypod--selector=app.kubernetes.io/component=controller--timeout=120s
# Vérifier# Vérifier le pod NGINX Ingress et son nœud
kubectlgetpods-ningress-nginx-owide
9.2 — Modifier le service pour utiliser NodePort 30080 et 30443
bash
# Sur MASTER-01# Patcher le service pour forcer les NodePorts 30080 et 30443
kubectlpatchsvcingress-nginx-controller-ningress-nginx--type='json'-p='[ {"op":"replace","path":"/spec/ports/0/nodePort","value":30080}, {"op":"replace","path":"/spec/ports/1/nodePort","value":30443}]'# Vérifier les NodePorts# Vérifier les NodePorts 30080/30443 du service
kubectlgetsvcingress-nginx-controller-ningress-nginx
# Résultat attendu : 80:30080/TCP,443:30443/TCP# Vérifier les pods# Vérifier le pod NGINX Ingress et son nœud
kubectlgetpods-ningress-nginx-owide
# Vérifier tous les objets# Lister toutes les ressources du namespace ingress-nginx
kubectlgetall-ningress-nginx
Services de base
Étape 10 — Déployer une Application de Test
Créons une application simple pour valider tout le flow :
bash
# Créer une application de test
cat>/root/k8s-xsel/test-app.yaml<< 'EOF'apiVersion: v1kind: Namespacemetadata: name: test-app---apiVersion: apps/v1kind: Deploymentmetadata: name: hello-world namespace: test-appspec: replicas: 2 selector: matchLabels: app: hello-world template: metadata: labels: app: hello-world spec: containers: - name: hello image: gcr.io/google-samples/hello-app:1.0 ports: - containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: hello-world namespace: test-appspec: selector: app: hello-world ports: - port: 80 targetPort: 8080---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: hello-world namespace: test-app annotations: nginx.ingress.kubernetes.io/rewrite-target: /spec: ingressClassName: nginx rules: - host: hello.xsel-test.local http: paths: - path: / pathType: Prefix backend: service: name: hello-world port: number: 80EOF# Appliquer l'application de test# Déployer l'application de test dans le namespace test-app
kubectlapply-f/root/k8s-xsel/test-app.yaml
# Attendre que les pods démarrent# Attendre que le Deployment soit disponible (max 2 min)
kubectlwait--for=condition=available--timeout=120sdeployment/hello-world-ntest-app
# Vérifier les ressources# Vérifier toutes les ressources déployées dans test-app
kubectlgetall-ntest-app
# Vérifier la règle Ingress de l'application de test
kubectlgetingress-ntest-app
# Tester l'accès via NodePort
curl-H"Host: hello.xsel-test.local"http://192.168.1.1:30080curl-H"Host: hello.xsel-test.local"http://192.168.1.2:30080
# Tester via le VIP HAProxy (si configuré)
curl-H"Host: hello.xsel-test.local"http://192.168.0.254
# Devrait retourner:# Hello, world!# Version: 1.0.0# Hostname: hello-world-xxxxxxxxxx-xxxxx
Architecture déployée
text
Internet
│
▼
DNS : hello.xsel-test.local → 95.111.238.94 (IP publique HAProxy)
│
▼
HAProxy dédié (xsel-jumphost-01)
IP publique : 95.111.238.94
IP WG100 : 192.168.0.254
│ :80 → round-robin
├──────────────────────────┐
▼ ▼
DEVNODE-01 DEVNODE-02
192.168.1.1:30080 192.168.1.2:30080
│ │
└──────────┬───────────────┘
▼
NGINX Ingress Controller
Host: hello.xsel-test.local → Service hello-world
│
▼
Kubernetes Service (ClusterIP)
│
┌──────────┴──────────┐
▼ ▼
Pod 1 Pod 2
172.16.11.X 172.16.12.X
(DEVNODE-01) (DEVNODE-02)
Services de base
Étape 11 — Installation du Kubernetes Dashboard
11.1 — Installer le Dashboard
bash
# Sur MASTER-01# Se positionner dans le répertoire de travailcd/root/k8s-xsel
# Télécharger le manifeste# Télécharger le manifeste du Kubernetes Dashboard v2.7.0
curl-okubernetes-dashboard.yamlhttps://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
# Appliquer# Déployer le Kubernetes Dashboard
kubectlapply-fkubernetes-dashboard.yaml
# Vérifier l'installation# Vérifier les pods du Dashboard
kubectlgetpods-nkubernetes-dashboard
# Vérifier les services du Dashboard
kubectlgetsvc-nkubernetes-dashboard
# Créer un token pour l'utilisateur admin# Générer un token d'accès admin valable 10 ans
kubectl-nkubernetes-dashboardcreatetokenadmin-user--duration=87600h
# Tester l'accès# Tester l'accès au Dashboard via HAProxy
curl-k-H"Host: dashboard.xsel-ci.com"https://192.168.0.254
# Sur votre Mac# Éditer le fichier hosts local pour résoudre le domaine sans DNS public
sudovi/etc/hosts
# Ajouter cette ligne :95.111.238.94dashboard.xsel-ci.com
# Puis dans votre navigateur :# https://dashboard.xsel-ci.com
11.5 — Commandes Utiles
bash
# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
# Lister tous les pods de tous les namespaces
kubectlgetpods--all-namespaces
# Afficher les endpoints du cluster
kubectlcluster-info
kubectlgetevents--all-namespaces--sort-by='.lastTimestamp'|tail-20
kubectllogs-nkube-system<pod-name>
kubectldrain<node-name>--ignore-daemonsets--delete-empt
ydir-data
kubectluncordon<node-name>
# Générer une commande de join fraîche pour les nouveaux workers
kubeadmtokencreate--print-join-command
# Voir tous les roles par namespace# Lister tous les Roles par namespace
kubectlgetroles-A
# Voir tous les clusterroles# Lister les ClusterRoles personnalisés (hors système)
kubectlgetclusterroles|grep-v"system:"# Voir les rolebindings# Lister tous les RoleBindings
kubectlgetrolebindings-A
# Voir les clusterrolebindings# Lister les ClusterRoleBindings personnalisés
kubectlgetclusterrolebindings|grep-v"system:"# Détail d'un role spécifique
kubectldescriberole<nom-role>-n<namespace>
# Détail d'un clusterrole
kubectldescribeclusterrole<nom-clusterrole>
# Vérifier les permissions d'un utilisateur/serviceaccount
kubectlauthcan-i--list--as=system:serviceaccount:<names
pace>:<serviceaccount>
# Vérifier une permission spécifique
kubectlauthcan-igetpods--as=system:serviceaccount:dev:
default-ndev
# Sur DEVNODE-01# Se connecter à DEVNODE-01 via SSH
sshroot@207.180.196.59
# Arrêter le pare-feu
systemctlstopfirewalld
# Démarrer le pare-feu
systemctlstartfirewalld
# Activer le pare-feu au démarrage
systemctlenablefirewalld
# Vérifier l'état du pare-feu
systemctlstatusfirewalld
Contactezl'administrateurWireGuardaveccesinformations:
Problème : Tunnel WireGuard cassé entre MASTER-02 et DEVNODE-01
Symptômes :
Pas de handshake récent (19 jours)
Ping 100 % packet loss dans les deux sens
Autres tunnels fonctionnent (MASTER-01, DEVNODE-02)
Besoin :
Vérifier la config WireGuard sur les deux nœuds
Potentiellement recréer le peer entre ces deux serveurs
11.6 — Dépannage
11.6.1 — Un Nœud Reste NotReady
bash
# Afficher les 100 dernières lignes de logs du kubelet
journalctl-ukubelet-n100--no-pager
# Vérifier l'état des pods Calico
kubectlgetpods-nkube-system|grepcalico
# Inspecter les logs d'un pod Calico spécifique
kubectllogs-nkube-system<calico-pod-name>
# Redémarrer containerd d'abord# Redémarrer containerd
systemctlrestartcontainerd
echo"Containerd redémarré, attente de 5 secondes..."
sleep5# Redémarrer kubelet après containerd
systemctlrestartkubelet
echo"Kubelet redémarré, attente de 15 secondes..."
sleep15# Vérifier s'il y a encore des erreurs CNI# Vérifier les erreurs CNI récentes dans les logs kubelet
journalctl-ukubelet-n5--no-pager|grep-icni
11.6.2 — Des Pods ne Démarrent pas
bash
# Afficher les événements et la configuration du pod en erreur
kubectldescribepod<pod-name>
# Afficher les logs du pod en erreur
kubectllogs<pod-name>
Isolation, stockage et environnements
Étape 12 — Configuration des Namespaces et Isolation Réseau
ℹ️ Cette étape configure l'isolation complète du cluster : namespaces dédiés, quotas de ressources, NetworkPolicies et RBAC. Les environnements applicatifs (dev, staging, prod) sont gérés via des vClusters — seuls les namespaces infrastructure sont créés ici.
12.1 — Création des Namespaces
Les namespaces infrastructure du cluster physique :
cat>/root/k8s-xsel/namespaces.yaml<< 'EOF'---apiVersion: v1kind: Namespacemetadata: name: tools labels: name: tools type: services---apiVersion: v1kind: Namespacemetadata: name: monitoring labels: name: monitoring type: services---apiVersion: v1kind: Namespacemetadata: name: devmysql labels: name: devmysql type: databases---apiVersion: v1kind: Namespacemetadata: name: devpostgresql labels: name: devpostgresql type: databasesEOF# Créer les namespaces infrastructure dans le cluster
kubectlapply-fnamespaces.yaml
# Vérifier que les namespaces ont bien été créés
kubectlgetnamespaces
12.2 — Quotas de Ressources
ℹ️ Les requests doivent être proches de l'usage réel observé (kubectl top pods). Des requests trop élevées saturent artificiellement les nodes.
Usage réel observé en production :
Composant
CPU réel
RAM réelle
CPU request
RAM request
PXC MySQL (par node)
45–62m
870–1005Mi
100m
512Mi
ProxySQL (par node)
42–221m
256–308Mi
100m
256Mi
PostgreSQL primary
76m
196Mi
100m
256Mi
PostgreSQL replica
21–46m
116–123Mi
50m
128Mi
PgBouncer
1–11m
5–7Mi
50m
64Mi
PMM
464m
1524Mi
500m
2Gi
Grafana
95m
385Mi
100m
256Mi
Prometheus
~200m
~512Mi
200m
512Mi
Harbor (total)
~30m
~300Mi
50m
64Mi
⚠️ Ne jamais dépasser 70 % de requests allouées sur un node pour laisser de la marge au scheduling.
bash
cat>/root/k8s-xsel/resource-quotas.yaml<< 'EOF'---# TOOLS — Harbor + GitLab + JenkinsapiVersion: v1kind: ResourceQuotametadata: name: tools-quota namespace: toolsspec: hard: requests.cpu: "8" requests.memory: 20Gi limits.cpu: "24" limits.memory: 48Gi pods: "50" services: "50" persistentvolumeclaims: "20"---# MONITORING — Prometheus + Grafana + AlertManager + Loki + Promtail + PMM# ⚠️ requests.cpu réduit à 2 car les workers sont saturés par PXC/ProxySQL/Longhorn# Calcul réel :# Prometheus : 200m# AlertManager : 50m (min 64Mi memory imposé par LimitRange)# Grafana : 150m (3 containers × 50m)# Node Exporter : 4 × 50m = 200m (1 par nœud)# Kube State Met. : 50m# PMM : 50m# Loki : 100m# Promtail : 4 × 50m = 200m (1 par nœud)# Total : ~1000m → quota à 2 avec marge# ⚠️ Si les workers sont à > 85% de CPU requests avant installation :# réduire les requests PXC (600m→200m) et ProxySQL (600m→100m) d'abord# (voir §22.6 Prérequis CPU)apiVersion: v1kind: ResourceQuotametadata: name: monitoring-quota namespace: monitoringspec: hard: requests.cpu: "2" requests.memory: 16Gi limits.cpu: "16" limits.memory: 32Gi pods: "30" services: "20" persistentvolumeclaims: "15"---# DEVMYSQL — PXC x3 + ProxySQL x3 + operator + sidecars logcollector# requests.cpu : 3 PXC (600m×3) + 3 ProxySQL (600m×3) + operator (100m) = ~3700m → quota 6# limits.cpu : 3 PXC (1×3=3) + 3 ProxySQL (1×3=3) + operator (200m) + logcollector sidecars (~200m×6=1.2) = ~7.4# ⚠️ Ne pas mettre limits.cpu < 16 : les sidecars Percona (logcollector, pxc-monit)# s'ajoutent aux limits des containers principaux et saturent rapidement le quotaapiVersion: v1kind: ResourceQuotametadata: name: devmysql-quota namespace: devmysqlspec: hard: requests.cpu: "6" requests.memory: 12Gi limits.cpu: "16" limits.memory: 24Gi pods: "20" services: "15" persistentvolumeclaims: "15"---# DEVPOSTGRESQL — PG x3 + PgBouncer x3 + operator + sidecars# requests.cpu : même logique que devmysql → quota 6# limits.cpu : même logique que devmysql → quota 16# ⚠️ Ne pas mettre limits.cpu < 16 : les sidecars Percona s'ajoutent aux limits principalesapiVersion: v1kind: ResourceQuotametadata: name: devpostgresql-quota namespace: devpostgresqlspec: hard: requests.cpu: "6" requests.memory: 12Gi limits.cpu: "16" limits.memory: 24Gi pods: "20" services: "15" persistentvolumeclaims: "15"EOF# Appliquer les quotas de ressources sur les namespaces
kubectlapply-fresource-quotas.yaml
# Vérifier les quotas appliqués sur tous les namespaces
kubectlgetresourcequota-A
12.3 — LimitRanges
ℹ️ Le defaultRequest s'applique aux pods qui ne spécifient pas de resources. Garder ces valeurs basses (50m CPU) pour éviter la saturation artificielle. Les opérateurs Percona définissent leurs propres resources via le CR.
⚠️ Les namespaces tools et monitoring doivent avoir un minimum de 10m/64Mi pour permettre aux pods temporaires de cert-manager (ACME solver) de démarrer.
⚠️ Les namespaces devmysql et devpostgresql doivent avoir min memory: 20Mi — l'opérateur Percona (pxc-operator, pg-operator) demande 20Mi par défaut dans ses charts Helm. Un minimum supérieur (ex: 64Mi) bloque le pod opérateur avec FailedCreate.
bash
cat>/root/k8s-xsel/limit-ranges.yaml<< 'EOF'---# TOOLS — min 10m/64Mi pour cert-manager cm-acme-http-solverapiVersion: v1kind: LimitRangemetadata: name: tools-limit-range namespace: toolsspec: limits: - type: Container max: cpu: "4" memory: 8Gi min: cpu: "10m" memory: 64Mi default: cpu: "500m" memory: 512Mi defaultRequest: cpu: "50m" memory: 128Mi---# MONITORINGapiVersion: v1kind: LimitRangemetadata: name: monitoring-limit-range namespace: monitoringspec: limits: - type: Container max: cpu: "4" memory: 8Gi min: cpu: "10m" memory: 64Mi default: cpu: "500m" memory: 1Gi defaultRequest: cpu: "50m" memory: 128Mi---# DEVMYSQL# ⚠️ min: 20Mi — pxc-operator demande 20Mi par défaut dans ses charts Helm# ⚠️ default/defaultRequest définis explicitement pour éviter que Kubernetes# n'injecte max (8Gi) comme limit par défaut → explose le quota immédiatement# ⚠️ Les pods PXC/ProxySQL ont leurs propres limits dans le CR (2G/1CPU)# donc le LimitRange ne leur injecte rien — il ne s'applique qu'aux# containers sans limits déclarées (sidecars légers, init containers)apiVersion: v1kind: LimitRangemetadata: name: devmysql-limit-range namespace: devmysqlspec: limits: - type: Container max: cpu: "4" memory: 8Gi min: cpu: "10m" memory: 20Mi default: cpu: "500m" memory: 256Mi defaultRequest: cpu: "100m" memory: 128Mi---# DEVPOSTGRESQL — mêmes contraintes que devmysql# ⚠️ min: 20Mi — pg-operator demande 20Mi par défaut dans ses charts HelmapiVersion: v1kind: LimitRangemetadata: name: devpostgresql-limit-range namespace: devpostgresqlspec: limits: - type: Container max: cpu: "4" memory: 8Gi min: cpu: "10m" memory: 20Mi default: cpu: "500m" memory: 256Mi defaultRequest: cpu: "100m" memory: 128MiEOF# Appliquer les LimitRanges sur les namespaces
kubectlapply-flimit-ranges.yaml
# Vérifier les LimitRanges appliquées
kubectlgetlimitrange-A
Vérifier les ressources disponibles :
bash
# Afficher la consommation CPU/RAM de chaque nœud
kubectltopnodes
# Afficher les ressources allouées sur DEVNODE-01
kubectldescribenodexsel-devnode-01|grep-A6"Allocated resources"# Afficher les ressources allouées sur DEVNODE-02
kubectldescribenodexsel-devnode-02|grep-A6"Allocated resources"
ℹ️ Résultat sain attendu : CPU requests < 70 % et Memory requests < 70 % par node.
Diagnostic si un pod est bloqué en Pending :
bash
# Diagnostiquer pourquoi un pod est bloqué en Pending
kubectldescribepod<pod-name>-n<namespace>|grep-A10Events
12.4 — NetworkPolicies
ℹ️ Par défaut, tous les pods peuvent communiquer entre eux. Les NetworkPolicies restreignent ce comportement pour isoler les namespaces.
# Installer NFS# Installer les utilitaires NFS (serveur et client)
dnfinstall-ynfs-utils
# Créer les répertoires# Créer l'arborescence des répertoires NFS par service
mkdir-p/srv/nfs/kubernetes/{prometheus,grafana,loki,gitlab,jenkins,registry,certmanager}# Autoriser l'accès en lecture/écriture à tous les pods NFS
chmod-R777/srv/nfs/kubernetes
# Configurer les exports
cat>/etc/exports<< 'EOF'/srv/nfs/kubernetes 192.168.0.0/16(rw,sync,no_subtree_check,no_root_squash)EOF# Démarrer NFS# Démarrer et activer le serveur NFS au démarrage
systemctlenable--nownfs-server
# Publier les exports NFS définis dans /etc/exports
exportfs-rav
# Vérifier# Vérifier les exports NFS disponibles localement
showmount-elocalhost
ℹ️ firewalld est désactivé sur master-01 — les règles firewall-cmd ne sont pas nécessaires.
13.2 — Sur MASTER-02, DEVNODE-01, DEVNODE-02 (Clients NFS)
bash
# Installer les utilitaires NFS (serveur et client)
dnfinstall-ynfs-utils
# Vérifier les exports NFS disponibles depuis le serveur master-01
showmount-e192.168.0.1
kubectlapply-f-<< 'EOF'---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: test-pvc namespace: defaultspec: accessModes: - ReadWriteMany storageClassName: nfs-storage resources: requests: storage: 1GiEOF
sleep10# Vérifier que le PVC de test est en état Bound
kubectlgetpvctest-pvc
# Nettoyage# Supprimer le PVC de test après validation
kubectldeletepvctest-pvc-ndefault
=== NFS Server ===
Active: active (exited) ← normal sur systemd, le service délègue au kernel
=== Exports ===
/srv/nfs/kubernetes 192.168.0.0/16(sync,wdelay,hide,no_subtree_check,rw,no_root_squash)
=== Provisioner ===
nfs-provisioner-6d55b9967d-sntrs 1/1 Running ✅
=== StorageClasses ===
nfs-storage (default) nfs.io/nfs-provisioner ✅
nfs-prometheus nfs.io/nfs-provisioner ✅
nfs-grafana nfs.io/nfs-provisioner ✅
nfs-loki nfs.io/nfs-provisioner ✅
nfs-gitlab nfs.io/nfs-provisioner ✅
nfs-jenkins nfs.io/nfs-provisioner ✅
nfs-registry nfs.io/nfs-provisioner ✅
=== Dossiers ===
certmanager/ gitlab/ grafana/ jenkins/
loki/ prometheus/ registry/
devmysql-*/ (données Percona MySQL existantes)
ℹ️ active (exited) est le comportement normal de nfs-server sur systemd — le service a démarré et a délégué au kernel NFS. Les exports fonctionnent correctement, confirmé par le PVC test Bound.
⚠️ Si la commande mkdir a été coupée lors de la saisie, un dossier parasite peut être créé. Le supprimer avec :
À exécuter sur MASTER-01, MASTER-02, DEVNODE-01, DEVNODE-02 :
bash
# Installer open-iscsi (requis par Longhorn)# Installer le client iSCSI requis par Longhorn
dnfinstall-yiscsi-initiator-utils
# Démarrer et activer le service# Démarrer et activer le daemon iSCSI au démarrage
systemctlenable--nowiscsid
# Vérifier# Vérifier que le service iSCSI est actif
systemctlstatusiscsid
⚠️ Sans open-iscsi, Longhorn ne pourra pas monter les volumes sur les nodes.
14.3 — Vérifier les prérequis avec le script officiel
À exécuter uniquement depuis MASTER-01 (les workers n'ont pas de KUBECONFIG) :
bash
# Lancer le script de vérification des prérequis Longhorn sur tous les nœuds
curl-sSfLhttps://raw.githubusercontent.com/longhorn/longhorn/v1.6.0/scripts/environment_check.sh|bash
Résultat attendu :
code
[INFO] Required dependencies 'kubectl jq mktemp sort printf' are installed.
[INFO] All nodes have unique hostnames.
[INFO] All longhorn-environment-check pods are ready (2/2).
[INFO] MountPropagation is enabled
[INFO] Checking kernel release...
[INFO] Checking iscsid...
[INFO] Checking multipathd...
[INFO] Checking packages...
[INFO] Checking nfs client...
[INFO] Cleanup completed.
ℹ️ Le warning Failed to check CONFIG_NFS_V4_2 est non bloquant — Longhorn utilise iSCSI/block, pas NFS pour ses volumes.
14.4 — Ajouter le repo Helm
bash
# Ajouter le repo Helm officiel Longhorn
helmrepoaddlonghornhttps://charts.longhorn.io
# Mettre à jour le cache des repos Helm
helmrepoupdate
# Vérifier# Vérifier les versions disponibles de Longhorn
helmsearchrepolonghorn/longhorn
14.5 — Créer le namespace
bash
# Créer le namespace pour Longhorn
kubectlcreatenamespacelonghorn-system
14.6 — Installer Longhorn v1.6.2
⚠️ Utiliser impérativement v1.6.2 — Longhorn v1.11.x n'est pas compatible avec RHEL 9 / Rocky Linux 9. Le frontend blockdev de v1.11 requiert tgtd (iSCSI target daemon) absent des repos RHEL 9. La v1.6.2 embarque tgt dans ses propres containers sans dépendance système supplémentaire.
ℹ️ defaultReplicaCount=2 : le cluster XSEL dispose de 2 workers. Les masters ont le taint NoSchedule — Longhorn ne peut pas y scheduler de replicas. 2 replicas sur 2 nodes distincts garantit la HA.
⚠️ Ne pas mettre defaultReplicaCount=3 avec seulement 2 nodes — Longhorn placerait 2 replicas sur le même node, sans HA supplémentaire.
14.7 — Vérifications post-installation
bash
# Vérifier que tous les pods Longhorn sont en Running
kubectlgetpods-nlonghorn-system
ℹ️ Longhorn ne tourne que sur les 2 workers — les masters ont le taint NoSchedule. C'est attendu.
14.8 — Définir Longhorn comme StorageClass par défaut
⚠️ Longhorn s'installe automatiquement en (default). Si nfs-storage est aussi marquée (default), Kubernetes ne sait pas laquelle choisir.
bash
# Retirer le statut "default" de nfs-storage
kubectlpatchstorageclassnfs-storage\-p'{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "false"}}}'# Vérifier — seul longhorn doit avoir (default)# Lister les StorageClasses disponibles dans le cluster
kubectlgetstorageclass
Le dashboard permet de visualiser en temps réel l'état de chaque volume et ses replicas, l'espace disque disponible sur chaque node, ainsi que les snapshots et backups programmés.
14.10 — DNS à configurer
Domaine
Type
Valeur
longhorn.xselcloud.com
A
95.111.238.94
14.11 — Résumé
code
✅ open-iscsi : installé sur tous les nodes (dnf install iscsi-initiator-utils)
✅ Longhorn v1.6.2 : installé dans longhorn-system
✅ StorageClass : longhorn (default)
✅ Réplication : 2 replicas par volume (devnode-01 + devnode-02)
✅ Dashboard : https://longhorn.xselcloud.com
✅ Cohabitation NFS : nfs-storage (RWX) reste disponible (non-default)
Isolation, stockage et environnements
Étape 15 — Déploiement des vClusters (DEV, TEST, STAGING, PROD)
ℹ️ Les environnements applicatifs sont isolés dans des vClusters dédiés. Chaque vCluster stocke son état dans un PVC persistant sur Longhorn. Longhorn (étape 14) doit être installé avant cette étape.
ℹ️ Le NGINX Ingress Controller tourne dans le cluster physique. Pour qu'il voie les règles Ingress créées à l'intérieur d'un vCluster, la synchronisation Ingress doit être activée (sync.toHost.ingresses.enabled: true). Elle est incluse dans le values.yaml ci-dessous.
Environnement
StorageClass
Justification
dev
longhorn
Réplication HA, persistance garantie entre redémarrages
test
longhorn
Réplication HA, persistance garantie entre redémarrages
staging
longhorn
Miroir de prod, stabilité et HA stockage obligatoire
prod
longhorn
Production, HA stockage obligatoire
15.2 — Installer vCluster CLI
bash
if!command-vvcluster&>/dev/null;thenmkdir-p/root/k8s-xsel/vcluster
curl-L-o/root/k8s-xsel/vcluster/vcluster-cli\"https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-linux-amd64"chmod+x/root/k8s-xsel/vcluster/vcluster-cli
ln-sf/root/k8s-xsel/vcluster/vcluster-cli/usr/local/bin/vcluster
fi# Vérifier la version du CLI vCluster installé
vclusterversion
# Résultat : vcluster version 0.34.0
ℹ️ sync.toHost.ingresses.enabled: true est la syntaxe correcte depuis vCluster v0.20+. L'ancienne syntaxe sync.ingresses.enabled n'est plus valide et provoque une erreur de schema.
# Lister tous les vClusters déployés dans le cluster physique
vclusterlist
# Vérifier que les PVCs Longhorn des vClusters sont en état Bound
kubectlgetpvc-A|grepvcluster
15.7.2 — Déployer dans vcluster-dev et vcluster-prod
bash
# DEV
vclusterconnectvcluster-dev--namespacevcluster-dev--\kubectlapply-f/root/k8s-xsel/vcluster/apps/nginx-dev.yaml
echo"✅ nginx déployé dans vcluster-dev"# PROD
vclusterconnectvcluster-prod--namespacevcluster-prod--\kubectlapply-f/root/k8s-xsel/vcluster/apps/nginx-prod.yaml
echo"✅ nginx déployé dans vcluster-prod"
15.7.3 — Vérifier les déploiements
bash
# Vérifier depuis l'intérieur de chaque vClusterforenvindevprod;doecho"=== vcluster-$env ==="vclusterconnectvcluster-$env--namespacevcluster-$env--\kubectlgetpods,svc,ingress-ndefault
done
Résultat attendu :
text
=== vcluster-dev ===
NAME READY STATUS RESTARTS
pod/nginx-demo-<hash> 1/1 Running 0
NAME TYPE CLUSTER-IP PORT(S)
service/nginx-demo ClusterIP 10.x.x.x 80/TCP
NAME CLASS HOSTS
ingress.networking.k8s.io/nginx-demo nginx demo-dev.xsel-ci.com
=== vcluster-prod ===
NAME READY STATUS RESTARTS
pod/nginx-demo-<hash> 1/1 Running 0
NAME TYPE CLUSTER-IP PORT(S)
service/nginx-demo ClusterIP 10.x.x.x 80/TCP
NAME CLASS HOSTS
ingress.networking.k8s.io/nginx-demo nginx prod-dev.xsel-ci.com
bash
# Vérifier que les Ingress sont bien synchronisés dans le cluster physique# Vérifier la synchronisation des Ingress depuis les vClusters vers le cluster physique
kubectlgetingress-A|grepvcluster
# Résultat attendu :# vcluster-dev nginx-demo-x-default-x-vcluster-dev nginx demo-dev.xsel-ci.com 192.168.1.1# vcluster-prod nginx-demo-x-default-x-vcluster-prod nginx prod-dev.xsel-ci.com 192.168.1.1
ℹ️ Le préfixe nginx-demo-x-default-x-vcluster-dev est le comportement normal de vCluster — il renomme les ressources syncées pour éviter les collisions entre vClusters.
ℹ️ cert-manager automatise l'émission et le renouvellement des certificats TLS via Let's Encrypt. Il s'intègre avec NGINX Ingress via des annotations.
16.1 — Installer cert-manager
bash
# Créer le namespace pour cert-manager
kubectlcreatenamespacecert-manager
# Déployer cert-manager v1.14.0 avec tous ses composants
kubectlapply-fhttps://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
kubectlwait--for=condition=readypod\-lapp.kubernetes.io/instance=cert-manager\-ncert-manager--timeout=120s
# Vérifier les pods cert-manager (controller, cainjector, webhook)
kubectlgetpods-ncert-manager
16.2 — Configurer les ClusterIssuers Let's Encrypt
bash
cat>/root/k8s-xsel/cert-manager-issuers.yaml<< 'EOF'---apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-stagingspec: acme: server: https://acme-staging-v02.api.letsencrypt.org/directory email: ton-email@xsel-services.com privateKeySecretRef: name: letsencrypt-staging-key solvers: - http01: ingress: ingressClassName: nginx---apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-prodspec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: ton-email@xsel-services.com privateKeySecretRef: name: letsencrypt-prod-key solvers: - http01: ingress: ingressClassName: nginxEOF# Créer les ClusterIssuers Let's Encrypt (staging et production)
kubectlapply-f/root/k8s-xsel/cert-manager-issuers.yaml
# Vérifier que les ClusterIssuers sont en état READY: True
kubectlgetclusterissuer
16.3 — Tester avec un certificat staging
ℹ️ Créer d'abord un enregistrement DNS de type A pointant vers l'IP publique HAProxy (95.111.238.94), puis vérifier la propagation.
bash
# Vérifier la propagation DNS# Vérifier la propagation DNS du domaine de test via Google DNS
nslookupsgh24.xsel-ci.com8.8.8.8
# Résultat attendu :# Nom : sgh24.xsel-ci.com# Address: 95.111.238.94
bash
# Créer un Certificate de test
cat>/root/k8s-xsel/test-cert.yaml<< 'EOF'apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: test-cert namespace: defaultspec: dnsNames: - sgh24.xsel-ci.com issuerRef: kind: ClusterIssuer name: letsencrypt-staging secretName: test-cert-tlsEOF# Demander l'émission d'un certificat de test via Let's Encrypt staging
kubectlapply-f/root/k8s-xsel/test-cert.yaml
# Vérifier après 2 à 3 minutes# Vérifier l'état de l'émission du certificat de test
kubectldescribecertificatetest-cert-ndefault|grep-A3"Conditions"
ℹ️ Le Role et RoleBinding nfs-provisioner-role-endpoints sont déjà inclus dans nfs-all.yaml depuis l'étape 13. Aucune modification du fichier NFS n'est nécessaire.
17.1 — Déployer Harbor avec Helm
bash
# Ajouter le repo Helm officiel Harbor
helmrepoaddharborhttps://helm.goharbor.io
# Mettre à jour le cache des repos Helm
helmrepoupdate
helminstallharborharbor/harbor\--namespacetools\--setexpose.type=ingress\--setexpose.ingress.hosts.core=registry.xsel-ci.com\--setexpose.ingress.className=nginx\--setexpose.ingress.annotations."nginx\.ingress\.kubernetes\.io/ssl-redirect"=false\--setexpose.tls.certSource=auto\--setexpose.tls.auto.commonName=registry.xsel-ci.com\--setexternalURL=https://registry.xsel-ci.com\--setharborAdminPassword=XselHarbor2025!\--setpersistence.persistentVolumeClaim.registry.storageClass=nfs-registry\--setpersistence.persistentVolumeClaim.database.storageClass=nfs-storage\--setpersistence.persistentVolumeClaim.redis.storageClass=nfs-storage
17.2 — Attendre l'émission du certificat TLS
bash
# Surveiller l'émission du certificat (2-5 min)# Surveiller en temps réel l'émission du certificat TLS Harbor
kubectlgetcertificate-ntools-w
# Vérifier les challenges ACME# Vérifier le challenge ACME HTTP-01 en cours
kubectlgetchallenges-ntools
# Vérifier la validité du certificat une fois émis
kubectlgetsecretharbor-tls-ntools-ojsonpath='{.data.tls\.crt}'|\base64-d|opensslx509-noout-dates-subject2>/dev/null
17.3 — Vérification complète
bash
# Vérifier que tous les pods Harbor sont en Running
kubectlgetpods-ntools
# Vérifier les volumes persistants Harbor
kubectlgetpvc-ntools
# Vérifier l'Ingress Harbor
kubectlgetingress-ntools
# Vérifier le statut du certificat TLS Harbor
kubectlgetcertificate-ntools
ℹ️ Ces bases de données sont pour les services internes XSEL (GitLab, monitoring, phpMyAdmin). Pour les clients, voir le Template DBaaS Client.
18.1 — Percona MySQL (PXC)
18.1.1 — Namespace et opérateur
bash
# Créer le namespace dédié à MySQL
kubectlcreatenamespacedevmysql
# Ajouter le repo Helm officiel Percona
helmrepoaddperconahttps://percona.github.io/percona-helm-charts/
# Mettre à jour le cache des repos Helm
helmrepoupdate
# Créer le fichier de values pour surcharger la request mémoire par défaut (20Mi → 64Mi)# ⚠️ Sans ce fichier, pxc-operator demande 20Mi ce qui est rejeté par le LimitRange# devmysql (min: 20Mi). On monte à 64Mi pour une marge opérationnelle correcte.
cat>/root/k8s-xsel/pxc-operator-values.yaml<< 'EOF'resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "500Mi" cpu: "200m"EOF# Installer pxc-operator avec les values personnalisées
helminstallpxc-operatorpercona/pxc-operator\--namespacedevmysql\--values/root/k8s-xsel/pxc-operator-values.yaml
# Vérifier que le pod opérateur démarre correctement
kubectlgetpods-ndevmysql-lapp.kubernetes.io/name=pxc-operator-w
18.1.2 — Déployer le cluster MySQL
⚠️ Il est impératif de passer des limits explicites via --set lors du helm install.
Sans elles, le LimitRange injecte max (8Gi/4CPU) comme limit par défaut sur chaque container.
6 pods × 8Gi = 48Gi de limits → dépasse immédiatement le quota limits.memory=24Gi et bloque tous les pods.
bash
helminstallpxc-clusterpercona/pxc-db\--namespacedevmysql\--setpxc.size=3\--setpxc.antiAffinityTopologyKey="none"\--sethaproxy.enabled=false\--setproxysql.enabled=true\--setproxysql.size=3\--setproxysql.antiAffinityTopologyKey="none"\--setpxc.persistence.storageClass=nfs-storage\--setpxc.persistence.size=20Gi\--setpxc.resources.requests.memory="1G"\--setpxc.resources.requests.cpu="600m"\--setpxc.resources.limits.memory="2G"\--setpxc.resources.limits.cpu="1"\--setproxysql.resources.requests.memory="1G"\--setproxysql.resources.requests.cpu="600m"\--setproxysql.resources.limits.memory="2G"\--setproxysql.resources.limits.cpu="1"# Surveiller en temps réel le démarrage des pods (3-5 min)
watchkubectlgetpods-ndevmysql
⚠️ Si les pods échouent avec exceeded quota: devmysql-quota (requests.cpu saturé) :
bash
# Mettre à jour le quota — requests.cpu à 6 pour couvrir 6 pods × 600m + sidecars
kubectlapply-f-<< 'EOF'apiVersion: v1kind: ResourceQuotametadata: name: devmysql-quota namespace: devmysqlspec: hard: requests.cpu: "6" requests.memory: 12Gi limits.cpu: "12" limits.memory: 24Gi pods: "20" services: "15" persistentvolumeclaims: "15"EOF
⚠️ Si les pods échouent avec must be less than or equal to memory limit (LimitRange injecte ses defaults sur les limits vides) :
bash
# Forcer les limits sur le CR — l'opérateur propage ensuite aux StatefulSets
kubectlpatchpxcpxc-cluster-pxc-db-ndevmysql--type='json'-p='[ {"op":"replace","path":"/spec/pxc/resources/limits","value":{"memory":"2G","cpu":"1"}}, {"op":"replace","path":"/spec/proxysql/resources/limits","value":{"memory":"2G","cpu":"1"}}]'# Surveiller la reprise
watchkubectlgetpods-ndevmysql
⚠️ Si des pods restent en Pending à cause de l'antiAffinity (seulement 2 workers) :
18.1.3 — Configurer ProxySQL pour mysql_native_password
⚠️ Étape critique — sans cette configuration, phpMyAdmin et les applications ne peuvent pas se connecter via ProxySQL. L'opérateur Percona configure ProxySQL avec caching_sha2_password par défaut, ce qui est incompatible avec les connexions sans SSL depuis phpMyAdmin.
bash
kubectlpatchpxcpxc-cluster-pxc-db-ndevmysql--type=merge-p'{ "spec": { "proxysql": { "configuration": "datadir=\"/var/lib/proxysql\"\n\nmysql_variables=\n{\n\thave_ssl=false\n\tdefault_authentication_plugin=\"mysql_native_password\"\n}\n" } }}'# L'opérateur redémarre ProxySQL avec la nouvelle config# Surveiller en temps réel le démarrage des pods MySQL
kubectlgetpods-ndevmysql-w
# VérifierROOT_PASS=$(kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data.root}'|base64-d)PROXYADMIN_PASS=$(kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data.proxyadmin}'|base64-d)
kubectlexecpxc-cluster-pxc-db-proxysql-0-ndevmysql-cproxysql--\mysql-uproxyadmin-p"$PROXYADMIN_PASS"-h127.0.0.1-P60322>/dev/null\-e"SELECT variable_name, variable_value FROM global_variables WHERE variable_name IN ('mysql-have_ssl','mysql-default_authentication_plugin');"
# Vérifier le statut du cluster MySQL Percona XtraDB
kubectlgetpxc-ndevmysql
# Vérifier l'état des pods MySQL (PXC + ProxySQL)
kubectlgetpods-ndevmysql
Résultat attendu :
text
NAME ENDPOINT STATUS PXC PROXYSQL
pxc-cluster-pxc-db pxc-cluster-pxc-db-proxysql.devmysql ready 3/3 3/3
18.1.5 — Récupérer les credentials
bash
# Tous les users créés par l'opérateur
kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data}'|\python3-c"import sys, json, base64data = json.load(sys.stdin)for k,v in data.items(): print(f'{k}: {base64.b64decode(v).decode()}')"
Credentials générés par l'opérateur pour le cluster XSEL :
User
Mot de passe
Rôle
root
Q?lWpfy-k]QTp@(j
Super admin MySQL
proxyadmin
^fO96mre>bh<]?I(!T
Admin interface ProxySQL (port 6032)
operator
%1B?p}zw$?*1IW*(
User interne opérateur Percona
monitor
LUY%x5M~p)WvOo#Y
Monitoring Galera / ProxySQL
replication
]Kj-I%fIaN4(YvN]
Réplication Galera inter-nœuds
xtrabackup
]vxwq}%=Or2l-^e+-
Backups XtraBackup
⚠️ Ces credentials sont stockés dans le secret pxc-cluster-pxc-db-secrets. Ne jamais les modifier manuellement — l'opérateur les synchronise automatiquement.
18.1.6 — Créer le user applicatif xsel_admin
bash
# Récupérer le mot de passe rootROOT_PASS=$(kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data.root}'|base64-d)# Créer le user xsel_admin avec mysql_native_password (compatible phpMyAdmin et apps sans SSL)
kubectlexecpxc-cluster-pxc-db-pxc-0-ndevmysql-cpxc--\mysql-uroot-p"$ROOT_PASS"-e" CREATE USER IF NOT EXISTS 'xsel_admin'@'%' IDENTIFIED WITH mysql_native_password BY 'XselAdmin2025!'; GRANT ALL PRIVILEGES ON *.* TO 'xsel_admin'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; SELECT user, host, plugin FROM mysql.user WHERE user='xsel_admin'; "
⚠️ Étape obligatoire — Tout user MySQL doit être déclaré dans ProxySQL pour pouvoir se connecter via lui. ProxySQL maintient sa propre liste d'utilisateurs indépendamment de MySQL.
bash
# Récupérer le mot de passe proxyadminPROXYADMIN_PASS=$(kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data.proxyadmin}'|base64-d)# Vérifier les hostgroups disponibles (10=writer, 11=reader)
kubectlexecpxc-cluster-pxc-db-proxysql-0-ndevmysql-cproxysql--\mysql-uproxyadmin-p"$PROXYADMIN_PASS"-h127.0.0.1-P6032\-e"SELECT hostgroup_id, hostname, port, status FROM mysql_servers;"# Enregistrer xsel_admin dans ProxySQL sur le hostgroup writer (10)
kubectlexecpxc-cluster-pxc-db-proxysql-0-ndevmysql-cproxysql--\mysql-uproxyadmin-p"$PROXYADMIN_PASS"-h127.0.0.1-P6032-e" INSERT INTO mysql_users (username, password, default_hostgroup, active) VALUES ('xsel_admin', 'XselAdmin2025!', 10, 1); LOAD MYSQL USERS TO RUNTIME; SAVE MYSQL USERS TO DISK; SELECT username, active, default_hostgroup FROM mysql_users WHERE username='xsel_admin'; "
bash
# Tester la connexion via ProxySQL
kubectlexecpxc-cluster-pxc-db-proxysql-0-ndevmysql-cproxysql--\mysql-uxsel_admin-p'XselAdmin2025!'\-h127.0.0.1-P3306\-e"SELECT user(), @@hostname, @@version;"
18.2.3 — Créer les tables de configuration phpMyAdmin
bash
# Redéfinir ROOT_PASS si nécessaireROOT_PASS=$(kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data.root}'|base64-d)# Télécharger le script SQL sur master-01
curl-shttps://raw.githubusercontent.com/phpmyadmin/phpmyadmin/STABLE/sql/create_tables.sql\-o/tmp/create_tables.sql
# Copier dans le pod PXC
kubectlcp/tmp/create_tables.sql\devmysql/pxc-cluster-pxc-db-pxc-0:/tmp/create_tables.sql-cpxc
# Créer la base phpmyadmin
kubectlexecpxc-cluster-pxc-db-pxc-0-ndevmysql-cpxc--\bash-c"MYSQL_PWD='$ROOT_PASS' mysql -uroot \ -e 'CREATE DATABASE IF NOT EXISTS phpmyadmin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"2>/dev/null
# Importer les tables via redirection stdin# ⚠️ Ne pas utiliser -e "source ..." : la commande source ne fonctionne pas en mode non-interactif# ⚠️ Passer le password via MYSQL_PWD pour éviter les problèmes avec les caractères spéciaux
kubectlexecpxc-cluster-pxc-db-pxc-0-ndevmysql-cpxc--\bash-c"MYSQL_PWD='$ROOT_PASS' mysql -uroot phpmyadmin < /tmp/create_tables.sql"2>/dev/null
# Vérifier — doit afficher 19
kubectlexecpxc-cluster-pxc-db-pxc-0-ndevmysql-cpxc--\bash-c"MYSQL_PWD='$ROOT_PASS' mysql -uroot \ -e \"SELECT COUNT(*) as tables_pma FROM information_schema.tables WHERE table_schema='phpmyadmin';\""2>/dev/null
18.2.4 — Accès phpMyAdmin
text
URL : https://phpmyadmin.xselcloud.com
User : xsel_admin
Password : XselAdmin2025!
⚠️ Ne pas se connecter avec root via phpMyAdmin — root utilise caching_sha2_password et passe par ProxySQL qui peut rejeter la connexion. Utiliser xsel_admin qui est enregistré dans ProxySQL avec mysql_native_password.
Résultats obtenus en production :
text
Serveur : pxc-cluster-pxc-db-proxysql.devmysql.svc.cluster.local:3306
Type : Percona XtraDB Cluster 8.4.8-8.1
User : xsel_admin@pxc-cluster-pxc-db-proxysql-2
Version : phpMyAdmin 5.2.3 ✅
Bases : information_schema, mysql, performance_schema, phpmyadmin, sys ✅
⚠️ Avertissement SSL : Connexion au serveur : SSL n'est pas utilisé — normal car on a configuré ProxySQL avec have_ssl=false pour le frontend. Le trafic est chiffré par HTTPS entre le navigateur et NGINX Ingress.
ℹ️ CoreDNS : Si la résolution DNS échoue depuis les pods, vérifier que CoreDNS utilise des DNS publics et non /etc/resolv.conf du node (qui peut contenir un domaine de recherche externe). Corriger via :
# Créer le namespace dédié à PostgreSQL
kubectlcreatenamespacedevpostgresql
# Créer le fichier de values pour surcharger la request mémoire par défaut (20Mi → 64Mi)# ⚠️ Même raison que pxc-operator : pg-operator demande 20Mi par défaut,# ce qui est rejeté par le LimitRange devpostgresql (min: 20Mi).
cat>/root/k8s-xsel/pg-operator-values.yaml<< 'EOF'resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "500Mi" cpu: "200m"EOF# Installer pg-operator avec les values personnalisées
helminstallpg-operatorpercona/pg-operator\--namespacedevpostgresql\--values/root/k8s-xsel/pg-operator-values.yaml
# Vérifier que le pod opérateur démarre correctement
kubectlgetpods-ndevpostgresql-lapp.kubernetes.io/name=pg-operator-w
18.3.2 — Déployer le cluster PostgreSQL
bash
helminstallpg-clusterpercona/pg-db\--namespacedevpostgresql\--setinstances[0].name=instance1\--setinstances[0].replicas=3\--setinstances[0].dataVolumeClaimSpec.storageClassName=nfs-storage\--setinstances[0].dataVolumeClaimSpec.accessModes[0]=ReadWriteOnce\--setinstances[0].dataVolumeClaimSpec.resources.requests.storage=20Gi
# Vérifier le statut du cluster PostgreSQL Percona
kubectlgetpg-ndevpostgresql
# Vérifier l'état des pods PostgreSQL (instances + PgBouncer)
kubectlgetpods-ndevpostgresql
# Voir les secrets disponibles# Lister les secrets de credentials PostgreSQL créés par l'opérateur
kubectlgetsecrets-ndevpostgresql|greppguser
# Récupérer tous les credentials
kubectlgetsecretpg-cluster-pg-db-pguser-pg-cluster-pg-db\-ndevpostgresql\-ojsonpath='{.data}'|\python3-c"import sys, json, base64data = json.load(sys.stdin)for k,v in data.items(): print(f'{k}: {base64.b64decode(v).decode()}')"
# Test TCP depuis pgAdmin vers PostgreSQL
kubectlexec-it\$(kubectlgetpod-ndevpostgresql-lapp=pgadmin-oname)\-ndevpostgresql--\python3-c"import sockets = socket.socket()try: s.connect(('pg-cluster-pg-db-pgbouncer.devpostgresql.svc.cluster.local', 5432)) print('CONNEXION OK port 5432')except Exception as e: print('ERREUR:', e)s.close()"# Résultat attendu : CONNEXION OK port 5432
Une fois connecté, ajouter le serveur PostgreSQL :
text
Clic droit sur "Servers" → Register → Server
Onglet Général :
Nom : XSEL PostgreSQL
Onglet Connexion :
Nom d'hôte / Adresse : pg-cluster-pg-db-primary.devpostgresql.svc
Port : 5432
Base de données : pg-cluster-pg-db
Identifiant : pg-cluster-pg-db
Mot de passe : n*_2hI;uALg^m]/+7.+(b/Ua
Enregistrer le mot de passe : ✅ Activer
Onglet Paramètres → Paramètres de Connexion :
Mode SSL (sslmode) : require
⚠️ Se connecter au primary directement (pg-cluster-pg-db-primary) et non via PgBouncer — PgBouncer utilise SCRAM-SHA-256 qui cause une erreur SASL authentication failed dans pgAdmin.
⚠️ Le mot de passe contient des caractères spéciaux — le copier-coller depuis le terminal est recommandé.
⚠️ Si l'erreur You must sign in to view this resource apparaît lors de l'enregistrement du serveur, la session pgAdmin a expiré — cliquer sur Enregistrer quand même, pgAdmin redemandera la connexion puis appliquera la config.
Résultats obtenus en production :
text
STATUS : ready ✅
POSTGRES : 3/3 ✅
PGBOUNCER: 3/3 ✅
Connexion pgAdmin → PostgreSQL primary : OK ✅
Serveur : pg-cluster-pg-db-primary.devpostgresql.svc:5432
SSL : require ✅
Version : Percona Distribution for PostgreSQL 18.4
18.4 — PMM (Percona Monitoring and Management)
bash
helminstallpmmpercona/pmm\--namespacemonitoring\--setsecret.pmm_password=XselPMM2025!\--setpersistence.storageClassName=nfs-storage\--setpersistence.dataSize=20Gi\--setservice.type=ClusterIP
cat>/root/k8s-xsel/pmm-ingress.yaml<< 'EOF'---apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: pmm-ingress namespace: monitoring annotations: nginx.ingress.kubernetes.io/ssl-redirect: "true" nginx.ingress.kubernetes.io/proxy-read-timeout: "900" cert-manager.io/cluster-issuer: "letsencrypt-prod"spec: ingressClassName: nginx tls: - hosts: - pmm.xselcloud.com secretName: pmm-tls-secret rules: - host: pmm.xselcloud.com http: paths: - path: / pathType: Prefix backend: service: name: monitoring-service port: number: 443EOF# Créer l'Ingress pour accéder à PMM via NGINX
kubectlapply-f/root/k8s-xsel/pmm-ingress.yaml
# Vérifier que le pod PMM est en Running
kubectlgetpods-nmonitoring|greppmm
# Corriger le nom du service et le protocole backend
kubectlpatchingresspmm-ingress-nmonitoring--type=json-p='[ { "op": "replace", "path": "/spec/rules/0/http/paths/0/backend/service/name", "value": "monitoring-service" }]'
kubectlannotateingresspmm-ingress-nmonitoring\nginx.ingress.kubernetes.io/backend-protocol=HTTPS\--overwrite
18.5 — Mode HA des clusters de bases de données
18.5.1 — MySQL PXC — Active/Active (Galera)
bash
ROOT_PASS=$(kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data.root}'|base64-d)
kubectlexec-itpxc-cluster-pxc-db-pxc-0-ndevmysql-cpxc--\mysql-uroot-p"$ROOT_PASS"2>/dev/null\-e"SHOW STATUS LIKE 'wsrep_cluster_size'; SHOW STATUS LIKE 'wsrep_local_state_comment'; SHOW STATUS LIKE 'wsrep_cluster_status';"
NAME ROLE
pg-cluster-pg-db-instance1-r58g-0 primary ← écritures + lectures
pg-cluster-pg-db-instance1-tk8f-0 replica ← lectures seulement
pg-cluster-pg-db-instance1-z7dr-0 replica ← lectures seulement
pg-cluster-pg-db-pgbouncer-xxx pgbouncer (x3)
18.5.3 — Résumé Comparatif
Base
Mode
Nodes
Écritures
Lectures
Failover
MySQL PXC
Active/Active (Galera)
3/3
Tous les nodes
Tous les nodes
Automatique
PostgreSQL
Active/Standby (Patroni)
1+2
Primary uniquement
Primary + Replicas
Automatique (Patroni)
ℹ️ PGBouncer distribue automatiquement les connexions. En cas de failover PostgreSQL, Patroni élit un nouveau primary et PGBouncer reroute automatiquement sans intervention manuelle.
Étape 19 — Exposition Externe des Bases de Données via HAProxy
Cette section documente la configuration de l'accès externe au cluster de bases de données (MySQL et PostgreSQL) depuis un serveur externe (ex : Plesk), en passant par HAProxy.
19.1 — Contexte et Architecture
Les services MySQL (ProxySQL) et PostgreSQL (PGBouncer) sont déployés en ClusterIP dans Kubernetes — ils ne sont donc accessibles qu'en interne au cluster. HAProxy tourne en Docker sur le host (xsel-haproxy-lb) et ne peut pas router directement vers les ClusterIPs.
La solution retenue :
Créer des NodePorts Kubernetes pour exposer les services sur les workers
Ajouter des frontends/backends TCP dans HAProxy pour router le trafic entrant vers ces NodePorts
⚠️ L'opérateur Percona PG écrase immédiatement toute modification directe du service pg-cluster-pg-db-pgbouncer. Il faut créer un service NodePort indépendant avec le bon label selector.
# Vérifier que le service NodePort PostgreSQL pointe vers les bons pods
kubectlgetendpointspg-pgbouncer-nodeport-ndevpostgresql
# Attendu : 3 endpoints (172.16.11.x:5432, 172.16.12.x:5432, 172.16.12.x:5432)# Tester la connectivité TCP vers le NodePort PGBouncer
nc-zv192.168.1.130432# Attendu : Connected
Copier la config existante, ajouter les blocs DB, puis recharger :
bash
# Copier la config depuis le container# Extraire la configuration HAProxy actuelle depuis le conteneur
dockercpxsel-haproxy-lb:/usr/local/etc/haproxy/haproxy.cfg/tmp/haproxy.cfg
# Ajouter les blocs MySQL et PostgreSQL
cat>>/tmp/haproxy.cfg<< 'EOFCFG'# ========================================# MYSQL - Percona XtraDB via ProxySQL# ========================================frontend mysql_frontend bind *:3306 mode tcp option tcplog default_backend mysql_backendbackend mysql_backend mode tcp balance roundrobin option tcp-check server devnode-01 192.168.1.1:30307 check inter 5s fall 3 rise 2 server devnode-02 192.168.1.2:30307 check inter 5s fall 3 rise 2 server devnode-03 192.168.1.3:30307 check inter 5s fall 3 rise 2# ========================================# POSTGRESQL - PGBouncer# ========================================frontend postgres_frontend bind *:5432 mode tcp option tcplog default_backend postgres_backendbackend postgres_backend mode tcp balance roundrobin option tcp-check server devnode-01 192.168.1.1:30432 check inter 5s fall 3 rise 2 server devnode-02 192.168.1.2:30432 check inter 5s fall 3 rise 2 server devnode-03 192.168.1.3:30432 check inter 5s fall 3 rise 2EOFCFG# Valider la config# Remettre la configuration modifiée dans le conteneur
dockercp/tmp/haproxy.cfgxsel-haproxy-lb:/usr/local/etc/haproxy/haproxy.cfg
# Valider la nouvelle configuration avant rechargement
dockerexecxsel-haproxy-lbhaproxy-f/usr/local/etc/haproxy/haproxy.cfg-c
# Attendu : Configuration file is valid# Recharger HAProxy# Redémarrer HAProxy pour appliquer la nouvelle configuration
dockerrestartxsel-haproxy-lb
19.5 — Créer un user dédié pour l'accès externe MySQL
⚠️ Ne pas utiliser root pour les connexions externes. L'opérateur Percona PXC réinitialise régulièrement le mot de passe de root dans ProxySQL, ce qui coupe les connexions. Il faut créer un user dédié (xsel_admin) que l'opérateur ne gère pas.
bash
ROOT_PASS=$(kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data.root}'|base64-d)PROXYADMIN_PASS=$(kubectlgetsecretpxc-cluster-pxc-db-secrets\-ndevmysql-ojsonpath='{.data.proxyadmin}'|base64-d)# 1. Créer le user xsel_admin dans PXC
kubectlexec-itpxc-cluster-pxc-db-pxc-0-ndevmysql-cpxc--\mysql-uroot-p"$ROOT_PASS"-e" CREATE USER IF NOT EXISTS 'xsel_admin'@'%' IDENTIFIED WITH mysql_native_password BY 'XselAdmin2025!'; GRANT ALL PRIVILEGES ON *.* TO 'xsel_admin'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES; "# 2. Ajouter xsel_admin dans ProxySQL
kubectlexec-itpxc-cluster-pxc-db-proxysql-0-ndevmysql-cproxysql--\mysql-uproxyadmin-p"$PROXYADMIN_PASS"-h127.0.0.1-P6032-e" INSERT INTO mysql_users(username, password, active, default_hostgroup) VALUES ('xsel_admin', 'XselAdmin2025!', 1, 11); LOAD MYSQL USERS TO RUNTIME; SAVE MYSQL USERS TO DISK; "
19.5.2 — Créer un user dédié pour l'accès externe PostgreSQL
⚠️ Ne pas utiliser pg-cluster-pg-db pour les connexions externes. Ce user est limité à sa propre base et ne peut pas créer de nouvelles bases. PGBouncer gère ses users via le CR PerconaPostgreSQL — il faut y déclarer le user officiellement.
⚠️ Le nom du user ne peut pas contenir de underscore (_) dans le CR Percona — utiliser xseladmin (sans underscore).
bash
# 1. Activer exposeSuperusers dans PGBouncer
kubectlpatchperconapgclusterpg-cluster-pg-db-ndevpostgresql--type=json-p='[ { "op": "replace", "path": "/spec/proxy/pgBouncer/exposeSuperusers", "value": true }]'# 2. Ajouter le user xseladmin dans le CR
kubectlpatchperconapgclusterpg-cluster-pg-db-ndevpostgresql--type=merge-p='{ "spec": { "users": [ { "name": "xseladmin", "databases": ["pg-cluster-pg-db"], "options": "SUPERUSER" } ] }}'# 3. Attendre la reconciliation
kubectlgetperconapgclusterpg-cluster-pg-db-ndevpostgresql-w
# Attendre STATUS=ready# 4. Récupérer le mot de passe généré par l'opérateur
kubectlgetsecretpg-cluster-pg-db-pguser-xseladmin\-ndevpostgresql\-ojsonpath='{.data}'|\python3-c"import sys, json, base64data = json.load(sys.stdin)for k,v in data.items(): print(f'{k}: {base64.b64decode(v).decode()}')"
Mot de passe obtenu en production : boy<?a|XVk6LD9t0=7SB[;?*
⚠️ Problème SCRAM-SHA-256 : PGBouncer utilise auth_query = pgbouncer.get_auth() pour authentifier les users. Par défaut, cette fonction retourne un hash MD5 incompatible avec SCRAM-SHA-256. Il faut forcer la mise à jour du mot de passe en SCRAM :
bash
# Récupérer le mot de passe en clairPG_PASS=$(kubectlgetsecretpg-cluster-pg-db-pguser-xseladmin\-ndevpostgresql\-ojsonpath='{.data.password}'|base64-d)# Mettre à jour avec SCRAM dans PostgreSQL
kubectlexec-itpg-cluster-pg-db-instance1-r58g-0\-ndevpostgresql-cdatabase--\psql-Upostgres-c"SET password_encryption = 'scram-sha-256';ALTER USER xseladmin PASSWORD '$PG_PASS';"# Vérifier que get_auth retourne SCRAM
kubectlexec-itpg-cluster-pg-db-instance1-r58g-0\-ndevpostgresql-cdatabase--\psql-Upostgres\-c"SELECT username, LEFT(password,30) FROM pgbouncer.get_auth('xseladmin');"# Attendu : SCRAM-SHA-256$4096:...
19.6 — Tests de Connexion
bash
# MySQL# Tester la connexion MySQL externe via HAProxy
mysql-h95.111.238.94-P3306-uxsel_admin-p'XselAdmin2025!'# Attendu : Welcome to the MySQL monitor — Percona XtraDB Cluster 8.4.7# PostgreSQL (installer le client si absent)# Installer le client PostgreSQL
dnfinstall-ypostgresql
# ⚠️ PGSSLMODE=require obligatoire — PGBouncer force SSL# ⚠️ Utiliser printf pour écrire .pgpass (évite l'interprétation des caractères spéciaux)PG_PASS=$(kubectlgetsecretpg-cluster-pg-db-pguser-xseladmin\-ndevpostgresql\-ojsonpath='{.data.password}'|base64-d)printf'95.111.238.94:5432:pg-cluster-pg-db:xseladmin:%s\n'"$PG_PASS">~/.pgpass
# Sécuriser le fichier .pgpass (lecture seule pour l'utilisateur)
chmod600~/.pgpass
# Tester la connexion PostgreSQL externe via HAProxy avec SSL obligatoirePGSSLMODE=requirepsql-h95.111.238.94-p5432-Uxseladmin-dpg-cluster-pg-db
# Attendu : SSL connection (TLSv1.3, TLS_AES_256_GCM_SHA384) — Percona PostgreSQL 18.3.1
19.7 — Résultats obtenus en production
text
=== MySQL ===
Server version: 8.4.7 Percona XtraDB Cluster (GPL) ✅
Connexion via HAProxy → ProxySQL → PXC : OK ✅
=== PostgreSQL ===
Server: 18.3 - Percona Server for PostgreSQL 18.3.1 ✅
SSL connection (TLSv1.3, TLS_AES_256_GCM_SHA384) ✅
Connexion via HAProxy → PGBouncer → PostgreSQL : OK ✅
19.8 — Récapitulatif des Accès Externes
Base
Host (public)
Port
User
Password
MySQL
95.111.238.94
3306
xsel_admin
XselAdmin2025!
PostgreSQL
95.111.238.94
5432
xseladmin
boy<?a|XVk6LD9t0=7SB[;?*
⚠️ Ces credentials sont sensibles — ne pas les transmettre en clair par email.
⚠️ Ne pas utiliser root pour MySQL depuis l'extérieur — l'opérateur Percona réinitialise son mot de passe dans ProxySQL.
⚠️ Ne pas utiliser pg-cluster-pg-db pour PostgreSQL depuis l'extérieur — ce user ne peut pas créer de bases.
⚠️ Pour PostgreSQL, utiliser PGPASSWORD='...' en variable d'environnement à cause des caractères spéciaux dans le mot de passe.
Bases de données
Étape 20 — Template DBaaS Client
ℹ️ Ce template permet de déployer un environnement complet MySQL + PostgreSQL pour un nouveau client en une seule commande. Chaque client obtient des clusters dédiés, une interface web (phpMyAdmin + pgAdmin) et un domaine sécurisé HTTPS.
⚠️ Chaque port NodePort premium doit être unique sur le cluster. Tenir un registre des ports utilisés.
⚠️ pgAdmin rejette les domaines .local — utiliser admin@[client].xsel-ci.com comme email par défaut.
⚠️ Les opérateurs MySQL et PostgreSQL (pxc-operator dans devmysql, pg-operator dans devpostgresql) surveillent automatiquement tous les namespaces — aucune configuration supplémentaire n'est nécessaire.
ℹ️ ProxySQL configuration — la section configuration dans le CR force mysql_native_password et désactive SSL frontend pour la compatibilité avec phpMyAdmin.
⚠️ ProxySQL crVersion — si l'opérateur déploie avec une crVersion plus récente (ex: 1.19.1), il peut ignorer la configuration ProxySQL. Dans ce cas, patcher manuellement après déploiement :
bash
`kubectlpatchpxcpxc-[CLIENT]-nclient-[CLIENT]-db--type=merge-p...`"spec":{"proxysql":{"configuration":"datadir=\"/var/lib/proxysql\"\n\nmysql_variables=\n{\n\thave_ssl=false\n\tdefault_authentication_plugin=\"mysql_native_password\"\n}\n"}}}'# Surveiller le redémarrage`kubectlgetpods-nclient-[CLIENT]-db-w`
20.6 — Résultats obtenus en production (client monetix)
# Supprimer complètement un client# Supprimer complètement l'environnement d'un client (données incluses)
kubectldeletenamespaceclient-<nom>-db
# Vérifier la suppression des PVCs NFS# Vérifier la suppression des répertoires NFS du client
ls/srv/nfs/kubernetes/|grep<nom>
⚠️ La suppression du namespace supprime aussi les PVCs et les données. Faire un backup avant.
vCluster permet de créer des clusters Kubernetes virtuels complets à l'intérieur du cluster XSEL physique. Chaque client obtient son propre Kubernetes isolé sans avoir besoin de serveurs dédiés.
# Créer le dossier
mkdir-p/root/k8s-xsel/vcluster
# Télécharger le CLI
curl-L-o/root/k8s-xsel/vcluster/vcluster-cli\"https://github.com/loft-sh/vcluster/releases/latest/download/vcluster-linux-amd64"
chmod+x/root/k8s-xsel/vcluster/vcluster-cli
# Lien symbolique global
ln-sf/root/k8s-xsel/vcluster/vcluster-cli/usr/local/bin/vcluster
# Vérifier# Vérifier la version du CLI vCluster installé
vclusterversion
# Résultat : vcluster version 0.34.0
21.3 — Créer un vCluster
bash
# Créer le namespace
kubectlcreatenamespacevcluster-xsel
# Créer le vCluster avec sync Ingress activé
vclustercreatevcluster-xsel\--namespacevcluster-xsel\--connect=false\--setsync.toHost.ingresses.enabled=true# Surveiller le démarrage
kubectlgetpods-nvcluster-xsel-w
Résultats attendus :
text
NAME READY STATUS RESTARTS
vcluster-xsel-0 1/1 Running 0
⚠️ --set sync.toHost.ingresses.enabled=true est obligatoire pour que les Ingress créés dans le vCluster soient synchronisés vers le cluster XSEL physique et accessibles via HAProxy.
21.4 — Se connecter au vCluster
bash
vclusterconnectvcluster-xsel--namespacevcluster-xsel
# Résultat : Switched active kube context to vcluster_vcluster-xsel_...# Vérifier les nodes# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
# NAME STATUS ROLES VERSION# xsel-devnode-01 Ready <none> v1.35.0# Vérifier les namespaces (propres au vCluster)# Vérifier que les namespaces ont bien été créés
kubectlgetnamespaces
# Se déconnecter# Restaurer le contexte kubectl vers le cluster physique
vclusterdisconnect
21.5 — Déployer une application dans le vCluster
bash
# Se connecter au vCluster
vclusterconnectvcluster-xsel--namespacevcluster-xsel
# Créer un namespace applicatif
kubectlcreatenamespacetest-app
# Déployer une app
kubectlcreatedeploymenthello-vcluster\--image=nginx:alpine\--namespacetest-app
# Exposer via Service
kubectlexposedeploymenthello-vcluster\--port=80\--target-port=80\--namespacetest-app
# Créer l'Ingress
cat<< 'EOF' | kubectl apply -f -apiVersion: networking.k8s.io/v1kind: Ingressmetadata: name: hello-ingress namespace: test-app annotations: nginx.ingress.kubernetes.io/ssl-redirect: "false"spec: ingressClassName: nginx rules: - host: hello-vcluster.xsel-ci.com http: paths: - path: / pathType: Prefix backend: service: name: hello-vcluster port: number: 80EOF# Vérifier l'Ingress — doit avoir une adresse# Vérifier la règle Ingress de l'application de test
kubectlgetingress-ntest-app
# NAME CLASS HOSTS ADDRESS# hello-ingress nginx hello-vcluster.xsel-ci.com 192.168.1.1
21.6 — Vérification de l'isolation
bash
# Ce que le client voit (dans le vCluster)
kubectlgetpods-ntest-app
# NAME READY# hello-vcluster-6c9cd74cd9-xg6dg 1/1 Running# Ce que XSEL voit (cluster physique)
kubectlgetpods-nvcluster-xsel
# NAME READY# vcluster-xsel-0 1/1 Running# hello-vcluster-6c9cd74cd9-xg6dg-x-test-app-x-vcluster-xsel 1/1 Running
Le pod est renommé automatiquement par vCluster :
text
<nom-pod>-x-<namespace>-x-<nom-vcluster>
Le client ne voit jamais ce nom — il voit uniquement son nom original dans son namespace.
21.7 — Test du flow complet
bash
# Test via NGINX Ingress (NodePort)
curl-H"Host: hello-vcluster.xsel-ci.com"http://192.168.1.1:30080
# Résultat : Welcome to nginx! ✅# Test via HAProxy (IP publique)
curl-H"Host: hello-vcluster.xsel-ci.com"http://95.111.238.94
# Résultat : Welcome to nginx! ✅# Test via DNS (si configuré)
curlhttp://hello-vcluster.xsel-ci.com
# Résultat : Welcome to nginx! ✅
Flow validé en production :
text
Internet
│
▼
HAProxy (95.111.238.94)
│
▼
NGINX Ingress (NodePort 30080/30443)
│ routing par Host header
▼
Service hello-vcluster (dans vCluster)
│
▼
Pod nginx (hello-vcluster-xxx) ✅
21.8 — Commandes utiles vCluster
bash
# Lister les vClusters# Lister tous les vClusters déployés dans le cluster physique
vclusterlist
# Se connecter
vclusterconnect<nom>--namespace<namespace>
# Se déconnecter# Restaurer le contexte kubectl vers le cluster physique
vclusterdisconnect
# Supprimer un vCluster
vclusterdelete<nom>--namespace<namespace>
# Voir les logs du vCluster
kubectllogsvcluster-<nom>-0-nvcluster-<namespace>
21.9 — Architecture multi-clients avec vCluster
text
Cluster physique XSEL
│
├── vcluster-bni/ ← cluster virtuel client BNI
├── vcluster-monetix/ ← cluster virtuel client MONETIX
├── vcluster-aceepci/ ← cluster virtuel client ACEEPCI
│
└── Chaque client :
├── kubectl access complet sur son vCluster
├── Ingress synchronisé vers NGINX XSEL
├── DNS : *.clientdomaine.com → 95.111.238.94
└── Isolation totale entre clients
Quand un client a des serveurs physiques dédiés :
bash
# 1. Joindre les serveurs au cluster XSEL
kubeadmjoin192.168.0.254:6443--token<token>...
# 2. Labelliser les nodes
kubectllabelnodenode-bni-db-01client=bnirole=database
# 3. Taint pour isolation stricte
kubectltaintnodenode-bni-db-01client=bni:NoSchedule
# 4. Créer le vCluster sur ses nodes dédiés
vclustercreatevcluster-bni\--namespacevcluster-bni\--setsync.toHost.ingresses.enabled=true\--setsyncer.extraArgs[0]="--node-selector=client=bni"\--setsyncer.extraArgs[1]="--enforce-node-selector"
21.10 — vCluster avec serveurs dédiés client
ℹ️ Cette section documente le déploiement d'un vCluster pour un client qui dispose de ses propres serveurs physiques. Les serveurs rejoignent le cluster XSEL comme workers dédiés via WireGuard VPN, puis un vCluster est créé sur ces nodes uniquement.
21.10.3 — Connecter les serveurs au cluster XSEL via WireGuard
bash
# Sur master-01 — générer le token de join# Générer une commande de join fraîche pour les nouveaux workers
kubeadmtokencreate--print-join-command
# Résultat (exemple) :# kubeadm join 192.168.0.254:6443 --token <token> # --discovery-token-ca-cert-hash sha256:<hash># Sur chaque serveur client — rejoindre le cluster
kubeadmjoin192.168.0.254:6443\--token<token>\--discovery-token-ca-cert-hashsha256:<hash>
21.10.4 — Labelliser et isoler les nodes client
bash
# Labelliser les nodes client
kubectllabelnodenode-bni-db-01client=bnirole=databasetier=premium
kubectllabelnodenode-bni-db-02client=bnirole=databasetier=premium
# Labelliser le nœud APP-01
kubectllabelnodenode-bni-app-01client=bnirole=apptier=premium
# Labelliser le nœud APP-02
kubectllabelnodenode-bni-app-02client=bnirole=apptier=premium
# Taint — aucun pod étranger ne peut aller sur ces nodes# Empêcher les pods non-BNI de s'exécuter sur ce nœud
kubectltaintnodenode-bni-db-01client=bni:NoSchedule
# Empêcher les pods non-BNI de s'exécuter sur ce nœud
kubectltaintnodenode-bni-db-02client=bni:NoSchedule
# Empêcher les pods non-BNI de s'exécuter sur ce nœud
kubectltaintnodenode-bni-app-01client=bni:NoSchedule
# Empêcher les pods non-BNI de s'exécuter sur ce nœud
kubectltaintnodenode-bni-app-02client=bni:NoSchedule
# Vérifier# Vérifier les labels et taints des nœuds client BNI
kubectlgetnodes--show-labels|grepbni
21.10.5 — Créer le vCluster sur les nodes dédiés
bash
# Créer le namespace client# Créer le namespace dédié au vCluster client BNI
kubectlcreatenamespacevcluster-bni
# Créer le vCluster en ciblant UNIQUEMENT les nodes du client
vclustercreatevcluster-bni\--namespacevcluster-bni\--connect=false\--setsync.toHost.ingresses.enabled=true\--setsyncer.extraArgs[0]="--node-selector=client=bni"\--setsyncer.extraArgs[1]="--enforce-node-selector"\--settolerations[0].key=client\--settolerations[0].operator=Equal\--settolerations[0].value=bni\--settolerations[0].effect=NoSchedule
# Surveiller le démarrage# Surveiller le démarrage du pod vCluster BNI
kubectlgetpods-nvcluster-bni-w
21.10.6 — Générer le kubeconfig client
bash
# Générer le kubeconfig pour le client
vclusterconnectvcluster-bni\--namespacevcluster-bni\--print-kubeconfig>/root/k8s-xsel/vcluster/kubeconfig-bni.yaml
# Le client utilisera ce fichier pour accéder à son vCluster# Transmettre au client de façon sécurisée# Afficher le kubeconfig à transmettre au client
cat/root/k8s-xsel/vcluster/kubeconfig-bni.yaml
⚠️ Remplacer 127.0.0.1 dans le kubeconfig par l'IP publique ou le domaine du cluster XSEL avant de le transmettre au client.
bash
# Remplacer l'IP locale par l'IP publique dans le kubeconfig
sed-i's/127.0.0.1/95.111.238.94/g'\/root/k8s-xsel/vcluster/kubeconfig-bni.yaml
21.10.7 — Accès client selon le plan
Plan Business — accès kubectl complet
Le client reçoit le fichier kubeconfig-bni.yaml et gère son cluster lui-même :
bash
# Le client installe kubectl sur sa machine# Linux
curl-LO"https://dl.k8s.io/release/v1.29.15/bin/linux/amd64/kubectl"
chmod+xkubectl&&mvkubectl/usr/local/bin/
# macOS
brewinstallkubectl
# Windows
chocoinstallkubernetes-cli
# Le client configure son accèsexportKUBECONFIG=./kubeconfig-bni.yaml
# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
# Vérifier que les namespaces ont bien été créés
kubectlgetnamespaces
Plan Enterprise — accès kubectl + interface web (Lens)
Le client reçoit le kubeconfig ET accès à Lens (IDE Kubernetes) :
bash
# XSEL installe Lens sur le serveur du client ou fournit l'accès# Lens se connecte avec le même kubeconfig# Interface graphique complète pour gérer le vCluster
Plan Standard — interface web uniquement (Rancher)
Le client n'a pas accès à kubectl — XSEL installe Rancher comme interface web :
bash
# XSEL déploie Rancher dans le vCluster
helminstallrancherrancher-latest/rancher\--namespacecattle-system\--sethostname=rancher.bni.ci\--setbootstrapPassword=XselRancher2025!
# Le client accède à https://rancher.bni.ci# Interface graphique sans kubectl
Plan
kubectl
Interface web
Prix
Standard
❌
Rancher ✅
💰
Business
✅
Optionnel
💰💰
Enterprise
✅
Lens ✅
💰💰💰
21.10.8 — Vérifications
bash
# Depuis XSEL — vérifier les nodes du vCluster
vclusterconnectvcluster-bni--namespacevcluster-bni
# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
# Le client ne doit voir QUE ses nodes dédiés# NAME STATUS ROLES# node-bni-app-01 Ready <none># node-bni-app-02 Ready <none># (les nodes BDD ne sont pas exposés dans le vCluster)# Restaurer le contexte kubectl vers le cluster physique
vclusterdisconnect
# Depuis XSEL — vérifier l'isolation# Vérifier l'état du vCluster BNI depuis le cluster physique
kubectlgetpods-nvcluster-bni
# Tous les pods du client sont visibles ici avec le suffixe -x-<namespace>-x-vcluster-bni
21.10.9 — Ce que le client peut faire
bash
# Le client configure son kubectlexportKUBECONFIG=./kubeconfig-bni.yaml
# Il voit ses nodes# Afficher les nœuds du cluster et leur statut
kubectlgetnodes
# Il crée ses namespaces
kubectlcreatenamespaceproduction
kubectlcreatenamespacestaging
# Il déploie ses applications
kubectlapply-fmon-app.yaml
# Il crée ses Ingress
kubectlapply-fmon-ingress.yaml
# → automatiquement synchronisé vers NGINX XSEL# Il installe ses propres outils
helminstallprometheusprometheus-community/prometheus
21.10.10 — Exposer le vCluster API via HAProxy
Pour que le client puisse utiliser kubectl depuis l'extérieur :
bash
# Créer un NodePort pour l'API server du vCluster
kubectlpatchservicevcluster-bni\-nvcluster-bni\--type=merge\-p'{"spec": {"type": "NodePort", "ports": [{"port": 443, "targetPort": 8443, "nodePort": 30643}]}}'# Ajouter dans HAProxy
cat>>/opt/xsel-haproxy/config/haproxy.cfg<< 'EOF'# vCluster BNI — API Serverfrontend vcluster_bni_frontend bind *:6443 mode tcp option tcplog default_backend vcluster_bni_backendbackend vcluster_bni_backend mode tcp option tcp-check server devnode-01 192.168.1.1:30643 check inter 5s server devnode-02 192.168.1.2:30643 check inter 5s server devnode-03 192.168.1.3:30643 check inter 5sEOF# Redémarrer HAProxy pour appliquer la nouvelle configuration
dockerrestartxsel-haproxy-lb
ℹ️ La stack monitoring XSEL se compose de deux parties complémentaires :
kube-prometheus-stack : collecte des métriques (Prometheus), visualisation (Grafana), alertes (AlertManager)
Loki + Promtail : centralisation et visualisation des logs de tous les pods
Les deux sont visualisés dans la même interface Grafana.
22.1 — Vérifier le namespace monitoring
bash
# Vérifier que le namespace monitoring existe# Vérifier que le namespace monitoring existe déjà
kubectlgetnamespacemonitoring
22.2 — Ajouter le repo Helm et rechercher le chart
bash
# Ajouter le repo Helm# Ajouter le repo Helm officiel de la communauté Prometheus
helmrepoaddprometheus-community\https://prometheus-community.github.io/helm-charts
# Mettre à jour le cache des repos Helm
helmrepoupdate
# Vérifier les charts disponibles# Vérifier la dernière version disponible du chart
helmsearchrepoprometheus-community/kube-prometheus-stack
22.3 — Créer le fichier de values
bash
cat>/root/k8s-xsel/kube-prometheus-stack-values.yaml<< 'EOF'# =============================================# XSEL — kube-prometheus-stack values# =============================================# Grafana — seul outil exposé publiquement# ✅ Grafana est l'interface de visualisation avec authentificationgrafana: enabled: true adminPassword: XselGrafana2025! persistence: enabled: true storageClassName: longhorn size: 5Gi ingress: enabled: true ingressClassName: nginx annotations: cert-manager.io/cluster-issuer: letsencrypt-prod hosts: - grafana.xselcloud.com tls: - secretName: grafana-tls hosts: - grafana.xselcloud.com# Prometheus — accès interne uniquement via port-forward# ❌ Pas d'Ingress public — Prometheus expose toutes les métriques du cluster# ✅ Accès : kubectl port-forward svc/kube-prometheus-stack-prometheus 9090:9090 -n monitoring# ✅ Grafana se connecte en interne via http://kube-prometheus-stack-prometheus:9090prometheus: prometheusSpec: retention: 30d storageSpec: volumeClaimTemplate: spec: storageClassName: longhorn accessModes: ["ReadWriteOnce"] resources: requests: storage: 20Gi resources: requests: cpu: "200m" memory: "400Mi" limits: cpu: "1" memory: "2Gi" # 🔒 Active la réception de métriques poussées depuis l'extérieur du cluster. # Nécessaire uniquement si un agent externe utilise prometheus.remote_write # (voir note à l'Étape 24 — cette méthode n'est PAS celle recommandée pour # les serveurs externes XSEL, qui utilisent le scraping pull à la place). # Sans ce flag, l'endpoint /api/v1/write n'existe pas et rejette toute requête. enableRemoteWriteReceiver: true # Cibles surveillées par scraping pull (Prometheus va chercher les métriques # lui-même) — voir Étape 24 pour la méthode recommandée de surveillance # des serveurs externes (HAProxy, futurs serveurs clients, etc.) additionalScrapeConfigs: # ⚠️ IP validée en pratique le 16/07/2026 : 192.168.0.2 (xsel-jumphost-01). # Le tableau d'adressage §🏗️ Architecture Cible indique 192.168.0.254 # pour xsel-jumphost-01 — à réconcilier avec la réalité du terrain # (vérifier `wg show` sur le jumphost pour confirmer l'IP WG100 réelle). - job_name: "haproxy-xsel" static_configs: - targets: ["192.168.0.2:9100"] labels: instance: "haproxy-xsel" role: "loadbalancer" # Ajouter ici chaque nouveau serveur externe surveillé (voir §24.4) : # - job_name: "serveur-client-nom" # static_configs: # - targets: ["192.168.X.X:9100"] # labels: # instance: "serveur-client-nom" # client: "NOM_CLIENT" ingress: enabled: false# AlertManager — accès interne uniquement via port-forward# ❌ Pas d'Ingress public — AlertManager permet de modifier les règles d'alerte# ✅ Accès : kubectl port-forward svc/kube-prometheus-stack-alertmanager 9093:9093 -n monitoringalertmanager: alertmanagerSpec: storage: volumeClaimTemplate: spec: storageClassName: longhorn accessModes: ["ReadWriteOnce"] resources: requests: storage: 2Gi resources: requests: cpu: "50m" memory: "64Mi" limits: cpu: "200m" memory: "256Mi" ingress: enabled: false# Node Exporter — métriques des nodesnodeExporter: enabled: true# Kube State Metrics — métriques KuberneteskubeStateMetrics: enabled: trueEOFecho"✅ Fichier créé"# Vérifier la syntaxe YAML
python3-c"import yaml; yaml.safe_load(open('/root/k8s-xsel/kube-prometheus-stack-values.yaml'))"&&echo"✅ YAML valide"
22.4 — Composants déployés
Composant
Rôle
URL
Grafana
Dashboards & visualisation métriques + logs
https://grafana.xselcloud.com (public)
Prometheus
Collecte et stockage des métriques
interne uniquement — kubectl port-forward
AlertManager
Gestion des alertes
interne uniquement — kubectl port-forward
Node Exporter
Métriques système des nodes
interne
Kube State Metrics
Métriques objets Kubernetes
interne
Loki
Centralisation et stockage des logs
interne
Promtail
Collecte des logs de tous les pods (DaemonSet)
interne
22.5 — StorageClasses utilisées
Le monitoring utilise Longhorn (installé à l'étape 14) pour la haute disponibilité :
Composant
StorageClass
Taille
Replicas
Grafana
longhorn
5 Gi
2
Prometheus
longhorn
20 Gi
2
AlertManager
longhorn
2 Gi
2
Loki
longhorn
20 Gi
1 (SingleBinary)
⚠️ La rétention Prometheus et Loki est configurée à 30 jours. Ajuster les tailles de stockage en conséquence selon le volume de métriques et de logs attendu.
⚠️ Les replicas Longhorn sont à 2 — le cluster XSEL dispose de 2 workers uniquement.
22.6 — Checklist pré-installation (obligatoire avant démo)
⚠️ À exécuter systématiquement avant toute installation de la stack monitoring, que ce soit pour la première installation ou pour une démonstration. Le cluster XSEL a des workers avec des CPU requests potentiellement saturés par PXC + ProxySQL + Longhorn + PostgreSQL + vClusters.
Étape 1 — Vérifier les ressources disponibles
bash
# Consommation CPU/RAM réelle des nœuds
kubectltopnodes
# CPU requests alloués sur chaque worker
kubectldescribenodexsel-devnode-01|grep-A6"Allocated resources"
kubectldescribenodexsel-devnode-02|grep-A6"Allocated resources"
Seuils acceptables pour installer la stack monitoring :
Métrique
Seuil OK
Action si dépassé
CPU requests devnode-01
< 85%
Réduire PXC/ProxySQL (Étape 2)
CPU requests devnode-02
< 85%
Réduire PXC/ProxySQL (Étape 2)
RAM
< 80%
Vérifier les pods consommateurs
Étape 2 — Réduire PXC/ProxySQL si workers > 85% CPU requests
bash
# Réduire les requests CPU de PXC et ProxySQL# ⚠️ Les performances MySQL ne sont pas impactées — seules les requests# (réservations de scheduling) sont réduites, pas les limits
kubectlpatchpxcpxc-cluster-pxc-db-ndevmysql--type=merge-p'{ "spec": { "pxc": { "resources": { "requests": {"memory": "1G", "cpu": "200m"}, "limits": {"memory": "2G", "cpu": "1"} } }, "proxysql": { "resources": { "requests": {"memory": "512Mi", "cpu": "100m"}, "limits": {"memory": "1G", "cpu": "500m"} } } }}'# Attendre que le rolling update PXC soit terminé — OBLIGATOIRE avant de continuer
watchkubectlgetpods-ndevmysql
# Résultat attendu : tous les pods pxc et proxysql en 3/3 Running# Vérifier que les workers sont maintenant < 85%
kubectldescribenodexsel-devnode-01|grep-A4"Allocated resources"
kubectldescribenodexsel-devnode-02|grep-A4"Allocated resources"
Étape 3 — Vérifier le quota monitoring
bash
# Vérifier que le quota monitoring est correctement configuré
kubectldescriberesourcequotamonitoring-quota-nmonitoring
# Résultat attendu :# requests.cpu : 0/2 ← quota à 2, pas 6# limits.cpu : 0/16# limits.memory : 0/32Gi
⚠️ Si requests.cpu Hard est à 6 au lieu de 2, appliquer la correction :
bash
kubectlapply-f/root/k8s-xsel/resource-quotas.yaml
Étape 4 — Vérifier le LimitRange monitoring
bash
kubectldescribelimitrange-nmonitoring
# Résultat attendu :# Container memory Min: 64Mi ← AlertManager doit avoir requests.memory >= 64Mi# Container cpu Min: 10m
⚠️ C'est déjà configuré dans le fichier kube-prometheus-stack-values.yaml de cette doc (alertmanager.resources.requests.memory: "64Mi").
ℹ️ L'installation prend environ 3 à 5 minutes. Le flag --wait attend que tous les pods soient Ready avant de rendre la main.
22.8 — Vérifications post-installation
bash
# Vérifier que tous les pods sont Running# Vérifier que tous les pods de la stack monitoring sont en Running
kubectlgetpods-nmonitoring
# Résultat attendu :# alertmanager-kube-prometheus-stack-alertmanager-0 2/2 Running# kube-prometheus-stack-grafana-<hash> 3/3 Running# kube-prometheus-stack-kube-state-metrics-<hash> 1/1 Running# kube-prometheus-stack-operator-<hash> 1/1 Running# kube-prometheus-stack-prometheus-node-exporter-... 1/1 Running (1 par node)# prometheus-kube-prometheus-stack-prometheus-0 2/2 Running# Vérifier les services# Vérifier les services Prometheus, Grafana et AlertManager
kubectlgetsvc-nmonitoring
# Vérifier les Ingress et certificats TLS# Vérifier les Ingress pour l'accès externe
kubectlgetingress-nmonitoring
# Vérifier les certificats TLS des interfaces web
kubectlgetcertificate-nmonitoring
# Vérifier les PVCs (stockage)# Vérifier que les volumes persistants sont en état Bound
kubectlgetpvc-nmonitoring
# Toutes doivent être Bound
22.9 — Vérifier les targets Prometheus
bash
# Port-forward pour accéder à Prometheus en local (si DNS pas encore configuré)
kubectlport-forwardsvc/kube-prometheus-stack-prometheus\-nmonitoring9090:9090&# Ouvrir dans un navigateur :# http://localhost:9090/targets# Toutes les targets doivent être en état "UP"# Stopper le port-forward# Arrêter le port-forward en arrière-plankill%1
ℹ️ Grafana est préconfiguré avec Prometheus comme datasource et inclut des dashboards Kubernetes prêts à l'emploi (nodes, pods, namespaces, etcd, etc.).
22.11 — DNS à configurer
⚠️ Seul Grafana est exposé publiquement. Prometheus, AlertManager et Loki sont accessibles uniquement en interne via port-forward pour des raisons de sécurité — ils exposent des données sensibles du cluster et ne doivent pas être accessibles depuis internet.
Domaine
Type
Valeur
Accès
grafana.xselcloud.com
A
95.111.238.94
✅ Public
prometheus.xselcloud.com
—
—
❌ Interne uniquement
alertmanager.xselcloud.com
—
—
❌ Interne uniquement
Accès interne à Prometheus et AlertManager (équipe XSEL uniquement) :
bash
# Accéder à Prometheus via tunnel depuis votre poste
kubectlport-forwardsvc/kube-prometheus-stack-prometheus\-nmonitoring9090:9090
# → http://localhost:9090# Accéder à AlertManager via tunnel
kubectlport-forwardsvc/kube-prometheus-stack-alertmanager\-nmonitoring9093:9093
# → http://localhost:9093
22.13 — Installation Loki + Promtail (Centralisation des logs)
ℹ️ Loki est le système de centralisation des logs développé par Grafana Labs. Il complète Prometheus : Prometheus collecte les métriques (CPU, RAM, latence), Loki collecte les logs (ce qui se passe dans chaque pod). Les deux sont visualisés dans la même interface Grafana.
Promtail est l'agent qui tourne sur chaque nœud, collecte les logs de tous les pods et les envoie à Loki.
ℹ️ Architecture XSEL :
text
Pods (toutes namespaces)
│
▼
Promtail (DaemonSet — 1 pod par nœud)
│
▼
Loki (stockage des logs — Longhorn)
│
▼
Grafana (visualisation métriques + logs)
22.12.1 — Ajouter le repo Grafana
bash
# Ajouter le repo Helm officiel Grafana
helmrepoaddgrafanahttps://grafana.github.io/helm-charts
# Mettre à jour le cache des repos Helm
helmrepoupdate
# Vérifier les versions disponibles
helmsearchrepografana/loki
helmsearchrepografana/promtail
22.12.2 — Créer le fichier de values Loki
bash
cat>/root/k8s-xsel/loki-values.yaml<< 'EOF'# =============================================# XSEL — Loki values# =============================================loki: auth_enabled: false # Stockage des logs sur Longhorn storage: type: filesystem # Rétention des logs : 30 jours limits_config: retention_period: 720h ingestion_rate_mb: 16 ingestion_burst_size_mb: 32 commonConfig: replication_factor: 1 schemaConfig: configs: - from: "2024-01-01" store: tsdb object_store: filesystem schema: v13 index: prefix: index_ period: 24h# Mode single binary (adapté à 2 workers)deploymentMode: SingleBinarysingleBinary: replicas: 1 persistence: enabled: true storageClass: longhorn size: 20Gi# Désactiver les composants distribués (inutiles avec 2 workers)backend: replicas: 0read: replicas: 0write: replicas: 0ingester: replicas: 0querier: replicas: 0queryFrontend: replicas: 0queryScheduler: replicas: 0distributor: replicas: 0compactor: replicas: 0indexGateway: replicas: 0bloomCompactor: replicas: 0bloomGateway: replicas: 0# Grafana intégré désactivé (on utilise celui de kube-prometheus-stack)grafana: enabled: false# ⚠️ CORRECTIF (incidents des 16/07 et 20-21/07/2026) — sans ces deux blocs,# le chart active par défaut deux caches Memcached dimensionnés pour de gros# clusters, ce qui a causé deux pannes distinctes sur XSEL :## 1. resultsCache par défaut : 500m CPU — a fait grimper les requests CPU du# worker à 98%, bloquant le scheduling d'autres pods (ex: node-collector# de Trivy Operator).# 2. chunksCache par défaut : allocatedMemory 8Gi → calcule une limite de# 8Gi×1.2=9830Mi, qui dépasse le plafond du LimitRange du namespace# monitoring (max 8Gi/conteneur) → le pod ne peut jamais se créer# ("FailedCreate ... maximum memory usage per Container is 8Gi").## Configurer ces deux caches dès l'install évite de refaire ce diagnostic.resultsCache: resources: requests: cpu: 100m memory: 512Mi limits: memory: 1229MichunksCache: allocatedMemory: 2048 resources: requests: cpu: 100m memory: 2Gi limits: memory: 2560MiEOFecho"✅ Fichier loki-values.yaml créé"
⚠️ Vérifier le LimitRange du namespace AVANT d'installer — si votre plafond diffère de 8Gi, ajustez chunksCache.allocatedMemory en conséquence (la limite calculée est allocatedMemory × 1.2 ; gardez au moins 10-15% de marge sous le plafond) :
⚠️ Piège Helm observé à plusieurs reprises (16/07, 20/07, 21/07/2026) : helm install avec --wait peut échouer avec context deadline exceeded sur ce chart (le pod met parfois plus de temps à devenir Ready que le timeout, notamment au premier démarrage). Le release reste alors bloqué en STATUS: failed, et toute nouvelle tentative avec helm install échoue ensuite avec cannot re-use a name. Utiliser helm upgrade --install d'emblée (idempotent, fonctionne que le release existe ou non) et lancer avec nohup pour survivre à une éventuelle coupure SSH :
bash
# Installer Loki dans le namespace monitoring
nohuphelmupgrade--installlokigrafana/loki\--namespacemonitoring\--values/root/k8s-xsel/loki-values.yaml\--wait\--timeout8m>/tmp/loki-install.log2>&1&# Patienter puis vérifier le résultat
cat/tmp/loki-install.log
helmstatusloki-nmonitoring
# Vérifier que le pod Loki est Running
kubectlgetpods-nmonitoring|greploki
⚠️ Si le release reste en pending-install/pending-upgrade/failed malgré tout (ex: après une coupure réseau pendant l'exécution) : ne PAS relancer helm install à répétition, cela empile des révisions bloquées. Identifier et supprimer les secrets Helm des révisions non déployées, puis relancer upgrade --install :
bash
helmhistoryloki-nmonitoring
kubectlgetsecrets-nmonitoring-lowner=helm,name=loki
# Supprimer UNIQUEMENT les révisions en pending-*/failed, jamais une révision deployed
kubectldeletesecret-nmonitoringsh.helm.release.v1.loki.vX
Résultat attendu :
text
loki-0 1/1 Running 0 2m
22.12.4 — Créer le fichier de values Promtail
bash
cat>/root/k8s-xsel/promtail-values.yaml<< 'EOF'# =============================================# XSEL — Promtail values# =============================================# Envoyer les logs vers Loki dans le namespace monitoringconfig: clients: - url: http://loki-gateway.monitoring.svc.cluster.local/loki/api/v1/push # ⚠️ Ignorer les logs plus anciens que 24h au démarrage # Sans cette config, Promtail tente d'envoyer TOUT l'historique des fichiers # de logs existants (peut remonter à plusieurs mois) → Loki les rejette avec # "timestamp too old" car sa rétention est de 30 jours mais l'ingestion # n'accepte que les logs récents (< reject_old_samples_max_age) positions: filename: /run/promtail/positions.yaml # ⚠️ CORRECTIF (incident du 15/07/2026) — ne PAS utiliser extraScrapeConfigs # ici : le chart Promtail définit déjà un job par défaut nommé "kubernetes-pods". # Le redéfinir via extraScrapeConfigs crée un conflit de nom au démarrage : # "found multiple scrape configs with job name kubernetes-pods" # → CrashLoopBackOff sur tous les pods Promtail. # pipelineStages injecte les étapes (comme le drop ci-dessous) DANS le job # existant, sans le dupliquer. snippets: pipelineStages: - match: selector: '{namespace!=""}' stages: - drop: older_than: 24h drop_counter_reason: "too old"# Promtail tourne comme DaemonSet — 1 pod par nœud worker# Il collecte automatiquement les logs de tous les pods via /var/log/podstolerations:- key: node-role.kubernetes.io/control-plane operator: Exists effect: NoScheduleEOFecho"✅ Fichier promtail-values.yaml créé"
22.12.5 — Installer Promtail
bash
# Installer Promtail dans le namespace monitoring
helminstallpromtailgrafana/promtail\--namespacemonitoring\--values/root/k8s-xsel/promtail-values.yaml\--wait\--timeout5m
# Vérifier que les pods Promtail sont Running (1 par nœud)
kubectlgetpods-nmonitoring|greppromtail
22.12.6 — Configurer la datasource Loki dans Grafana
bash
# Récupérer le nom du pod GrafanaGRAFANA_POD=$(kubectlgetpod-nmonitoring-lapp.kubernetes.io/name=grafana-ojsonpath='{.items[0].metadata.name}')echo"Pod Grafana : $GRAFANA_POD"# Ajouter Loki comme datasource via l'API Grafana
kubectlexec-nmonitoring$GRAFANA_POD-cgrafana--\curl-s-XPOST\-H"Content-Type: application/json"\-uadmin:XselGrafana2025!\http://localhost:3000/api/datasources\-d'{ "name": "Loki", "type": "loki", "url": "http://loki-gateway.monitoring.svc.cluster.local", "access": "proxy", "isDefault": false }'|python3-mjson.tool
1. Aller sur https://grafana.xselcloud.com
2. Menu → Explore
3. Datasource → sélectionner "Loki"
4. Dans le champ Label filters → sélectionner "namespace"
5. Choisir un namespace (ex: devmysql)
→ Les logs de tous les pods du namespace apparaissent en temps réel
ℹ️ Cette section permet de désinstaller proprement Loki et Promtail après validation, pour les réinstaller lors d'une démonstration.
bash
# Désinstaller Promtail
helmuninstallpromtail-nmonitoring
# Désinstaller Loki
helmuninstallloki-nmonitoring
# Supprimer le PVC Loki (données de logs)
kubectldeletepvc-nmonitoring-lapp.kubernetes.io/name=loki
# Vérifier que tout est supprimé
kubectlgetpods-nmonitoring|grep-E"loki|promtail"
22.14 — Corrélation entre les outils de monitoring
ℹ️ Chaque outil a un rôle précis et complémentaire. Ensemble ils forment une stack d'observabilité complète : métriques, logs et alertes dans une seule interface.
Exemple concret : un pod MySQL crashe à 3h du matin
Outil
Ce qu'il détecte
Node Exporter
RAM de devnode-02 à 95% à 02h58
Kube State Metrics
Pod pxc-cluster-pxc-db-pxc-2 passe en CrashLoopBackOff à 03h01
Prometheus
Enregistre les deux métriques et déclenche une alerte
AlertManager
Envoie un email à l'équipe XSEL à 03h01
Promtail
Collecte les logs du pod MySQL avant qu'il crashe
Loki
Stocke [ERROR] Out of memory: Kill process à 03h00
Grafana
Affiche sur le même graphique : RAM qui monte + logs d'erreur + moment du crash
ℹ️ Sans Loki : on sait que le pod a crashé mais pas pourquoi.
Sans Prometheus : on a les logs mais pas le contexte système.
Ensemble : picture complète — cause, contexte, timeline — en un seul endroit.
Valeur business pour XSEL
Situation
Sans monitoring
Avec cette stack
Un pod crashe en production
On le découvre quand le client appelle
AlertManager notifie avant que le client s'en aperçoive
"Pourquoi c'était lent hier à 14h ?"
Impossible à répondre
Grafana affiche le graphique exact avec les logs
Dimensionner les ressources d'un client
Estimation au doigt mouillé
Décision basée sur 30 jours de données réelles
Justifier la facture d'infrastructure
Difficile
Métriques d'utilisation par namespace/client
Déboguer une application
kubectl logs pod par pod
Recherche full-text sur tous les pods simultanément
22.15 — Troubleshooting Loki + Promtail
Erreur : "timestamp too old" dans les logs Promtail
Symptôme :
text
level=error msg="final error sending batch" status=400
error="entry has timestamp too old: 2026-05-06T08:28:10Z,
oldest acceptable timestamp is: 2026-07-01T13:15:52Z"
Cause :
Promtail lit les fichiers de logs depuis le début — y compris les logs historiques datant de plusieurs mois. Loki refuse les logs plus vieux que sa reject_old_samples_max_age (7 jours par défaut). Sur un cluster existant, les fichiers de logs dans /var/log/pods/ peuvent contenir des entrées très anciennes.
Conséquence :
Grafana affiche "No data" dans Explore → Loki car Promtail est encore en train de traiter les anciens logs avant d'arriver aux récents.
Résolution 1 — Attendre (temporaire) :
Promtail finit par traiter tous les anciens logs et arrive aux logs récents. Selon la taille des fichiers, cela peut prendre 5-30 minutes. Surveiller :
bash
# Quand les erreurs "timestamp too old" disparaissent → logs récents en cours d'envoi
kubectllogs-nmonitoring-lapp.kubernetes.io/name=promtail-f2>/dev/null|\grep-v"timestamp too old"|head-20
Résolution 2 — Redéployer Promtail avec filtre de date (permanent) :
bash
# Mettre à jour le fichier de values avec le filtre drop older_than# (déjà inclus dans le fichier promtail-values.yaml de cette doc)# Réinstaller Promtail
helmupgradepromtailgrafana/promtail\--namespacemonitoring\--values/root/k8s-xsel/promtail-values.yaml
# Vérifier que les erreurs ont disparu
kubectllogs-nmonitoring-lapp.kubernetes.io/name=promtail--tail=102>/dev/null
Résolution 3 — Vérifier les labels disponibles dans Loki :
bash
# Vérifier que Loki reçoit bien des logs récents
kubectlexec-nmonitoringloki-0-cloki--\wget-q-O-"http://localhost:3100/loki/api/v1/labels"2>/dev/null|\python3-mjson.tool
# Si "namespace" apparaît dans les labels → Loki reçoit des logs# Retourner dans Grafana → Explore → Loki → Run query
Erreur : "No data" dans Grafana Loki
Vérifications à faire :
bash
# 1. Vérifier que les pods Promtail envoient sans erreur
kubectllogs-nmonitoring-lapp.kubernetes.io/name=promtail--tail=52>/dev/null
# 2. Vérifier que Loki reçoit les logs
kubectlexec-nmonitoringloki-0-cloki--\wget-q-O-"http://localhost:3100/loki/api/v1/labels"2>/dev/null
# 3. Vérifier que la datasource Loki est bien configurée dans Grafana# Connections → Data sources → Loki → Test → "Data source connected and labels found"
ℹ️ Cette section permet de désinstaller proprement toute la stack monitoring pour la réinstaller lors d'une démonstration devant un public.
bash
# 1. Désinstaller Promtail
helmuninstallpromtail-nmonitoring
echo"✅ Promtail désinstallé"# 2. Désinstaller Loki
helmuninstallloki-nmonitoring
echo"✅ Loki désinstallé"# 3. Désinstaller kube-prometheus-stack
helmuninstallkube-prometheus-stack-nmonitoring
echo"✅ kube-prometheus-stack désinstallé"# 4. Supprimer les PVCs monitoring — en excluant PMM# ⚠️ Ne jamais utiliser --all : cela supprimerait aussi pmm-storage-pmm-0
kubectldeletepvc-nmonitoring\$(kubectlgetpvc-nmonitoring--no-headers|grep-vpmm|awk'{print $1}')echo"✅ PVCs supprimés (PMM conservé)"# Vérifier — seul pmm-storage-pmm-0 doit rester
kubectlgetpvc-nmonitoring
# 5. Supprimer les CRDs Prometheus (optionnel — les recréer au prochain install)
kubectldeletecrdprometheuses.monitoring.coreos.com\alertmanagers.monitoring.coreos.com\prometheusrules.monitoring.coreos.com\servicemonitors.monitoring.coreos.com\podmonitors.monitoring.coreos.com\probes.monitoring.coreos.com\thanosrulers.monitoring.coreos.com2>/dev/null||trueecho"✅ CRDs supprimées"# 6. Vérifier que tout est supprimé
kubectlgetpods-nmonitoring
kubectlgetpvc-nmonitoring
⚠️ La suppression des PVCs efface toutes les données de métriques et logs. En production, ne jamais supprimer les PVCs sans backup préalable.
22.17 — Résumé complet stack monitoring
text
✅ Namespace monitoring : créé
✅ kube-prometheus-stack : installé
✅ Grafana : https://grafana.xselcloud.com (admin/XselGrafana2025!) — public
✅ Prometheus : interne uniquement — port-forward :9090 (rétention 30j, Longhorn 20Gi)
✅ AlertManager : interne uniquement — port-forward :9093 (Longhorn 2Gi)
✅ Node Exporter : DaemonSet actif sur tous les nodes
✅ Kube State Metrics : actif
✅ Loki : centralisation des logs (rétention 30j, Longhorn 20Gi, SingleBinary)
✅ Promtail : DaemonSet collectant les logs de tous les pods
✅ TLS : Let's Encrypt (cert-manager) sur tous les Ingress
✅ Stockage : Longhorn (2 replicas, devnode-01 + devnode-02)
Supervision et administration
Étape 23 — Installation Rancher
ℹ️ Rancher est une plateforme de gestion Kubernetes multi-cluster. Pour XSEL, il permet de gérer le cluster physique + tous les vClusters clients depuis une interface web unique, avec isolation des accès par client, gestion RBAC visuelle, catalog Helm intégré et monitoring par cluster.
ℹ️ Rancher s'installe dans le cluster XSEL existant dans un namespace dédié cattle-system. Il expose son propre Ingress TLS via cert-manager.
23.1 — Prérequis
bash
# Vérifier que cert-manager est opérationnel (requis par Rancher)
kubectlgetpods-ncert-manager
# Vérifier que le cluster est en état Ready
kubectlgetnodes
# Vérifier les ressources disponibles (Rancher nécessite min 2 CPU / 4Gi RAM libres)
kubectltopnodes
23.2 — Installer Rancher
bash
# Ajouter le repo Helm Rancher (version stable)
helmrepoaddrancher-stablehttps://releases.rancher.com/server-charts/stable
# Mettre à jour le cache des repos Helm
helmrepoupdate
# Vérifier la dernière version disponible
helmsearchreporancher-stable/rancher
# Créer le namespace cattle-system
kubectlcreatenamespacecattle-system
# Installer Rancher avec cert-manager comme gestionnaire TLS# ⚠️ hostname doit correspondre à un enregistrement DNS pointant vers HAProxy (95.111.238.94)
helminstallrancherrancher-stable/rancher\--namespacecattle-system\--sethostname=rancher.xselcloud.com\--setbootstrapPassword=XselRancher2025!\--setingress.tls.source=letsEncrypt\--setletsEncrypt.email=admin@xsel-services.com\--setletsEncrypt.ingress.class=nginx\--setreplicas=1# ⚠️ Rancher crée son Ingress sans ingressClassName — NGINX ne le prend pas en charge# Il faut patcher manuellement pour que l'ADDRESS soit assignée
kubectlpatchingressrancher-ncattle-system\--type=merge-p'{"spec":{"ingressClassName":"nginx"}}'# Surveiller le déploiement (peut prendre 3-5 minutes)
kubectlrolloutstatusdeployment/rancher-ncattle-system
watchkubectlgetpods-ncattle-system
23.3 — Vérifier l'installation
bash
# Vérifier les pods Rancher
kubectlgetpods-ncattle-system
# Vérifier l'Ingress
kubectlgetingress-ncattle-system
# Vérifier le certificat TLS
kubectlgetcertificate-ncattle-system
Résultat attendu :
text
NAME READY STATUS RESTARTS AGE
rancher-xxxx 1/1 Running 0 3m
rancher-webhook-xxxx 1/1 Running 0 2m
23.4 — Configurer le DNS
Ajouter l'enregistrement DNS pointant vers HAProxy :
ℹ️ Au premier accès, Rancher demande de confirmer le bootstrap password puis de définir un nouveau mot de passe admin et l'URL du serveur (https://rancher.xselcloud.com).
23.6 — Importer le cluster XSEL dans Rancher
ℹ️ Rancher importe automatiquement le cluster sur lequel il est installé sous le nom local — aucune action manuelle nécessaire.
Résultats obtenus en production :
text
Cluster : local (Active) ✅
Provider : Local / Imported
Kubernetes : v1.29.15 amd64
CPU : 12 cores
Memory : 23 GiB
Pods : 79/220
Version : Rancher v2.14.3
Pour importer un cluster externe supplémentaire :
text
1. Menu → Cluster Management → Import Existing
2. Choisir "Generic"
3. Nom : xsel-cluster-client
4. Cliquer "Create"
5. Copier la commande kubectl apply fournie par Rancher
6. L'exécuter sur le master du cluster cible
23.7 — Configurer les accès clients dans Rancher
text
1. Sélectionner le cluster xsel-cluster
2. Menu → Projects/Namespaces
3. Créer un Project par client (ex: "BNI", "SafePartners")
4. Assigner le namespace vcluster-bni au projet BNI
5. Menu → Members → Add
6. Créer un user local pour le client
7. Lui assigner le rôle "Project Member" sur son projet uniquement
→ Le client voit uniquement son namespace dans l'interface Rancher
23.8 — Troubleshooting
⚠️ Si Rancher reste en Pending ou CrashLoopBackOff :
bash
# Vérifier les logs
kubectllogs-ncattle-system-lapp=rancher--tail=30# Vérifier que le webhook cert-manager fonctionne
kubectlgetvalidatingwebhookconfigurations|greprancher
Étape 24 — Surveillance des serveurs externes (HAProxy, serveurs clients)
ℹ️ Révision du 16/07/2026 — la version initiale de cette étape utilisait Grafana Alloy en mode push (remote_write/loki.write) depuis chaque serveur externe directement vers http://192.168.0.1:9090. Cette approche a échoué en pratique : kube-prometheus-stack-prometheus est un service Kubernetes de type ClusterIP, injoignable depuis l'extérieur du cluster même via un tunnel WireGuard qui aboutit à un nœud (le tunnel atteint le nœud, pas les pods derrière leurs ClusterIP). Résultat systématique : Connection refused.
La méthode ci-dessous a été validée en production le 16/07/2026 (test de connectivité pod → Calico → WireGuard → serveur externe réussi, cible haproxy-xsel confirmée up dans Prometheus). Elle utilise le pull : c'est Prometheus, à l'intérieur du cluster, qui va chercher les métriques — aucune exposition de service supplémentaire n'est nécessaire.
Cas d'usage XSEL :
Infrastructure XSEL externe (HAProxy) et serveurs clients Linux → node_exporter + scraping pull (§24.3/24.4, méthode recommandée)
Sites web / API externes → sonde HTTP/ICMP depuis le cluster, déjà en pull nativement (§24.5)
Serveurs clients Windows, ou besoin de centraliser des logs externes → Alloy reste pertinent mais nécessite d'exposer un endpoint (voir note §24.6)
24.1 — Architecture (méthode pull)
text
Cluster XSEL (namespace monitoring)
┌────────────────────────────────────────────┐
│ Prometheus ──────┐ │
│ (pod sur un │ va chercher │
│ devnode) │ les métriques │
│ Grafana (visualise) │ (scrape pull) │
└──────────────────────┼───────────────────────┘
│
Calico (NAT sortant)
│
WireGuard (wg100)
│
▼
Serveur externe (HAProxy, client...)
node_exporter --web.listen-address=<IP_WG100>:9100
(écoute UNIQUEMENT sur l'interface WireGuard)
Avantage : aucun port exposé côté cluster, aucun NodePort,
aucune règle de pare-feu supplémentaire à gérer.
C'est le cluster qui initie la connexion, jamais l'inverse.
⚠️ Prérequis réseau — le pod Prometheus tourne sur un des workers (xsel-devnode-01/02), pas sur un master. Il faut que ces nœuds disposent bien de l'interface wg100 (réseau 192.168.X.0/24) et que le NAT sortant Calico soit actif pour laisser transiter ce trafic. Vérifier avant toute nouvelle cible :
bash
# Sur le nœud qui héberge le pod Prometheus
kubectlgetpods-nmonitoring-lapp.kubernetes.io/name=prometheus-owide
ipaddrshowwg100
24.2 — Test de connectivité (à faire avant d'ajouter une nouvelle cible)
bash
# Remplacer <NOEUD> par le nœud qui héberge le pod Prometheus (voir 24.1)# Remplacer <IP_WG100_CIBLE> par l'IP WireGuard du serveur à surveiller
kubectlrunnetdebug--image=nicolaka/netshoot\--overrides='{"spec":{"nodeSelector":{"kubernetes.io/hostname":"<NOEUD>"}}}'\-nmonitoring--sleep3600
kubectlexec-nmonitoringnetdebug--ping-c3<IP_WG100_CIBLE>
kubectlexec-nmonitoringnetdebug--curl-v--max-time5http://<IP_WG100_CIBLE>:9100/metrics
kubectldeletepodnetdebug-nmonitoring
ℹ️ Si le ping échoue depuis le pod alors qu'il réussit depuis le nœud lui-même, le trafic est bloqué au niveau Calico (NAT sortant ou NetworkPolicy) — vérifier kubectl get ippool -o yaml | grep -A3 natOutgoing et kubectl get networkpolicy -n monitoring.
24.3 — Installer node_exporter sur le serveur externe (méthode recommandée)
ℹ️ Cette procédure est à exécuter sur le serveur externe (HAProxy, serveur client Linux) — pas dans le cluster XSEL.
bash
# 1. Télécharger et installer node_exporter
curl-LOhttps://github.com/prometheus/node_exporter/releases/latest/download/node_exporter-1.9.1.linux-amd64.tar.gz
tarxvfznode_exporter-*.tar.gz
sudomvnode_exporter-*/node_exporter/usr/local/bin/
# 2. Créer le service systemd# ⚠️ Remplacer 192.168.X.X par l'IP WireGuard (wg100) réelle de CE serveur# (vérifier avec : ip addr show wg100)
sudotee/etc/systemd/system/node_exporter.service<< 'EOF'[Unit]Description=Node ExporterAfter=network.target[Service]ExecStart=/usr/local/bin/node_exporter --web.listen-address=192.168.X.X:9100Restart=always[Install]WantedBy=multi-user.targetEOF
sudosystemctldaemon-reload
sudosystemctlenable--nownode_exporter
sudosystemctlstatusnode_exporter
✅ Sécurité native : --web.listen-address fait écouter node_exporter uniquement sur l'interface WireGuard. Le port 9100 n'existe simplement pas sur l'interface publique du serveur — aucun pare-feu supplémentaire n'est nécessaire.
24.4 — Ajouter la cible côté Prometheus
Éditer /root/k8s-xsel/kube-prometheus-stack-values.yaml — ajouter (ou compléter) additionalScrapeConfigs sous prometheus.prometheusSpec (voir §22.3 pour le bloc déjà en place pour HAProxy) :
yaml
prometheus:prometheusSpec:additionalScrapeConfigs:-job_name:"nom-du-serveur"static_configs:-targets:["192.168.X.X:9100"]labels:instance:"nom-du-serveur"client:"NOM_CLIENT"# si applicable
⚠️ Bug Helm observé le 16/07/2026 : helm upgrade --wait peut rester bloqué en pending-upgrade indéfiniment si la session SSH se coupe pendant l'exécution, même après expiration du --timeout. Toujours lancer avec nohup ... & pour survivre à une coupure :
Si un release reste bloqué en pending-upgrade/pending-rollback malgré tout, identifier et supprimer les secrets Helm des révisions bloquées (jamais la révision deployed) :
kubectlport-forward-nmonitoringsvc/kube-prometheus-stack-prometheus9090:9090&
curl-s'http://localhost:9090/api/v1/targets'|python3-c"import json, sysd = json.load(sys.stdin)for t in d['data']['activeTargets']: print(t['labels']['instance'], '->', t['health'])"
24.5 — Surveillance sans agent (HTTP/ICMP), via Alloy in-cluster
ℹ️ Ce mécanisme est déjà en pull nativement (Alloy dans le cluster sonde vers l'extérieur) — aucun problème d'exposition réseau, contrairement à l'ancienne méthode §24 en push. Il complète le node_exporter pour la disponibilité HTTP/HTTPS, le certificat SSL, la latence, sans rien installer sur la cible.
bash
# Ajouter le repo Helm Grafana (déjà fait à l'étape 22)
helmrepoaddgrafanahttps://grafana.github.io/helm-charts
helmrepoupdate
# Créer le namespace
kubectlcreatenamespacealloy
# Créer le fichier de values Alloy
cat>/root/k8s-xsel/alloy-values.yaml<< 'EOF'alloy: configMap: content: | // Envoi métriques vers Prometheus prometheus.remote_write "xsel_prometheus" { endpoint { url = "http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090/api/v1/write" } } // Surveillance HTTP/HTTPS sans agent prometheus.exporter.blackbox "http_checks" { config = "{ modules: { http_2xx: { prober: http, timeout: 10s } } }" # ⚠️ Le bloc de définition d'une cible s'appelle "target" (singulier), # pas "targets" (pluriel) — utiliser "targets" ici causera un # CrashLoopBackOff ("missing required block target"). target { name = "haproxy-xsel" address = "https://95.111.238.94" module = "http_2xx" labels = { service = "haproxy", env = "production" } } target { name = "grafana-xsel" address = "https://grafana.xselcloud.com" module = "http_2xx" labels = { service = "grafana", env = "production" } } // Ajouter ici les URLs des clients à surveiller // target { // name = "site-client-bni" // address = "https://bni.example.com" // module = "http_2xx" // labels = { client = "BNI" } // } } prometheus.scrape "blackbox" { targets = prometheus.exporter.blackbox.http_checks.targets forward_to = [prometheus.remote_write.xsel_prometheus.receiver] scrape_interval = "60s" } // Surveillance ICMP (ping) sans agent prometheus.exporter.blackbox "icmp_checks" { config = "{ modules: { icmp: { prober: icmp, timeout: 5s } } }" target { name = "devnode-01" address = "192.168.1.1" module = "icmp" labels = { role = "worker", node = "devnode-01" } } target { name = "devnode-02" address = "192.168.1.2" module = "icmp" labels = { role = "worker", node = "devnode-02" } } // Ajouter ici les serveurs clients à pinger // target { // name = "serveur-bni" // address = "10.0.1.5" // module = "icmp" // labels = { client = "BNI" } // } } prometheus.scrape "icmp" { targets = prometheus.exporter.blackbox.icmp_checks.targets forward_to = [prometheus.remote_write.xsel_prometheus.receiver] scrape_interval = "30s" }EOFecho"✅ Fichier alloy-values.yaml créé"# Installer Alloy (utiliser --install pour gérer install ET upgrade en une commande)
helmupgrade--installalloygrafana/alloy\--namespacealloy\--values/root/k8s-xsel/alloy-values.yaml
# Vérifier
kubectlgetpods-nalloy
Pour ajouter une nouvelle cible HTTP/ICMP, décommenter/ajouter un bloc target { ... } puis :
24.6 — Cas Windows et logs externes (à exposer avec précaution)
Deux besoins que la méthode pull (§24.3) ne couvre pas :
Besoin
Pourquoi le pull ne suffit pas
Piste
Serveur Windows client
node_exporter est Linux uniquement
windows_exporter (équivalent pull) ou Alloy avec config Windows
Logs de serveurs externes
Loki fonctionne uniquement en push — pas d'équivalent pull
Exposer uniquement Loki (pas Prometheus) via NodePort
⚠️ Si l'exposition de Loki est nécessaire : la surface de risque est plus limitée qu'exposer Prometheus (endpoint /loki/api/v1/push est write-only, aucune donnée consultable en retour), mais elle reste réelle. Un NodePort Kubernetes s'ouvre par défaut sur toutes les interfaces de tous les nœuds — un pare-feu explicite restreignant le port à l'interface wg100 est obligatoire sur chaque nœud, pas optionnel. Alternative plus sûre mais plus lourde : NetworkPolicy au niveau host via Calico (HostEndpoint). À valider avec l'équipe avant mise en œuvre.
24.7 — Dashboards Grafana pour la surveillance externe
text
Dans Grafana → Dashboards → Import :
Dashboard ID 1860 → Node Exporter Full (serveurs surveillés en pull)
Dashboard ID 7587 → Blackbox Exporter (disponibilité HTTP/ICMP)
Dashboard ID 14928 → Alloy Overview (si Alloy in-cluster utilisé)
24.8 — Résumé
text
✅ HAProxy + serveurs Linux : node_exporter (pull) — validé en production le 16/07/2026
✅ Disponibilité HTTP/ICMP : Alloy in-cluster (pull natif, déjà fonctionnel)
✅ Aucune exposition réseau supplémentaire nécessaire pour les métriques
✅ Serveurs Windows / logs externes : nécessitent une exposition ciblée (§24.6) — à planifier
✅ Visualisation : Grafana — dashboards dédiés par client
Supervision et administration
Étape 25 — Installation Portainer (Gestion Graphique des Containers)
ℹ️ Portainer est une interface web de gestion des containers qui permet de gérer depuis un seul endroit :
Le cluster Kubernetes XSEL (pods, deployments, services, volumes)
Des serveurs Docker externes (serveurs clients, infra XSEL hors cluster)
NAME READY STATUS RESTARTS AGE
portainer-xxxx 1/1 Running 0 1m
25.4 — Vérifier l'installation
bash
# Vérifier les pods
kubectlgetpods-nportainer
# Vérifier l'Ingress
kubectlgetingress-nportainer
# Vérifier le certificat TLS
kubectlgetcertificate-nportainer
# Vérifier le PVC
kubectlgetpvc-nportainer
25.5 — DNS à configurer
Domaine
Type
Valeur
Accès
portainer.xselcloud.com
A
95.111.238.94
✅ Public (équipe XSEL)
25.6 — Premier accès
text
URL : https://portainer.xselcloud.com
⚠️ Au premier accès, Portainer demande de créer un compte admin dans les 5 minutes — passé ce délai, il faut redémarrer le pod.
bash
# Si le délai est dépassé
kubectlrolloutrestartdeployment/portainer-nportainer
text
1. Aller sur https://portainer.xselcloud.com
2. Créer le compte admin :
Username : admin
Password : XselPortainer2025!
3. Choisir "Get Started" → le cluster local est automatiquement détecté
25.7 — Connecter un environnement Docker externe
ℹ️ Pour chaque serveur externe avec Docker à gérer depuis Portainer, installer l'agent Portainer sur ce serveur.
bash
# Sur le serveur Docker externe (ex: serveur client)# ⚠️ Remplacer PORTAINER_SERVER_IP par l'IP WireGuard de master-01 (192.168.0.1)# ou l'IP publique si le serveur n'est pas dans le VPN
dockerrun-d\-p9001:9001\--nameportainer_agent\--restart=always\-v/var/run/docker.sock:/var/run/docker.sock\-v/var/lib/docker/volumes:/var/lib/docker/volumes\portainer/agent:latest
Ensuite dans l'interface Portainer :
text
1. Menu → Environments → Add Environment
2. Choisir "Docker Standalone"
3. Choisir "Agent"
4. Name : Serveur-Client-BNI (ou autre)
5. Environment URL : IP_DU_SERVEUR:9001
6. Cliquer "Connect"
→ Le serveur Docker apparaît dans la liste des environnements
25.8 — Connecter un cluster Kubernetes externe
bash
# Sur le cluster Kubernetes externe
kubectlapply-fhttps://downloads.portainer.io/ce2-20/portainer-agent-k8s-lb.yaml
# Récupérer l'IP de l'agent
kubectlgetsvc-nportainer-agent
Étape 26 — Installation Trivy Operator (Sécurité et Scan de Vulnérabilités)
ℹ️ Trivy Operator est un outil de sécurité Kubernetes qui scanne automatiquement :
Les images Docker des pods (vulnérabilités CVE)
Les configurations Kubernetes (misconfigurations, non-conformités CIS)
Les secrets exposés dans les manifests
Les droits RBAC trop permissifs
Il tourne en continu et génère des rapports consultables via kubectl ou Grafana.
26.1 — Architecture
text
Trivy Operator (DaemonSet dans le cluster)
│
┌────┴─────────────────────────────┐
│ │ │
Scan images Scan configs Scan RBAC
(CVE) (misconfig) (droits)
│ │ │
└────────────┬─┘ │
▼ │
VulnerabilityReports │
ConfigAuditReports ────────┘
RbacAssessmentReports
│
▼
kubectl get vulnerabilityreports -A
ou Grafana Dashboard Trivy
26.2 — Installer Trivy Operator via Helm
⚠️ Piège YAML (incident du 20/07/2026) : si vous éditez ce fichier en plusieurs fois avec cat >> fichier.yaml << 'EOF' ... pour ajouter des sections, assurez-vous de ne jamais créer deux blocs avec la même clé racine (ex: deux fois operator:). YAML ne fusionne pas deux blocs de même clé — le second écrase silencieusement le premier, sans erreur de syntaxe. python3 -c "yaml.safe_load(...)" valide la syntaxe mais ne détecte PAS ce genre de doublon. Toujours relire le fichier complet (cat fichier.yaml) après un ajout, pas seulement les dernières lignes.
bash
# Ajouter le repo Helm Aqua Security
helmrepoaddaquahttps://aquasecurity.github.io/helm-charts/
helmrepoupdate
# Vérifier la version disponible
helmsearchrepoaqua/trivy-operator
# Créer le namespace
kubectlcreatenamespacetrivy-system
# Créer le fichier de values
cat>/root/k8s-xsel/trivy-operator-values.yaml<< 'EOF'# =============================================# XSEL — Trivy Operator values# =============================================trivy: # Utiliser le mode offline si pas d'accès internet # ou "remote" pour télécharger les CVE depuis internet mode: standalone # Sévérité des vulnérabilités à reporter severity: CRITICAL,HIGH,MEDIUM# Activer tous les types de scanoperator: scanJobTimeout: 5m # Scanner les vulnérabilités des images vulnerabilityScannerEnabled: true # Scanner les configurations Kubernetes configAuditScannerEnabled: true # Scanner les secrets exposés secretScannerEnabled: true # Scanner les droits RBAC rbacAssessmentScannerEnabled: true # Fréquence de scan — toutes les 12h vulnerabilityScannerScanOnlyCurrentRevisions: true# Ressources du Deployment trivy-operator lui-même — clé à la RACINE du# values (pas sous operator:, vérifié via `helm show values aqua/trivy-operator`).# ⚠️ 512Mi de limite mémoire est insuffisant en pratique : l'opérateur# surveille TOUS les namespaces et, avec un cluster chargé (PXC, PostgreSQL,# vClusters, monitoring...), il a été observé à 465Mi/512Mi (91%) puis# OOMKilled à répétition lors d'un pic de scans en parallèle (incident du# 20/07/2026). 1Gi donne une marge suffisante.resources: requests: cpu: "100m" memory: "256Mi" limits: cpu: "500m" memory: "1Gi"# node-collector scanne aussi les masters (décision XSEL du 20/07/2026).# ⚠️ Ceci déroge à la règle XSEL "masters = pods système uniquement,# tout le reste sur les workers" — décision explicite pour ne pas laisser# les masters hors du périmètre de sécurité (CVE OS, packages système).# Sans cette tolérance, node-collector reste bloqué en Pending sur les# masters à cause du taint control-plane ("untolerated taint").# Si vous préférez respecter strictement la règle et exclure les masters# du scan de sécurité, ne pas ajouter ce bloc.nodeCollector: tolerations: - key: "node-role.kubernetes.io/control-plane" operator: "Exists" effect: "NoSchedule"EOFecho"✅ Fichier trivy-operator-values.yaml créé"
python3-c"import yaml; yaml.safe_load(open('/root/k8s-xsel/trivy-operator-values.yaml'))"&&echo"✅ YAML valide"# Installer Trivy Operator (--install permet de relancer cette même# commande sans se soucier si le release existe déjà)
nohuphelmupgrade--installtrivy-operatoraqua/trivy-operator\--namespacetrivy-system\--values/root/k8s-xsel/trivy-operator-values.yaml\--wait\--timeout5m>/tmp/trivy-upgrade.log2>&1&# Vérifier (patienter puis relire le log)
cat/tmp/trivy-upgrade.log
kubectlgetpods-ntrivy-system
⚠️ Vérifications post-installation obligatoires (les 3 incidents du 20/07/2026 ne se voient qu'après quelques minutes, pas immédiatement après l'install) :
bash
# 1. L'opérateur reste-t-il stable (pas d'OOMKilled) après quelques minutes ?
sleep120
kubectlgetpods-ntrivy-system|greptrivy-operator
kubectltoppod-ntrivy-system-lapp.kubernetes.io/name=trivy-operator
# 2. node-collector arrive-t-il à se scheduler sur TOUS les nœuds (masters inclus) ?
kubectlgetpods-ntrivy-system-owide|grepnode-collector
# 3. Si node-collector reste Pending, vérifier la marge CPU du nœud ciblé# (le nodeSelector force node-collector sur un nœud précis, pas de failover)
kubectldescribepod-ntrivy-system<nom-node-collector>|tail-10
kubectldescribenode<noeud-concerné>|grep-A6"Allocated resources"
Si un node-collector reste bloqué pour cause de CPU insuffisant sur un worker, appliquer la même checklist que pour le monitoring (§22.6) : réduire temporairement les requests PXC/ProxySQL. Si loki-values.yaml a été créé avant la correction de §22.12.2 (donc sans resultsCache/chunksCache explicites), le chart réserve par défaut 500m CPU pour resultsCache — ajoutez la config de §22.12.2 à votre fichier existant puis :
NAME READY STATUS RESTARTS AGE
trivy-operator-xxxx 1/1 Running 0 1m
26.3 — Vérifier les scans
bash
# Attendre 2-3 minutes que les premiers scans se terminent# Voir les rapports de vulnérabilités par namespace
kubectlgetvulnerabilityreports-ndevmysql
kubectlgetvulnerabilityreports-ndevpostgresql
kubectlgetvulnerabilityreports-ntools
# Voir tous les rapports de tous les namespaces
kubectlgetvulnerabilityreports-A
# Voir le détail d'un rapport
kubectldescribevulnerabilityreport\$(kubectlgetvulnerabilityreports-ndevmysql-oname|head-1)\-ndevmysql
# Voir les misconfigurations Kubernetes
kubectlgetconfigauditreports-A
# Voir les secrets exposés
kubectlgetexposedsecretreports-A
# Voir les problèmes RBAC
kubectlgetrbacassessmentreports-A
26.4 — Filtrer par sévérité
⚠️ Piège bash (incident du 20/07/2026) : commande | python3 - << 'EOF' ... EOF ne fonctionne PAS comme on pourrait l'espérer. Le heredoc et le pipe se disputent tous les deux l'entrée standard (stdin) du script — python3 - lit le heredoc comme étant le code du script, et quand ce script essaie ensuite de lire sys.stdin pour récupérer les données de kubectl, il ne reste plus rien à lire (stdin déjà consommé). Résultat : json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0), sans que rien ne semble syntaxiquement faux. Toujours écrire le script dans un fichier séparé plutôt que de combiner heredoc et pipe.
bash
# Voir uniquement les vulnérabilités CRITICAL
kubectlgetvulnerabilityreports-A\-ojsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{" CRITICAL:"}{.report.summary.criticalCount}{"\n"}{end}'|\sort-t:-k2-rn|head-20
# Script résumé global — écrit dans un fichier séparé (voir piège ci-dessus)
cat>/tmp/trivy-summary.py<< 'EOF'import json, sysdata = json.load(sys.stdin)total = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}for item in data["items"]: s = item.get("report", {}).get("summary", {}) total["CRITICAL"] += s.get("criticalCount", 0) total["HIGH"] += s.get("highCount", 0) total["MEDIUM"] += s.get("mediumCount", 0) total["LOW"] += s.get("lowCount", 0)print("=== Résumé global des vulnérabilités ===")for sev, count in total.items(): print(" {:<8} : {}".format(sev, count))EOF
kubectlgetvulnerabilityreports-A-ojson|python3/tmp/trivy-summary.py
⚠️ Piège Python (serveurs sous Python 3.9, ex: RHEL/CentOS 9) : les f-strings avec des guillemets imbriqués identiques (f'{"texte":<10}') ne sont valides qu'à partir de Python 3.12 (SyntaxError: f-string: expecting '}'). Utilisez .format() ou f"{'texte':<10}" (guillemets différents à l'intérieur) pour rester compatible. Vérifiez votre version avec python3 --version avant d'écrire des scripts inline.
Pour identifier les workloads les plus à risque (top N par nombre de CVE CRITICAL) :
bash
kubectlgetvulnerabilityreports-A-ojson|python3-c"import json, sysdata = json.load(sys.stdin)results = []for item in data['items']: ns = item['metadata']['namespace'] name = item['metadata']['name'] s = item.get('report', {}).get('summary', {}) crit = s.get('criticalCount', 0) high = s.get('highCount', 0) if crit > 0: results.append((crit, high, ns, name))results.sort(reverse=True)print('{:<10}{:<8}{:<25}{}'.format('CRITICAL', 'HIGH', 'NAMESPACE', 'NAME'))for crit, high, ns, name in results[:20]: print('{:<10}{:<8}{:<25}{}'.format(crit, high, ns, name))"
Pour voir le détail (CVE ID, version installée, correctif disponible) d'un rapport précis :
bash
kubectlgetvulnerabilityreport<nom-du-rapport>-n<namespace>-ojson|python3-c"import json, sysdata = json.load(sys.stdin)vulns = data.get('report', {}).get('vulnerabilities', [])critical = [v for v in vulns if v.get('severity') == 'CRITICAL']print(f'{len(critical)} vulnérabilités CRITICAL:')for v in critical: print('-', v.get('vulnerabilityID'), '|', v.get('resource'), v.get('installedVersion'), '->', v.get('fixedVersion', 'AUCUN CORRECTIF'))"
26.5 — Dashboard Grafana pour Trivy
⚠️ Piège kubectl patch --type=merge (incident du 20/07/2026) : avec ce type de patch (JSON Merge Patch), les tableaux sont remplacés entièrement, pas fusionnés élément par élément. Fournir {"containers": [{"name": "trivy-operator", "env": [...]}]} remplace TOUT le tableau containers par cet objet partiel — qui n'a plus ni image ni resources. Résultat : The Deployment "trivy-operator" is invalid: spec.template.spec.containers[0].image: Required value. Pour ajouter une seule variable d'environnement sans toucher au reste du conteneur, utilisez kubectl set env, prévu pour ça :
bash
# Activer les métriques Trivy pour Prometheus
kubectlsetenvdeployment/trivy-operator-ntrivy-systemOPERATOR_METRICS_BIND_ADDRESS=:8080
# Vérifier que la variable est bien appliquée
kubectlgetdeploymenttrivy-operator-ntrivy-system-ojsonpath='{.spec.template.spec.containers[0].env}'|python3-mjson.tool
# Vérifier que le pod redémarre proprement (peut générer une vague de# re-scans temporaire — normal, se stabilise en quelques minutes)
kubectlgetpods-ntrivy-system
Dans Grafana :
text
Dashboards → Import → Dashboard ID : 17813
→ "Trivy Operator Dashboard" — résumé des CVE par namespace,
sévérité, image, avec historique des scans
26.6 — Intégration avec Harbor
ℹ️ Trivy peut scanner les images avant qu'elles soient déployées dans le cluster — directement dans Harbor. Configurer Harbor pour rejeter les images avec des CVE CRITICAL :
text
Dans Harbor :
1. Administration → Interrogation services → Trivy
2. Activer "Prevent vulnerable images from running"
3. Sévérité : Critical
→ Harbor bloque automatiquement le push/pull d'images avec des CVE critiques
26.7 — Résumé
text
✅ Namespace trivy-system : créé
✅ Helm chart installé : aqua/trivy-operator
✅ Scan images (CVE) : actif — rapport toutes les 12h
✅ Scan configurations : actif — misconfigurations Kubernetes
✅ Scan secrets exposés : actif
✅ Scan RBAC : actif
✅ Sévérités reportées : CRITICAL, HIGH, MEDIUM
✅ Métriques Prometheus : activées
✅ Dashboard Grafana : ID 17813
✅ Intégration Harbor : scan avant déploiement
26.8 — Stratégie de sécurité en profondeur XSEL
ℹ️ XSEL applique une stratégie de sécurité à 3 couches pour couvrir l'ensemble de l'infrastructure — cluster et serveurs externes.
text
┌─────────────────────────────────────────────────────────────┐
│ STRATÉGIE SÉCURITÉ XSEL — 3 COUCHES │
├─────────────────────────────────────────────────────────────┤
│ │
│ COUCHE 1 — PRÉVENTION (Harbor + Trivy) │
│ ✅ Scan des images au push dans Harbor │
│ ✅ Blocage automatique si CVE CRITICAL détecté │
│ → Une image vulnérable ne rentre JAMAIS dans le cluster │
│ │
│ COUCHE 2 — DÉTECTION EN CONTINU (Trivy Operator) │
│ ✅ Scan automatique de tous les pods en cours │
│ ✅ Scan des configurations Kubernetes │
│ ✅ Scan des secrets exposés et droits RBAC │
│ → Alertes Grafana si une vulnérabilité apparaît │
│ │
│ COUCHE 3 — SCAN NOCTURNE EXTERNE (Trivy CLI CronJob) │
│ ✅ Scan des serveurs clients hors cluster │
│ ✅ Scan des repositories Git des projets │
│ ✅ Scan de tous les registries Harbor │
│ → Rapport complet chaque matin dans Grafana │
│ │
└─────────────────────────────────────────────────────────────┘
26.9 — Installer Trivy CLI sur master-01
bash
# Installer Trivy CLI pour les scans à la demande
curl-sfLhttps://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh|\sh-s---b/usr/local/bin
# Vérifier
trivy--version
# Test rapide — scanner une image
trivyimagepercona/percona-xtradb-cluster:8.4.8-8.1\--severityCRITICAL,HIGH\--no-progress
⚠️ État au 21/07/2026 : Harbor est vide (repo_count: 0, aucun projet xsel, aucune image poussée). Le CronJob ci-dessous est prêt et fonctionnel, mais tant qu'aucune image n'existe réellement dans Harbor, chaque scan se solde silencieusement par 0 image trouvée — pas "0 CVE détectée" par sécurité, mais littéralement rien à scanner. Vérifier l'état réel avant de faire confiance au résultat :
Voir §26.10bis pour la marche à suivre une fois les premières images poussées.
bash
cat>/root/k8s-xsel/trivy-cronjob.yaml<< 'EOF'---# =============================================# XSEL — Trivy CronJob scan nocturne# Tourne toutes les nuits à 2h00# Scanne : images Harbor + repos Git + serveurs# =============================================apiVersion: batch/v1kind: CronJobmetadata: name: trivy-nightly-scan namespace: trivy-systemspec: # Toutes les nuits à 2h00 schedule: "0 2 * * *" concurrencyPolicy: Forbid successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: trivy-scanner # ✅ Version fixe — évite les surprises de mise à jour image: aquasec/trivy:0.72.0 resources: requests: cpu: "200m" memory: "256Mi" limits: cpu: "1" memory: "1Gi" env: # ✅ username ET password — le registre Harbor exige les deux - name: TRIVY_USERNAME valueFrom: secretKeyRef: name: trivy-harbor-secret key: username - name: TRIVY_PASSWORD valueFrom: secretKeyRef: name: trivy-harbor-secret key: password command: - /bin/sh - -c - | echo "=== Démarrage scan nocturne XSEL $(date) ===" # ---------------------------------------- # 1. Scanner les images Harbor XSEL # ✅ URL réelle du registre (vérifiée via l'API Harbor) : # registry.xsel-ci.com — PAS harbor.xselcloud.com # (nom de domaine générique resté d'une version antérieure # de cette doc, jamais le vrai domaine du registre XSEL) # ---------------------------------------- echo "--- Scan images Harbor ---" for image in \ "registry.xsel-ci.com/library/percona-mysql:latest" \ "registry.xsel-ci.com/library/percona-postgresql:latest" \ "registry.xsel-ci.com/xsel/api:latest"; do echo "Scan: $image" trivy image \ --severity CRITICAL,HIGH \ --no-progress \ --format json \ --output /tmp/scan-$(echo $image | tr '/:' '-').json \ $image 2>&1 | tee /tmp/last-scan-error.log || \ echo "⚠️ ÉCHEC du scan de $image — voir /tmp/last-scan-error.log" done # ---------------------------------------- # 2. Scanner les repositories Git # ---------------------------------------- echo "--- Scan repositories Git ---" # Ajouter ici les repos des projets clients # trivy repo https://github.com/xsel/projet-client \ # --severity CRITICAL,HIGH \ # --no-progress 2>/dev/null || true # ---------------------------------------- # 3. Résumé et envoi métriques Prometheus # ---------------------------------------- echo "--- Résumé ---" CRITICAL_COUNT=$(grep -r '"Severity":"CRITICAL"' /tmp/*.json 2>/dev/null | wc -l) HIGH_COUNT=$(grep -r '"Severity":"HIGH"' /tmp/*.json 2>/dev/null | wc -l) SCANNED_COUNT=$(ls /tmp/scan-*.json 2>/dev/null | wc -l) echo "Images scannées avec succès : $SCANNED_COUNT / 3" echo "CVE CRITICAL trouvées : $CRITICAL_COUNT" echo "CVE HIGH trouvées : $HIGH_COUNT" if [ "$SCANNED_COUNT" -eq 0 ]; then echo "⚠️ ATTENTION : aucune image n'a pu être scannée — le résultat" echo " 0 CRITICAL / 0 HIGH ne reflète PAS un cluster sain, il" echo " signifie qu'aucune image n'existe dans Harbor sous ces noms." fi # Envoyer les métriques vers Prometheus pushgateway (si installé) # curl -X POST http://prometheus-pushgateway:9091/metrics/job/trivy-scan \ # --data-binary "trivy_critical_total $CRITICAL_COUNT\ntrivy_high_total $HIGH_COUNT" echo "=== Scan terminé $(date) ==="EOFecho"✅ Fichier trivy-cronjob.yaml créé"# Créer le secret Harbor pour Trivy (username + password requis)# ⚠️ Piège (incident du 20/07/2026) : `kubectl create secret ... || true` ne# met PAS à jour un secret déjà existant — si vous modifiez username/password# plus tard, cette commande échouera silencieusement (le `|| true` avale# l'erreur "AlreadyExists") et l'ancien secret restera en place inchangé.# Pour modifier un secret existant, le supprimer explicitement d'abord :# kubectl delete secret trivy-harbor-secret -n trivy-system
kubectlcreatesecretgenerictrivy-harbor-secret\--from-literal=username=admin\--from-literal=password=XselHarbor2025!\-ntrivy-system2>/dev/null||true# Vérifier que le secret contient bien les deux clés
kubectlgetsecrettrivy-harbor-secret-ntrivy-system-ojsonpath='{.data}'|python3-mjson.tool
# Appliquer le CronJob
kubectlapply-f/root/k8s-xsel/trivy-cronjob.yaml
# Vérifier
kubectlgetcronjob-ntrivy-system
26.10bis — Une fois les premières images poussées dans Harbor
ℹ️ La liste d'images codée en dur dans §26.10 ne fonctionne que pour ces 3 noms précis. Dès que le pipeline CI/CD XSEL pousse ses premières images, deux options :
Option A — Mise à jour manuelle de la liste (rapide, mais nécessite d'éditer le CronJob à chaque nouvelle image) :
bash
# Lister les projets et repositories réels dans Harbor
curl-s-uadmin:XselHarbor2025!\"https://registry.xsel-ci.com/api/v2.0/projects"|python3-mjson.tool
curl-s-uadmin:XselHarbor2025!\"https://registry.xsel-ci.com/api/v2.0/projects/<PROJET>/repositories"|python3-mjson.tool
Mettre à jour la liste for image in ... du CronJob (§26.10) avec les noms réels, puis :
bash
kubectlapply-f/root/k8s-xsel/trivy-cronjob.yaml
Option B — Découverte dynamique via l'API Harbor (recommandé à moyen terme, aucune maintenance manuelle) : remplacer la boucle statique du CronJob par une découverte automatique de tous les projets/repositories/tags existants :
bash
# Extrait à intégrer dans le script du CronJob, à la place du bloc# "for image in ..." codé en durPROJECTS=$(curl-s-u"$TRIVY_USERNAME:$TRIVY_PASSWORD"\"https://registry.xsel-ci.com/api/v2.0/projects"|\python3-c"import json,sys; print('\n'.join(p['name'] for p in json.load(sys.stdin)))")forprojectin$PROJECTS;doREPOS=$(curl-s-u"$TRIVY_USERNAME:$TRIVY_PASSWORD"\"https://registry.xsel-ci.com/api/v2.0/projects/$project/repositories"|\python3-c"import json,sys; print('\n'.join(r['name'] for r in json.load(sys.stdin)))")forrepoin$REPOS;doimage="registry.xsel-ci.com/${repo}:latest"echo"Scan: $image"trivyimage--severityCRITICAL,HIGH--no-progress\--formatjson--output/tmp/scan-$(echo$image|tr'/:''-').json\$image2>&1||echo"⚠️ ÉCHEC du scan de $image"donedone
Après toute modification, valider avec un job de test avant de faire confiance au prochain scan planifié :
Vérifier que la ligne Images scannées avec succès : X / Y correspond bien au nombre d'images réellement présentes dans Harbor.
26.11 — Lancer un scan à la demande
bash
# Scan immédiat d'une image
trivyimage\--severityCRITICAL,HIGH\harbor.xselcloud.com/library/mon-image:latest
# Scan d'un repository Git
trivyrepo\--severityCRITICAL,HIGH\https://github.com/mon-organisation/mon-projet
# Scan d'un fichier de configuration Kubernetes
trivyconfig\--severityCRITICAL,HIGH\/root/k8s-xsel/
# Scan d'un serveur distant (filesystem)
trivyfs\--severityCRITICAL,HIGH\/var/lib/
# Lancer le CronJob immédiatement pour tester
kubectlcreatejobtrivy-test\--from=cronjob/trivy-nightly-scan\-ntrivy-system
# Voir les logs du scan
kubectllogs-ntrivy-system\$(kubectlgetpod-ntrivy-system-ljob-name=trivy-test-oname)-f
26.12 — Résumé stratégie sécurité complète
text
╔══════════════════════════════════════════════════════════════╗
║ SÉCURITÉ XSEL — RÉCAPITULATIF COMPLET ║
╠══════════════════════════════════════════════════════════════╣
║ ║
║ COUCHE 1 — PRÉVENTION ║
║ ✅ Harbor + Trivy : scan au push, blocage CVE CRITICAL ║
║ ║
║ COUCHE 2 — DÉTECTION CONTINUE (in-cluster) ║
║ ✅ Trivy Operator : pods, configs, secrets, RBAC ║
║ ✅ Fréquence : continu + toutes les 12h ║
║ ✅ Résultats : kubectl get vulnerabilityreports ║
║ ✅ Dashboard : Grafana ID 17813 ║
║ ║
║ COUCHE 3 — SCAN NOCTURNE EXTERNE ║
║ ✅ Trivy CronJob : toutes les nuits à 2h00 ║
║ ✅ Cibles : images Harbor, repos Git, serveurs ║
║ ✅ Trivy CLI : scans à la demande sur master-01 ║
║ ║
║ RÉSULTAT : aucune vulnérabilité critique ne passe inaperçue ║
╚══════════════════════════════════════════════════════════════╝
Opérations et CI/CD
Étape 27 — Ajout de DEVNODE-03 et Remédiation Infrastructure
ℹ️ Session du 26 août au 1er septembre 2026. Point de départ : ajout d'un 3ᵉ worker (xsel-devnode-03) au cluster à 2 devnodes. L'opération a révélé plusieurs dérives accumulées depuis l'installation initiale (répartition HA figée à 2 nodes, probes trop courtes, absence de sauvegardes PXC, etc.), traitées dans la foulée jusqu'à la cause racine plutôt qu'en contournement.
27.1 — Rejoindre DEVNODE-03 au cluster
DEVNODE-03 a rejoint via l'overlay WireGuard (IP 192.168.1.3) avec un pool Calico dédié 172.16.13.0/24.
bash
# Premier essai — join via le VIP HAProxy, timeout
sudokubeadmjoin192.168.0.254:6443\--token<TOKEN>\--discovery-token-ca-cert-hashsha256:<HASH>\--node-namexsel-devnode-03
# Échec : le VIP n'est pas routable depuis l'overlay wg100 à ce moment# Contournement retenu — join direct via l'IP d'un master
sudokubeadmjoin192.168.0.1:6443\--token<TOKEN>\--discovery-token-ca-cert-hashsha256:<HASH>\--node-namexsel-devnode-03
⚠️ Pour un futur DEVNODE-04 : toute modification de la config WireGuard sur un master (ajout de peer, wg-quick down/up) coupe brièvement (~90s) sa route vers le VIP HAProxy. Le kubelet local peut alors perdre la connexion à l'apiserver le temps de la bascule, provoquant un redémarrage ponctuel de kube-scheduler/kube-controller-manager sur ce master. Comportement attendu — le second master reste disponible pour l'élection de leader, aucune action requise.
ℹ️ Cohérent avec la convention existante (master-01-pool, master-02-pool, devnode-01-pool, devnode-02-pool), un pool /24 dédié par node.
27.3 — Nettoyage préalable au rééquilibrage
Trois anomalies ont été corrigées avant de répartir la charge sur le nouveau node :
Un volume Longhorn degraded (53Go) attribué à tort à Harbor appartenait en réalité à Prometheus.
harbor-database était en boucle de corruption WAL PostgreSQL (invalid record length).
Un membre PostgreSQL (pg-cluster-pg-db-instance1-6zc2-0) tournait en 3/4, un conteneur bloqué en recovery.
ℹ️ Décision retenue : wipe complet plutôt que réparation (aucune donnée de production en jeu à ce stade). Volumes Prometheus et Harbor réinitialisés via helm uninstall/suppression de PVC ; membre PostgreSQL reconstruit via patronictl reinit.
bash
# Reconstruire proprement un membre PostgreSQL bloqué
kubectlexec-itpg-cluster-pg-db-instance1-bcln-0-ndevpostgresql-cdatabase--\patronictlreinitpg-cluster-pg-db-hapg-cluster-pg-db-instance1-6zc2-0
27.4 — Corriger la répartition HA : preferred → required
Plusieurs composants multi-replica utilisaient une podAntiAffinity en preferred (héritée de l'époque à 2 devnodes), laissant le scheduler regrouper plusieurs replicas sur le même node malgré le 3ᵉ node disponible. Composants concernés : PXC/ProxySQL (devmysql) et les composants Longhorn (csi-attacher, csi-provisioner, csi-resizer, csi-snapshotter, longhorn-ui).
⚠️ Le patch seul ne bouge pas les pods existants. Il faut supprimer les anciens ReplicaSet (pas seulement les pods, sous peine de course entre anciennes et nouvelles générations) pour laisser le rollout converger :
Deadlock APIService (19 jours de crash-loop). Rancher restait en CrashLoopBackOff permanent (exit 1, cycle ~2min). Cause : Rancher attend au démarrage que sa propre APIService (v1.ext.cattle.io) devienne disponible, or celle-ci est servie par Rancher lui-même (imperative-api-extension, port 6666) — deadlock circulaire.
Probe et ressources insuffisantes. Après ce premier fix, la startupProbe par défaut (failureThreshold: 12 × periodSeconds: 10s = 2min) s'est révélée trop courte pour l'initialisation complète (restauration des repos Git, application de ~25 CRD).
⚠️ Ce patch peut être écrasé par un patch ultérieur sur le même conteneur (ex. ajout de resources, §27.9) si le Deployment est reconcilié entre-temps — vérifier après coup :
pmm-0 affichait 313 redémarrages historiques. Cause : startupProbe limitée à 5 min, alors que l'amorçage complet (ClickHouse, VictoriaMetrics, PostgreSQL interne, Grafana, puis /v1/readyz qui répond 200) peut prendre jusqu'à ~14 minutes.
ℹ️ La readinessProbe par défaut (/v1/readyz) n'a pas eu besoin d'être modifiée — elle finit par répondre correctement une fois la startupProbe assez tolérante.
27.7 — Trivy : migration en mode ClientServer
Le mode Standalone par défaut causait deux défaillances cumulées : des OOM récurrents (limite mémoire client à 500M, insuffisante pour scanner certains pods lourds) et des conflits de verrou de cache (cache may be in use by another process: timeout) quand plusieurs conteneurs du même pod scanné se disputaient un cache local partagé.
yaml
# Extrait du Deployment trivy-server — cache persistant, probes généreusesargs:-"server"-"--listen=0.0.0.0:4954"-"--cache-dir=/home/scanner/.cache/trivy"readinessProbe:initialDelaySeconds:60failureThreshold:30periodSeconds:15livenessProbe:initialDelaySeconds:180failureThreshold:10
bash
# Basculer l'opérateur en mode ClientServer
helmupgradetrivy-operatoraqua/trivy-operator-ntrivy-system\--version0.34.0--reuse-values\--settrivy.mode=ClientServer\--settrivy.serverURL=http://trivy-server.trivy-system.svc.cluster.local:4954\--settrivy.clientServerSkipUpdate=true
⚠️ Le PVC du serveur Trivy est en ReadWriteOnce — tout changement de Deployment (patch de probe, etc.) peut créer un conflit d'attachement entre ancien et nouveau pod. Déblocage : kubectl scale deployment trivy-server --replicas=0, attendre la disparition complète, nettoyer les ReplicaSet en trop, puis --replicas=1.
27.8 — Longhorn : deadlock de rebuild et StorageClass obsolète
Restarts très élevés (671 / 312 / 123 selon les nodes) observés sur longhorn-csi-plugin. Cause : 3 volumes (Loki, PMM, Prometheus) bloqués en degraded depuis plusieurs semaines, chacun retentant un rebuild en boucle sans jamais aboutir (Skipped rebuilding of replica because there is another rebuild in progress), générant une charge/instabilité continue qui finissait par faire échouer la liveness probe du plugin CSI.
bash
# Wipe complet des 3 volumes concernés (aucune donnée de prod en jeu)
kubectlscalestatefulsetloki-nmonitoring--replicas=0
kubectlscalestatefulsetpmm-nmonitoring--replicas=0
kubectlpatchprometheuskube-prometheus-stack-prometheus-nmonitoring--typemerge-p'{"spec":{"replicas":0}}'# Attendre la terminaison complète, puis :
kubectldeletepvc<pvc-loki><pvc-pmm><pvc-prometheus>-nmonitoring
# Puis remonter à replicas: 1
ℹ️ Confirmé résolu : 0 nouveau restart sur longhorn-csi-plugin sur les 2 minutes suivant le fix (contre plusieurs dizaines auparavant).
La StorageClass Longhorn par défaut n'avait jamais été mise à jour après le passage de 2 à 3 devnodes — tous les volumes créés depuis restaient limités à 2 replicas, systématiquement sur devnode-01/02, devnode-03 quasi exclu.
⚠️ Point technique important : la StorageClass Longhorn est régénérée automatiquement par longhorn-driver-deployer depuis le ConfigMap longhorn-storageclass — un kubectl patch storageclass direct est immédiatement écrasé. Le bon point d'intervention est le ConfigMap source :
⚠️ Une tentative d'upgrade Helm (longhorn 1.6.2 → 1.12.1, saut de version trop important) a échoué (pre-upgrade hooks failed, rollback impossible ensuite — service longhorn-conversion-webhook manquant). L'historique Helm reste en statut failed/superseded sans impact fonctionnel, mais à traiter proprement lors d'un futur upgrade planifié avec une version intermédiaire compatible.
ℹ️ Les volumes créés avant ce correctif restent à 2 replicas (pas de rééquilibrage rétroactif automatique dans Longhorn) — amélioration optionnelle non urgente pour une prochaine session.
27.9 — Backup PXC : mise en place complète
Le cluster PXC n'avait aucune sauvegarde configurée (storages: {}, schedule: []). Configuration retenue : stockage filesystem sur le NFS existant, planification quotidienne à 2h du matin, rétention de 5 backups.
⚠️ Point CRD à retenir : le champ persistentVolumeClaim attend une spec complète (l'opérateur crée le PVC lui-même), pas une référence claimName vers un PVC existant.
bash
# Valider avec un backup manuel de test
cat<<EOF | kubectl apply -f -apiVersion: pxc.percona.com/v1kind: PerconaXtraDBClusterBackupmetadata: name: manual-backup-test namespace: devmysqlspec: pxcCluster: pxc-cluster-pxc-db storageName: fs-pvcEOF
kubectlgetperconaxtradbclusterbackup-ndevmysql-w
Résultat : Succeeded en 119s.
27.10 — ResourceQuota sur les namespaces système
Ajout de quotas (absents jusqu'ici) sur cattle-system, trivy-system, longhorn-system, dimensionnés avec une marge ~3x la consommation observée (kubectl top pods). kube-system volontairement exclu (composants vitaux du cluster, ne jamais y contraindre les ressources).
27.11 — Resources manquantes sur les composants système
De nombreux pods système (rancher, rancher-webhook, api-extension, et l'ensemble des composants Longhorn : csi-attacher/provisioner/resizer/snapshotter, longhorn-csi-plugin, longhorn-manager, engine-image, longhorn-ui, longhorn-driver-deployer) n'avaient aucun resources.requests/limits défini — les rendant vulnérables à une éviction prioritaire (classe BestEffort) en cas de pression sur un node, et rendant les ResourceQuota du §27.10 inopérants pour eux.
bash
# Exemple type — calibré sur la consommation réelle observée (kubectl top pods), avec marge
kubectlpatchdeployment<nom>-n<namespace>--type='json'-p='[ {"op":"add","path":"/spec/template/spec/containers/0/resources","value":{ "requests":{"cpu":"10m","memory":"32Mi"}, "limits":{"cpu":"100m","memory":"128Mi"} }}]'
bash
# Pour un DaemonSet multi-conteneurs (ex. longhorn-csi-plugin, 3 conteneurs),# confirmer l'ordre exact avant de patcher chaque index séparément
kubectlgetdaemonset<nom>-n<namespace>-ojsonpath='{.spec.template.spec.containers[*].name}'
⚠️ Point de vigilance découvert en pratique : ce type de patch déclenche un rolling update qui peut se bloquer en Pending si une podAntiAffinity: required (1 pod/node, §27.4) est déjà en place et que l'ancien pod du node occupe encore l'unique slot disponible — nécessite de nettoyer manuellement l'ancien ReplicaSet comme au §27.4.
27.12 — Nettoyage et audit divers
keepalived tournait sur xsel-master-01 depuis 3 mois avec la configuration d'exemple par défaut du projet (jamais personnalisée, aucun VIP réel géré). Désactivé — sans lien avec le VIP HAProxy réel, qui est sur le serveur dédié séparé (Étape 0).
Compte globaladmin-user-vvvmc audité : il s'agit du compte administrateur Rancher par défaut (bootstrap, username: admin), légitime. Recommandation : vérifier la robustesse de son mot de passe (password-min-length Rancher configuré à 12 caractères, aucune exigence de complexité au-delà).
fail2ban installé et activé sur xsel-master-01 suite à la détection d'un brute-force SSH actif (plusieurs milliers de tentatives échouées).
bash
dnfinstall-yfail2ban
systemctlenable--nowfail2ban
27.13 — Points restants pour une prochaine session
Rééquilibrer les volumes Longhorn créés avant le fix numberOfReplicas (toujours à 2 replicas, devnode-01/02 uniquement) — non urgent, tous healthy.
Nettoyer/reprendre proprement l'historique Helm longhorn (statuts failed/superseded) lors d'un futur upgrade planifié avec une version intermédiaire compatible.
Étendre la couverture ResourceQuota + resources.requests/limits aux autres namespaces système si de nouveaux composants y sont ajoutés.
Opérations et CI/CD
Étape 28 — Installation et Initialisation de GitLab
ℹ️ Session du 21 septembre au 2 octobre 2026. GitLab (chart gitlab-10.3.1, app version v19.3.1) était déjà déployé dans le namespace tools avec une base PostgreSQL externe, mais la base de données n'avait jamais été correctement initialisée (schéma seul, sans données de seed) — aucun utilisateur n'existait, rendant l'instance inutilisable. Cette étape couvre le diagnostic et la remise en service complète.
28.1 — Dépendances dédiées : Garage (stockage S3) et Redis
GitLab s'appuie sur deux services internes déployés en amont, chacun en release Helm séparée dans tools, plutôt que sur les sous-charts intégrés du chart GitLab (postgresql.install: false, Redis/MinIO du chart également désactivés).
Garage — implémentation S3 légère et auto-hébergée (alternative à MinIO), utilisée pour tous les stockages objet de GitLab (artifacts, LFS, packages, uploads, backups — voir global.appConfig.object_store au §28.2) :
ℹ️ 3 répliques en quorum avec requiredDuringSchedulingIgnoredDuringExecution (1 pod/node strict, même logique qu'au §27.4) — chaque réplique a son propre volume data (10Gi) et meta (500Mi) Longhorn, soit 6 PVC au total (data-gitlab-garage-{0,1,2}, meta-gitlab-garage-{0,1,2}).
Service
ClusterIP
Ports
gitlab-garage
10.96.186.24
3900 (S3 API), 3902 (admin)
gitlab-garage-headless
None (headless)
3900, 3902
gitlab-garage-metrics
None (headless)
3903
⚠️ gitlab-garage-0 affiche un compteur de redémarrages élevé (546 au moment de la rédaction) sur une instabilité du pod — à investiguer séparément (hors scope de cette session) : vérifier kubectl describe pod gitlab-garage-0 -n tools et les logs pour déterminer si c'est lié à l'incident disque devnode-01/devnode-03 (§29) ou une cause propre à Garage.
Redis dédié GitLab — queue Sidekiq et cache applicatif, instance distincte du Redis de Harbor (harbor-redis, non documenté ici, scope Harbor §17) :
yaml
# helm get values gitlab-redis -n toolsarchitecture:standaloneauth:enabled:truepassword:XselRedis2026!master:persistence:size:8GistorageClass:longhorn
Service
ClusterIP
Port
gitlab-redis-master
10.96.60.173
6379
gitlab-redis-headless
None (headless)
6379
ℹ️ Référencé par le chart GitLab via global.redis.host: gitlab-redis-master.tools.svc.cluster.local et le Secret gitlab-redis (clé redis-password) — voir §28.2.
⚠️ Mot de passe Redis en clair dans les values Helm (auth.password) plutôt que via un Secret externe référencé — à améliorer dans une prochaine itération (cohérent avec la pratique du chart GitLab lui-même, qui référence ses propres secrets via global.redis.auth.secret plutôt que de les écrire en clair).
Installation initiale — ce qui est confirmé vs incertain :
bash
# Redis — chart Bitnami déjà présent dans les dépôts (helm repo list), installation reconstituable avec certitude
helmrepoaddbitnamihttps://charts.bitnami.com/bitnami
helminstallgitlab-redisbitnami/redis-ntools--version28.0.12-fgitlab-redis-values.yaml
# 1 seule révision ("Install complete", 01/09/2026 16:29:59) — jamais retouché depuis.
⚠️ Garage — origine du chart non reconstituable avec certitude.helm repo list ne contient aucun dépôt correspondant au chart garage-0.9.3 (le dépôt officiel du projet Garage, habituellement https://helm.deuxfleurs.fr, n'y figure pas). Il a soit été ajouté puis retiré après l'installation, soit le chart a été installé depuis un fichier .tgz local ou une registry OCI. Ne pas documenter une commande helm repo add/helm install inventée — si besoin de réinstaller un jour, rajouter le dépôt Garage officiel et vérifier la compatibilité de version avant de lancer.
Historique Helm (helm history gitlab-garage -n tools) : 7 révisions, dont 2 échecs d'upgrade identiques —
code
Upgrade "gitlab-garage" failed: cannot patch "gitlab-garage" with kind StatefulSet:
StatefulSet.apps "gitlab-garage" is invalid: spec: Forbidden: updates to statefulset spec
for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy',
'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden
Cause : Kubernetes interdit de modifier certains champs d'un StatefulSet existant (ex. volumeClaimTemplates, serviceName) via un simple helm upgrade — seuls replicas, template, updateStrategy et quelques autres sont mutables en place. Un changement de values touchant un champ immuable (probablement la taille ou la storageClass des PVC, §27.4/§27.11 documentent le même type de contrainte ailleurs) a été rejeté deux fois avant qu'un ajustement compatible n'aboutisse (révision 7, actuelle).
À retenir pour toute future modification de Garage : si un helm upgrade gitlab-garage échoue avec ce message, le champ modifié touche la structure du volume — il faut soit revenir à la valeur d'origine, soit supprimer/recréer le StatefulSet (helm uninstall/réinstallation), jamais forcer un patch direct dessus.
28.2 — Configuration du chart Helm déployée
GitLab utilise des services externes au chart plutôt que les sous-charts intégrés (PostgreSQL, Redis, Registry, Prometheus, NGINX Ingress tous désactivés au profit de l'infrastructure XSEL existante) :
yaml
# Valeurs effectives (helm get values gitlab -n tools)certmanager-issuer:email:admin@xsel-services.comgitlab:gitaly:persistence:size:15GistorageClass:longhornmigrations:activeDeadlineSeconds:14400toolbox:enabled:truebackups:objectStorage:backend:s3config:key:configsecret:gitlab-s3cmd-configresources:requests:{ cpu:200m, memory:1Gi}limits:{ cpu:1, memory:2Gi}gitlab-kas:enabled:false# Kubernetes Agent Server non utiliséglobal:hosts:domain:xselcloud.comgitlab:name:gitlab.xselcloud.comingress:class:nginxconfigureCertmanager:false# certificat géré manuellement, pas par le chartenabled:truetls:secretName:gitlab-tlspsql:database:gitlabhq_productionhost:pg-cluster-pg-db-pgbouncer.devpostgresql.svc.cluster.localport:5432username:gitlabpassword:{ secret:gitlab-psql-secret, key:password}ssl:secret:gitlab-psql-sslserverCA:server-ca.pemclientCertificate:client-certificate.pemclientKey:client-key.pemredis:host:gitlab-redis-master.tools.svc.cluster.localport:6379auth:{ enabled:true, secret:gitlab-redis, key:redis-password}appConfig:object_store:enabled:trueproxy_download:trueconnection:{ secret:gitlab-object-storage, key:connection}artifacts:{ bucket:gitlab-artifacts}lfs:{ bucket:gitlab-lfs}packages:{ bucket:gitlab-packages}uploads:{ bucket:gitlab-uploads}backups:{ bucket:gitlab-backups, tmpBucket:gitlab-backups}installCertmanager:falsenginx-ingress:{ enabled:false}postgresql:{ install:false}prometheus:{ install:false}registry:{ enabled:false}
ℹ️ Le stockage objet (artifacts, LFS, packages, uploads, backups) pointe vers Garage (gitlab-garage, chart garage-0.9.3, release séparée dans tools), une implémentation S3 auto-hébergée légère — cohérent avec le choix de ne pas dépendre d'un registry/object storage externe.
⚠️ Le certificat TLS (gitlab-tls) est géré manuellement, pas par cert-manager via le chart (configureCertmanager: false, installCertmanager: false) — l'Ingress porte l'annotation cert-manager.io/cluster-issuer en dehors des valeurs du chart lui-même.
Cause racine : un gitlab-rake db:migrate manuel (exécuté lors d'une session antérieure) avait créé le schéma gitlabhq_production mais pas les données de seed — aucun Job gitlab-migrations standard (qui exécute migrations et seed) n'avait jamais existé dans ce cluster.
⚠️ Protocole retenu pour toute interaction gitlab-rails console : coller les commandes une par une, en attendant le résultat de chacune. Un collage multi-lignes rapide fait entrer l'irb dans un état de prompt imbriqué (gitlab(prod)*) difficile à récupérer — nécessite Ctrl+C pour en sortir proprement.
28.4 — Correction : seed de la base et compte root
Crée l'utilisateur root (mot de passe aléatoire à réinitialiser obligatoirement), l'ApplicationSetting par défaut, l'Organization par défaut, les clés de signature CI JWT/Job Token et les clés Cloud Connector.
Définition du mot de passe root via la console (commande par commande) :
⚠️ Le validateur de robustesse de mot de passe de GitLab rejette les combinaisons dictionnaire-like même complexes en apparence (XselAdmin2026!Secure → rejeté : « combinaisons de mots ou de lettres couramment utilisées »). Une chaîne véritablement aléatoire (Xk9#mQ2vLp7$wZt4) est passée sans problème.
NoMethodError: undefined method 'id' for nil
at RegistrationsController#create
Cause : échec de la validation de robustesse du mot de passe (new_user_password_is_invalid) déclenche un bug secondaire propre à GitLab 19.3.1 — resource reste nil et le contrôleur tente malgré tout d'appeler .id dessus.
ℹ️ Bug produit GitLab, non lié à la configuration infra. Pas de correctif côté cluster — contournement : créer les comptes via gitlab-rails console plutôt que par le formulaire public tant que l'instance n'est pas patchée (voir §28.6).
28.6 — Patch de sécurité critique (reporté)
Un popup admin signale une faille critique sur la version installée (19.3.1), avec recommandation de mise à jour immédiate vers 19.3.3 ou 19.4.1.
⚠️ Non appliqué en session live — reporté via « Remind me again in 3 days » dans l'UI admin. À planifier comme tâche dédiée (changement de version majeur sur un chart gitlab-10.3.1, hors scope d'une intervention à chaud).
28.7 — Restriction des inscriptions publiques
Recommandation appliquée : désactiver les inscriptions publiques sur une instance à usage interne.
Admin Area → Settings → General → Sign-up restrictions → Enable sign-up → Deactivate
ℹ️ Référence croisée avec le bug §28.5 : désactiver les inscriptions publiques supprime aussi l'exposition à ce bug (les comptes sont alors créés uniquement par un admin).
28.8 — Redémarrages en boucle des pods webservice et sidekiq (sonde de liveness trop courte)
✅ Corrigé le 15 octobre 2026. Cette section remplace l'ancienne version (« instabilité transitoire, auto-résolue »), qui était fausse : les pods n'avaient fait que sembler stables. Au 13 octobre, gitlab-webservice-default-t4vl6 en était à 1042 redémarrages, et gnc87 est passé de 67 à 356 en deux jours.
Symptômes
gitlab-webservice-default en 1/2, CrashLoopBackOff, avec Readiness probe failed … :8080/-/readiness … connection refused dans le tableau de bord.
kubectl get pod -o jsonpath : reason=Error exit=137 (jamais OOMKilled) sur le conteneur webservice.
Un nouveau pod sidekiq qui s'arrête proprement (reason=Error exit=1, log stopped … background task stopped) environ 3 min après son lancement, sans aucune erreur applicative avant.
Les logs s'arrêtent à chaque fois en plein démarrage (Preloading application, Checking state of dynamic postgres partitions).
Cause : le démarrage de Rails dépasse la fenêtre de la livenessProbe par défaut du chart (premier test à 20 s, un test par minute, 3 échecs = pod tué après environ 2 min 20). Le conteneur webservice est limité à 1 cœur CPU, ce qui allonge probablement le chargement (métriques, « Syncing dynamic postgres partitions »). Seul le démarrage échoue : une fois lancé, un pod reste stable, mais tout redémarrage (par exemple lors d'une vague d'incidents disque, §28.10) l'expose de nouveau à la boucle.
Hypothèse écartée : l'OOM. Elle était plausible (un pod utilisait 3032 Mi pour une limite de 3 Gi), mais elle ne tient pas : aucun des ~380 redémarrages n'est marqué OOMKilled, et un pod à 4 Gi de limite (3012 Mi utilisés) redémarrait tout autant.
⚠️ Piège de diagnostic. L'absence d'événement Killing dans kubectl get events ne prouve pas que la sonde n'est pas en cause : Kubernetes limite le nombre d'événements par objet (un seul Created/Started visible malgré 16 redémarrages). Se fier plutôt au moment de l'arrêt dans les logs (environ 2 min après le début) et au motif de sortie.
Correctif : élargir la sonde. La clé gitlab.webservice.deployment.livenessProbe s'applique au webservice et gitlab.sidekiq.livenessProbe à sidekiq (valeurs complètes dans gitlab-values-correctifs.yaml, §28.10) :
Résultat : après le correctif, les deux pods webservice ont tenu plus de 45 minutes sans aucun redémarrage (contre un redémarrage toutes les quelques minutes avant), et le nouveau pod sidekiq a passé la barre des 14 minutes. Même logique que Rancher (§27.5), PMM (§27.6) et Jenkins (§31.4).
28.9 — Pièges du namespace tools : ResourceQuota, LimitRange et valeurs par défaut du chart
ℹ️ Ces trois mécanismes ont bloqué pendant des jours des déploiements GitLab sans qu'aucun pod ne plante. Ils ne se voient que dans les événements des ReplicaSets : kubectl describe rs <nom> -n tools | tail.
1. tools-quota (ResourceQuota)
Plafond
Valeur d'origine
Valeur actuelle (15/10/2026)
requests.memory
12 Gi
20 Gi
limits.memory
24 Gi
40 Gi
limits.cpu
16
24
requests.cpu
6
6 (inchangé)
Le second pod sidekiq réclamé par l'autoscaler était refusé depuis 13 jours (FailedCreate … exceeded quota, ×592 dans les événements), et le premier rolling update du webservice (limite portée à 4 Gi) n'a pas pu créer son pod.
Un rolling update a besoin de place pour un pod de plus : environ 3 Gi de requests et 4,6 Gi de limites pour le webservice, 2 Gi pour sidekiq. Avec un quota presque plein, la mise à jour se bloque sans erreur visible côté pods.
ℹ️ Un quota est un plafond, pas une réservation : relever ces valeurs ne consomme rien sur les nœuds.
2. tools-limit-range (LimitRange)
Par conteneur : minimum 10m CPU et 64 Mi, maximum 4 CPU et 8 Gi, requête par défaut 50m et 128 Mi, limite par défaut 500m et 512 Mi.
Un pod sans limite explicite hérite donc de 512 Mi : c'est ce qui a tué (OOMKilled) le job de migrations après un helm upgrade. On a fixé gitlab.migrations.resources.limits.memory: 2Gi (requests 1 Gi).
Le chart GitLab demande par défaut cpu: 0 et memory: 6M pour gitlab-shell, sous le minimum du LimitRange. Après un helm upgrade, le nouveau ReplicaSet (UP-TO-DATE 0) était refusé (minimum cpu usage per Container is 10m, but request is 0). Les anciens pods fonctionnaient encore, avec des ressources posées auparavant à la main. Correctif : gitlab.gitlab-shell.resources.requests à 50m et 64Mi dans les valeurs Helm.
⚠️ Règle : tout réglage de ressources fait avec kubectl patch ou kubectl edit sur un Deployment géré par Helm est effacé au prochain helm upgrade. Tout doit vivre dans les valeurs Helm (§28.10).
3. CPU réservé très au-dessus de l'usage réel
Les trois nœuds de travail étaient à 89-92 % de CPU réservé (requests) pour 19 à 35 % de CPU réellement utilisé (kubectl top nodes).
Un pod sidekiq supplémentaire (900m de requests par défaut, pour environ 300m consommés) restait Pending (Insufficient cpu). On a baissé gitlab.sidekiq.resources.requests.cpu à 500m.
Même sans quota atteint, un rolling update bloque si aucun nœud n'a la place d'un pod en plus. À revérifier avant toute mise à jour GitLab :
# Sauvegarde des valeurs avant toute modification
helmgetvaluesgitlab-ntools-oyaml>~/k8s-xsel/gitlab-values-avant-$(date+%F).yaml
helmupgradegitlabgitlab/gitlab-ntools--version10.3.1--reuse-values\-f~/k8s-xsel/gitlab-values-correctifs.yaml--timeout30m
# Retour en arrière si besoin (la révision 11 est l'état du 21/09/2026)
helmhistorygitlab-ntools
helmrollbackgitlab<révision>-ntools
ℹ️ Après un changement du template, comptez 4 à 5 minutes avant qu'un pod webservice ou sidekiq soit prêt. L'ancien pod reste en service tant que le nouveau n'est pas prêt, donc il n'y a pas d'interruption.
ℹ️ Les notes du chart signalent « PostgreSQL minimum 17 » : la base PG du cluster est en 18.4 (Percona), c'est compatible.
Incident gitlab-redis-master-0 (13 octobre 2026)
Symptôme : CrashLoopBackOff, 216 redémarrages en 18 h ; webservice 1/2, kas 0/1, exporter qui redémarre.
Log : Opening the temp file for AOF rewrite in rewriteAppendOnlyFile(): Input/output error. Le pod tournait sur xsel-devnode-03 et son volume Longhorn y était attaché (attached, degraded).
Correctif : kubectl delete pod -n tools gitlab-redis-master-0 (le pod, jamais le PVC) pour forcer un nouveau montage du volume. Redis est reparti en 1/1 et tient depuis 2 jours sur le même nœud.
Même schéma que gitlab-garage-2 (LMDB, os error 5) et awx-postgres-15-0 (chmod … Input/output error). Un montage frais corrige, ce qui indique un défaut intermittent du chemin d'attache des volumes, pas un nœud définitivement cassé.
Les vagues de redémarrage touchent les trois nœuds de travail (devnode-03 le 12/10, devnode-02 et devnode-01 le 15/10 au matin), avec recréation des instance-manager Longhorn. Un courriel à l'hébergeur (analyse côté hyperviseur) est prêt et reste à envoyer.
Une erreur isolée RedisClient::ReadTimeoutError … Waited 1.0 seconds a été vue au démarrage d'un sidekiq : à surveiller comme signe de latence de stockage.
Note sur la limite mémoire du webservice. Elle a été portée de 3 à 4 Gi pendant le diagnostic. Ce n'était pas la cause (§28.8), mais c'est une marge utile : un pod en régime stable consomme environ 3 Gi.
ℹ️ Survenu en cours de session, sans lien avec les travaux GitLab/AWX/Jenkins en cours — détecté via une vague de redémarrages de pods inattendue (kubectl get pods -A -o wide) pendant les travaux sur GitLab. Deuxième occurrence du même signature exact que l'incident xsel-devnode-03 d'une session antérieure (§27).
29.1 — Signature de la panne
code
pgbackrest/loki/longhorn-manager: attempt to access beyond end of device
Aborting journal on device sdk-8
JBD2: I/O error when updating journal superblock
EXT4-fs (sdk): shut down requested
Suivi d'un démontage/remontage en cascade de 15+ disques (sdb, sdc, sdg, sdh, sdi, sdj, sdk, sdl, sdo, sdp, sdq, sdr, sds, sdt, sdu, sdv, sdw, sdx, sdy) à 12:26:09.
29.2 — Impact et auto-récupération
Redémarrages en cascade sur de nombreux namespaces : alertmanager, grafana, loki-0, gitlab-redis-master-0, harbor-jobservice, harbor-trivy-0, vcluster-prod/staging/test-0, pxc-cluster-pxc-db-proxysql-0, pg-cluster-pg-db-repo-host-0, instance-manager (Longhorn), gitlab-garage-1, gitlab-webservice-default.
✅ Tous les pods concernés se sont rétablis d'eux-mêmes en quelques minutes — aucune action corrective manuelle n'a été nécessaire côté Kubernetes.
29.3 — Hypothèse initiale corrigée
Hypothèse de départ : devnode-01 et devnode-03 partagent un même backend iSCSI externe. Infirmée par la vérification :
bash
iscsiadm-msession
devnode-01 se connecte à 172.16.11.175:3260 (son propre instance-manager Longhorn local) et devnode-03 à 172.16.13.59:3260 (le sien) — chaque nœud exporte ses propres répliques de volume via iSCSI local, c'est le fonctionnement normal de l'architecture Longhorn. Il n'y a donc pas de backend de stockage externe partagé entre les deux nœuds.
⚠️ Le facteur commun probable reste la couche hyperviseur/stockage physique de l'hébergeur, affectant plusieurs VMs indépendamment. Les deux incidents (devnode-03 en session antérieure, devnode-01 ici) constituent un faisceau d'indices suffisant pour justifier une escalade formelle auprès de l'hébergeur, citant les deux occurrences comme preuve d'un problème systémique potentiel.
29.4 — Point restant
Escalade hébergeur à rédiger et envoyer (citant les deux incidents devnode-01 et devnode-03) — non encore faite à la clôture de cette session.
Rappel : la réplique PostgreSQL pg-cluster-pg-db-instance1-6zc2-0 (§27.3) reste en réparation différée (« Option C »), en attente de cette même confirmation hébergeur sur la fiabilité matérielle de devnode-03.
ℹ️ Premier des deux outils restants de la feuille de route infra (AWX + Jenkins). Installé via le chart Helm officiel de l'opérateur, dans un namespace dédié awx (plutôt que tools, choix délibéré pour isoler le cycle de vie de l'opérateur AWX — CRD, reconciliation — du reste des outils).
30.1 — Décisions prises avant installation
Question
Décision retenue
Namespace
awx dédié (pas tools)
Méthode d'installation
Helm (chart officiel communautaire), pas Kustomize
Fichiers de config
/root/k8s-xsel/ (cohérent avec le reste de l'infra-as-code)
Scheduling sur devnode-03
Autorisé pour le compute uniquement (« Option B ») — stockage Longhorn volontairement exclu, nœud sous surveillance de fiabilité matérielle (§27, §29)
Ordre d'installation
AWX avant Jenkins — AWX a une dépendance de stockage RWX (share-manager Longhorn) plus susceptible de révéler des problèmes d'infra latents ; mieux vaut les lever avant d'installer un second outil
ℹ️ Toutes les ressources (web/task/ee/redis) ont été explicitement chiffrées dès le départ pour éviter de reproduire le schéma de bug récurrent §30.5 (quotas/LimitRange trop serrés rencontrés 5 fois cette session).
ℹ️ Aucune affinité de nœud forcée vers devnode-03 — choix délibéré de laisser le scheduler standard agir, devnode-01/02 étant déjà saturés en requests CPU (88-93 %, bien que l'usage réel ne soit que de 26-36 % — kubectl top nodes ≠ kubectl describe nodes | grep -A5 "Allocated resources"), et devnode-03 disposant de marge (49 %). Évite de concentrer AWX sur un nœud unique à la fiabilité incertaine.
30.4 — Bug : permission refusée sur le volume Postgres
Cause : bug connu en amont (ansible/awx-operator #1770/#1813/#1952, correctif #1805) — le PVC Longhorn n'est pas possédé par l'UID du conteneur Postgres au montage. Résolu par postgres_data_volume_init: true + postgres_init_container_commands (voir §30.3, déjà intégré dans le YAML final) : ajoute un init-container init (⚠️ pas init-postgresql, piège de nommage) tournant en root (runAsUser: 0) qui chown/chmod le volume avant le conteneur postgres principal.
bash
# Après kubectl apply, vérifier que le StatefulSet a bien été reconcilié avec l'init-container :
kubectlgetstatefulset-nawxawx-postgres-15-oyaml|grep-A10initContainers
# Puis seulement, recycler le pod :
kubectldeletepodawx-postgres-15-0
⚠️ Une première suppression du pod juste après l'apply était prématurée — l'opérateur n'avait pas encore mis à jour le template du StatefulSet. Toujours vérifier la présence de l'init-container dans le YAML du StatefulSet avant de supprimer le pod.
30.5 — Blocage de scheduling : awx-task en attente 82 minutes
code
0/5 nodes are available: 1 node(s) were unschedulable,
2 Insufficient cpu, 2 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }
Cause 1 — devnode-03 toujours cordonné au niveau Kubernetes (Ready,SchedulingDisabled) malgré la décision §30.1 Option B :
bash
kubectluncordonxsel-devnode-03
ℹ️ Décordonnement K8s uniquement. Le flag Longhorn allowScheduling: false sur ce nœud reste volontairement désactivé — le compute est autorisé, pas le stockage persistant, conformément à l'Option B.
Cause 2 — quota CPU longhorn-system insuffisant pour le pod share-manager requis par le PVC RWX awx-projects-claim (Longhorn expose les volumes RWX via un pod NFS dédié par volume) :
⚠️ 5ᵉ occurrence du même schéma de bug cette session (après Longhorn initial, trivy-system, et plusieurs composants GitLab) : un ResourceQuota dimensionné sans marge suffisante bloque silencieusement la création de pods système auxiliaires (ici le ShareManager, resté en state: stopped indéfiniment sans jamais produire d'erreur visible côté AWX — seulement dans les logs de longhorn-manager). Réflexe de diagnostic à systématiser : face à un PVC/pod bloqué sans erreur explicite, vérifier systématiquement les quotas du namespace fournissant la ressource sous-jacente (ici longhorn-system, pas awx).
30.6 — Migrations de base de données
Comme GitLab, AWX exécute ses migrations Django via un Job dédié (awx-migration-24.6.1-<hash>) que l'init-container init-database du pod awx-task attend (backoff exponentiel plafonné à 30s) avant de démarrer :
ℹ️ Plusieurs centaines de migrations Django (contenttypes, auth, main.0001 → main.0178 et au-delà, social_django, sso...) — processus normal, pas un blocage, analogue aux 2330 migrations de GitLab (§28). Une fois le Job Completed, awx-task progresse automatiquement Init:0/3 → Init:3/3 → 4/4 Running sans intervention.
30.7 — Validation finale
bash
kubectlgetpods-nawx
kubectlexec--namespaceawx-itdeploy/awx-task--cat/etc/tower/SECRET_KEY# si besoin de retrouver le secret
Accès confirmé sur https://awx.xselcloud.com, connexion admin réussie, tableau de bord fonctionnel (0 erreur de synchronisation, ressources par défaut en place).
Pod
État final
awx-operator-controller-manager
2/2 Running
awx-postgres-15-0
1/1 Running
awx-web
3/3 Running
awx-task
4/4 Running
Opérations et CI/CD
Étape 31 — Installation de Jenkins (CI/CD)
ℹ️ Second outil de la feuille de route, installé immédiatement après la stabilisation complète d'AWX. Namespace dédié jenkins (cohérent avec le choix fait pour AWX plutôt que tools).
Aucun résidu d'installation antérieure. La StorageClass nfs-jenkins (provisioner nfs.io/nfs-provisioner, 100 jours d'ancienneté, même âge que nfs-gitlab/nfs-grafana/etc.) confirmée comme une préparation d'infra volontaire et cohérente — un provisioner NFS dédié par outil, planifié dès l'origine (voir §13, Table des Matières).
31.2 — Installation via Helm
bash
helmrepoaddjenkinscihttps://charts.jenkins.io
helmrepoupdate
# Chart jenkinsci/jenkins 5.9.65, app version 2.568.3
yaml
# /root/k8s-xsel/jenkins-values.yamlcontroller:admin:username:"admin"# Mot de passe admin auto-généré par le chart dans un Secret (jenkins)serviceType:ClusterIPingress:enabled:trueingressClassName:nginxannotations:cert-manager.io/cluster-issuer:letsencrypt-prodhostName:jenkins.xselcloud.comtls:-secretName:jenkins-xselcloud-tlshosts:-jenkins.xselcloud.comresources:requests:{ cpu:"500m", memory:"1Gi"}limits:{ cpu:"2", memory:"2Gi"}javaOpts:"-Xmx1536m-Xms512m"startupProbe:failureThreshold:60periodSeconds:10persistence:enabled:truestorageClass:nfs-jenkinssize:10GiaccessMode:ReadWriteOnceagent:enabled:trueresources:requests:{ cpu:"250m", memory:"512Mi"}limits:{ cpu:"1", memory:"1Gi"}
⚠️ Piège de version du chart : controller.adminUser a été renommé controller.admin.username dans les versions récentes du chart (5.x). L'ancien champ fait échouer l'installation (execution error ... no longer exists). Toujours vérifier helm show values jenkinsci/jenkins si un champ est rejeté au premier essai.
31.3 — Certificat TLS émis avant la création du enregistrement DNS
Le DNS (jenkins.xselcloud.com) a été créé après l'application du manifeste Ingress — sans conséquence : le solveur HTTP-01 de cert-manager (cm-acme-http-solver-*) retente automatiquement avec un backoff jusqu'à ce que le domaine résolve, puis valide et émet le certificat normalement (confirmé en pratique : certificat READY: True obtenu en moins de 90s une fois le DNS propagé).
31.4 — Bug : startup probe trop courte au premier démarrage
code
Exit Code: 143 # SIGTERM du kubelet, pas un OOM (qui serait 137)
Cause : même schéma que GitLab (§28.8) et Rancher (§27.5) — le startupProbe par défaut du chart (~2 minutes) est insuffisant pour le tout premier démarrage de Jenkins, le temps que l'installation des plugins par défaut se termine (jenkins.InitReactorRunner$1#onAttained: Listed all plugins survient vers 90-110s, juste en dehors de la fenêtre par défaut).
Correctif (déjà intégré dans le YAML final du §31.2) :
yaml
controller:startupProbe:failureThreshold:60periodSeconds:10# 10 min de marge au lieu de ~2 min
ℹ️ Schéma récurrent à retenir pour tout futur outil installé via un chart tiers (déjà vu sur Rancher §27.5, PMM §27.6, GitLab §28.8, Jenkins ici) : les startupProbe/livenessProbe par défaut des charts communautaires sont systématiquement calibrées pour un redémarrage à chaud (cache déjà chaud, plugins déjà installés) et non pour un tout premier démarrage à froid. Réflexe à appliquer avant même l'installation : vérifier et élargir préventivement ces probes dans les values plutôt que d'attendre le premier CrashLoopBackOff.
Accès confirmé sur https://jenkins.xselcloud.com, connexion admin réussie, tableau de bord Jenkins 2.568.3 fonctionnel, certificat TLS valide.
Pod
État final
jenkins-0
2/2 Running
Points restants pour une prochaine session (§28-31)
Escalade hébergeur à rédiger (incidents disque devnode-01 + devnode-03, §29.3).
Mise à jour sécurité GitLab 19.3.1 → 19.3.3/19.4.1 (reportée, §28.6) à planifier comme fenêtre de maintenance dédiée.
Confirmer que la désactivation des inscriptions publiques GitLab (§28.7) a bien été enregistrée.
Configurer la sécurité Jenkins (realm d'authentification, stratégie d'autorisation) — actuellement en admin par défaut uniquement.
Installer les plugins recommandés et configurer un premier pipeline Jenkins.
Réparation de la réplique PostgreSQL pg-cluster-pg-db-instance1-6zc2-0 (§27.3) — toujours en pause (« Option C »), en attente de la confirmation hébergeur sur la fiabilité matérielle de devnode-03 (même nœud que l'incident §29).
Nettoyer le répertoire temporaire ~/awx-operator sur xsel-master-01 (brouillon pré-Helm, superseded par /root/k8s-xsel/awx-instance.yaml).
ℹ️ Les commandes Helm des trois derniers outils étaient dispersées dans leurs sections (§30.2 et §31.2 pour AWX et Jenkins) ou absentes (GitLab, §28 ne documente que les valeurs). Cette annexe les regroupe pour pouvoir tout réinstaller depuis zéro. Les fichiers de configuration vivent dans /root/k8s-xsel/.
aucune valeur custom, instance dans awx-instance.yaml
Jenkins
jenkins
jenkins
jenkinsci → https://charts.jenkins.io
jenkinsci/jenkins 5.9.65 (app 2.568.3)
jenkins-values.yaml
32.2 — GitLab
⚠️ Commande reconstituée. L'historique Helm ne conserve pas la commande d'origine (release à la révision 11 au 21/09/2026, révision 17 après les correctifs du 15/10/2026). Celle ci-dessous est déduite de helm list et de helm get values gitlab -n tools, ce qui reproduit l'état actuel.
Prérequis avant de lancer le chart :
Les releases gitlab-garage et gitlab-redis déployées dans tools (§28.1).
La base gitlabhq_production et l'utilisateur gitlab créés sur PostgreSQL (§18).
Les Secrets suivants dans tools : gitlab-psql-secret (clé password), gitlab-psql-ssl (server-ca.pem, client-certificate.pem, client-key.pem), gitlab-redis (clé redis-password), gitlab-object-storage (clé connection), gitlab-s3cmd-config (clé config) et gitlab-tls (certificat TLS).
bash
helmrepoaddgitlabhttps://charts.gitlab.io/
helmrepoupdate
# Sauvegarder les valeurs actuellement déployées
helmgetvaluesgitlab-ntools-oyaml>~/k8s-xsel/gitlab-values-actuelles.yaml
# Installer ou mettre à jour (les valeurs de correction viennent EN DERNIER : elles priment)
helmupgrade--installgitlabgitlab/gitlab-ntools\--version10.3.1\-f~/k8s-xsel/gitlab-values-actuelles.yaml\-f~/k8s-xsel/gitlab-values-correctifs.yaml\--timeout30m
⚠️ Le fichier gitlab-values-correctifs.yaml (sondes de liveness, ressources webservice, sidekiq, migrations et gitlab-shell, §28.8 à §28.10) est indispensable : sans lui, les pods webservice et sidekiq retombent en boucle de redémarrage et gitlab-shell est refusé par le LimitRange. Le quota tools-quota doit aussi avoir été relevé (§28.9).
ℹ️ Sur une base vide, le chart ne suffit pas : la migration et le seed de la base ont été faits à la main (gitlab-rake db:migrate puis db:seed_fu, voir §28.3 et §28.4), puis le mot de passe root défini via la console Rails.
32.3 — AWX
bash
helmrepoaddawx-operatorhttps://ansible-community.github.io/awx-operator-helm/
helmrepoupdate
# 1) L'opérateur
helminstall-nawx--create-namespacemy-awx-operatorawx-operator/awx-operator--version3.2.1
# 2) L'instance AWX (ressource personnalisée, voir §30.3)
kubectlapply-f~/k8s-xsel/awx-instance.yaml
kubectlgetpods-nawx
# Mot de passe admin (créé par l'opérateur)
kubectlgetsecretawx-admin-password-nawx-ojsonpath='{.data.password}'|base64-d;echo
ℹ️ L'installation d'origine n'épinglait pas la version du chart. --version 3.2.1 reproduit l'état actuel.
⚠️ Prévoir environ 10 minutes pour la migration Django (§30.6) : awx-task reste en Init:0/3 tant que le Job awx-migration-* n'est pas terminé.
32.4 — Jenkins
bash
helmrepoaddjenkinscihttps://charts.jenkins.io
helmrepoupdate
helmupgrade--installjenkinsjenkinsci/jenkins-njenkins--create-namespace\--version5.9.65\-f~/k8s-xsel/jenkins-values.yaml
kubectlgetpods-njenkins
# Mot de passe admin (généré par le chart)
kubectlexec--namespacejenkins-itsvc/jenkins-cjenkins--/bin/cat/run/secrets/additional/chart-admin-password&&echo
ℹ️ L'installation d'origine (révision 1) puis le correctif de startupProbe (révision 2, §31.4) sont déjà fusionnés dans jenkins-values.yaml : une seule commande suffit pour réinstaller.
32.5 — Sauvegarder les valeurs Helm des trois outils