Cluster XSELDocumentation de mise en place

MISE EN PLACE DU CLUSTER XSEL

Documentation complète — Étapes 0 à 32


MISE EN PLACE DU CLUSTER XSEL

Comment utiliser cette documentation

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.

Les grandes parties

📋 Table des Matières

# Section
— 🎯 Objectif
— ✅ Prérequis
— 🏗️ Architecture Cible
0 Configuration du Serveur HAProxy Dédié
1 Préparation sur MASTER-01
2 Initialisation du Premier Master
3 Joindre le Second Master (MASTER-02)
4 Joindre les Workers
5 Installation de Calico CNI (VXLAN)
6 Configurer les IP Pools Calico Personnalisés
7 Tests et Vérifications Finales
8 Installation du Metrics Server
9 Installation NGINX Ingress Controller
10 Déployer une Application de Test
11 Installation du Kubernetes Dashboard
12 Configuration des Namespaces et Isolation Réseau
13 Installation NFS
14 Installation Longhorn (Stockage HA Distribué)
15 Déploiement des vClusters (DEV, TEST, STAGING, PROD)
16 Installation Cert-Manager
17 Configuration Harbor Registry
18 Installation des Bases de Données
19 Exposition Externe des Bases de Données via HAProxy
20 Template DBaaS Client
21 vCluster (Kubernetes Virtuel Multi-Tenant)
22 Installation kube-prometheus-stack (Monitoring)
23 Installation Rancher (Gestion Graphique Multi-Cluster)
24 Surveillance des serveurs externes avec Prometheus Alloy
25 Installation Portainer (Gestion Graphique des Containers)
26 Installation Trivy Operator (Sécurité et Scan de Vulnérabilités)
27 Ajout de DEVNODE-03 et Remédiation Infrastructure
28 Installation et Initialisation de GitLab (+ Garage/Redis dédiés)
29 Incident Parallèle : Panne Disque xsel-devnode-01
30 Installation d'AWX (Ansible Automation Controller)
31 Installation de Jenkins (CI/CD)
32 Récapitulatif des commandes Helm : GitLab, AWX, Jenkins


Démarrer

🎯 Objectif

Mettre en place un cluster Kubernetes multi-master (HA) derrière un HAProxy sur serveur dédié, avec :

  • HAProxy dédié comme point d'entrée unique (séparé des masters)
  • Calico CNI en VXLAN
  • IP pools Calico par nœud (un /24 par nœud)
  • Connectivité pod-to-pod validée
  • vClusters par environnement (DEV, PROD, CLX)

Démarrer

✅ Prérequis

  • Serveur HAProxy dédié configuré (voir Étape 0)
  • Tous les nœuds nettoyés (Kubernetes supprimé)
  • /etc/kubernetes absent sur les masters/workers
  • WireGuard opérationnel (WG0 et WG100)
  • Connectivité réseau validée entre tous les nœuds


Démarrer

🏗️ Architecture Cible

Plan d'adressage réseau

Réseau Plage Usage
OM Network (WG0) 10.0.0.0/16 Administration SSH
Nodes Network (WG100) 192.168.0.0/16 Contrôle plane Kubernetes
Pods Network 172.16.X.0/24 Communication inter-pods
Cluster IPs 10.96.0.0/12 Services Kubernetes

Nœuds du cluster

Rôle Nom Hostname public WG0 (OM) WG100 (Nodes) Pod CIDR
Load Balancer xsel-jumphost-01 xsel-jumphost-01.xselcloud.com 10.0.0.254 192.168.0.254 —
Master xsel-master-01 xsel-master-01.xselcloud.com 10.0.0.1 192.168.0.1 172.16.1.0/24
Master xsel-master-02 xsel-master-02.xselcloud.com 10.0.0.2 192.168.0.2 172.16.2.0/24
Worker DEV xsel-devnode-01 xsel-devnode-01.xselcloud.com 10.0.1.1 192.168.1.1 172.16.11.0/24
Worker DEV xsel-devnode-02 xsel-devnode-02.xselcloud.com 10.0.1.2 192.168.1.2 172.16.12.0/24
Worker DEV xsel-devnode-03 xsel-devnode-03.xselcloud.com 10.0.1.3 192.168.1.3 172.16.13.0/24

Règles d'adressage

  • Masters : 192.168.0.1-9 (WG100)
  • Workers DEV : 192.168.1.X (WG100)
  • VIPs Load Balancer : multiples de 10 (ex: 192.168.0.254)
  • Serveurs hors Kubernetes : 192.168.X.201-249

Flux de trafic

text
Internet
    │
    ▼
HAProxy dédié (192.168.0.254)
    │
    ├─── :6443   ──→ xsel-master-01 (192.168.0.1)
    │                xsel-master-02 (192.168.0.2)
    │
    ├─── :80/443 ──→ xsel-devnode-01 (192.168.1.1:30080/30443)
    │                xsel-devnode-02 (192.168.1.2:30080/30443)
    │                xsel-devnode-03 (192.168.1.3:30080/30443)
    │                    │
    │                    └─→ NGINX Ingress → Pods applicatifs
    │
    └─── :3306   ──→ MySQL via ProxySQL (NodePort)
         :5432   ──→ PostgreSQL via PGBouncer (NodePort)
         :5433   ──→ PostgreSQL direct (NodePort)


Cluster Kubernetes

Étape 0 — Configuration du Serveur HAProxy Dédié

ℹ️ 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é.

code
Client WireGuard → 173.249.55.153:51821  (master-01 directement)
                 → 207.180.237.139:51821 (master-02 directement)
text
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

Résultat attendu :

text
80/tcp 443/tcp 6443/tcp 51821/udp 3306/tcp 5432/tcp 5433/tcp 8404/tcp

ℹ️ 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 -k https://192.168.0.254:6443 2>&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 --version 2>/dev/null && echo "✅ Docker déjà installé" || echo "❌ Docker absent"

# Installer Docker si absent
if ! command -v docker &>/dev/null; then
  dnf install -y docker-ce docker-ce-cli containerd.io
  systemctl enable --now docker
  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: 3
EOF

echo "✅ 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 50000

defaults
    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_masters

backend 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_workers

backend 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_workers

backend 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_workers

backend 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_workers

backend 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_workers

backend 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 2
HAEOFEOF

echo "✅ haproxy.cfg créé"

0.5 — Démarrer HAProxy

bash
cd /opt/xsel-haproxy

# Valider la configuration avant de démarrer
docker run --rm -v $(pwd)/config/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro \
  haproxy:3.0-alpine haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
# Attendu : Configuration file is valid

# Démarrer
# Démarrer HAProxy en arrière-plan
docker compose up -d

# Vérifier
# Vérifier le statut du conteneur
docker compose ps
# Afficher les logs du conteneur HAProxy
docker logs xsel-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
docker logs -f xsel-haproxy-lb

# Recharger la config sans downtime
# Recharger la configuration à chaud sans interruption de trafic
docker kill -s HUP xsel-haproxy-lb

# Redémarrer
# Redémarrer le conteneur
docker compose restart

# Arrêter
# Arrêter et supprimer le conteneur
docker compose down

# Mettre à jour l'image HAProxy
# Récupérer la dernière version de l'image HAProxy
docker compose pull
# Démarrer HAProxy en arrière-plan
docker compose up -d

0.7 — Vérification

bash
# Vérifier le container
# Vérifier l'état du conteneur HAProxy
docker compose -f /opt/xsel-haproxy/docker-compose.yml ps

# API Kubernetes via HAProxy
# Tester l'accès à l'API Kubernetes via HAProxy
curl -k https://192.168.0.254:6443/version

# Stats HAProxy
# Accéder aux statistiques HAProxy
curl http://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 placeholders PUBLIC_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
docker ps | grep haproxy

# Arrêter et supprimer le container
# Arrêter le conteneur HAProxy (silencieux si absent)
docker stop xsel-haproxy-lb 2>/dev/null || true
# Supprimer le conteneur HAProxy
docker rm xsel-haproxy-lb 2>/dev/null || true

# Supprimer l'image
# Supprimer l'image HAProxy locale
docker rmi haproxy 2>/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
sudo kubeadm reset -f --cri-socket unix:///var/run/containerd/containerd.sock

# Nettoyage Kubernetes
# Supprimer la configuration du cluster Kubernetes
sudo rm -rf /etc/kubernetes/
# Supprimer les données du kubelet
sudo rm -rf /var/lib/kubelet/
# Supprimer les données etcd
sudo rm -rf /var/lib/etcd/
# Supprimer la configuration kubectl de l'utilisateur
sudo rm -rf ~/.kube/

# Nettoyer les configurations CNI (Calico)
# Supprimer les configurations CNI résiduelles
sudo rm -rf /etc/cni/net.d/
# Supprimer les données CNI résiduelles
sudo rm -rf /var/lib/cni/

# Nettoyer iptables rules créées par Kubernetes
# Vider toutes les règles iptables IPv4 créées par Kubernetes
sudo iptables -F && sudo iptables -t nat -F && sudo iptables -t mangle -F && sudo iptables -X
# Vider toutes les règles iptables IPv6
sudo ip6tables -F && sudo ip6tables -t nat -F && sudo ip6tables -t mangle -F && sudo ip6tables -X

# Nettoyer les interfaces réseau virtuelles de Kubernetes/Calico
# Supprimer l'interface bridge CNI si elle existe
sudo ip link delete cni0 2>/dev/null || true
# Supprimer l'interface Flannel si elle existe
sudo ip link delete flannel.1 2>/dev/null || true
# Supprimer toutes les interfaces veth virtuelles résiduelles
sudo ip link show | grep veth | awk '{print $2}' | cut -d@ -f1 | xargs -I {} sudo ip link delete {} 2>/dev/null || true

# Nettoyer les routes liées à Calico
# Supprimer les routes BGP ajoutées par Calico
sudo ip route flush proto bird 2>/dev/null || true

# Redémarrer containerd
# Redémarrer containerd pour repartir d'un état propre
sudo systemctl restart containerd
# Vérifier que tout est propre
echo "=== Vérification post-nettoyage ==="
echo "Docker containers:"
docker ps
echo ""
echo "HAProxy status:"
# Vérifier si un conteneur HAProxy est en cours d'exécution
docker ps | grep haproxy
echo ""
echo "WireGuard interfaces:"
ip addr show wg0 | grep inet
ip addr show wg100 | grep inet
echo ""
echo "VIP status:"
ip addr show | 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
ssh root@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 travail
cd /root/k8s-xsel

1.2 — Créer le fichier kubeadm-config.yaml

bash
vi kubeadm-config.yaml

Copier le contenu du fichier kubeadm-config.yaml fourni.

bash
cat > /root/k8s-xsel/kubeadm-config.yaml << 'EOF'
# ClusterConfiguration
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: v1.29.15
clusterName: xsel-cluster
controlPlaneEndpoint: "192.168.0.254:6443"
networking:
  serviceSubnet: "10.96.0.0/16"
  podSubnet: "172.16.0.0/16"
  dnsDomain: "cluster.local"
apiServer:
  certSANs:
  - "192.168.0.254"
  - "192.168.0.1"
  - "192.168.0.2"
  - "10.0.0.10"
  - "10.0.0.1"
  - "10.0.0.2"
  - "173.249.55.153"
  - "207.180.237.139"
  - "xsel-master-01"
  - "xsel-master-02"
  - "xsel-master-01.xsel-services.com"
  - "xsel-master-02.xsel-services.com"
  extraArgs:
    authorization-mode: "Node,RBAC"
controllerManager:
  extraArgs:
    bind-address: "0.0.0.0"
    allocate-node-cidrs: "false"
scheduler:
  extraArgs:
    bind-address: "0.0.0.0"
etcd:
  local:
    dataDir: /var/lib/etcd
    extraArgs:
      listen-metrics-urls: "http://0.0.0.0:2381"
---
# InitConfiguration
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
localAPIEndpoint:
  advertiseAddress: "192.168.0.1"
  bindPort: 6443
nodeRegistration:
  criSocket: "unix:///var/run/containerd/containerd.sock"
  name: "xsel-master-01"
  kubeletExtraArgs:
    node-ip: "192.168.0.1"
  taints:
  - effect: NoSchedule
    key: node-role.kubernetes.io/control-plane
---
# KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
serverTLSBootstrap: true
---
# KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "iptables"
EOF

1.3 — Vérifications préalables

bash
# Vérifier l'état du service kubelet
systemctl status kubelet
# Activer kubelet au démarrage
systemctl enable kubelet.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 -c 2 192.168.0.2
# Tester la connectivité vers DEVNODE-01 via WG100
ping -c 2 192.168.1.1
# Tester la connectivité vers DEVNODE-02 via WG100
ping -c 2 192.168.1.2

# Tester l'accès à HAProxy avant l'initialisation du cluster
curl -k https://192.168.0.254:6443 2>&1 | head -5

Cluster Kubernetes

Étape 2 — Initialisation du Premier Master

2.1 — Initialiser le cluster

bash
# Se positionner dans le répertoire de travail
cd /root/k8s-xsel
# Initialiser le cluster Kubernetes depuis la configuration et téléverser les certificats pour le second master
sudo kubeadm init --config kubeadm-config.yaml --upload-certs

2.2 — Sauvegarder les commandes de join (très important)

À la fin, kubeadm affichera :

  • Commande pour joindre un CONTROL PLANE (master) :
bash
kubeadm join 192.168.0.254:6443 --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --control-plane \
    --certificate-key <CERT-KEY>
    kubeadm join 192.168.0.254:6443 --token 8rloti.fnsr95d2nqhb7vt1 \
    --discovery-token-ca-cert-hash sha256:bcdb06a82173f675ad89a52512f73233075235aec4061b9cfa564c0003ddad4b \
    --control-plane --certificate-key 39c9f3461ff3f2c75cbc626ea5e2f1a0db1f795c29d65ef38512fb1858e943ce \
    --cri-socket unix:///var/run/containerd/containerd.sock \
    --apiserver-advertise-address 192.168.0.2

  • Commande pour joindre un WORKER :
bash
kubeadm join 192.168.0.254:6443 --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH>
    kubeadm join 192.168.0.254:6443 \
    --token rpk5ec.8chrzyx60vzs4lj9 \
    --discovery-token-ca-cert-hash sha256:bcdb06a82173f675ad89a52512f73233075235aec4061b9cfa564c0003ddad4b \
    --cri-socket unix:///var/run/containerd/containerd.sock

2.3 — Configurer kubectl sur MASTER-01

bash
# Créer le répertoire de configuration kubectl
mkdir -p $HOME/.kube
# Copier le kubeconfig admin généré par kubeadm
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
# Donner la propriété du fichier à l'utilisateur courant
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# Vérifier
# Afficher les nœuds du cluster et leur statut
kubectl get nodes
# Vérifier les pods du plan de contrôle Kubernetes
kubectl get pods -n kube-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
ssh root@207.180.237.139
# Vérifier les prérequis
# Vérifier l'état du service kubelet
systemctl status kubelet
# 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"

3.2 — Joindre le cluster (control plane)

Reprends la commande control plane et ajoute :

  • –cri-socket unix:///var/run/containerd/containerd.sock
  • –apiserver-advertise-address 192.168.0.2
bash
# Sur MASTER-02
# Utiliser la commande sauvegardée à l'étape 2.2
# IMPORTANT: Ajouter ces paramètres à la commande de join:
sudo kubeadm join 192.168.0.254:6443 \
    --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --control-plane \
    --certificate-key <CERT-KEY> \
    --cri-socket unix:///var/run/containerd/containerd.sock \
    --apiserver-advertise-address 192.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
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
# Donner la propriété du fichier à l'utilisateur courant
sudo chown $(id -u):$(id -g) $HOME/.kube/config

# Vérifier — devrait montrer les 2 masters
# Afficher les nœuds du cluster et leur statut
kubectl get nodes

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
ssh root@207.180.196.59
# Utiliser la commande worker sauvegardée à l'étape 2.2
sudo kubeadm join 192.168.0.254:6443 \
    --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --cri-socket unix:///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
ssh root@149.102.138.247
# Même commande que DEVNODE-01
sudo kubeadm join 192.168.0.254:6443 \
    --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --cri-socket unix:///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
kubectl get nodes -o wide
# Devrait montrer les 4 nœuds en Ready:
# NAME STATUS ROLE AGE VERSION
INTERNAL-IP
# xsel-master-01 Ready control-plane 15m v1.29.15
192.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 travail
cd /root/k8s-xsel
# Télécharger le manifeste Calico CNI v3.28.0
curl https://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 -A 1 "CALICO_IPV4POOL_CIDR\|CALICO_IPV4POOL_IPIP\|CALICO_IPV4POOL_VXLAN" calico.yaml | grep -v "^--$"

Résultat attendu :

yaml
- name: CALICO_IPV4POOL_CIDR
  value: "172.16.0.0/16"
- name: CALICO_IPV4POOL_IPIP
  value: "Never"
- name: CALICO_IPV4POOL_VXLAN
  value: "Always"

⚠️ 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
kubectl apply -f calico.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)
kubectl wait --for=condition=ready pod -l k8s-app=calico-node -n kube-system --timeout=300s
# Vérifier les pods Calico
# Vérifier l'état des pods Calico
kubectl get pods -n kube-system | grep calico

 Tous les pods calico-node doivent être 1 / 1 Running

Activer l’auto-approbation des csr.

bash
#D'abord approuver les CSR pending actuels :

# Lister les demandes de signature de certificat en attente
kubectl get csr | grep Pending

# Approuver automatiquement tous les CSR en attente
kubectl get csr --no-headers | grep Pending | awk '{print $1}' | xargs kubectl certificate approve

cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: auto-approve-kubelet-serving-csrs
subjects:
- kind: Group
  name: system:nodes
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
  apiGroup: rbac.authorization.k8s.io
EOF

# Vérifier le statut final des CSR
kubectl get csr

#Vérifier l'état global du cluster:

# Afficher les nœuds du cluster et leur statut
kubectl get nodes

# Pods système
# Lister tous les pods de tous les namespaces
kubectl get pods -A

# Routes VXLAN (le problème initial)
# Vérifier la présence des routes VXLAN Calico
ip route | grep 172.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/v3
kind: BGPConfiguration
metadata:
  name: default
spec:
  nodeToNodeMeshEnabled: false
  asNumber: 64512
EOF

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
kubectl get daemonset calico-node -n kube-system \
  -o jsonpath='{range .spec.template.spec.containers[0].env[*]}{.name}{"\n"}{end}' \
  | grep IP_AUTODETECTION

# 2) Si ABSENTE → appliquer le patch
kubectl patch daemonset calico-node -n kube-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
kubectl rollout status daemonset calico-node -n kube-system

# 3) Si PRÉSENTE → ignorer le patch, juste redémarrer les pods Calico
# Forcer le redémarrage de tous les pods Calico
kubectl delete pod -n kube-system -l k8s-app=calico-node

# Laisser le temps aux pods de redémarrer
sleep 30

# Vérifier l'état des pods Calico
kubectl get pods -n kube-system | grep calico

# Attendre que les pods Calico soient prêts (max 3 min)
kubectl wait --for=condition=ready pod -l k8s-app=calico-node -n kube-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
kubectl get nodes
# Vérifier les pods système avec leur placement sur les nœuds
kubectl get pods -n kube-system -o wide

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 travail
cd /root/k8s-xsel
# Télécharger calicoctl
# Télécharger le binaire calicoctl v3.28.0
curl -L https://github.com/projectcalico/calico/releases/download/v3.28.0/calicoctl-linux-amd64 -o calicoctl

# Rendre le binaire exécutable
chmod +x calicoctl

# Installer calicoctl globalement
sudo mv calicoctl /usr/local/bin/
# Vérifier
# Vérifier que calicoctl est correctement installé
calicoctl version

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

bash
cat > /root/k8s-xsel/calico-ippool.yaml << 'EOF'
---
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: master-01-pool
spec:
  cidr: 172.16.1.0/24
  blockSize: 26
  ipipMode: Never
  vxlanMode: Always
  natOutgoing: true
  nodeSelector: kubernetes.io/hostname == "xsel-master-01"
---
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: master-02-pool
spec:
  cidr: 172.16.2.0/24
  blockSize: 26
  ipipMode: Never
  vxlanMode: Always
  natOutgoing: true
  nodeSelector: kubernetes.io/hostname == "xsel-master-02"
---
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: devnode-01-pool
spec:
  cidr: 172.16.11.0/24
  blockSize: 26
  ipipMode: Never
  vxlanMode: Always
  natOutgoing: true
  nodeSelector: kubernetes.io/hostname == "xsel-devnode-01"
---
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: devnode-02-pool
spec:
  cidr: 172.16.12.0/24
  blockSize: 26
  ipipMode: Never
  vxlanMode: Always
  natOutgoing: true
  nodeSelector: kubernetes.io/hostname == "xsel-devnode-02"
EOF

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/v3
kind: FelixConfiguration
metadata:
  name: default
spec:
  vxlanEnabled: true
  vxlanPort: 4789
  vxlanVNI: 4096
  #vxlanMTU: 1450    # À décommenter et ajuster si problème de connectivité pod-to-pod
EOF

# Appliquer
# Appliquer la configuration FelixConfiguration pour activer VXLAN
calicoctl apply -f /root/k8s-xsel/felixconfiguration.yaml

# Vérifier
# Vérifier les paramètres VXLAN dans la FelixConfiguration
kubectl get felixconfiguration default -o yaml | 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
calicoctl get ippool -o wide
# Supprimer le pool par défaut
# Supprimer le pool IP par défaut Calico
calicoctl delete ippool default-ipv4-ippool
# Appliquer les pools
# Appliquer les IP pools personnalisés par nœud
calicoctl apply -f /root/k8s-xsel/calico-ippool.yaml
# Vérifier les pools
# Lister les IP pools Calico existants avec les détails
calicoctl get ippool -o wide

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
kubectl rollout restart daemonset calico-node -n kube-system

# 1. Redémarrer CoreDNS
# Redémarrer CoreDNS pour actualiser la configuration réseau
kubectl rollout restart deployment coredns -n kube-system

# 2. Redémarrer Calico kube-controllers
# Redémarrer les contrôleurs Calico
kubectl rollout restart deployment calico-kube-controllers -n kube-system

# 3. Attendre que les nouveaux pods démarrent
# Attendre que CoreDNS soit disponible
kubectl rollout status deployment coredns -n kube-system
# Attendre que les contrôleurs Calico soient disponibles
kubectl rollout status deployment calico-kube-controllers -n kube-system

# 4. Vérifier les nouvelles IPs
# Vérifier les IPs assignées à CoreDNS et Calico controllers
kubectl get pods -n kube-system -o wide | 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
ssh root@192.168.1.1 "conntrack -F 2>/dev/null; echo OK"
# Vider la table de suivi des connexions sur DEVNODE-02
ssh root@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 nodes
for node_ip in 192.168.0.1 192.168.0.2 192.168.1.1 192.168.1.2; do
  ssh root@$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
sleep 10
# Lister les demandes de signature de certificat en attente
kubectl get csr | grep Pending

# Approuver tous les CSRs en attente
kubectl get csr --no-headers | grep Pending | awk '{print $1}' | \
  xargs kubectl certificate approve

# Vérifier — aucun Pending restant
# Lister les demandes de signature de certificat en attente
kubectl get csr | grep Pending

# Tester la connectivité pod → API server
kubectl run test-api --image=nicolaka/netshoot --restart=Never \
  --command -- sh -c "curl -sk --max-time 5 https://10.96.0.1/healthz; echo"

sleep 10
# Vérifier la sortie du pod de test (attendu : "ok")
kubectl logs test-api
# Résultat attendu : ok ✅
# Supprimer le pod de test temporaire
kubectl delete pod test-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
kubectl get nodes -o wide
# Vérifier les pods système avec leur placement sur les nœuds
kubectl get pods -n kube-system -o wide

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
kubectl run test-pod-1 --image=nicolaka/netshoot   --overrides='{"spec": {"nodeSelector": {"kubernetes.io/hostname": "xsel-devnode-01"}}}'   --command -- sleep 3600

# Créer test-pod-2 sur DEVNODE-02
kubectl run test-pod-2 --image=nicolaka/netshoot   --overrides='{"spec": {"nodeSelector": {"kubernetes.io/hostname": "xsel-devnode-02"}}}'   --command -- sleep 3600

# Attendre que les pods soient prêts
kubectl wait --for=condition=Ready pod/test-pod-1 --timeout=60s
kubectl wait --for=condition=Ready pod/test-pod-2 --timeout=60s

# Vérifier les IPs et les nodes
kubectl get pods test-pod-1 test-pod-2 -o wide
# 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 ping
POD1_IP=$(kubectl get pod test-pod-1 -o jsonpath='{.status.podIP}')
# Récupérer l'IP du pod 2
POD2_IP=$(kubectl get pod test-pod-2 -o jsonpath='{.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-2
echo "=== Test ping DEVNODE-01 → DEVNODE-02 ==="
# Tester la connectivité pod-to-pod : DEVNODE-01 vers DEVNODE-02
kubectl exec test-pod-1 -- ping -c 5 $POD2_IP

# Test ping cross-node depuis test-pod-2 → test-pod-1
echo "=== Test ping DEVNODE-02 → DEVNODE-01 ==="
# Tester la connectivité pod-to-pod : DEVNODE-02 vers DEVNODE-01
kubectl exec test-pod-2 -- ping -c 5 $POD1_IP

# Nettoyage
# Supprimer les pods de test
kubectl delete pod test-pod-1 test-pod-2

Résultat attendu :

text
5 packets transmitted, 5 received, 0% packet loss

7.3 — Vérifier HAProxy

bash
curl http://192.168.0.254:8404/ -u admin:xsel2025 | grep k 8s_api_backend

7.4 — Informations du Cluster

  • Kubernetes: v1.29.15
  • CNI: Calico v3.28.0 (VXLAN)
  • Service CIDR: 10.96.0.0/16
  • Pod CIDR: 172.16.0.0/16
  • API Endpoint: 192.168.0.254:6443
  • DNS: cluster.local


Services de base

Étape 8 — Installation du Metrics Server

ℹ️ 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
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
# Mettre à jour le cache des repos Helm
helm repo update

# Installer le Metrics Server
# --kubelet-insecure-tls est nécessaire car les kubelets utilisent des certificats auto-signés
helm upgrade --install metrics-server metrics-server/metrics-server \
  --namespace kube-system \
  --set args[0]="--kubelet-insecure-tls" \
  --wait \
  --timeout 2m

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
kubectl get pods -n kube-system | grep metrics-server
# Résultat attendu :
# metrics-server-<hash>   1/1   Running   0   ...

# Vérifier le service
# Vérifier le service Metrics Server
kubectl get svc -n kube-system metrics-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
kubectl top nodes
# 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
kubectl top pods -n kube-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 travail
cd /root/k8s-xsel

# Télécharger le manifeste NGINX Ingress Controller
# Télécharger le manifeste NGINX Ingress Controller pour bare-metal
curl -o ingress-nginx.yaml https://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 -lh ingress-nginx.yaml

# Appliquer le manifeste
# Déployer le contrôleur NGINX Ingress
kubectl apply -f ingress-nginx.yaml

# Attendre que le controller soit prêt
# Attendre que le pod NGINX Ingress soit opérationnel (max 2 min)
kubectl wait --namespace ingress-nginx   --for=condition=ready pod   --selector=app.kubernetes.io/component=controller   --timeout=120s

# Vérifier
# Vérifier le pod NGINX Ingress et son nœud
kubectl get pods -n ingress-nginx -o wide

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
kubectl patch svc ingress-nginx-controller -n ingress-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
kubectl get svc ingress-nginx-controller -n ingress-nginx
# Résultat attendu : 80:30080/TCP,443:30443/TCP

# Vérifier les pods
# Vérifier le pod NGINX Ingress et son nœud
kubectl get pods -n ingress-nginx -o wide

# Vérifier tous les objets
# Lister toutes les ressources du namespace ingress-nginx
kubectl get all -n ingress-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: v1
kind: Namespace
metadata:
  name: test-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world
  namespace: test-app
spec:
  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: v1
kind: Service
metadata:
  name: hello-world
  namespace: test-app
spec:
  selector:
    app: hello-world
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  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: 80
EOF

# Appliquer l'application de test
# Déployer l'application de test dans le namespace test-app
kubectl apply -f /root/k8s-xsel/test-app.yaml

# Attendre que les pods démarrent
# Attendre que le Deployment soit disponible (max 2 min)
kubectl wait --for=condition=available --timeout=120s deployment/hello-world -n test-app
# Vérifier les ressources
# Vérifier toutes les ressources déployées dans test-app
kubectl get all -n test-app
# Vérifier la règle Ingress de l'application de test
kubectl get ingress -n test-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 travail
cd /root/k8s-xsel
# Télécharger le manifeste
# Télécharger le manifeste du Kubernetes Dashboard v2.7.0
curl -o kubernetes-dashboard.yaml https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
# Appliquer
# Déployer le Kubernetes Dashboard
kubectl apply -f kubernetes-dashboard.yaml
# Vérifier l'installation
# Vérifier les pods du Dashboard
kubectl get pods -n kubernetes-dashboard
# Vérifier les services du Dashboard
kubectl get svc -n kubernetes-dashboard

11.2 — Créer un utilisateur admin

bash
cat > dashboard-admin.yaml << 'EOF'
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: admin-user
  namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin-user
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: admin-user
  namespace: kubernetes-dashboard
EOF

# Créer le ServiceAccount admin et son ClusterRoleBinding
kubectl apply -f dashboard-admin.yaml

11.3 — Exposer le Dashboard via Ingress

bash
cat > dashboard-ingress.yaml << 'EOF'
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
  annotations:
    nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  rules:
  - host: dashboard.xsel-ci.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: kubernetes-dashboard
            port:
              number: 443
EOF

# Exposer le Dashboard via NGINX Ingress
kubectl apply -f dashboard-ingress.yaml

### 11.4 — Obtenir le token d'accès d’accès

bash
# Créer un token pour l'utilisateur admin
# Générer un token d'accès admin valable 10 ans
kubectl -n kubernetes-dashboard create token admin-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
sudo vi /etc/hosts
# Ajouter cette ligne :
95.111.238.94 dashboard.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
kubectl get nodes
# Lister tous les pods de tous les namespaces
kubectl get pods --all-namespaces
# Afficher les endpoints du cluster
kubectl cluster-info
kubectl get events --all-namespaces --sort-by='.lastTimesta
mp' | tail -20
kubectl logs -n kube-system <pod-name>
kubectl drain <node-name> --ignore-daemonsets --delete-empt
ydir-data
kubectl uncordon <node-name>
# Générer une commande de join fraîche pour les nouveaux workers
kubeadm token create --print-join-command
# Voir tous les roles par namespace
# Lister tous les Roles par namespace
kubectl get roles -A
# Voir tous les clusterroles
# Lister les ClusterRoles personnalisés (hors système)
kubectl get clusterroles | grep -v "system:"
# Voir les rolebindings
# Lister tous les RoleBindings
kubectl get rolebindings -A
# Voir les clusterrolebindings
# Lister les ClusterRoleBindings personnalisés
kubectl get clusterrolebindings | grep -v "system:"
# Détail d'un role spécifique
kubectl describe role <nom-role> -n <namespace>
# Détail d'un clusterrole
kubectl describe clusterrole <nom-clusterrole>
# Vérifier les permissions d'un utilisateur/serviceaccount
kubectl auth can-i --list --as=system:serviceaccount:<names
pace>:<serviceaccount>
# Vérifier une permission spécifique
kubectl auth can-i get pods --as=system:serviceaccount:dev:
default -n dev

# Sur DEVNODE-01
# Se connecter à DEVNODE-01 via SSH
ssh root@207.180.196.59
# Arrêter le pare-feu
systemctl stop firewalld
# Démarrer le pare-feu
systemctl start firewalld
# Activer le pare-feu au démarrage
systemctl enable firewalld
# Vérifier l'état du pare-feu
systemctl status firewalld
Contactez l'administrateur WireGuard avec ces informations:

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 -u kubelet -n 100 --no-pager
# Vérifier l'état des pods Calico
kubectl get pods -n kube-system | grep calico
# Inspecter les logs d'un pod Calico spécifique
kubectl logs -n kube-system <calico-pod-name>
# Redémarrer containerd d'abord
# Redémarrer containerd
systemctl restart containerd
echo "Containerd redémarré, attente de 5 secondes..."
sleep 5
# Redémarrer kubelet après containerd
systemctl restart kubelet
echo "Kubelet redémarré, attente de 15 secondes..."
sleep 15
# Vérifier s'il y a encore des erreurs CNI
# Vérifier les erreurs CNI récentes dans les logs kubelet
journalctl -u kubelet -n 5 --no-pager | grep -i cni

11.6.2 — Des Pods ne Démarrent pas

bash
# Afficher les événements et la configuration du pod en erreur
kubectl describe pod <pod-name>
# Afficher les logs du pod en erreur
kubectl logs <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 :

text
Cluster physique XSEL
  ├── tools         ← outils DevOps (Harbor, Jenkins…)
  ├── monitoring    ← stack monitoring (Prometheus, Grafana, Loki)
  ├── devmysql      ← bases de données MySQL
  └── devpostgresql ← bases de données PostgreSQL
bash
cat > /root/k8s-xsel/namespaces.yaml << 'EOF'
---
apiVersion: v1
kind: Namespace
metadata:
  name: tools
  labels:
    name: tools
    type: services
---
apiVersion: v1
kind: Namespace
metadata:
  name: monitoring
  labels:
    name: monitoring
    type: services
---
apiVersion: v1
kind: Namespace
metadata:
  name: devmysql
  labels:
    name: devmysql
    type: databases
---
apiVersion: v1
kind: Namespace
metadata:
  name: devpostgresql
  labels:
    name: devpostgresql
    type: databases
EOF

# Créer les namespaces infrastructure dans le cluster
kubectl apply -f namespaces.yaml
# Vérifier que les namespaces ont bien été créés
kubectl get namespaces

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 + Jenkins
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tools-quota
  namespace: tools
spec:
  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: v1
kind: ResourceQuota
metadata:
  name: monitoring-quota
  namespace: monitoring
spec:
  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 quota
apiVersion: v1
kind: ResourceQuota
metadata:
  name: devmysql-quota
  namespace: devmysql
spec:
  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 principales
apiVersion: v1
kind: ResourceQuota
metadata:
  name: devpostgresql-quota
  namespace: devpostgresql
spec:
  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
kubectl apply -f resource-quotas.yaml
# Vérifier les quotas appliqués sur tous les namespaces
kubectl get resourcequota -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-solver
apiVersion: v1
kind: LimitRange
metadata:
  name: tools-limit-range
  namespace: tools
spec:
  limits:
  - type: Container
    max:
      cpu: "4"
      memory: 8Gi
    min:
      cpu: "10m"
      memory: 64Mi
    default:
      cpu: "500m"
      memory: 512Mi
    defaultRequest:
      cpu: "50m"
      memory: 128Mi
---
# MONITORING
apiVersion: v1
kind: LimitRange
metadata:
  name: monitoring-limit-range
  namespace: monitoring
spec:
  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: v1
kind: LimitRange
metadata:
  name: devmysql-limit-range
  namespace: devmysql
spec:
  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 Helm
apiVersion: v1
kind: LimitRange
metadata:
  name: devpostgresql-limit-range
  namespace: devpostgresql
spec:
  limits:
  - type: Container
    max:
      cpu: "4"
      memory: 8Gi
    min:
      cpu: "10m"
      memory: 20Mi
    default:
      cpu: "500m"
      memory: 256Mi
    defaultRequest:
      cpu: "100m"
      memory: 128Mi
EOF

# Appliquer les LimitRanges sur les namespaces
kubectl apply -f limit-ranges.yaml
# Vérifier les LimitRanges appliquées
kubectl get limitrange -A

Vérifier les ressources disponibles :

bash
# Afficher la consommation CPU/RAM de chaque nœud
kubectl top nodes
# Afficher les ressources allouées sur DEVNODE-01
kubectl describe node xsel-devnode-01 | grep -A6 "Allocated resources"
# Afficher les ressources allouées sur DEVNODE-02
kubectl describe node xsel-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
kubectl describe pod <pod-name> -n <namespace> | grep -A10 Events

12.4 — NetworkPolicies

ℹ️ Par défaut, tous les pods peuvent communiquer entre eux. Les NetworkPolicies restreignent ce comportement pour isoler les namespaces.

bash
cat > /root/k8s-xsel/network-policies.yaml << 'EOF'
---
# TOOLS — ingress depuis ingress-nginx et monitoring uniquement
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: tools-network-policy
  namespace: tools
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: tools
  egress:
  - {}
---
# MONITORING — accès à tous les namespaces pour scraper les métriques
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: monitoring-network-policy
  namespace: monitoring
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
  egress:
  - {}
---
# DEVMYSQL — isolation BDD
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: devmysql-network-policy
  namespace: devmysql
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: devmysql
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: tools
---
# DEVPOSTGRESQL — isolation BDD
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: devpostgresql-network-policy
  namespace: devpostgresql
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: devpostgresql
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: tools
EOF

# Appliquer les NetworkPolicies d'isolation inter-namespaces
kubectl apply -f network-policies.yaml
# Vérifier les NetworkPolicies sur tous les namespaces
kubectl get networkpolicy -A

12.5 — RBAC

ℹ️ Principe du moindre privilège — chaque composant n'a accès qu'à ce dont il a besoin.

bash
cat > /root/k8s-xsel/rbac.yaml << 'RBACEOF'
---
# ServiceAccount tools — lecture seule
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tools-sa
  namespace: tools
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tools-role
  namespace: tools
rules:
- apiGroups: [""]
  resources: ["pods", "services", "configmaps", "secrets"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "statefulsets"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tools-rolebinding
  namespace: tools
subjects:
- kind: ServiceAccount
  name: tools-sa
  namespace: tools
roleRef:
  kind: Role
  name: tools-role
  apiGroup: rbac.authorization.k8s.io
---
# ServiceAccount monitoring — lecture cluster-wide pour Prometheus
apiVersion: v1
kind: ServiceAccount
metadata:
  name: monitoring-sa
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring-cluster-role
rules:
- apiGroups: [""]
  resources: ["nodes", "pods", "services", "endpoints", "namespaces"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "statefulsets", "daemonsets"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["extensions", "networking.k8s.io"]
  resources: ["ingresses"]
  verbs: ["get", "list", "watch"]
- nonResourceURLs: ["/metrics"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: monitoring-cluster-rolebinding
subjects:
- kind: ServiceAccount
  name: monitoring-sa
  namespace: monitoring
roleRef:
  kind: ClusterRole
  name: monitoring-cluster-role
  apiGroup: rbac.authorization.k8s.io
---
# ServiceAccount databases — accès restreint
apiVersion: v1
kind: ServiceAccount
metadata:
  name: databases-sa
  namespace: devmysql
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: databases-role
  namespace: devmysql
rules:
- apiGroups: [""]
  resources: ["pods", "services", "configmaps"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get"]
RBACEOF

# Appliquer les règles RBAC (ServiceAccounts, Roles, Bindings)
kubectl apply -f /root/k8s-xsel/rbac.yaml
# Lister tous les Roles par namespace
kubectl get roles -A
# Lister les ClusterRoles personnalisés (hors système)
kubectl get clusterroles | grep -v "system:"


Isolation, stockage et environnements

Étape 13 — Installation NFS

13.1 — Sur MASTER-01 (Serveur NFS)

bash
# Installer NFS
# Installer les utilitaires NFS (serveur et client)
dnf install -y nfs-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 -R 777 /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
systemctl enable --now nfs-server
# Publier les exports NFS définis dans /etc/exports
exportfs -rav

# Vérifier
# Vérifier les exports NFS disponibles localement
showmount -e localhost

ℹ️ 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)
dnf install -y nfs-utils
# Vérifier les exports NFS disponibles depuis le serveur master-01
showmount -e 192.168.0.1

13.3 — Déployer NFS Provisioner + StorageClasses

bash
# Se positionner dans le répertoire de travail
cd /root/k8s-xsel
cat > /root/k8s-xsel/nfs-all.yaml << 'EOF'
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-provisioner
  namespace: nfs-provisioner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nfs-provisioner-role
rules:
- apiGroups: [""]
  resources: ["persistentvolumes"]
  verbs: ["get","list","watch","create","delete"]
- apiGroups: [""]
  resources: ["persistentvolumeclaims"]
  verbs: ["get","list","watch","update"]
- apiGroups: ["storage.k8s.io"]
  resources: ["storageclasses"]
  verbs: ["get","list","watch"]
- apiGroups: [""]
  resources: ["events"]
  verbs: ["create","update","patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: nfs-provisioner-role-endpoints
  namespace: nfs-provisioner
rules:
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get","list","watch","create","update","patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: nfs-provisioner-binding
subjects:
- kind: ServiceAccount
  name: nfs-provisioner
  namespace: nfs-provisioner
roleRef:
  kind: ClusterRole
  name: nfs-provisioner-role
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: nfs-provisioner-binding-endpoints
  namespace: nfs-provisioner
subjects:
- kind: ServiceAccount
  name: nfs-provisioner
  namespace: nfs-provisioner
roleRef:
  kind: Role
  name: nfs-provisioner-role-endpoints
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-provisioner
  namespace: nfs-provisioner
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-provisioner
  template:
    metadata:
      labels:
        app: nfs-provisioner
    spec:
      serviceAccountName: nfs-provisioner
      containers:
      - name: nfs-provisioner
        image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
        env:
        - name: PROVISIONER_NAME
          value: nfs.io/nfs-provisioner
        - name: NFS_SERVER
          value: "192.168.0.1"
        - name: NFS_PATH
          value: "/srv/nfs/kubernetes"
        volumeMounts:
        - name: nfs-root
          mountPath: /persistentvolumes
      volumes:
      - name: nfs-root
        nfs:
          server: 192.168.0.1
          path: /srv/nfs/kubernetes
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs.io/nfs-provisioner
parameters:
  archiveOnDelete: "false"
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-prometheus
provisioner: nfs.io/nfs-provisioner
parameters:
  pathPattern: "prometheus/{.PVC.namespace}/{.PVC.name}"
  archiveOnDelete: "false"
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-grafana
provisioner: nfs.io/nfs-provisioner
parameters:
  pathPattern: "grafana/{.PVC.namespace}/{.PVC.name}"
  archiveOnDelete: "false"
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-loki
provisioner: nfs.io/nfs-provisioner
parameters:
  pathPattern: "loki/{.PVC.namespace}/{.PVC.name}"
  archiveOnDelete: "false"
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-gitlab
provisioner: nfs.io/nfs-provisioner
parameters:
  pathPattern: "gitlab/{.PVC.namespace}/{.PVC.name}"
  archiveOnDelete: "false"
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-jenkins
provisioner: nfs.io/nfs-provisioner
parameters:
  pathPattern: "jenkins/{.PVC.namespace}/{.PVC.name}"
  archiveOnDelete: "false"
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-registry
provisioner: nfs.io/nfs-provisioner
parameters:
  pathPattern: "registry/{.PVC.namespace}/{.PVC.name}"
  archiveOnDelete: "false"
EOF

# ⚠️ Créer le namespace AVANT d'appliquer le manifeste
# Créer le namespace dédié au provisionneur NFS
kubectl create namespace nfs-provisioner
# Déployer le provisionneur NFS et les StorageClasses
kubectl apply -f /root/k8s-xsel/nfs-all.yaml

# Attendre le démarrage
sleep 15
# Vérifier que le pod NFS provisioner est en Running
kubectl get pods -n nfs-provisioner
# Lister les StorageClasses disponibles dans le cluster
kubectl get storageclass

13.4 — Tester avec un PVC

bash
kubectl apply -f - << 'EOF'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
  namespace: default
spec:
  accessModes:
  - ReadWriteMany
  storageClassName: nfs-storage
  resources:
    requests:
      storage: 1Gi
EOF

sleep 10
# Vérifier que le PVC de test est en état Bound
kubectl get pvc test-pvc

# Nettoyage
# Supprimer le PVC de test après validation
kubectl delete pvc test-pvc -n default

13.5 — Vérification finale

bash
echo "=== NFS Server ===" && systemctl status nfs-server | grep Active
echo "=== Exports ===" && exportfs -v
echo "=== Provisioner ===" && kubectl get pods -n nfs-provisioner
echo "=== StorageClasses ===" && kubectl get storageclass
echo "=== Dossiers ===" && ls -la /srv/nfs/kubernetes/

Résultats obtenus en production :

text
=== 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 :

bash
rm -rf "/srv/nfs/kubernetes/{prometheus,grafana,loki,gitla"


Isolation, stockage et environnements

Étape 14 — Installation Longhorn (Stockage HA Distribué)

Version validée en production : Longhorn v1.6.2 Environnement : Rocky Linux 9 / RHEL 9 — Kubernetes multi-master

14.1 — Architecture de stockage

text
Pod (ex: Grafana)
      │
      ▼
Volume Longhorn "grafana-pvc"
      │
      ├── Replica 1 → xsel-devnode-01  /var/lib/longhorn/
      └── Replica 2 → xsel-devnode-02  /var/lib/longhorn/

Cohabitation NFS + Longhorn :

StorageClass Type Usage
nfs-storage ReadWriteMany (RWX) Fichiers partagés entre plusieurs pods
longhorn ReadWriteOnce (RWO) Volumes critiques HA (monitoring, BDD)

14.2 — Prérequis sur tous les nodes

À 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
dnf install -y iscsi-initiator-utils

# Démarrer et activer le service
# Démarrer et activer le daemon iSCSI au démarrage
systemctl enable --now iscsid

# Vérifier
# Vérifier que le service iSCSI est actif
systemctl status iscsid

⚠️ 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 -sSfL https://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
helm repo add longhorn https://charts.longhorn.io
# Mettre à jour le cache des repos Helm
helm repo update

# Vérifier
# Vérifier les versions disponibles de Longhorn
helm search repo longhorn/longhorn

14.5 — Créer le namespace

bash
# Créer le namespace pour Longhorn
kubectl create namespace longhorn-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.

bash
helm install longhorn longhorn/longhorn \
  --namespace longhorn-system \
  --version 1.6.2 \
  --set defaultSettings.defaultReplicaCount=2 \
  --set defaultSettings.storageMinimalAvailablePercentage=10 \
  --set ingress.enabled=true \
  --set ingress.ingressClassName=nginx \
  --set ingress.host=longhorn.xselcloud.com \
  --wait \
  --timeout 10m

ℹ️ 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
kubectl get pods -n longhorn-system

Résultat attendu (tous les pods en Running) :

code
NAME                                    READY   STATUS    NODE
csi-attacher-*                          1/1     Running   devnode-01/02
csi-provisioner-*                       1/1     Running   devnode-01/02
csi-resizer-*                           1/1     Running   devnode-01/02
csi-snapshotter-*                       1/1     Running   devnode-01/02
engine-image-ei-b0369a5d-*             1/1     Running   devnode-01
engine-image-ei-b0369a5d-*             1/1     Running   devnode-02
instance-manager-*                      1/1     Running   devnode-01
instance-manager-*                      1/1     Running   devnode-02
longhorn-csi-plugin-*                   3/3     Running   devnode-01
longhorn-csi-plugin-*                   3/3     Running   devnode-02
longhorn-driver-deployer-*              1/1     Running   devnode-01
longhorn-manager-*                      1/1     Running   devnode-01
longhorn-manager-*                      1/1     Running   devnode-02
longhorn-ui-*                           1/1     Running   devnode-01
longhorn-ui-*                           1/1     Running   devnode-02
bash
# Vérifier les nodes Longhorn
# Vérifier que les workers sont bien enregistrés dans Longhorn
kubectl get nodes.longhorn.io -n longhorn-system
code
NAME              READY   ALLOWSCHEDULING   SCHEDULABLE
xsel-devnode-01   True    true              True
xsel-devnode-02   True    true              True

ℹ️ 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
kubectl patch storageclass nfs-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
kubectl get storageclass

Résultat attendu :

code
NAME                    PROVISIONER               RECLAIMPOLICY
longhorn (default)      driver.longhorn.io        Delete
longhorn-static         driver.longhorn.io        Delete
nfs-storage             nfs.io/nfs-provisioner    Delete
nfs-grafana             nfs.io/nfs-provisioner    Delete
nfs-prometheus          nfs.io/nfs-provisioner    Delete
...

14.9 — Accéder au dashboard

bash
# Option 1 — Via Ingress (DNS configuré)
# https://longhorn.xselcloud.com

# Option 2 — Port-forward (sans DNS)
kubectl port-forward svc/longhorn-frontend \
  -n longhorn-system 8080:80 &
# http://localhost:8080

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.

15.1 — Architecture

text
Cluster physique XSEL
  ├── vcluster-dev     ← Kubernetes DEV     (longhorn)
  ├── vcluster-test    ← Kubernetes TEST    (longhorn)
  ├── vcluster-staging ← Kubernetes STAGING (longhorn)
  └── vcluster-prod    ← Kubernetes PROD    (longhorn)
text
Navigateur → demo-dev.xsel-ci.com
    → HAProxy (95.111.238.94:443)
    → NGINX Ingress physique (devnode-01/02:30443)
    → Pod nginx-demo (dans vcluster-dev)

ℹ️ 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 -v vcluster &>/dev/null; then
  mkdir -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é
vcluster version
# Résultat : vcluster version 0.34.0

15.3 — Créer le fichier de values

bash
mkdir -p /root/k8s-xsel/vcluster

cat > /root/k8s-xsel/vcluster/values.yaml << 'EOF'
sync:
  toHost:
    ingresses:
      enabled: true
controlPlane:
  statefulSet:
    persistence:
      volumeClaim:
        enabled: true
        storageClass: "longhorn"
        size: 5Gi
        retentionPolicy: Retain
        accessModes: ["ReadWriteOnce"]
EOF

echo "✅ values.yaml créé"

ℹ️ 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.

15.4 — Déployer les vClusters

bash
for env in dev test staging prod; do
  kubectl create namespace vcluster-$env --dry-run=client -o yaml | kubectl apply -f -
  vcluster create vcluster-$env \
    --namespace vcluster-$env \
    --connect=false \
    -f /root/k8s-xsel/vcluster/values.yaml
  echo "✅ vcluster-$env créé"
done

15.5 — Vérifier

bash
# Lister tous les vClusters déployés dans le cluster physique
vcluster list
# Vérifier que les PVCs Longhorn des vClusters sont en état Bound
kubectl get pvc -A | grep vcluster

Résultats attendus :

text
NAME             | NAMESPACE        | STATUS  | VERSION
vcluster-dev     | vcluster-dev     | Running | 0.34.0
vcluster-test    | vcluster-test    | Running | 0.34.0
vcluster-staging | vcluster-staging | Running | 0.34.0
vcluster-prod    | vcluster-prod    | Running | 0.34.0

NAMESPACE          PVC                       STATUS   STORAGECLASS
vcluster-dev       data-vcluster-dev-0       Bound    longhorn     ✅
vcluster-test      data-vcluster-test-0      Bound    longhorn     ✅
vcluster-staging   data-vcluster-staging-0   Bound    longhorn     ✅
vcluster-prod      data-vcluster-prod-0      Bound    longhorn     ✅

15.6 — Tableau des accès

Environnement Namespace StorageClass
DEV vcluster-dev longhorn
TEST vcluster-test longhorn
STAGING vcluster-staging longhorn
PROD vcluster-prod longhorn

ℹ️ Pour exécuter des commandes dans un vCluster, utiliser vcluster connect directement :

bash
vcluster connect vcluster-dev --namespace vcluster-dev -- kubectl get pods -A

15.7 — Déployer les applications de démonstration nginx

L'application nginx est déployée dans vcluster-dev et vcluster-prod, chacune affichant le nom de son environnement avec une couleur distincte.

15.7.1 — Créer les manifests nginx

bash
mkdir -p /root/k8s-xsel/vcluster/apps

# DEV — bleu
cat > /root/k8s-xsel/vcluster/apps/nginx-dev.yaml << 'EOF'
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-index
  namespace: default
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>XSEL — DEV</title>
      <style>
        body {
          margin: 0;
          font-family: 'Segoe UI', sans-serif;
          background: #0f172a;
          color: #f1f5f9;
          display: flex;
          align-items: center;
          justify-content: center;
          height: 100vh;
        }
        .card {
          background: #1e293b;
          border: 2px solid #3B82F6;
          border-radius: 12px;
          padding: 48px 64px;
          text-align: center;
          box-shadow: 0 0 40px #3B82F644;
        }
        .env-badge {
          background: #3B82F6;
          color: #fff;
          font-size: 14px;
          font-weight: 700;
          letter-spacing: 3px;
          padding: 6px 18px;
          border-radius: 20px;
          display: inline-block;
          margin-bottom: 24px;
        }
        h1 { font-size: 48px; margin: 0 0 8px; }
        p  { color: #94a3b8; margin: 4px 0; font-size: 14px; }
      </style>
    </head>
    <body>
      <div class="card">
        <div class="env-badge">DEV</div>
        <h1>XSEL Cloud</h1>
        <p>vCluster · Kubernetes virtuel</p>
        <p>Environnement : <strong style="color:#3B82F6">DEV</strong></p>
      </div>
    </body>
    </html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
  namespace: default
  labels:
    app: nginx-demo
    env: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
        env: dev
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html
        configMap:
          name: nginx-index
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo
  namespace: default
spec:
  selector:
    app: nginx-demo
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-demo
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: demo-dev.xsel-ci.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-demo
            port:
              number: 80
EOF
echo "✅ Manifest nginx-dev.yaml créé"

# PROD — vert
cat > /root/k8s-xsel/vcluster/apps/nginx-prod.yaml << 'EOF'
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: nginx-index
  namespace: default
data:
  index.html: |
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title>XSEL — PROD</title>
      <style>
        body {
          margin: 0;
          font-family: 'Segoe UI', sans-serif;
          background: #0f172a;
          color: #f1f5f9;
          display: flex;
          align-items: center;
          justify-content: center;
          height: 100vh;
        }
        .card {
          background: #1e293b;
          border: 2px solid #10B981;
          border-radius: 12px;
          padding: 48px 64px;
          text-align: center;
          box-shadow: 0 0 40px #10B98144;
        }
        .env-badge {
          background: #10B981;
          color: #fff;
          font-size: 14px;
          font-weight: 700;
          letter-spacing: 3px;
          padding: 6px 18px;
          border-radius: 20px;
          display: inline-block;
          margin-bottom: 24px;
        }
        h1 { font-size: 48px; margin: 0 0 8px; }
        p  { color: #94a3b8; margin: 4px 0; font-size: 14px; }
      </style>
    </head>
    <body>
      <div class="card">
        <div class="env-badge">PROD</div>
        <h1>XSEL Cloud</h1>
        <p>vCluster · Kubernetes virtuel</p>
        <p>Environnement : <strong style="color:#10B981">PROD</strong></p>
      </div>
    </body>
    </html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
  namespace: default
  labels:
    app: nginx-demo
    env: prod
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
        env: prod
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html
        configMap:
          name: nginx-index
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo
  namespace: default
spec:
  selector:
    app: nginx-demo
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-demo
  namespace: default
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: prod-dev.xsel-ci.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-demo
            port:
              number: 80
EOF
echo "✅ Manifest nginx-prod.yaml créé"

15.7.2 — Déployer dans vcluster-dev et vcluster-prod

bash
# DEV
vcluster connect vcluster-dev --namespace vcluster-dev -- \
  kubectl apply -f /root/k8s-xsel/vcluster/apps/nginx-dev.yaml
echo "✅ nginx déployé dans vcluster-dev"

# PROD
vcluster connect vcluster-prod --namespace vcluster-prod -- \
  kubectl apply -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 vCluster
for env in dev prod; do
  echo "=== vcluster-$env ==="
  vcluster connect vcluster-$env --namespace vcluster-$env -- \
    kubectl get pods,svc,ingress -n default
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
kubectl get ingress -A | grep vcluster
# 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.

15.7.4 — DNS à configurer

Domaine Type Valeur
demo-dev.xsel-ci.com A 95.111.238.94
prod-dev.xsel-ci.com A 95.111.238.94

15.7.5 — Résumé

text
✅ vcluster-dev    : Running — longhorn
✅ vcluster-test   : Running — longhorn
✅ vcluster-staging: Running — longhorn
✅ vcluster-prod   : Running — longhorn
✅ Sync Ingress    : activée (sync.toHost.ingresses.enabled: true)
✅ nginx-demo DEV  : http://demo-dev.xsel-ci.com  (bleu)
✅ nginx-demo PROD : http://prod-dev.xsel-ci.com  (vert)


Certificats et registre

Étape 16 — Installation Cert-Manager

ℹ️ 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
kubectl create namespace cert-manager

# Déployer cert-manager v1.14.0 avec tous ses composants
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml

kubectl wait --for=condition=ready pod \
  -l app.kubernetes.io/instance=cert-manager \
  -n cert-manager --timeout=120s

# Vérifier les pods cert-manager (controller, cainjector, webhook)
kubectl get pods -n cert-manager

16.2 — Configurer les ClusterIssuers Let's Encrypt

bash
cat > /root/k8s-xsel/cert-manager-issuers.yaml << 'EOF'
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  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/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ton-email@xsel-services.com
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          ingressClassName: nginx
EOF

# Créer les ClusterIssuers Let's Encrypt (staging et production)
kubectl apply -f /root/k8s-xsel/cert-manager-issuers.yaml
# Vérifier que les ClusterIssuers sont en état READY: True
kubectl get clusterissuer

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
nslookup sgh24.xsel-ci.com 8.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/v1
kind: Certificate
metadata:
  name: test-cert
  namespace: default
spec:
  dnsNames:
  - sgh24.xsel-ci.com
  issuerRef:
    kind: ClusterIssuer
    name: letsencrypt-staging
  secretName: test-cert-tls
EOF

# Demander l'émission d'un certificat de test via Let's Encrypt staging
kubectl apply -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
kubectl describe certificate test-cert -n default | grep -A3 "Conditions"

16.4 — Vérification finale

bash
echo "=== Pods Cert-Manager ===" && kubectl get pods -n cert-manager
echo "=== ClusterIssuers ===" && kubectl get clusterissuer
echo "=== Secrets TLS ===" && kubectl get secrets -A | grep tls

Résultats attendus :

text
=== ClusterIssuers ===
NAME                  READY
letsencrypt-staging   True
letsencrypt-prod      True

16.5 — Utilisation dans un Ingress

Ajouter ces annotations et la section tls à tout Ingress pour activer le HTTPS automatique :

yaml
metadata:
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"   # production
    # cert-manager.io/cluster-issuer: "letsencrypt-staging"  # tests
spec:
  tls:
  - hosts:
    - mon-service.xsel-services.com
    secretName: mon-service-tls


Certificats et registre

Étape 17 — Configuration Harbor Registry

ℹ️ 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
helm repo add harbor https://helm.goharbor.io
# Mettre à jour le cache des repos Helm
helm repo update

helm install harbor harbor/harbor \
  --namespace tools \
  --set expose.type=ingress \
  --set expose.ingress.hosts.core=registry.xsel-ci.com \
  --set expose.ingress.className=nginx \
  --set expose.ingress.annotations."nginx\.ingress\.kubernetes\.io/ssl-redirect"=false \
  --set expose.tls.certSource=auto \
  --set expose.tls.auto.commonName=registry.xsel-ci.com \
  --set externalURL=https://registry.xsel-ci.com \
  --set harborAdminPassword=XselHarbor2025! \
  --set persistence.persistentVolumeClaim.registry.storageClass=nfs-registry \
  --set persistence.persistentVolumeClaim.database.storageClass=nfs-storage \
  --set persistence.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
kubectl get certificate -n tools -w

# Vérifier les challenges ACME
# Vérifier le challenge ACME HTTP-01 en cours
kubectl get challenges -n tools

# Vérifier la validité du certificat une fois émis
kubectl get secret harbor-tls -n tools -o jsonpath='{.data.tls\.crt}' | \
  base64 -d | openssl x509 -noout -dates -subject 2>/dev/null

17.3 — Vérification complète

bash
# Vérifier que tous les pods Harbor sont en Running
kubectl get pods -n tools
# Vérifier les volumes persistants Harbor
kubectl get pvc -n tools
# Vérifier l'Ingress Harbor
kubectl get ingress -n tools
# Vérifier le statut du certificat TLS Harbor
kubectl get certificate -n tools

Résultats obtenus en production :

text
harbor-core        1/1 Running ✅
harbor-database    1/1 Running ✅
harbor-jobservice  1/1 Running ✅
harbor-portal      1/1 Running ✅
harbor-redis       1/1 Running ✅
harbor-registry    2/2 Running ✅
harbor-trivy       1/1 Running ✅

harbor-ingress   nginx   registry.xsel-ci.com   192.168.1.1   80, 443 ✅
Certificat TLS   READY: True ✅

⚠️ harbor-jobservice peut démarrer en CrashLoopBackOff si harbor-core n'est pas encore prêt. Une fois harbor-core en 1/1 Running, redémarrer :

bash
kubectl rollout restart deployment harbor-jobservice -n tools

17.4 — Accès Harbor

17.5 — Dépannage TLS

Si le certificat reste READY: False après 5 minutes :

Cause Symptôme Fix
DNS non propagé Challenge pending Attendre la propagation DNS
ssl-redirect bloque ACME Challenge échoue Annoter nginx.ingress.kubernetes.io/ssl-redirect: "false"
LimitRange trop strict Solver pod Pending Baisser min cpu/memory dans le namespace tools
bash
# Forcer la réémission du certificat
# Supprimer le certificat existant pour forcer la réémission
kubectl delete certificate harbor-tls -n tools 2>/dev/null || true
# Supprimer le secret TLS associé
kubectl delete secret harbor-tls -n tools 2>/dev/null || true

# Recréer
cat << 'EOF' | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: harbor-tls
  namespace: tools
spec:
  secretName: harbor-tls
  dnsNames:
  - registry.xsel-ci.com
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
EOF

# Surveiller en temps réel l'émission du certificat TLS Harbor
kubectl get certificate -n tools -w

17.6 — Accès Harbor (Récapitulatif)

text
URL   : https://registry.xsel-ci.com
Login : admin
Pass  : XselHarbor2025!


Bases de données

Étape 18 — Installation des Bases de Données

ℹ️ 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
kubectl create namespace devmysql

# Ajouter le repo Helm officiel Percona
helm repo add percona https://percona.github.io/percona-helm-charts/
# Mettre à jour le cache des repos Helm
helm repo update

# 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
helm install pxc-operator percona/pxc-operator \
  --namespace devmysql \
  --values /root/k8s-xsel/pxc-operator-values.yaml

# Vérifier que le pod opérateur démarre correctement
kubectl get pods -n devmysql -l app.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
helm install pxc-cluster percona/pxc-db \
  --namespace devmysql \
  --set pxc.size=3 \
  --set pxc.antiAffinityTopologyKey="none" \
  --set haproxy.enabled=false \
  --set proxysql.enabled=true \
  --set proxysql.size=3 \
  --set proxysql.antiAffinityTopologyKey="none" \
  --set pxc.persistence.storageClass=nfs-storage \
  --set pxc.persistence.size=20Gi \
  --set pxc.resources.requests.memory="1G" \
  --set pxc.resources.requests.cpu="600m" \
  --set pxc.resources.limits.memory="2G" \
  --set pxc.resources.limits.cpu="1" \
  --set proxysql.resources.requests.memory="1G" \
  --set proxysql.resources.requests.cpu="600m" \
  --set proxysql.resources.limits.memory="2G" \
  --set proxysql.resources.limits.cpu="1"

# Surveiller en temps réel le démarrage des pods (3-5 min)
watch kubectl get pods -n devmysql

⚠️ 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
kubectl apply -f - << 'EOF'
apiVersion: v1
kind: ResourceQuota
metadata:
  name: devmysql-quota
  namespace: devmysql
spec:
  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
kubectl patch pxc pxc-cluster-pxc-db -n devmysql --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
watch kubectl get pods -n devmysql

⚠️ Si des pods restent en Pending à cause de l'antiAffinity (seulement 2 workers) :

bash
kubectl patch pxc pxc-cluster-pxc-db -n devmysql --type=json -p '[
  {"op":"replace","path":"/spec/pxc/affinity","value":{"advanced":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":1,"podAffinityTerm":{"labelSelector":{"matchLabels":{"app.kubernetes.io/component":"pxc"}},"topologyKey":"kubernetes.io/hostname"}}]}}}},
  {"op":"replace","path":"/spec/proxysql/affinity","value":{"advanced":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":1,"podAffinityTerm":{"labelSelector":{"matchLabels":{"app.kubernetes.io/component":"proxysql"}},"topologyKey":"kubernetes.io/hostname"}}]}}}}
]'
kubectl delete pod -l app.kubernetes.io/component=pxc -n devmysql
kubectl delete pod -l app.kubernetes.io/component=proxysql -n devmysql

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
kubectl patch pxc pxc-cluster-pxc-db -n devmysql --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
kubectl get pods -n devmysql -w

# Vérifier
ROOT_PASS=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.root}' | base64 -d)

PROXYADMIN_PASS=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.proxyadmin}' | base64 -d)

kubectl exec pxc-cluster-pxc-db-proxysql-0 -n devmysql -c proxysql -- \
  mysql -uproxyadmin -p"$PROXYADMIN_PASS" -h 127.0.0.1 -P 6032 2>/dev/null \
  -e "SELECT variable_name, variable_value FROM global_variables
      WHERE variable_name IN ('mysql-have_ssl','mysql-default_authentication_plugin');"

Résultat attendu :

text
mysql-default_authentication_plugin   mysql_native_password
mysql-have_ssl                        false

18.1.4 — Vérifier le cluster

bash
# Vérifier le statut du cluster MySQL Percona XtraDB
kubectl get pxc -n devmysql
# Vérifier l'état des pods MySQL (PXC + ProxySQL)
kubectl get pods -n devmysql

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
kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data}' | \
  python3 -c "
import sys, json, base64
data = 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 root
ROOT_PASS=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.root}' | base64 -d)

# Créer le user xsel_admin avec mysql_native_password (compatible phpMyAdmin et apps sans SSL)
kubectl exec pxc-cluster-pxc-db-pxc-0 -n devmysql -c pxc -- \
  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';
  "

Résultat attendu :

text
+------------+------+----------------------+
| user       | host | plugin               |
+------------+------+----------------------+
| xsel_admin | %    | mysql_native_password |
+------------+------+----------------------+

⚠️ É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 proxyadmin
PROXYADMIN_PASS=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.proxyadmin}' | base64 -d)

# Vérifier les hostgroups disponibles (10=writer, 11=reader)
kubectl exec pxc-cluster-pxc-db-proxysql-0 -n devmysql -c proxysql -- \
  mysql -uproxyadmin -p"$PROXYADMIN_PASS" -h 127.0.0.1 -P 6032 \
  -e "SELECT hostgroup_id, hostname, port, status FROM mysql_servers;"

# Enregistrer xsel_admin dans ProxySQL sur le hostgroup writer (10)
kubectl exec pxc-cluster-pxc-db-proxysql-0 -n devmysql -c proxysql -- \
  mysql -uproxyadmin -p"$PROXYADMIN_PASS" -h 127.0.0.1 -P 6032 -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
kubectl exec pxc-cluster-pxc-db-proxysql-0 -n devmysql -c proxysql -- \
  mysql -uxsel_admin -p'XselAdmin2025!' \
  -h 127.0.0.1 -P 3306 \
  -e "SELECT user(), @@hostname, @@version;"

Résultat attendu :

text
+------------------------------------------------------------------+---------------------------+-----------+
| user()                                                           | @@hostname                | @@version |
+------------------------------------------------------------------+---------------------------+-----------+
| xsel_admin@pxc-cluster-pxc-db-proxysql-0...devmysql.svc...local | pxc-cluster-pxc-db-pxc-1  | 8.4.8-8.1 |
+------------------------------------------------------------------+---------------------------+-----------+

ℹ️ @@hostname affiche le nœud PXC vers lequel ProxySQL a routé la requête (load balancing automatique entre les 3 writers).

18.2 — phpMyAdmin

18.2.1 — Déployer phpMyAdmin

bash
cat > /root/k8s-xsel/phpmyadmin.yaml << 'EOF'
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: phpmyadmin-config
  namespace: devmysql
data:
  config.secret.inc.php: |
    <?php
    $cfg['blowfish_secret'] = 'XSELs3cr3t32CharsL0ngK3y!X2025!!';
  config.user.inc.php: |
    <?php
    $cfg['Servers'][$i]['auth_type'] = 'cookie';
    $cfg['LoginCookieValidity'] = 14400;
    $cfg['DefaultLang'] = 'fr';
  php-session.ini: |
    session.gc_maxlifetime = 14400
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: phpmyadmin
  namespace: devmysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: phpmyadmin
  template:
    metadata:
      labels:
        app: phpmyadmin
    spec:
      containers:
      - name: phpmyadmin
        image: phpmyadmin:5.2.3
        ports:
        - containerPort: 80
        env:
        - name: PMA_HOST
          value: "pxc-cluster-pxc-db-proxysql.devmysql.svc.cluster.local"
        - name: PMA_PORT
          value: "3306"
        - name: PMA_ABSOLUTE_URI
          value: "https://phpmyadmin.xselcloud.com/"
        - name: UPLOAD_LIMIT
          value: "100M"
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        volumeMounts:
        - name: config
          mountPath: /etc/phpmyadmin/config.secret.inc.php
          subPath: config.secret.inc.php
        - name: config
          mountPath: /etc/phpmyadmin/config.user.inc.php
          subPath: config.user.inc.php
        - name: config
          mountPath: /usr/local/etc/php/conf.d/php-session.ini
          subPath: php-session.ini
      volumes:
      - name: config
        configMap:
          name: phpmyadmin-config
---
apiVersion: v1
kind: Service
metadata:
  name: phpmyadmin
  namespace: devmysql
spec:
  selector:
    app: phpmyadmin
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: phpmyadmin-ingress
  namespace: devmysql
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "100m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - phpmyadmin.xselcloud.com
    secretName: phpmyadmin-tls-secret
  rules:
  - host: phpmyadmin.xselcloud.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: phpmyadmin
            port:
              number: 80
EOF

# Déployer phpMyAdmin dans le namespace devmysql
kubectl apply -f /root/k8s-xsel/phpmyadmin.yaml
# Attendre que le déploiement phpMyAdmin soit prêt
kubectl rollout status deployment/phpmyadmin -n devmysql
# Vérifier l'Ingress phpMyAdmin
kubectl get ingress -n devmysql

18.2.2 — Vérifier la connexion

bash
ROOT_PASS=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.root}' | base64 -d)

kubectl exec -it \
  $(kubectl get pod -n devmysql -l app=phpmyadmin -o name) \
  -n devmysql -- \
  php -r "
\$c=@mysqli_connect(
  'pxc-cluster-pxc-db-proxysql.devmysql.svc.cluster.local',
  'root','$ROOT_PASS',null,3306
);
echo \$c ? 'CONNEXION OK' : 'ERREUR: '.mysqli_connect_error();
" 2>/dev/null
# Résultat attendu : CONNEXION OK

18.2.3 — Créer les tables de configuration phpMyAdmin

bash
# Redéfinir ROOT_PASS si nécessaire
ROOT_PASS=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.root}' | base64 -d)

# Télécharger le script SQL sur master-01
curl -s https://raw.githubusercontent.com/phpmyadmin/phpmyadmin/STABLE/sql/create_tables.sql \
  -o /tmp/create_tables.sql

# Copier dans le pod PXC
kubectl cp /tmp/create_tables.sql \
  devmysql/pxc-cluster-pxc-db-pxc-0:/tmp/create_tables.sql -c pxc

# Créer la base phpmyadmin
kubectl exec pxc-cluster-pxc-db-pxc-0 -n devmysql -c pxc -- \
  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
kubectl exec pxc-cluster-pxc-db-pxc-0 -n devmysql -c pxc -- \
  bash -c "MYSQL_PWD='$ROOT_PASS' mysql -uroot phpmyadmin < /tmp/create_tables.sql" 2>/dev/null

# Vérifier — doit afficher 19
kubectl exec pxc-cluster-pxc-db-pxc-0 -n devmysql -c pxc -- \
  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 :

bash
kubectl edit configmap coredns -n kube-system
# Remplacer : forward . /etc/resolv.conf
# Par       : forward . 8.8.8.8 1.1.1.1
kubectl rollout restart deployment coredns -n kube-system

18.3 — Percona PostgreSQL

18.3.1 — Namespace et opérateur

bash
# Créer le namespace dédié à PostgreSQL
kubectl create namespace devpostgresql

# 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
helm install pg-operator percona/pg-operator \
  --namespace devpostgresql \
  --values /root/k8s-xsel/pg-operator-values.yaml

# Vérifier que le pod opérateur démarre correctement
kubectl get pods -n devpostgresql -l app.kubernetes.io/name=pg-operator -w

18.3.2 — Déployer le cluster PostgreSQL

bash
helm install pg-cluster percona/pg-db \
  --namespace devpostgresql \
  --set instances[0].name=instance1 \
  --set instances[0].replicas=3 \
  --set instances[0].dataVolumeClaimSpec.storageClassName=nfs-storage \
  --set instances[0].dataVolumeClaimSpec.accessModes[0]=ReadWriteOnce \
  --set instances[0].dataVolumeClaimSpec.resources.requests.storage=20Gi

# Vérifier le statut du cluster PostgreSQL Percona
kubectl get pg -n devpostgresql
# Vérifier l'état des pods PostgreSQL (instances + PgBouncer)
kubectl get pods -n devpostgresql

18.3.3 — Déployer pgAdmin

bash
cat > /root/k8s-xsel/pgadmin.yaml << 'EOF'
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pgadmin-pvc
  namespace: devpostgresql
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: nfs-storage
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgadmin
  namespace: devpostgresql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pgadmin
  template:
    metadata:
      labels:
        app: pgadmin
    spec:
      containers:
      - name: pgadmin
        image: dpage/pgadmin4:latest
        env:
        - name: PGADMIN_DEFAULT_EMAIL
          value: "admin@xsel-services.com"
        - name: PGADMIN_DEFAULT_PASSWORD
          value: "XselPgAdmin2025!"
        ports:
        - containerPort: 80
        # ⚠️ pgAdmin 4 est gourmand en mémoire — minimum 512Mi sinon OOMKilled
        resources:
          requests:
            memory: "256Mi"
            cpu: "100m"
          limits:
            memory: "768Mi"
            cpu: "500m"
        volumeMounts:
        - name: pgadmin-data
          mountPath: /var/lib/pgadmin
      volumes:
      - name: pgadmin-data
        persistentVolumeClaim:
          claimName: pgadmin-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: pgadmin
  namespace: devpostgresql
spec:
  selector:
    app: pgadmin
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: pgadmin-ingress
  namespace: devpostgresql
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
    nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - pgadmin.xselcloud.com
    secretName: pgadmin-tls-secret
  rules:
  - host: pgadmin.xselcloud.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: pgadmin
            port:
              number: 80
EOF

# Déployer pgAdmin 4 dans le namespace devpostgresql
kubectl apply -f /root/k8s-xsel/pgadmin.yaml
# Attendre que le déploiement pgAdmin soit prêt
kubectl rollout status deployment/pgadmin -n devpostgresql
# Vérifier l'Ingress pgAdmin
kubectl get ingress -n devpostgresql

18.3.4 — Récupérer les credentials PostgreSQL

bash
# Voir les secrets disponibles
# Lister les secrets de credentials PostgreSQL créés par l'opérateur
kubectl get secrets -n devpostgresql | grep pguser

# Récupérer tous les credentials
kubectl get secret pg-cluster-pg-db-pguser-pg-cluster-pg-db \
  -n devpostgresql \
  -o jsonpath='{.data}' | \
  python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
for k,v in data.items():
    print(f'{k}: {base64.b64decode(v).decode()}')
"

Credentials obtenus en production :

text
Host     : pg-cluster-pg-db-pgbouncer.devpostgresql.svc
Port     : 5432
Database : pg-cluster-pg-db
Username : pg-cluster-pg-db
Password : (voir secret pg-cluster-pg-db-pguser-pg-cluster-pg-db)

18.3.5 — Vérifier la connectivité

bash
# Test TCP depuis pgAdmin vers PostgreSQL
kubectl exec -it \
  $(kubectl get pod -n devpostgresql -l app=pgadmin -o name) \
  -n devpostgresql -- \
  python3 -c "
import socket
s = 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

18.3.6 — Accès pgAdmin

text
URL      : https://pgadmin.xselcloud.com
Email    : admin@xsel-services.com
Password : XselPgAdmin2025!

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
helm install pmm percona/pmm \
  --namespace monitoring \
  --set secret.pmm_password=XselPMM2025! \
  --set persistence.storageClassName=nfs-storage \
  --set persistence.dataSize=20Gi \
  --set service.type=ClusterIP

cat > /root/k8s-xsel/pmm-ingress.yaml << 'EOF'
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  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: 443
EOF

# Créer l'Ingress pour accéder à PMM via NGINX
kubectl apply -f /root/k8s-xsel/pmm-ingress.yaml
# Vérifier que le pod PMM est en Running
kubectl get pods -n monitoring | grep pmm

# Corriger le nom du service et le protocole backend
kubectl patch ingress pmm-ingress -n monitoring --type=json -p='[
  {
    "op": "replace",
    "path": "/spec/rules/0/http/paths/0/backend/service/name",
    "value": "monitoring-service"
  }
]'

kubectl annotate ingress pmm-ingress -n monitoring \
  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=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.root}' | base64 -d)

kubectl exec -it pxc-cluster-pxc-db-pxc-0 -n devmysql -c pxc -- \
  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';"

Résultats obtenus en production :

text
wsrep_cluster_size   : 3        ← 3 nodes actifs
wsrep_local_state    : Synced   ← tous synchronisés
wsrep_cluster_status : Primary  ← cluster actif

18.5.2 — PostgreSQL — Active/Standby (Patroni)

bash
kubectl get pods -n devpostgresql \
  -l postgres-operator.crunchydata.com/cluster=pg-cluster-pg-db \
  -o custom-columns='NAME:.metadata.name,ROLE:.metadata.labels.postgres-operator\.crunchydata\.com/role'

Résultats obtenus en production :

text
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.

18.6 — Vérification finale

bash
echo "=== MySQL ===" && kubectl get pxc -n devmysql
echo "=== Pods MySQL ===" && kubectl get pods -n devmysql
echo "=== PostgreSQL ===" && kubectl get pg -n devpostgresql
echo "=== Pods PostgreSQL ===" && kubectl get pods -n devpostgresql
echo "=== phpMyAdmin ===" && kubectl get ingress -n devmysql
echo "=== pgAdmin ===" && kubectl get ingress -n devpostgresql
echo "=== PMM ===" && kubectl get pods -n monitoring | grep pmm

Résultats obtenus en production :

text
=== MySQL ===
NAME                 ENDPOINT                               STATUS   PXC   PROXYSQL
pxc-cluster-pxc-db   pxc-cluster-pxc-db-proxysql.devmysql   ready    3     3       ✅

=== Pods MySQL ===
phpmyadmin              1/1 Running ✅
pxc-cluster-pxc-db-proxysql-0/1/2   3/3 Running ✅
pxc-cluster-pxc-db-pxc-0/1/2        3/3 Running ✅
pxc-operator            1/1 Running ✅

=== PostgreSQL ===
NAME               STATUS   POSTGRES   PGBOUNCER
pg-cluster-pg-db   ready    3          3         ✅

=== Pods PostgreSQL ===
instance1-r58g-0 / tk8f-0 / z7dr-0   4/4 Running ✅
pgbouncer (x3)                        2/2 Running ✅
pgadmin                               1/1 Running ✅

=== phpMyAdmin ===
phpmyadmin-ingress   nginx   phpmyadmin.xselcloud.com   80, 443 ✅

=== pgAdmin ===
pgadmin-ingress   nginx   pgadmin.xselcloud.com   80, 443 ✅

=== PMM ===
pmm-0   1/1 Running ✅

18.7 — Accès aux interfaces

Service URL Login Mot de passe
phpMyAdmin https://phpmyadmin.xselcloud.com root voir secret pxc-cluster-pxc-db-secrets
pgAdmin https://pgadmin.xselcloud.com admin@xsel-services.com XselPgAdmin2025!
PMM https://pmm.xselcloud.com admin XselPMM2025!



Bases de données

É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
text
Serveur Plesk
    │
    ▼
HAProxy (95.111.238.94)
    ├── :3306  →  devnode-01/02:30307  →  ProxySQL  →  PXC MySQL
    └── :5432  →  devnode-01/02:30432  →  PGBouncer →  PostgreSQL

19.2 — Récupérer les Credentials

19.2.1 — MySQL

bash
kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data}' | \
  python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
for k,v in data.items():
    print(f'{k}: {base64.b64decode(v).decode()}')
"

19.2.2 — PostgreSQL

bash
kubectl get secret pg-cluster-pg-db-pguser-pg-cluster-pg-db \
  -n devpostgresql \
  -o jsonpath='{.data}' | \
  python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
for k,v in data.items():
    print(f'{k}: {base64.b64decode(v).decode()}')
"

Credentials obtenus en production :

Base User Password Database
MySQL xsel_admin XselAdmin2025! —
PostgreSQL xseladmin boy<?a|XVk6LD9t0=7SB[;?* pg-cluster-pg-db

19.3 — Création des NodePorts Kubernetes

19.3.1 — MySQL — NodePort 30307

⚠️ Le port 30306 est déjà utilisé par le namespace client-monetix-db.

bash
kubectl patch svc pxc-cluster-pxc-db-proxysql -n devmysql \
  --type='json' -p='[
    {"op":"replace","path":"/spec/type","value":"NodePort"},
    {"op":"add","path":"/spec/ports/0/nodePort","value":30307}
  ]'

19.3.2 — PostgreSQL — Service NodePort dédié

⚠️ 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.

bash
cat << 'EOFYAML' | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: pg-pgbouncer-nodeport
  namespace: devpostgresql
spec:
  type: NodePort
  selector:
    postgres-operator.crunchydata.com/role: pgbouncer
  ports:
  - port: 5432
    targetPort: 5432
    nodePort: 30432
    protocol: TCP
EOFYAML

Vérifier les endpoints :

bash
# Vérifier que le service NodePort PostgreSQL pointe vers les bons pods
kubectl get endpoints pg-pgbouncer-nodeport -n devpostgresql
# 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 -zv 192.168.1.1 30432
# Attendu : Connected

Sauvegarder le manifest pour la persistance :

bash
kubectl get svc pg-pgbouncer-nodeport -n devpostgresql -o yaml \
  > /root/k8s-xsel/pg-pgbouncer-nodeport.yaml

19.4 — Configuration HAProxy

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
docker cp xsel-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_backend

backend 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_backend

backend 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 2
EOFCFG

# Valider la config
# Remettre la configuration modifiée dans le conteneur
docker cp /tmp/haproxy.cfg xsel-haproxy-lb:/usr/local/etc/haproxy/haproxy.cfg
# Valider la nouvelle configuration avant rechargement
docker exec xsel-haproxy-lb haproxy -f /usr/local/etc/haproxy/haproxy.cfg -c
# Attendu : Configuration file is valid

# Recharger HAProxy
# Redémarrer HAProxy pour appliquer la nouvelle configuration
docker restart xsel-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=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.root}' | base64 -d)

PROXYADMIN_PASS=$(kubectl get secret pxc-cluster-pxc-db-secrets \
  -n devmysql -o jsonpath='{.data.proxyadmin}' | base64 -d)

# 1. Créer le user xsel_admin dans PXC
kubectl exec -it pxc-cluster-pxc-db-pxc-0 -n devmysql -c pxc -- \
  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
kubectl exec -it pxc-cluster-pxc-db-proxysql-0 -n devmysql -c proxysql -- \
  mysql -uproxyadmin -p"$PROXYADMIN_PASS" -h 127.0.0.1 -P 6032 -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
kubectl patch perconapgcluster pg-cluster-pg-db -n devpostgresql --type=json -p='[
  {
    "op": "replace",
    "path": "/spec/proxy/pgBouncer/exposeSuperusers",
    "value": true
  }
]'

# 2. Ajouter le user xseladmin dans le CR
kubectl patch perconapgcluster pg-cluster-pg-db -n devpostgresql --type=merge -p='{
  "spec": {
    "users": [
      {
        "name": "xseladmin",
        "databases": ["pg-cluster-pg-db"],
        "options": "SUPERUSER"
      }
    ]
  }
}'

# 3. Attendre la reconciliation
kubectl get perconapgcluster pg-cluster-pg-db -n devpostgresql -w
# Attendre STATUS=ready

# 4. Récupérer le mot de passe généré par l'opérateur
kubectl get secret pg-cluster-pg-db-pguser-xseladmin \
  -n devpostgresql \
  -o jsonpath='{.data}' | \
  python3 -c "
import sys, json, base64
data = 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 clair
PG_PASS=$(kubectl get secret pg-cluster-pg-db-pguser-xseladmin \
  -n devpostgresql \
  -o jsonpath='{.data.password}' | base64 -d)

# Mettre à jour avec SCRAM dans PostgreSQL
kubectl exec -it pg-cluster-pg-db-instance1-r58g-0 \
  -n devpostgresql -c database -- \
  psql -U postgres -c "
SET password_encryption = 'scram-sha-256';
ALTER USER xseladmin PASSWORD '$PG_PASS';
"

# Vérifier que get_auth retourne SCRAM
kubectl exec -it pg-cluster-pg-db-instance1-r58g-0 \
  -n devpostgresql -c database -- \
  psql -U postgres \
  -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 -h 95.111.238.94 -P 3306 -u xsel_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
dnf install -y postgresql

# ⚠️ PGSSLMODE=require obligatoire — PGBouncer force SSL
# ⚠️ Utiliser printf pour écrire .pgpass (évite l'interprétation des caractères spéciaux)

PG_PASS=$(kubectl get secret pg-cluster-pg-db-pguser-xseladmin \
  -n devpostgresql \
  -o jsonpath='{.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)
chmod 600 ~/.pgpass

# Tester la connexion PostgreSQL externe via HAProxy avec SSL obligatoire
PGSSLMODE=require psql -h 95.111.238.94 -p 5432 -U xseladmin -d pg-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.

20.1 — Architecture par client

text
https://db.clientdomaine.com
        ↓
   Page d'accueil XSEL DBaaS
   ┌─────────────────────────┐
   │  🐬 MySQL │ 🐘 PostgreSQL│
   │ phpMyAdmin │   pgAdmin   │
   └─────────────────────────┘

client-<nom>-db/
  ├── pxc-<nom>           ← cluster MySQL 3 nodes + ProxySQL
  ├── pg-<nom>            ← cluster PostgreSQL 3 nodes + PgBouncer
  ├── phpmyadmin          ← interface MySQL
  ├── pgadmin             ← interface PostgreSQL
  └── landing             ← page d'accueil avec choix BDD

20.2 — Plans disponibles

Plan Accès Stockage Nodes
Standard Interface web uniquement Configurable 3 (HA)
Premium Interface web + ports TCP externes Configurable 3 (HA)

20.3 — Script de déploiement (deploy-client.sh)

bash
mkdir -p /root/k8s-xsel/templates
cat > /root/k8s-xsel/templates/deploy-client.sh << 'SCRIPTEOF'
#!/bin/bash
# =============================================================
# XSEL DBaaS — Script de déploiement client
# Usage: ./deploy-client.sh --client <nom> --domain <domaine>
#        --mysql-storage <taille> --pg-storage <taille>
#        --plan <standard|premium>
# =============================================================

set -e

CLIENT=""
DOMAIN=""
MYSQL_STORAGE="20Gi"
PG_STORAGE="20Gi"
PLAN="standard"
MYSQL_NODEPORT=""
PG_NODEPORT=""

while [[ $# -gt 0 ]]; do
  case $1 in
    --client)        CLIENT="$2";        shift 2 ;;
    --domain)        DOMAIN="$2";        shift 2 ;;
    --mysql-storage) MYSQL_STORAGE="$2"; shift 2 ;;
    --pg-storage)    PG_STORAGE="$2";    shift 2 ;;
    --plan)          PLAN="$2";          shift 2 ;;
    --mysql-port)    MYSQL_NODEPORT="$2";shift 2 ;;
    --pg-port)       PG_NODEPORT="$2";   shift 2 ;;
    *) echo "Option inconnue: $1"; exit 1 ;;
  esac
done

if [[ -z "$CLIENT" || -z "$DOMAIN" ]]; then
  echo "Usage: $0 --client <nom> --domain <domaine> [options]"
  echo "Exemple: $0 --client aceepci --domain db.aceepci.org --plan premium --mysql-port 30306 --pg-port 30543"
  exit 1
fi

NS="client-${CLIENT}-db"
MYSQL_PASS=$(openssl rand -base64 16 | tr -d '=+/')
PG_PASS=$(openssl rand -base64 16 | tr -d '=+/')
PMA_PASS=$(openssl rand -base64 16 | tr -d '=+/')
PGA_PASS=$(openssl rand -base64 16 | tr -d '=+/')

echo "============================================"
echo "  XSEL DBaaS — Déploiement client : $CLIENT"
echo "============================================"
echo "  Namespace    : $NS"
echo "  Domaine      : $DOMAIN"
echo "  MySQL Storage: $MYSQL_STORAGE"
echo "  PG Storage   : $PG_STORAGE"
echo "  Plan         : $PLAN"
echo "============================================"

# 1. Namespace
echo "[1/8] Création du namespace..."
kubectl create namespace $NS --dry-run=client -o yaml | kubectl apply -f -
kubectl label namespace $NS client=$CLIENT --overwrite

# 2. Secrets
echo "[2/8] Création des secrets..."
kubectl create secret generic mysql-client-secret \
  --from-literal=root=$MYSQL_PASS \
  --from-literal=phpmyadmin=$PMA_PASS \
  -n $NS --dry-run=client -o yaml | kubectl apply -f -

kubectl create secret generic pg-client-secret \
  --from-literal=password=$PG_PASS \
  -n $NS --dry-run=client -o yaml | kubectl apply -f -

# 3. Cluster MySQL
echo "[3/8] Déploiement cluster MySQL..."
cat << EOF | kubectl apply -f -
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
  name: pxc-$CLIENT
  namespace: $NS
spec:
  crVersion: "1.15.0"
  secretsName: mysql-client-secret
  pxc:
    size: 3
    image: percona/percona-xtradb-cluster:8.4.3-3.1
    affinity:
      antiAffinityTopologyKey: "none"
    configuration: |
      [mysqld]
      default_authentication_plugin=mysql_native_password
    volumeSpec:
      persistentVolumeClaim:
        storageClassName: nfs-storage
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: $MYSQL_STORAGE
  proxysql:
    enabled: true
    size: 3
    image: percona/percona-xtradb-cluster-operator:1.15.0-proxysql
    affinity:
      antiAffinityTopologyKey: "none"
    configuration: |
      datadir="/var/lib/proxysql"
      mysql_variables=
      {
        have_ssl=false
        default_authentication_plugin="mysql_native_password"
      }
  haproxy:
    enabled: false
EOF

# 4. Cluster PostgreSQL
echo "[4/8] Déploiement cluster PostgreSQL..."
cat << EOF | kubectl apply -f -
apiVersion: pgv2.percona.com/v2
kind: PerconaPGCluster
metadata:
  name: pg-$CLIENT
  namespace: $NS
spec:
  crVersion: "2.9.0"
  postgresVersion: 16
  instances:
  - name: instance1
    replicas: 3
    dataVolumeClaimSpec:
      storageClassName: nfs-storage
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: $PG_STORAGE
  proxy:
    pgBouncer:
      replicas: 3
  backups:
    pgbackrest:
      repos:
      - name: repo1
        volume:
          volumeClaimSpec:
            storageClassName: nfs-storage
            accessModes:
            - ReadWriteOnce
            resources:
              requests:
                storage: 5Gi
  users:
  - name: $CLIENT
    databases:
    - ${CLIENT}_db
    options: "SUPERUSER"
    password:
      type: AlphaNumeric
EOF

# 5. phpMyAdmin
echo "[5/8] Déploiement phpMyAdmin..."
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: phpmyadmin
  namespace: $NS
spec:
  replicas: 1
  selector:
    matchLabels:
      app: phpmyadmin
  template:
    metadata:
      labels:
        app: phpmyadmin
    spec:
      containers:
      - name: phpmyadmin
        image: phpmyadmin:5.2.3
        env:
        - name: PMA_HOST
          value: "pxc-${CLIENT}-proxysql.${NS}.svc.cluster.local"
        - name: PMA_PORT
          value: "3306"
        - name: PMA_ABSOLUTE_URI
          value: "https://${DOMAIN}/mysql/"
        - name: UPLOAD_LIMIT
          value: "100M"
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: phpmyadmin
  namespace: $NS
spec:
  selector:
    app: phpmyadmin
  ports:
  - port: 80
    targetPort: 80
EOF

# 6. pgAdmin
echo "[6/8] Déploiement pgAdmin..."
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pgadmin-pvc
  namespace: $NS
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: nfs-storage
  resources:
    requests:
      storage: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgadmin
  namespace: $NS
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pgadmin
  template:
    metadata:
      labels:
        app: pgadmin
    spec:
      containers:
      - name: pgadmin
        image: dpage/pgadmin4:latest
        env:
        - name: PGADMIN_DEFAULT_EMAIL
          value: "admin@${CLIENT}.xsel-ci.com"
        - name: PGADMIN_DEFAULT_PASSWORD
          value: "$PGA_PASS"
        - name: SCRIPT_NAME
          value: "/postgres"
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        volumeMounts:
        - name: pgadmin-data
          mountPath: /var/lib/pgadmin
      volumes:
      - name: pgadmin-data
        persistentVolumeClaim:
          claimName: pgadmin-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: pgadmin
  namespace: $NS
spec:
  selector:
    app: pgadmin
  ports:
  - port: 80
    targetPort: 80
EOF

# 7. Landing page
echo "[7/8] Création de la landing page..."
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: landing-page
  namespace: $NS
data:
  index.html: |
    <!DOCTYPE html>
    <html lang="fr">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>XSEL DBaaS</title>
      <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: Arial, sans-serif; background: #0d1117;
               color: white; display: flex; justify-content: center;
               align-items: center; height: 100vh; }
        .logo { font-size: 2.2em; font-weight: bold; color: #e94560;
                margin-bottom: 8px; }
        .subtitle { color: #8b949e; margin-bottom: 50px; font-size: 1em; }
        .container { text-align: center; }
        .cards { display: flex; gap: 24px; justify-content: center; }
        .card { background: #161b22; border: 1px solid #30363d;
                border-radius: 12px; padding: 40px 50px; text-decoration: none;
                color: white; transition: all 0.25s ease; display: block; }
        .card:hover { border-color: #e94560; transform: translateY(-4px);
                      box-shadow: 0 8px 24px rgba(233,69,96,0.2); }
        .icon { font-size: 3em; margin-bottom: 16px; }
        .name { font-size: 1.2em; font-weight: bold; margin-bottom: 4px; }
        .desc { color: #8b949e; font-size: 0.85em; }
      </style>
    </head>
    <body>
      <div class="container">
        <div class="logo">XSEL DBaaS</div>
        <div class="subtitle">Choisissez votre base de données</div>
        <div class="cards">
          <a class="card" href="/mysql/">
            <div class="icon">🐬</div>
            <div class="name">MySQL</div>
            <div class="desc">phpMyAdmin</div>
          </a>
          <a class="card" href="/postgres/">
            <div class="icon">🐘</div>
            <div class="name">PostgreSQL</div>
            <div class="desc">pgAdmin</div>
          </a>
        </div>
      </div>
    </body>
    </html>
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: landing
  namespace: $NS
spec:
  replicas: 1
  selector:
    matchLabels:
      app: landing
  template:
    metadata:
      labels:
        app: landing
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
      volumes:
      - name: html
        configMap:
          name: landing-page
---
apiVersion: v1
kind: Service
metadata:
  name: landing
  namespace: $NS
spec:
  selector:
    app: landing
  ports:
  - port: 80
    targetPort: 80
EOF

# 8. Ingress (rewrite regex par path)
echo "[8/8] Création de l'Ingress..."
cat << EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: client-ingress
  namespace: $NS
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "100m"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - $DOMAIN
    secretName: ${CLIENT}-tls-secret
  rules:
  - host: $DOMAIN
    http:
      paths:
      - path: /mysql(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: phpmyadmin
            port:
              number: 80
      - path: /postgres(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: pgadmin
            port:
              number: 80
      - path: /
        pathType: Prefix
        backend:
          service:
            name: landing
            port:
              number: 80
EOF

# Plan Premium — ports TCP externes
if [[ "$PLAN" == "premium" && -n "$MYSQL_NODEPORT" ]]; then
  cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: mysql-external
  namespace: $NS
spec:
  type: NodePort
  selector:
    app.kubernetes.io/component: proxysql
    app.kubernetes.io/instance: pxc-$CLIENT
  ports:
  - port: 3306
    targetPort: 3306
    nodePort: $MYSQL_NODEPORT
EOF
fi

if [[ "$PLAN" == "premium" && -n "$PG_NODEPORT" ]]; then
  cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: pg-external
  namespace: $NS
spec:
  type: NodePort
  selector:
    postgres-operator.crunchydata.com/cluster: pg-$CLIENT
    postgres-operator.crunchydata.com/role: primary
  ports:
  - port: 5432
    targetPort: 5432
    nodePort: $PG_NODEPORT
EOF
fi

# NetworkPolicy
cat << EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: isolate-client
  namespace: $NS
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          client: $CLIENT
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: devmysql
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: devpostgresql
EOF

echo ""
echo "============================================"
echo "  ✅ Client $CLIENT déployé avec succès !"
echo "============================================"
echo ""
echo "  DNS à configurer :"
echo "  $DOMAIN → 95.111.238.94"
echo ""
echo "  URLs :"
echo "  Accueil  : https://$DOMAIN"
echo "  MySQL    : https://$DOMAIN/mysql/"
echo "  Postgres : https://$DOMAIN/postgres/"
echo ""
echo "  Credentials MySQL :"
echo "  User     : root"
echo "  Password : $MYSQL_PASS"
echo ""
echo "  Credentials pgAdmin :"
echo "  Email    : admin@${CLIENT}.xsel-ci.com"
echo "  Password : $PGA_PASS"
echo ""
if [[ "$PLAN" == "premium" ]]; then
  echo "  Accès TCP direct :"
  [[ -n "$MYSQL_NODEPORT" ]] && echo "  MySQL     : 95.111.238.94:$MYSQL_NODEPORT"
  [[ -n "$PG_NODEPORT"    ]] && echo "  PostgreSQL: 95.111.238.94:$PG_NODEPORT"
  echo ""
fi
echo "  Surveiller le démarrage :"
echo "  kubectl get pods -n $NS -w"
echo "============================================"
SCRIPTEOF

# Rendre le script de déploiement client exécutable
chmod +x /root/k8s-xsel/templates/deploy-client.sh
echo "✅ Script créé"

20.4 — Utilisation du script

Plan Standard (interface web uniquement) :

bash
cd /root/k8s-xsel/templates

./deploy-client.sh \
  --client aceepci \
  --domain db.aceepci.org \
  --mysql-storage 20Gi \
  --pg-storage 20Gi \
  --plan standard

Plan Premium (avec accès TCP direct) :

bash
./deploy-client.sh \
  --client monetix \
  --domain db.monetix.com \
  --mysql-storage 50Gi \
  --pg-storage 50Gi \
  --plan premium \
  --mysql-port 30306 \
  --pg-port 30543

20.5 — Notes importantes

⚠️ 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
`kubectl patch pxc pxc-[CLIENT] -n client-[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
`kubectl get pods -n client-[CLIENT]-db -w`

20.6 — Résultats obtenus en production (client monetix)

text
✅ Namespace    : client-monetix-db créé
✅ MySQL PXC    : pxc-monetix ready 3/3 PXC + 3/3 ProxySQL
✅ PostgreSQL   : pg-monetix ready
✅ phpMyAdmin   : Running — connexion MySQL OK après patch ProxySQL
✅ pgAdmin      : Running — connexion PostgreSQL OK
✅ Landing page : Running — https://db-client.xsel-ci.com
✅ TLS          : Certificat Let's Encrypt émis automatiquement
✅ MySQL TCP    : 95.111.238.94:30306
✅ PostgreSQL TCP: 95.111.238.94:30543

Pods :
landing          1/1 Running ✅
pgadmin          1/1 Running ✅
phpmyadmin       1/1 Running ✅
pxc-monetix-proxysql-0/1/2  3/3 Running ✅
pxc-monetix-pxc-0/1/2       1/1 Running ✅

20.7 — Supprimer un client

bash
# Supprimer complètement un client
# Supprimer complètement l'environnement d'un client (données incluses)
kubectl delete namespace client-<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.




Multi-tenant

Étape 21 — vCluster (Kubernetes Virtuel Multi-Tenant)

21.1 — Concept

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.

text
Cluster physique XSEL
  │
  ├── devnode-01  ─┐ Workers partagés
  ├── devnode-02  ─┘
  │
  └── namespace: vcluster-<client>
        └── pod: vcluster-<client>-0
              ├── API Server Kubernetes
              ├── etcd
              └── Controller Manager

Ce que le client voit :

bash
kubectl get nodes       → ses nodes virtuels
kubectl get namespaces  → ses propres namespaces
kubectl apply -f app    → déploie librement

Ce que XSEL voit :

bash
kubectl get pods -n vcluster-<client>
→ vcluster-<client>-0  (tout le cluster du client dans 1 pod)
→ <pod>-x-<namespace>-x-vcluster-<client>  (pods réels renommés)

21.2 — Installation vCluster CLI

bash
# 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é
vcluster version
# Résultat : vcluster version 0.34.0

21.3 — Créer un vCluster

bash
# Créer le namespace
kubectl create namespace vcluster-xsel

# Créer le vCluster avec sync Ingress activé
vcluster create vcluster-xsel \
  --namespace vcluster-xsel \
  --connect=false \
  --set sync.toHost.ingresses.enabled=true

# Surveiller le démarrage
kubectl get pods -n vcluster-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
vcluster connect vcluster-xsel --namespace vcluster-xsel
# Résultat : Switched active kube context to vcluster_vcluster-xsel_...

# Vérifier les nodes
# Afficher les nœuds du cluster et leur statut
kubectl get nodes
# 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
kubectl get namespaces

# Se déconnecter
# Restaurer le contexte kubectl vers le cluster physique
vcluster disconnect

21.5 — Déployer une application dans le vCluster

bash
# Se connecter au vCluster
vcluster connect vcluster-xsel --namespace vcluster-xsel

# Créer un namespace applicatif
kubectl create namespace test-app

# Déployer une app
kubectl create deployment hello-vcluster \
  --image=nginx:alpine \
  --namespace test-app

# Exposer via Service
kubectl expose deployment hello-vcluster \
  --port=80 \
  --target-port=80 \
  --namespace test-app

# Créer l'Ingress
cat << 'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  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: 80
EOF

# Vérifier l'Ingress — doit avoir une adresse
# Vérifier la règle Ingress de l'application de test
kubectl get ingress -n test-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)
kubectl get pods -n test-app
# NAME                              READY
# hello-vcluster-6c9cd74cd9-xg6dg   1/1 Running

# Ce que XSEL voit (cluster physique)
kubectl get pods -n vcluster-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é)
curl http://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
vcluster list

# Se connecter
vcluster connect <nom> --namespace <namespace>

# Se déconnecter
# Restaurer le contexte kubectl vers le cluster physique
vcluster disconnect

# Supprimer un vCluster
vcluster delete <nom> --namespace <namespace>

# Voir les logs du vCluster
kubectl logs vcluster-<nom>-0 -n vcluster-<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
kubeadm join 192.168.0.254:6443 --token <token> ...

# 2. Labelliser les nodes
kubectl label node node-bni-db-01 client=bni role=database

# 3. Taint pour isolation stricte
kubectl taint node node-bni-db-01 client=bni:NoSchedule

# 4. Créer le vCluster sur ses nodes dédiés
vcluster create vcluster-bni \
  --namespace vcluster-bni \
  --set sync.toHost.ingresses.enabled=true \
  --set syncer.extraArgs[0]="--node-selector=client=bni" \
  --set syncer.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.1 — Architecture cible

text
Internet
    │
HAProxy XSEL (95.111.238.94)
    │
    ├── NGINX Ingress → vCluster client → Pods apps (node-client-app-01/02)
    └── HAProxy TCP  → MySQL/PostgreSQL  (node-client-db-01/02)

Cluster physique XSEL
  ├── xsel-master-01  (192.168.100.1)
  ├── xsel-master-02  (192.168.100.2)
  ├── xsel-devnode-01 (192.168.1.1)  ← workers XSEL partagés
  ├── xsel-devnode-02 (192.168.1.2)
  │
  └── Serveurs client (ex: BNI)
        ├── node-bni-db-01  (192.168.100.21)  ← BDD dédiée
        ├── node-bni-db-02  (192.168.100.22)  ← BDD dédiée
        ├── node-bni-app-01 (192.168.100.23)  ← Apps dédiées
        └── node-bni-app-02 (192.168.100.24)  ← Apps dédiées

21.10.2 — Prérequis sur les serveurs client

Même procédure que pour devnode-01 et devnode-02. Sur chaque serveur client :

bash
# 1. Désactiver swap
swapoff -a
sed -i '/swap/d' /etc/fstab

# 2. Désactiver SELinux
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config

# 3. Désactiver firewalld
systemctl disable --now firewalld

# 4. Modules kernel
cat >> /etc/modules-load.d/k8s.conf << EOF
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter

cat >> /etc/sysctl.d/k8s.conf << EOF
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sysctl --system

# 5. Installer containerd
dnf install -y containerd
systemctl enable --now containerd

# 6. Installer kubelet kubeadm kubectl
cat >> /etc/yum.repos.d/kubernetes.repo << EOF
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.29/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.29/rpm/repodata/repomd.xml.key
EOF

dnf install -y kubelet-1.29.15 kubeadm-1.29.15 kubectl-1.29.15
systemctl enable kubelet

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
kubeadm token create --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
kubeadm join 192.168.0.254:6443 \
  --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash>

21.10.4 — Labelliser et isoler les nodes client

bash
# Labelliser les nodes client
kubectl label node node-bni-db-01  client=bni role=database tier=premium
kubectl label node node-bni-db-02  client=bni role=database tier=premium
# Labelliser le nœud APP-01
kubectl label node node-bni-app-01 client=bni role=app      tier=premium
# Labelliser le nœud APP-02
kubectl label node node-bni-app-02 client=bni role=app      tier=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
kubectl taint node node-bni-db-01  client=bni:NoSchedule
# Empêcher les pods non-BNI de s'exécuter sur ce nœud
kubectl taint node node-bni-db-02  client=bni:NoSchedule
# Empêcher les pods non-BNI de s'exécuter sur ce nœud
kubectl taint node node-bni-app-01 client=bni:NoSchedule
# Empêcher les pods non-BNI de s'exécuter sur ce nœud
kubectl taint node node-bni-app-02 client=bni:NoSchedule

# Vérifier
# Vérifier les labels et taints des nœuds client BNI
kubectl get nodes --show-labels | grep bni

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
kubectl create namespace vcluster-bni

# Créer le vCluster en ciblant UNIQUEMENT les nodes du client
vcluster create vcluster-bni \
  --namespace vcluster-bni \
  --connect=false \
  --set sync.toHost.ingresses.enabled=true \
  --set syncer.extraArgs[0]="--node-selector=client=bni" \
  --set syncer.extraArgs[1]="--enforce-node-selector" \
  --set tolerations[0].key=client \
  --set tolerations[0].operator=Equal \
  --set tolerations[0].value=bni \
  --set tolerations[0].effect=NoSchedule

# Surveiller le démarrage
# Surveiller le démarrage du pod vCluster BNI
kubectl get pods -n vcluster-bni -w

21.10.6 — Générer le kubeconfig client

bash
# Générer le kubeconfig pour le client
vcluster connect vcluster-bni \
  --namespace vcluster-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 +x kubectl && mv kubectl /usr/local/bin/

# macOS
brew install kubectl

# Windows
choco install kubernetes-cli

# Le client configure son accès
export KUBECONFIG=./kubeconfig-bni.yaml
# Afficher les nœuds du cluster et leur statut
kubectl get nodes
# Vérifier que les namespaces ont bien été créés
kubectl get namespaces

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
helm install rancher rancher-latest/rancher \
  --namespace cattle-system \
  --set hostname=rancher.bni.ci \
  --set bootstrapPassword=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
vcluster connect vcluster-bni --namespace vcluster-bni
# Afficher les nœuds du cluster et leur statut
kubectl get nodes

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

# Depuis XSEL — vérifier l'isolation
# Vérifier l'état du vCluster BNI depuis le cluster physique
kubectl get pods -n vcluster-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 kubectl
export KUBECONFIG=./kubeconfig-bni.yaml

# Il voit ses nodes
# Afficher les nœuds du cluster et leur statut
kubectl get nodes

# Il crée ses namespaces
kubectl create namespace production
kubectl create namespace staging

# Il déploie ses applications
kubectl apply -f mon-app.yaml

# Il crée ses Ingress
kubectl apply -f mon-ingress.yaml
# → automatiquement synchronisé vers NGINX XSEL

# Il installe ses propres outils
helm install prometheus prometheus-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
kubectl patch service vcluster-bni \
  -n vcluster-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 Server
frontend vcluster_bni_frontend
    bind *:6443
    mode tcp
    option tcplog
    default_backend vcluster_bni_backend

backend 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 5s
EOF

# Redémarrer HAProxy pour appliquer la nouvelle configuration
docker restart xsel-haproxy-lb

Le client accède à son cluster via :

bash
kubectl --server=https://95.111.238.94:6443 get nodes

21.10.11 — Résumé des responsabilités

Tâche XSEL Client
Gestion nodes physiques ✅ ❌
Mise à jour Kubernetes ✅ ❌
Monitoring infrastructure ✅ ❌
Déploiement applications ❌ ✅
Gestion namespaces ❌ ✅
Configuration Ingress ❌ ✅
Gestion certificats TLS Cert-Manager XSEL ✅ via Ingress



Supervision et administration

Étape 22 — Installation Stack Monitoring (Prometheus, Grafana, AlertManager, Loki, Promtail)

ℹ️ 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à
kubectl get namespace monitoring

22.2 — Ajouter le repo Helm et rechercher le chart

bash
# Ajouter le repo Helm
# Ajouter le repo Helm officiel de la communauté Prometheus
helm repo add prometheus-community \
  https://prometheus-community.github.io/helm-charts
# Mettre à jour le cache des repos Helm
helm repo update

# Vérifier les charts disponibles
# Vérifier la dernière version disponible du chart
helm search repo prometheus-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 authentification
grafana:
  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:9090
prometheus:
  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 monitoring
alertmanager:
  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 nodes
nodeExporter:
  enabled: true

# Kube State Metrics — métriques Kubernetes
kubeStateMetrics:
  enabled: true
EOF
echo "✅ 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
kubectl top nodes

# CPU requests alloués sur chaque worker
kubectl describe node xsel-devnode-01 | grep -A6 "Allocated resources"
kubectl describe node xsel-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
kubectl patch pxc pxc-cluster-pxc-db -n devmysql --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
watch kubectl get pods -n devmysql
# Résultat attendu : tous les pods pxc et proxysql en 3/3 Running

# Vérifier que les workers sont maintenant < 85%
kubectl describe node xsel-devnode-01 | grep -A4 "Allocated resources"
kubectl describe node xsel-devnode-02 | grep -A4 "Allocated resources"

Étape 3 — Vérifier le quota monitoring

bash
# Vérifier que le quota monitoring est correctement configuré
kubectl describe resourcequota monitoring-quota -n monitoring

# 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
kubectl apply -f /root/k8s-xsel/resource-quotas.yaml

Étape 4 — Vérifier le LimitRange monitoring

bash
kubectl describe limitrange -n monitoring

# 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").

22.7 — Installer kube-prometheus-stack via Helm

bash
helm install kube-prometheus-stack \
  prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --values /root/k8s-xsel/kube-prometheus-stack-values.yaml \
  --wait \
  --timeout 10m

ℹ️ 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
kubectl get pods -n monitoring
# 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
kubectl get svc -n monitoring

# Vérifier les Ingress et certificats TLS
# Vérifier les Ingress pour l'accès externe
kubectl get ingress -n monitoring
# Vérifier les certificats TLS des interfaces web
kubectl get certificate -n monitoring

# Vérifier les PVCs (stockage)
# Vérifier que les volumes persistants sont en état Bound
kubectl get pvc -n monitoring
# 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é)
kubectl port-forward svc/kube-prometheus-stack-prometheus \
  -n monitoring 9090: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-plan
kill %1

22.10 — Accéder à Grafana

bash
# Option 1 — Via Ingress (DNS configuré)
# https://grafana.xselcloud.com
# Login : admin
# Password : XselGrafana2025!

# Option 2 — Port-forward (sans DNS)
kubectl port-forward svc/kube-prometheus-stack-grafana \
  -n monitoring 3000:80 &
# http://localhost:3000

ℹ️ 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
kubectl port-forward svc/kube-prometheus-stack-prometheus \
  -n monitoring 9090:9090
# → http://localhost:9090

# Accéder à AlertManager via tunnel
kubectl port-forward svc/kube-prometheus-stack-alertmanager \
  -n monitoring 9093:9093
# → http://localhost:9093

22.12 — Résumé kube-prometheus-stack

text
✅ Namespace monitoring       : créé
✅ Helm chart installé        : kube-prometheus-stack
✅ 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
✅ Stockage                   : Longhorn (2 replicas, devnode-01 + devnode-02)

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
helm repo add grafana https://grafana.github.io/helm-charts
# Mettre à jour le cache des repos Helm
helm repo update

# Vérifier les versions disponibles
helm search repo grafana/loki
helm search repo grafana/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: SingleBinary

singleBinary:
  replicas: 1
  persistence:
    enabled: true
    storageClass: longhorn
    size: 20Gi

# Désactiver les composants distribués (inutiles avec 2 workers)
backend:
  replicas: 0
read:
  replicas: 0
write:
  replicas: 0
ingester:
  replicas: 0
querier:
  replicas: 0
queryFrontend:
  replicas: 0
queryScheduler:
  replicas: 0
distributor:
  replicas: 0
compactor:
  replicas: 0
indexGateway:
  replicas: 0
bloomCompactor:
  replicas: 0
bloomGateway:
  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: 1229Mi

chunksCache:
  allocatedMemory: 2048
  resources:
    requests:
      cpu: 100m
      memory: 2Gi
    limits:
      memory: 2560Mi
EOF
echo "✅ 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) :

bash
kubectl get limitrange -n monitoring -o yaml | grep -A3 "max:"

22.12.3 — Installer Loki

⚠️ 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
nohup helm upgrade --install loki grafana/loki \
  --namespace monitoring \
  --values /root/k8s-xsel/loki-values.yaml \
  --wait \
  --timeout 8m > /tmp/loki-install.log 2>&1 &

# Patienter puis vérifier le résultat
cat /tmp/loki-install.log
helm status loki -n monitoring

# Vérifier que le pod Loki est Running
kubectl get pods -n monitoring | grep loki

⚠️ 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
helm history loki -n monitoring
kubectl get secrets -n monitoring -l owner=helm,name=loki
# Supprimer UNIQUEMENT les révisions en pending-*/failed, jamais une révision deployed
kubectl delete secret -n monitoring sh.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 monitoring
config:
  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/pods
tolerations:
- key: node-role.kubernetes.io/control-plane
  operator: Exists
  effect: NoSchedule
EOF
echo "✅ Fichier promtail-values.yaml créé"

22.12.5 — Installer Promtail

bash
# Installer Promtail dans le namespace monitoring
helm install promtail grafana/promtail \
  --namespace monitoring \
  --values /root/k8s-xsel/promtail-values.yaml \
  --wait \
  --timeout 5m

# Vérifier que les pods Promtail sont Running (1 par nœud)
kubectl get pods -n monitoring | grep promtail

Résultat attendu :

text
promtail-xxxxx   1/1   Running   0   1m   (xsel-devnode-01)
promtail-xxxxx   1/1   Running   0   1m   (xsel-devnode-02)

22.12.6 — Configurer la datasource Loki dans Grafana

bash
# Récupérer le nom du pod Grafana
GRAFANA_POD=$(kubectl get pod -n monitoring -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].metadata.name}')
echo "Pod Grafana : $GRAFANA_POD"

# Ajouter Loki comme datasource via l'API Grafana
kubectl exec -n monitoring $GRAFANA_POD -c grafana -- \
  curl -s -X POST \
  -H "Content-Type: application/json" \
  -u admin:XselGrafana2025! \
  http://localhost:3000/api/datasources \
  -d '{
    "name": "Loki",
    "type": "loki",
    "url": "http://loki-gateway.monitoring.svc.cluster.local",
    "access": "proxy",
    "isDefault": false
  }' | python3 -m json.tool

Résultat attendu :

text
{
  "datasource": { "name": "Loki", "type": "loki" },
  "message": "Datasource added"
}

22.12.7 — Vérifier dans Grafana

text
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

22.12.8 — Désinstaller Loki + Promtail (après validation)

ℹ️ 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
helm uninstall promtail -n monitoring

# Désinstaller Loki
helm uninstall loki -n monitoring

# Supprimer le PVC Loki (données de logs)
kubectl delete pvc -n monitoring -l app.kubernetes.io/name=loki

# Vérifier que tout est supprimé
kubectl get pods -n monitoring | 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.

Architecture globale

text
CLUSTER KUBERNETES (pods, nodes, apps)
           │
    ┌──────┴──────┐
    │             │
MÉTRIQUES       LOGS
    │             │
    ▼             ▼
Prometheus      Promtail
(collecte)    (collecte)
    │             │
    │             ▼
    │            Loki
    │           (stocke)
    └──────┬──────┘
           ▼
        Grafana
    (visualise tout)
           │
           ▼
      AlertManager
       (alerte)

Rôle de chaque outil

Outil Rôle Ce qu'il surveille
Node Exporter Collecte métriques système CPU, RAM, disque, réseau de chaque nœud
Kube State Metrics Collecte métriques Kubernetes État des pods, deployments, PVCs, nodes
Prometheus Stocke les métriques Historique 30 jours, requêtes PromQL
Promtail Collecte les logs Logs de tous les pods sur chaque nœud
Loki Stocke les logs Historique 30 jours, requêtes LogQL
AlertManager Gère les alertes Notifie l'équipe quand un seuil est dépassé
Grafana Visualise tout Métriques + logs dans une interface unique

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
kubectl logs -n monitoring -l app.kubernetes.io/name=promtail -f 2>/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
helm upgrade promtail grafana/promtail \
  --namespace monitoring \
  --values /root/k8s-xsel/promtail-values.yaml

# Vérifier que les erreurs ont disparu
kubectl logs -n monitoring -l app.kubernetes.io/name=promtail --tail=10 2>/dev/null

Résolution 3 — Vérifier les labels disponibles dans Loki :

bash
# Vérifier que Loki reçoit bien des logs récents
kubectl exec -n monitoring loki-0 -c loki -- \
  wget -q -O - "http://localhost:3100/loki/api/v1/labels" 2>/dev/null | \
  python3 -m json.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
kubectl logs -n monitoring -l app.kubernetes.io/name=promtail --tail=5 2>/dev/null

# 2. Vérifier que Loki reçoit les logs
kubectl exec -n monitoring loki-0 -c loki -- \
  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"

22.16 — Désinstallation complète (pour démonstration)

ℹ️ 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
helm uninstall promtail -n monitoring
echo "✅ Promtail désinstallé"

# 2. Désinstaller Loki
helm uninstall loki -n monitoring
echo "✅ Loki désinstallé"

# 3. Désinstaller kube-prometheus-stack
helm uninstall kube-prometheus-stack -n monitoring
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
kubectl delete pvc -n monitoring \
  $(kubectl get pvc -n monitoring --no-headers | grep -v pmm | awk '{print $1}')
echo "✅ PVCs supprimés (PMM conservé)"

# Vérifier — seul pmm-storage-pmm-0 doit rester
kubectl get pvc -n monitoring

# 5. Supprimer les CRDs Prometheus (optionnel — les recréer au prochain install)
kubectl delete crd prometheuses.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.com 2>/dev/null || true
echo "✅ CRDs supprimées"

# 6. Vérifier que tout est supprimé
kubectl get pods -n monitoring
kubectl get pvc -n monitoring

⚠️ 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)
kubectl get pods -n cert-manager

# Vérifier que le cluster est en état Ready
kubectl get nodes

# Vérifier les ressources disponibles (Rancher nécessite min 2 CPU / 4Gi RAM libres)
kubectl top nodes

23.2 — Installer Rancher

bash
# Ajouter le repo Helm Rancher (version stable)
helm repo add rancher-stable https://releases.rancher.com/server-charts/stable
# Mettre à jour le cache des repos Helm
helm repo update

# Vérifier la dernière version disponible
helm search repo rancher-stable/rancher

# Créer le namespace cattle-system
kubectl create namespace cattle-system

# Installer Rancher avec cert-manager comme gestionnaire TLS
# ⚠️ hostname doit correspondre à un enregistrement DNS pointant vers HAProxy (95.111.238.94)
helm install rancher rancher-stable/rancher \
  --namespace cattle-system \
  --set hostname=rancher.xselcloud.com \
  --set bootstrapPassword=XselRancher2025! \
  --set ingress.tls.source=letsEncrypt \
  --set letsEncrypt.email=admin@xsel-services.com \
  --set letsEncrypt.ingress.class=nginx \
  --set replicas=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
kubectl patch ingress rancher -n cattle-system \
  --type=merge -p '{"spec":{"ingressClassName":"nginx"}}'

# Surveiller le déploiement (peut prendre 3-5 minutes)
kubectl rollout status deployment/rancher -n cattle-system
watch kubectl get pods -n cattle-system

23.3 — Vérifier l'installation

bash
# Vérifier les pods Rancher
kubectl get pods -n cattle-system

# Vérifier l'Ingress
kubectl get ingress -n cattle-system

# Vérifier le certificat TLS
kubectl get certificate -n cattle-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 :

Domaine Type Valeur
rancher.xselcloud.com A 95.111.238.94

23.5 — Premier accès

text
URL      : https://rancher.xselcloud.com
Username : admin
Password : XselRancher2025!

ℹ️ 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
kubectl logs -n cattle-system -l app=rancher --tail=30

# Vérifier que le webhook cert-manager fonctionne
kubectl get validatingwebhookconfigurations | grep rancher

⚠️ Si le certificat TLS ne se génère pas :

bash
kubectl describe certificate -n cattle-system
kubectl get challenges -n cattle-system

23.9 — Résumé

text
✅ Namespace cattle-system   : créé
✅ Helm chart installé       : rancher-stable/rancher
✅ Ingress                   : https://rancher.xselcloud.com
✅ TLS                       : Let's Encrypt (cert-manager)
✅ Cluster XSEL importé      : xsel-cluster
✅ Isolation clients         : Projects/Namespaces par vCluster
✅ RBAC visuel               : rôles par projet


Supervision et administration

É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
kubectl get pods -n monitoring -l app.kubernetes.io/name=prometheus -o wide
ip addr show wg100

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
kubectl run netdebug --image=nicolaka/netshoot \
  --overrides='{"spec":{"nodeSelector":{"kubernetes.io/hostname":"<NOEUD>"}}}' \
  -n monitoring -- sleep 3600

kubectl exec -n monitoring netdebug -- ping -c 3 <IP_WG100_CIBLE>
kubectl exec -n monitoring netdebug -- curl -v --max-time 5 http://<IP_WG100_CIBLE>:9100/metrics

kubectl delete pod netdebug -n monitoring

ℹ️ 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 -LO https://github.com/prometheus/node_exporter/releases/latest/download/node_exporter-1.9.1.linux-amd64.tar.gz
tar xvfz node_exporter-*.tar.gz
sudo mv node_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)
sudo tee /etc/systemd/system/node_exporter.service << 'EOF'
[Unit]
Description=Node Exporter
After=network.target

[Service]
ExecStart=/usr/local/bin/node_exporter --web.listen-address=192.168.X.X:9100
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter
sudo systemctl status node_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
bash
helm upgrade kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --values /root/k8s-xsel/kube-prometheus-stack-values.yaml \
  --wait --timeout 5m

⚠️ 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 :

bash
nohup helm upgrade kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --values /root/k8s-xsel/kube-prometheus-stack-values.yaml \
  --wait --timeout 5m > /tmp/upgrade.log 2>&1 &

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) :

bash
helm history kube-prometheus-stack -n monitoring
kubectl get secrets -n monitoring -l owner=helm,name=kube-prometheus-stack
kubectl delete secret -n monitoring sh.helm.release.v1.kube-prometheus-stack.vX   # révision bloquée uniquement

Vérifier que la cible est bien up :

bash
kubectl port-forward -n monitoring svc/kube-prometheus-stack-prometheus 9090:9090 &
curl -s 'http://localhost:9090/api/v1/targets' | python3 -c "
import json, sys
d = 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)
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Créer le namespace
kubectl create namespace alloy

# 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"
      }
EOF
echo "✅ Fichier alloy-values.yaml créé"

# Installer Alloy (utiliser --install pour gérer install ET upgrade en une commande)
helm upgrade --install alloy grafana/alloy \
  --namespace alloy \
  --values /root/k8s-xsel/alloy-values.yaml

# Vérifier
kubectl get pods -n alloy

Pour ajouter une nouvelle cible HTTP/ICMP, décommenter/ajouter un bloc target { ... } puis :

bash
helm upgrade --install alloy grafana/alloy \
  --namespace alloy \
  --values /root/k8s-xsel/alloy-values.yaml

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)

Architecture XSEL :

text
Portainer Server (cluster XSEL)
        │
   ┌────┴────────────────────────────┐
   │                                 │
Portainer Agent               Portainer Agent
Kubernetes (in-cluster)       Docker (serveurs externes)
   │                                 │
Cluster XSEL                  Serveurs clients
(pods, services,              (containers Docker
 deployments...)               hors cluster)

25.1 — Prérequis

bash
# Vérifier que le cluster est opérationnel
kubectl get nodes

# Vérifier les ressources disponibles
kubectl top nodes

25.2 — Créer le namespace et les ressources RBAC

bash
# Créer le namespace Portainer
kubectl create namespace portainer

# Vérifier
kubectl get namespace portainer

25.3 — Installer Portainer Server via Helm

bash
# Ajouter le repo Helm Portainer
helm repo add portainer https://portainer.github.io/k8s/
helm repo update

# Vérifier la version disponible
helm search repo portainer/portainer

# Créer le fichier de values Portainer
cat > /root/k8s-xsel/portainer-values.yaml << 'EOF'
# =============================================
# XSEL — Portainer values
# =============================================

service:
  type: ClusterIP

ingress:
  enabled: true
  ingressClassName: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    # ⚠️ Portainer utilise des requêtes longues (websocket)
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
  hosts:
  - host: portainer.xselcloud.com
    paths:
    - path: /
      pathType: Prefix
  tls:
  - secretName: portainer-tls
    hosts:
    - portainer.xselcloud.com

persistence:
  enabled: true
  storageClass: longhorn
  size: 10Gi

# ⚠️ requests.cpu réduit — workers déjà chargés
resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"
EOF
echo "✅ Fichier portainer-values.yaml créé"

# Vérifier la syntaxe YAML
python3 -c "import yaml; yaml.safe_load(open('/root/k8s-xsel/portainer-values.yaml'))" && echo "✅ YAML valide"

# Installer Portainer
helm install portainer portainer/portainer \
  --namespace portainer \
  --values /root/k8s-xsel/portainer-values.yaml

# Surveiller le démarrage
watch kubectl get pods -n portainer

Résultat attendu :

text
NAME                         READY   STATUS    RESTARTS   AGE
portainer-xxxx               1/1     Running   0          1m

25.4 — Vérifier l'installation

bash
# Vérifier les pods
kubectl get pods -n portainer

# Vérifier l'Ingress
kubectl get ingress -n portainer

# Vérifier le certificat TLS
kubectl get certificate -n portainer

# Vérifier le PVC
kubectl get pvc -n portainer

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é
kubectl rollout restart deployment/portainer -n portainer
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

docker run -d \
  -p 9001:9001 \
  --name portainer_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
kubectl apply -f https://downloads.portainer.io/ce2-20/portainer-agent-k8s-lb.yaml

# Récupérer l'IP de l'agent
kubectl get svc -n portainer-agent

Ensuite dans Portainer :

text
1. Menu → Environments → Add Environment
2. Choisir "Kubernetes"
3. Choisir "Agent"
4. Environment URL : IP_AGENT:9001
5. Cliquer "Connect"

25.9 — Fonctionnalités clés pour XSEL

Fonctionnalité Description
Multi-environnements Gérer cluster XSEL + serveurs clients depuis une interface
Stacks Déployer des docker-compose sur les serveurs externes
Templates Créer des templates d'apps pour déploiement rapide
Users & Teams Créer des accès par client — chaque client voit son environnement
Logs en temps réel Voir les logs de n'importe quel container
Console Ouvrir un terminal dans un container directement depuis le navigateur
Registries Connecter Harbor XSEL pour déployer les images privées

25.10 — Connecter Harbor à Portainer

text
Dans Portainer :
1. Menu → Registries → Add Registry
2. Choisir "Custom Registry"
3. Name : Harbor XSEL
4. Registry URL : https://harbor.xselcloud.com
5. Username : admin
6. Password : (mot de passe Harbor)
7. Cliquer "Add Registry"
→ Les images Harbor sont disponibles lors des déploiements

25.11 — Résumé

text
✅ Namespace portainer         : créé
✅ Helm chart installé         : portainer/portainer
✅ Ingress                     : https://portainer.xselcloud.com
✅ TLS                         : Let's Encrypt (cert-manager)
✅ Stockage                    : Longhorn 10Gi
✅ Cluster XSEL                : géré (auto-détecté)
✅ Serveurs Docker externes    : via Portainer Agent (port 9001)
✅ Harbor connecté             : images privées disponibles
✅ Accès multi-clients         : isolation par environnement


Supervision et administration

É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
helm repo add aqua https://aquasecurity.github.io/helm-charts/
helm repo update

# Vérifier la version disponible
helm search repo aqua/trivy-operator

# Créer le namespace
kubectl create namespace trivy-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 scan
operator:
  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"
EOF
echo "✅ 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à)
nohup helm upgrade --install trivy-operator aqua/trivy-operator \
  --namespace trivy-system \
  --values /root/k8s-xsel/trivy-operator-values.yaml \
  --wait \
  --timeout 5m > /tmp/trivy-upgrade.log 2>&1 &

# Vérifier (patienter puis relire le log)
cat /tmp/trivy-upgrade.log
kubectl get pods -n trivy-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 ?
sleep 120
kubectl get pods -n trivy-system | grep trivy-operator
kubectl top pod -n trivy-system -l app.kubernetes.io/name=trivy-operator

# 2. node-collector arrive-t-il à se scheduler sur TOUS les nœuds (masters inclus) ?
kubectl get pods -n trivy-system -o wide | grep node-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)
kubectl describe pod -n trivy-system <nom-node-collector> | tail -10
kubectl describe node <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 :

bash
helm upgrade --install loki grafana/loki --namespace monitoring --values /root/k8s-xsel/loki-values.yaml --wait --timeout 8m

Résultat attendu :

text
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
kubectl get vulnerabilityreports -n devmysql
kubectl get vulnerabilityreports -n devpostgresql
kubectl get vulnerabilityreports -n tools

# Voir tous les rapports de tous les namespaces
kubectl get vulnerabilityreports -A

# Voir le détail d'un rapport
kubectl describe vulnerabilityreport \
  $(kubectl get vulnerabilityreports -n devmysql -o name | head -1) \
  -n devmysql

# Voir les misconfigurations Kubernetes
kubectl get configauditreports -A

# Voir les secrets exposés
kubectl get exposedsecretreports -A

# Voir les problèmes RBAC
kubectl get rbacassessmentreports -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
kubectl get vulnerabilityreports -A \
  -o jsonpath='{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, sys
data = 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

kubectl get vulnerabilityreports -A -o json | 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
kubectl get vulnerabilityreports -A -o json | python3 -c "
import json, sys
data = 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
kubectl get vulnerabilityreport <nom-du-rapport> -n <namespace> -o json | python3 -c "
import json, sys
data = 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
kubectl set env deployment/trivy-operator -n trivy-system OPERATOR_METRICS_BIND_ADDRESS=:8080

# Vérifier que la variable est bien appliquée
kubectl get deployment trivy-operator -n trivy-system -o jsonpath='{.spec.template.spec.containers[0].env}' | python3 -m json.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)
kubectl get pods -n trivy-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 -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | \
  sh -s -- -b /usr/local/bin

# Vérifier
trivy --version

# Test rapide — scanner une image
trivy image percona/percona-xtradb-cluster:8.4.8-8.1 \
  --severity CRITICAL,HIGH \
  --no-progress

26.10 — CronJob Kubernetes — Scan nocturne externe

⚠️ É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 :

bash
curl -s -u admin:XselHarbor2025! \
  "https://registry.xsel-ci.com/api/v2.0/projects" | python3 -m json.tool

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/v1
kind: CronJob
metadata:
  name: trivy-nightly-scan
  namespace: trivy-system
spec:
  # 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) ==="
EOF
echo "✅ 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
kubectl create secret generic trivy-harbor-secret \
  --from-literal=username=admin \
  --from-literal=password=XselHarbor2025! \
  -n trivy-system 2>/dev/null || true

# Vérifier que le secret contient bien les deux clés
kubectl get secret trivy-harbor-secret -n trivy-system -o jsonpath='{.data}' | python3 -m json.tool

# Appliquer le CronJob
kubectl apply -f /root/k8s-xsel/trivy-cronjob.yaml

# Vérifier
kubectl get cronjob -n trivy-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 -u admin:XselHarbor2025! \
  "https://registry.xsel-ci.com/api/v2.0/projects" | python3 -m json.tool

curl -s -u admin:XselHarbor2025! \
  "https://registry.xsel-ci.com/api/v2.0/projects/<PROJET>/repositories" | python3 -m json.tool

Mettre à jour la liste for image in ... du CronJob (§26.10) avec les noms réels, puis :

bash
kubectl apply -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 dur
PROJECTS=$(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)))")

for project in $PROJECTS; do
  REPOS=$(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)))")
  for repo in $REPOS; do
    image="registry.xsel-ci.com/${repo}:latest"
    echo "Scan: $image"
    trivy image --severity CRITICAL,HIGH --no-progress \
      --format json --output /tmp/scan-$(echo $image | tr '/:' '-').json \
      $image 2>&1 || echo "⚠️ ÉCHEC du scan de $image"
  done
done

Après toute modification, valider avec un job de test avant de faire confiance au prochain scan planifié :

bash
kubectl delete job trivy-test -n trivy-system 2>/dev/null
kubectl create job trivy-test --from=cronjob/trivy-nightly-scan -n trivy-system
sleep 20
kubectl logs -n trivy-system -l job-name=trivy-test -f

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
trivy image \
  --severity CRITICAL,HIGH \
  harbor.xselcloud.com/library/mon-image:latest

# Scan d'un repository Git
trivy repo \
  --severity CRITICAL,HIGH \
  https://github.com/mon-organisation/mon-projet

# Scan d'un fichier de configuration Kubernetes
trivy config \
  --severity CRITICAL,HIGH \
  /root/k8s-xsel/

# Scan d'un serveur distant (filesystem)
trivy fs \
  --severity CRITICAL,HIGH \
  /var/lib/

# Lancer le CronJob immédiatement pour tester
kubectl create job trivy-test \
  --from=cronjob/trivy-nightly-scan \
  -n trivy-system

# Voir les logs du scan
kubectl logs -n trivy-system \
  $(kubectl get pod -n trivy-system -l job-name=trivy-test -o name) -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
sudo kubeadm join 192.168.0.254:6443 \
    --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --node-name xsel-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
sudo kubeadm join 192.168.0.1:6443 \
    --token <TOKEN> \
    --discovery-token-ca-cert-hash sha256:<HASH> \
    --node-name xsel-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.

27.2 — Créer le pool Calico dédié au nouveau node

bash
cat <<EOF | calicoctl apply -f -
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: devnode-03-pool
spec:
  cidr: 172.16.13.0/24
  natOutgoing: true
  ipipMode: Never
  vxlanMode: Always
  nodeSelector: kubernetes.io/hostname == "xsel-devnode-03"
EOF

ℹ️ 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é
kubectl exec -it pg-cluster-pg-db-instance1-bcln-0 -n devpostgresql -c database -- \
  patronictl reinit pg-cluster-pg-db-ha pg-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).

bash
# Passer l'anti-affinity de preferred à required (1 replica strict par node)
kubectl patch deployment <nom> -n <namespace> --type='json' -p='[
  {"op": "remove", "path": "/spec/template/spec/affinity/podAntiAffinity/preferredDuringSchedulingIgnoredDuringExecution"},
  {"op": "add", "path": "/spec/template/spec/affinity/podAntiAffinity/requiredDuringSchedulingIgnoredDuringExecution", "value": [
    {"labelSelector": {"matchExpressions": [{"key": "app", "operator": "In", "values": ["<nom>"]}]}, "topologyKey": "kubernetes.io/hostname"}
  ]}
]'

⚠️ 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 :

bash
kubectl delete replicaset -n <namespace> <ancien-replicaset>

27.5 — Rancher : deux blocages distincts

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.

bash
kubectl set env deployment/rancher -n cattle-system IMPERATIVE_API_DIRECT=false

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).

bash
kubectl patch deployment rancher -n cattle-system --type='json' -p='[
  {"op": "replace", "path": "/spec/template/spec/containers/0/startupProbe/failureThreshold", "value": 60}
]'

⚠️ 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 :

bash
kubectl get deployment rancher -n cattle-system -o yaml | grep -A 10 "startupProbe:"

27.6 — PMM : startupProbe trop courte

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.

bash
kubectl patch statefulset pmm -n monitoring --type='json' -p='[
  {"op": "replace", "path": "/spec/template/spec/containers/0/startupProbe/failureThreshold", "value": 40},
  {"op": "replace", "path": "/spec/template/spec/containers/0/startupProbe/periodSeconds", "value": 15}
]'
# 40 × 15s = 10 min

ℹ️ 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éreuses
args:
  - "server"
  - "--listen=0.0.0.0:4954"
  - "--cache-dir=/home/scanner/.cache/trivy"
readinessProbe:
  initialDelaySeconds: 60
  failureThreshold: 30
  periodSeconds: 15
livenessProbe:
  initialDelaySeconds: 180
  failureThreshold: 10
bash
# Basculer l'opérateur en mode ClientServer
helm upgrade trivy-operator aqua/trivy-operator -n trivy-system \
  --version 0.34.0 --reuse-values \
  --set trivy.mode=ClientServer \
  --set trivy.serverURL=http://trivy-server.trivy-system.svc.cluster.local:4954 \
  --set trivy.clientServerSkipUpdate=true

Ajustements complémentaires appliqués en parallèle : scanJob.concurrentScanJobsLimit: 1, trivy.timeout: 10m0s, mémoire client 500M → 1Gi.

⚠️ 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)
kubectl scale statefulset loki -n monitoring --replicas=0
kubectl scale statefulset pmm -n monitoring --replicas=0
kubectl patch prometheus kube-prometheus-stack-prometheus -n monitoring --type merge -p '{"spec":{"replicas":0}}'
# Attendre la terminaison complète, puis :
kubectl delete pvc <pvc-loki> <pvc-pmm> <pvc-prometheus> -n monitoring
# 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 :

bash
kubectl patch configmap longhorn-storageclass -n longhorn-system --type merge -p '
{"data": {"storageclass.yaml": "kind: StorageClass\napiVersion: storage.k8s.io/v1\nmetadata:\n  name: longhorn\n  annotations:\n    storageclass.kubernetes.io/is-default-class: \"true\"\nprovisioner: driver.longhorn.io\nallowVolumeExpansion: true\nreclaimPolicy: \"Delete\"\nvolumeBindingMode: Immediate\nparameters:\n  numberOfReplicas: \"3\"\n  staleReplicaTimeout: \"30\"\n  fromBackup: \"\"\n  fsType: \"ext4\"\n  dataLocality: \"disabled\"\n  unmapMarkSnapChainRemoved: \"ignored\"\n"}}'

kubectl rollout restart deployment longhorn-driver-deployer -n longhorn-system

⚠️ 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.

bash
kubectl patch perconaxtradbcluster pxc-cluster-pxc-db -n devmysql --type merge -p '
{
  "spec": {
    "backup": {
      "storages": {
        "fs-pvc": {
          "type": "filesystem",
          "volume": {
            "persistentVolumeClaim": {
              "accessModes": ["ReadWriteOnce"],
              "resources": {"requests": {"storage": "50Gi"}},
              "storageClassName": "nfs-storage"
            }
          }
        }
      },
      "schedule": [
        {"name": "daily-backup", "schedule": "0 2 * * *", "keep": 5, "storageName": "fs-pvc"}
      ]
    }
  }
}'

⚠️ 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/v1
kind: PerconaXtraDBClusterBackup
metadata:
  name: manual-backup-test
  namespace: devmysql
spec:
  pxcCluster: pxc-cluster-pxc-db
  storageName: fs-pvc
EOF

kubectl get perconaxtradbclusterbackup -n devmysql -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).

Namespace requests.cpu requests.memory limits.cpu limits.memory
cattle-system 1 1Gi 3 3Gi
trivy-system 1 1Gi 3 3Gi
longhorn-system 6 8Gi 12 16Gi
bash
kubectl apply -f resourcequota-system-namespaces.yaml

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
kubectl patch deployment <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
kubectl get daemonset <nom> -n <namespace> -o jsonpath='{.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).
    bash
    systemctl stop keepalived && systemctl disable keepalived
    
  • 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
    dnf install -y fail2ban
    systemctl enable --now fail2ban
    

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) :

yaml
# helm get values gitlab-garage -n tools
deployment:
  kind: StatefulSet
  podManagementPolicy: Parallel
  replicaCount: 3

garage:
  consistencyMode: consistent
  replicationFactor: "3"

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app.kubernetes.io/name: garage
        topologyKey: kubernetes.io/hostname

persistence:
  enabled: true
  data:
    size: 10Gi
    storageClass: longhorn
  meta:
    size: 500Mi
    storageClass: longhorn

resources:
  requests: { cpu: 100m, memory: 96Mi }
  limits: { cpu: 500m, memory: 1Gi }

readinessProbe:
  httpGet: { path: /health, port: 3903 }
  initialDelaySeconds: 10
  periodSeconds: 15
livenessProbe:
  httpGet: { path: /health, port: 3903 }
  initialDelaySeconds: 15
  periodSeconds: 30

monitoring:
  metrics:
    enabled: true
    serviceMonitor:
      enabled: true

service:
  type: ClusterIP

ℹ️ 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 tools
architecture: standalone
auth:
  enabled: true
  password: XselRedis2026!
master:
  persistence:
    size: 8Gi
    storageClass: 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
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install gitlab-redis bitnami/redis -n tools --version 28.0.12 -f gitlab-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.com

gitlab:
  gitaly:
    persistence:
      size: 15Gi
      storageClass: longhorn
  migrations:
    activeDeadlineSeconds: 14400
  toolbox:
    enabled: true
    backups:
      objectStorage:
        backend: s3
        config:
          key: config
          secret: gitlab-s3cmd-config
    resources:
      requests: { cpu: 200m, memory: 1Gi }
      limits: { cpu: 1, memory: 2Gi }

gitlab-kas:
  enabled: false   # Kubernetes Agent Server non utilisé

global:
  hosts:
    domain: xselcloud.com
    gitlab:
      name: gitlab.xselcloud.com
  ingress:
    class: nginx
    configureCertmanager: false   # certificat géré manuellement, pas par le chart
    enabled: true
    tls:
      secretName: gitlab-tls
  psql:
    database: gitlabhq_production
    host: pg-cluster-pg-db-pgbouncer.devpostgresql.svc.cluster.local
    port: 5432
    username: gitlab
    password: { secret: gitlab-psql-secret, key: password }
    ssl:
      secret: gitlab-psql-ssl
      serverCA: server-ca.pem
      clientCertificate: client-certificate.pem
      clientKey: client-key.pem
  redis:
    host: gitlab-redis-master.tools.svc.cluster.local
    port: 6379
    auth: { enabled: true, secret: gitlab-redis, key: redis-password }
  appConfig:
    object_store:
      enabled: true
      proxy_download: true
      connection: { 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: false
nginx-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.

Ingress exposés :

Ingress Hôte Classe
gitlab-webservice-default gitlab.xselcloud.com nginx
gitlab-kas kas.xselcloud.com nginx (service désactivé, gitlab-kas.enabled: false)

28.3 — Diagnostic : base de données non initialisée

Connexion à https://gitlab.xselcloud.com avec les identifiants du Secret auto-généré impossible (Invalid login or password) :

bash
kubectl get secret gitlab-gitlab-initial-root-password -n tools -o jsonpath='{.data.password}' | base64 -d; echo

Le mot de passe était correct mais le compte root n'existait pas. Confirmation via la console Rails :

bash
kubectl exec -it -n tools deploy/gitlab-toolbox -- gitlab-rails console
ruby
gitlab(prod)> User.count
=> 0

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

bash
kubectl exec -it -n tools deploy/gitlab-toolbox -- gitlab-rake db:seed_fu

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) :

ruby
gitlab(prod)> user = User.find_by(username: 'root')
gitlab(prod)> user.password = 'Xk9#mQ2vLp7$wZt4'
gitlab(prod)> user.password_confirmation = 'Xk9#mQ2vLp7$wZt4'
gitlab(prod)> user.save!

⚠️ 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.

Connexion admin confirmée (« Welcome to GitLab, Administrator! »), webservice/sidekiq stables.


28.5 — Bug GitLab : erreur 500 à l'inscription publique

Tentative d'inscription via le formulaire public (/users/sign_up) → 500 Internal Server Error.

bash
kubectl logs -n tools deploy/gitlab-webservice-default -c webservice | grep -A 30 "<request-id>"
code
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) :

yaml
gitlab:
  webservice:
    deployment:
      livenessProbe:
        initialDelaySeconds: 300
        failureThreshold: 5
  sidekiq:
    livenessProbe:
      initialDelaySeconds: 300
      failureThreshold: 5

Vérification que la valeur est bien dans le Deployment :

bash
kubectl get deploy gitlab-sidekiq-all-in-1-v2 -n tools -o jsonpath='{.spec.template.spec.containers[0].livenessProbe}{"\n"}'
kubectl get deploy gitlab-webservice-default -n tools -o jsonpath='{.spec.template.spec.containers[0].livenessProbe}{"\n"}'

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.
bash
kubectl get resourcequota tools-quota -n tools -o yaml > ~/k8s-xsel/tools-quota-avant-$(date +%F).yaml
kubectl patch resourcequota tools-quota -n tools --type merge \
  -p '{"spec":{"hard":{"requests.memory":"20Gi","limits.memory":"40Gi","limits.cpu":"24"}}}'
kubectl get resourcequota tools-quota -n tools

ℹ️ 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 :
bash
kubectl top nodes
kubectl describe node xsel-devnode-01 | grep -A8 "Allocated resources"

28.10 — Valeurs Helm de correction (gitlab-values-correctifs.yaml) et incident Redis

Fichier de valeurs (/root/k8s-xsel/gitlab-values-correctifs.yaml), à appliquer en plus des valeurs d'origine (état au 15/10/2026, Helm révision 17) :

yaml
gitlab:
  webservice:
    resources:
      requests: { cpu: 300m, memory: 3Gi }
      limits:   { cpu: "1",  memory: 4Gi }
    deployment:
      livenessProbe:
        initialDelaySeconds: 300
        failureThreshold: 5
  sidekiq:
    resources:
      requests: { cpu: 500m }
    livenessProbe:
      initialDelaySeconds: 300
      failureThreshold: 5
  migrations:
    resources:
      requests: { memory: 1Gi }
      limits:   { memory: 2Gi }
  gitlab-shell:
    resources:
      requests: { cpu: 50m, memory: 64Mi }
bash
# Sauvegarde des valeurs avant toute modification
helm get values gitlab -n tools -o yaml > ~/k8s-xsel/gitlab-values-avant-$(date +%F).yaml

helm upgrade gitlab gitlab/gitlab -n tools --version 10.3.1 --reuse-values \
  -f ~/k8s-xsel/gitlab-values-correctifs.yaml --timeout 30m

# Retour en arrière si besoin (la révision 11 est l'état du 21/09/2026)
helm history gitlab -n tools
helm rollback gitlab <révision> -n tools

ℹ️ 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.


Opérations et CI/CD

Étape 29 — Incident Parallèle : panne disque xsel-devnode-01

ℹ️ 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 -m session

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.

Opérations et CI/CD

Étape 30 — Installation d'AWX (Ansible Automation Controller)

ℹ️ 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

30.2 — Installation de l'opérateur

bash
helm repo add awx-operator https://ansible-community.github.io/awx-operator-helm/
helm repo update
helm install -n awx --create-namespace my-awx-operator awx-operator/awx-operator

Chart awx-operator-helm v3.2.1, opérateur quay.io/ansible/awx-operator:2.19.1.

bash
# /root/k8s-xsel/awx-operator-values.yaml (documentation seule — aucune valeur custom)
# helm repo add awx-operator https://ansible-community.github.io/awx-operator-helm/
# helm install -n awx --create-namespace my-awx-operator awx-operator/awx-operator
# (installation par défaut, chart version 3.2.1)

30.3 — Ressource personnalisée AWX

yaml
# /root/k8s-xsel/awx-instance.yaml
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
  name: awx
  namespace: awx
spec:
  service_type: ClusterIP
  ingress_type: ingress
  ingress_class_name: nginx
  ingress_annotations: |
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hostname: awx.xselcloud.com
  ingress_tls_secret: awx-xselcloud-tls

  postgres_storage_class: longhorn
  postgres_storage_requirements:
    requests:
      storage: 8Gi
  postgres_data_volume_init: true
  postgres_init_container_commands: |
    chown 26:0 /var/lib/pgsql/data
    chmod 700 /var/lib/pgsql/data

  projects_persistence: true
  projects_storage_class: longhorn
  projects_storage_size: 8Gi

  web_resource_requirements:
    requests: { cpu: 500m, memory: 1Gi }
    limits: { cpu: "1", memory: 2Gi }
  task_resource_requirements:
    requests: { cpu: 500m, memory: 1Gi }
    limits: { cpu: "1", memory: 2Gi }
  ee_resource_requirements:
    requests: { cpu: 250m, memory: 512Mi }
    limits: { cpu: "1", memory: 1Gi }
  redis_resource_requirements:
    requests: { cpu: 100m, memory: 256Mi }
    limits: { cpu: 500m, memory: 512Mi }
bash
kubectl apply -f ~/k8s-xsel/awx-instance.yaml

ℹ️ 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

code
mkdir: cannot create directory '/var/lib/pgsql/data/userdata': Permission denied

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 :
kubectl get statefulset -n awx awx-postgres-15 -o yaml | grep -A 10 initContainers
# Puis seulement, recycler le pod :
kubectl delete pod awx-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
kubectl uncordon xsel-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) :

bash
kubectl logs -n longhorn-system -l app=longhorn-manager | grep quota
# → pods "share-manager-..." is forbidden: exceeded quota: longhorn-system-quota
#   requested: requests.cpu=1500m, used: requests.cpu=4840m, limited: requests.cpu=6

kubectl patch resourcequota longhorn-system-quota -n longhorn-system --type=merge -p \
  '{"spec":{"hard":{"requests.cpu":"12","limits.cpu":"24"}}}'

⚠️ 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 :

bash
kubectl logs -n awx awx-migration-24.6.1-<hash> --tail=30
kubectl logs -n awx awx-task-<pod> -c init-database --tail=30

ℹ️ 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
kubectl get pods -n awx
kubectl exec --namespace awx -it deploy/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).

31.1 — Vérifications préalables

bash
kubectl get storageclass
kubectl get all -A | grep -i jenkins
helm list -A | grep -i jenkins

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
helm repo add jenkinsci https://charts.jenkins.io
helm repo update
# Chart jenkinsci/jenkins 5.9.65, app version 2.568.3
yaml
# /root/k8s-xsel/jenkins-values.yaml
controller:
  admin:
    username: "admin"
  # Mot de passe admin auto-généré par le chart dans un Secret (jenkins)

  serviceType: ClusterIP

  ingress:
    enabled: true
    ingressClassName: nginx
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod
    hostName: jenkins.xselcloud.com
    tls:
      - secretName: jenkins-xselcloud-tls
        hosts:
          - jenkins.xselcloud.com

  resources:
    requests: { cpu: "500m", memory: "1Gi" }
    limits: { cpu: "2", memory: "2Gi" }

  javaOpts: "-Xmx1536m -Xms512m"

  startupProbe:
    failureThreshold: 60
    periodSeconds: 10

persistence:
  enabled: true
  storageClass: nfs-jenkins
  size: 10Gi
  accessMode: ReadWriteOnce

agent:
  enabled: true
  resources:
    requests: { cpu: "250m", memory: "512Mi" }
    limits: { cpu: "1", memory: "1Gi" }
bash
helm install jenkins jenkinsci/jenkins -n jenkins --create-namespace -f ~/k8s-xsel/jenkins-values.yaml

⚠️ 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: 60
    periodSeconds: 10   # 10 min de marge au lieu de ~2 min
bash
helm upgrade jenkins jenkinsci/jenkins -n jenkins -f ~/k8s-xsel/jenkins-values.yaml
kubectl delete pod -n jenkins jenkins-0

ℹ️ 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.

31.5 — Validation finale

bash
kubectl exec --namespace jenkins -it svc/jenkins -c jenkins -- /bin/cat /run/secrets/additional/chart-admin-password && echo

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).


Opérations et CI/CD

Étape 32 — Récapitulatif des commandes Helm : GitLab, AWX, Jenkins

ℹ️ 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/.

32.1 — Tableau récapitulatif

Outil Release Namespace Dépôt Helm Chart et version Configuration
GitLab gitlab tools gitlab → https://charts.gitlab.io/ gitlab/gitlab 10.3.1 (app v19.3.1) gitlab-values-actuelles.yaml + gitlab-values-correctifs.yaml (§28.10)
AWX Operator my-awx-operator awx awx-operator → https://ansible-community.github.io/awx-operator-helm/ awx-operator/awx-operator 3.2.1 (opérateur 2.19.1) 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
helm repo add gitlab https://charts.gitlab.io/
helm repo update

# Sauvegarder les valeurs actuellement déployées
helm get values gitlab -n tools -o yaml > ~/k8s-xsel/gitlab-values-actuelles.yaml

# Installer ou mettre à jour (les valeurs de correction viennent EN DERNIER : elles priment)
helm upgrade --install gitlab gitlab/gitlab -n tools \
  --version 10.3.1 \
  -f ~/k8s-xsel/gitlab-values-actuelles.yaml \
  -f ~/k8s-xsel/gitlab-values-correctifs.yaml \
  --timeout 30m

⚠️ 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
helm repo add awx-operator https://ansible-community.github.io/awx-operator-helm/
helm repo update

# 1) L'opérateur
helm install -n awx --create-namespace my-awx-operator awx-operator/awx-operator --version 3.2.1

# 2) L'instance AWX (ressource personnalisée, voir §30.3)
kubectl apply -f ~/k8s-xsel/awx-instance.yaml

kubectl get pods -n awx

# Mot de passe admin (créé par l'opérateur)
kubectl get secret awx-admin-password -n awx -o jsonpath='{.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
helm repo add jenkinsci https://charts.jenkins.io
helm repo update

helm upgrade --install jenkins jenkinsci/jenkins -n jenkins --create-namespace \
  --version 5.9.65 \
  -f ~/k8s-xsel/jenkins-values.yaml

kubectl get pods -n jenkins

# Mot de passe admin (généré par le chart)
kubectl exec --namespace jenkins -it svc/jenkins -c jenkins -- /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

bash
for r in "gitlab tools" "my-awx-operator awx" "jenkins jenkins"; do
  set -- $r
  helm get values "$1" -n "$2" -o yaml > ~/k8s-xsel/backup-values-"$1".yaml
done
helm history gitlab -n tools
helm history my-awx-operator -n awx
helm history jenkins -n jenkins