================================================================================
  ██╗  ██╗██╗   ██╗██████╗ ███████╗██████╗ ███╗   ██╗███████╗████████╗███████╗███████╗
  ██║ ██╔╝██║   ██║██╔══██╗██╔════╝██╔══██╗████╗  ██║██╔════╝╚══██╔══╝██╔════╝██╔════╝
  █████╔╝ ██║   ██║██████╔╝█████╗  ██████╔╝██╔██╗ ██║█████╗     ██║   █████╗  ███████╗
  ██╔═██╗ ██║   ██║██╔══██╗██╔══╝  ██╔══██╗██║╚██╗██║██╔══╝     ██║   ██╔══╝  ╚════██║
  ██║  ██╗╚██████╔╝██████╔╝███████╗██║  ██║██║ ╚████║███████╗   ██║   ███████╗███████║
  ╚═╝  ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝  ╚═╝╚═╝  ╚═══╝╚══════╝   ╚═╝   ╚══════╝╚══════╝
================================================================================
  KUBERNETES EN ÉQUIPE — APPLICATION FLASK COMPLÈTE AVEC DÉPLOIEMENT
  Guide ultra-détaillé pour GRAND DÉBUTANT
  Chaque action expliquée: POURQUOI? COMMENT? QUAND? QUI?
================================================================================

[BLACK_RIGHT-POINTING_TRIANGLE] À QUI S'ADRESSE CE GUIDE?
  Ce guide est écrit pour quelqu'un qui:
  - n'a jamais utilisé Kubernetes (ou très peu)
  - connaît un peu Docker mais veut passer à l'orchestration
  - veut comprendre POURQUOI avant de taper des commandes
  - travaille en équipe sur une application Python/Flask
  - veut voir des situations réelles et concrètes
  - veut explorer TOUTES les fonctionnalités de Kubernetes

[BLACK_RIGHT-POINTING_TRIANGLE] COMMENT LIRE CE GUIDE?
  Chaque section suit ce format:
  ┌─ POURQUOI? ──────────── Pourquoi cette ressource/commande existe-t-elle?
  ├─ QU'EST-CE QUE C'EST? ─ Explication claire du concept
  ├─ QUAND? ──────────────── Dans quelle situation l'utiliser?
  ├─ COMMENT? ────────────── Étapes concrètes, une par une
  └─ RÉSULTAT ATTENDU ────── Ce que vous devez voir à l'écran

[BLACK_RIGHT-POINTING_TRIANGLE] L'APPLICATION CONSTRUITE DANS CE GUIDE
  Nom: TaskManager API (évolution du guide Git)
  Description: API REST de gestion de tâches, déployée sur Kubernetes
  Technologies: Python 3.11 + Flask + PostgreSQL + Redis + Nginx
  Kubernetes: Minikube (local) -> kind -> cluster cloud (GKE/EKS/AKS)

  Ce que l'API expose:
  - POST   /tasks              -> Créer une tâche
  - GET    /tasks              -> Lister les tâches (filtres, pagination)
  - GET    /tasks/<id>         -> Voir une tâche
  - PUT    /tasks/<id>         -> Modifier une tâche
  - DELETE /tasks/<id>         -> Supprimer une tâche
  - POST   /auth/register      -> Créer un compte
  - POST   /auth/login         -> Se connecter (JWT)
  - GET    /health             -> Santé de l'API
  - GET    /metrics            -> Métriques Prometheus
  - GET    /ready              -> Readiness check
  - GET    /live               -> Liveness check

[BLACK_RIGHT-POINTING_TRIANGLE] L'ÉQUIPE
  Alice  — Lead DevOps, responsable cluster, déploiements, monitoring
           Ordinateur: MacBook Pro, macOS
           Rôle K8s: administre le cluster, configure RBAC, gère les namespaces

  Bob    — Développeur backend, écrit les manifestes pour ses features
           Ordinateur: PC fixe, Ubuntu 22.04
           Rôle K8s: déploie ses features, écrit les Deployments/Services

  Claire — Développeuse backend / QA, responsable tests et observabilité
           Ordinateur: PC portable, Windows 11
           Rôle K8s: configure monitoring, tests de charge, NetworkPolicies

[BLACK_RIGHT-POINTING_TRIANGLE] CE QUE CE GUIDE COUVRE (toutes les fonctionnalités Kubernetes):
  Partie 0  — Comprendre Kubernetes avant de toucher un terminal
  Partie 1  — Installation: kubectl, Minikube, kind, Helm
  Partie 2  — Les Pods: l'unité de base
  Partie 3  — Les Controllers: ReplicaSet, Deployment, StatefulSet, DaemonSet
  Partie 4  — Services et Networking: ClusterIP, NodePort, LoadBalancer, Ingress
  Partie 5  — Configuration: ConfigMaps et Secrets
  Partie 6  — Stockage: PersistentVolume, PVC, StorageClass
  Partie 7  — Namespaces et isolation
  Partie 8  — RBAC: rôles, bindings, ServiceAccounts
  Partie 9  — HPA, VPA, ressources: autoscaling intelligent
  Partie 10 — Jobs, CronJobs: tâches planifiées
  Partie 11 — Probes: liveness, readiness, startup
  Partie 12 — Helm: gestionnaire de paquets Kubernetes
  Partie 13 — Kustomize: configuration multi-environnement
  Partie 14 — Monitoring: Prometheus, Grafana
  Partie 15 — Logging: EFK Stack (Elasticsearch, Fluentd, Kibana)
  Partie 16 — NetworkPolicies: sécurité réseau
  Partie 17 — PodDisruptionBudgets, Affinity, Taints/Tolerations
  Partie 18 — Déploiement multi-environnement (dev/staging/prod)
  Partie 19 — GitOps avec ArgoCD
  Partie 20 — Scénarios complets Alice, Bob, Claire
  Partie 21 — Troubleshooting complet
  Partie 22 — Cheatsheet exhaustif

================================================================================
PARTIE 0 — COMPRENDRE KUBERNETES AVANT DE TOUCHER UN TERMINAL
================================================================================

Avant de taper la moindre commande, il faut comprendre CE QUE FAIT Kubernetes.
La plupart des débutants échouent car ils appliquent des YAML sans comprendre
pourquoi. Ce chapitre donne les bases conceptuelles essentielles.

────────────────────────────────────────────────────────────────────────────────
0.1 — LE PROBLÈME QUE KUBERNETES RÉSOUT
────────────────────────────────────────────────────────────────────────────────

  IMAGINEZ SANS KUBERNETES (avec Docker seul):
  ─────────────────────────────────────────────
  L'équipe déploie l'API TaskManager sur 3 serveurs.

  PROBLÈME 1 — Haute disponibilité manuelle:
  Le serveur 2 tombe en panne à 3h du matin.
  Alice doit se connecter manuellement et relancer le conteneur.
  -> Temps d'arrêt: 45 minutes (le temps qu'Alice se réveille).

  PROBLÈME 2 — Mise à l'échelle manuelle:
  Lundi matin: 500 requêtes/seconde. 1 serveur suffit.
  Vendredi soir: 5000 requêtes/seconde. Le serveur est surchargé.
  Alice doit manuellement lancer des conteneurs supplémentaires.
  -> Processus: SSH sur chaque machine, docker run, configurer le load balancer.

  PROBLÈME 3 — Déploiement sans interruption manuel:
  Nouvelle version à déployer. Alice doit:
  - Arrêter l'ancien conteneur
  - Lancer le nouveau
  - Pendant 2-3 secondes: l'API est indisponible.
  -> Inacceptable en production.

  PROBLÈME 4 — Configuration disparate:
  3 serveurs, 3 fichiers docker-compose.yml légèrement différents.
  Variables d'environnement définies à la main sur chaque machine.
  -> Un serveur a une vieille variable d'env. Bug mystérieux impossible à reproduire.

  AVEC KUBERNETES:
  ─────────────────
  Alice définit l'état DÉSIRÉ dans des fichiers YAML:
  "Je veux 3 copies de l'API, toujours actives, avec ces variables, ces ressources."
  Kubernetes s'assure en PERMANENCE que la réalité correspond à ce désir.

  -> Serveur 2 tombe: K8s relance le Pod sur un autre serveur en 30 secondes.
  -> Trafic explose: K8s ajoute automatiquement des Pods (HPA).
  -> Nouvelle version: K8s remplace les anciens Pods un par un (Rolling Update).
  -> Variables d'env: dans des ConfigMaps/Secrets centralisés, identiques partout.

────────────────────────────────────────────────────────────────────────────────
0.2 — LA MÉTAPHORE: Kubernetes est un chef de salle de restaurant
────────────────────────────────────────────────────────────────────────────────

  Imaginez un grand restaurant (votre cluster Kubernetes).

  LE CHEF DE SALLE (Control Plane) = cerveau du restaurant
  -> Reçoit les commandes (vos YAML)
  -> Décide qui fait quoi
  -> Surveille que tout se passe bien
  -> Si un serveur (Node) est malade, redistribue le travail

  LES SERVEURS/NODES (Worker Nodes) = les employés
  -> Exécutent vraiment le travail
  -> Chaque serveur peut porter plusieurs tables (Pods)
  -> Le chef de salle surveille leur état de santé en permanence

  LES TABLES (Pods) = unité de travail
  -> Une table peut accueillir 1 ou plusieurs convives (conteneurs)
  -> Une table est assignée à un serveur et reste avec lui
  -> Si une table est renversée (Pod crashe), le chef crée une nouvelle table

  LES COMMANDES (Deployments/Services) = les règles du restaurant
  -> "Il doit toujours y avoir 3 tables disponibles pour ce type de client"
  -> "Tous les clients de ce type passent par cette entrée (Service)"
  -> "Ces tables ont besoin de ceci (ConfigMap/Secret)"

  L'ÉTAT DÉSIRÉ vs L'ÉTAT RÉEL:
  ───────────────────────────────
  Vous dites au chef: "Je veux 3 tables occupées par des serveurs TaskManager."
  Le chef compare en permanence:
  - État désiré: 3 Pods TaskManager
  - État réel: actuellement 2 Pods (1 a crashé)
  -> Le chef crée immédiatement 1 nouveau Pod pour atteindre l'état désiré.

  C'est la RÉCONCILIATION, le concept central de Kubernetes.

────────────────────────────────────────────────────────────────────────────────
0.3 — ARCHITECTURE D'UN CLUSTER KUBERNETES
────────────────────────────────────────────────────────────────────────────────

  SCHÉMA COMPLET:
  ────────────────

  ┌─────────────────────────────────────────────────────────────────────────┐
  │                         CONTROL PLANE (Master)                         │
  │  ─────────────────────────────────────────────────────────────────────  │
  │                                                                         │
  │  ┌─────────────────┐  ┌──────────────────┐  ┌───────────────────────┐  │
  │  │   API Server    │  │ Controller Mgr   │  │      Scheduler        │  │
  │  │                 │  │                  │  │                       │  │
  │  │ Point d'entrée  │  │ Boucle de        │  │ Décide sur quel Node  │  │
  │  │ de toutes les   │  │ réconciliation:  │  │ placer chaque Pod     │  │
  │  │ commandes.      │  │ s'assure que     │  │ (ressources,          │  │
  │  │ kubectl parle   │  │ l'état réel =    │  │ affinités, taints...) │  │
  │  │ à lui via REST. │  │ l'état désiré.   │  │                       │  │
  │  └────────┬────────┘  └──────────────────┘  └───────────────────────┘  │
  │           │                                                              │
  │  ┌────────[BLACK_DOWN-POINTING_TRIANGLE]──────────────────────────────────────────────────────────┐  │
  │  │                           etcd                                    │  │
  │  │   Base de données clé-valeur distribuée. Stocke TOUT l'état du   │  │
  │  │   cluster: Pods, Services, Secrets, ConfigMaps, déploiements...  │  │
  │  │   Si etcd disparaît: le cluster perd sa mémoire. À sauvegarder!  │  │
  │  └───────────────────────────────────────────────────────────────────┘  │
  │                                                                         │
  └─────────────────────────────┬───────────────────────────────────────────┘
                                │ API
              ┌─────────────────┼─────────────────┐
              [BLACK_DOWN-POINTING_TRIANGLE]                 [BLACK_DOWN-POINTING_TRIANGLE]                 [BLACK_DOWN-POINTING_TRIANGLE]
  ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
  │   Worker Node 1   │ │   Worker Node 2   │ │   Worker Node 3   │
  │ ─────────────── ─ │ │ ────────────────  │ │ ─────────────── ─ │
  │ ┌─────────────┐   │ │ ┌─────────────┐   │ │ ┌─────────────┐   │
  │ │   kubelet   │   │ │ │   kubelet   │   │ │ │   kubelet   │   │
  │ │ Agent local │   │ │ │ Agent local │   │ │ │ Agent local │   │
  │ │ Gère les    │   │ │ │ Gère les    │   │ │ │ Gère les    │   │
  │ │ Pods sur ce │   │ │ │ Pods sur ce │   │ │ │ Pods sur ce │   │
  │ │ node.       │   │ │ │ node.       │   │ │ │ node.       │   │
  │ └─────────────┘   │ │ └─────────────┘   │ │ └─────────────┘   │
  │ ┌─────────────┐   │ │ ┌─────────────┐   │ │ ┌─────────────┐   │
  │ │ kube-proxy  │   │ │ │ kube-proxy  │   │ │ │ kube-proxy  │   │
  │ │ Règles      │   │ │ │ Règles      │   │ │ │ Règles      │   │
  │ │ réseau,     │   │ │ │ réseau,     │   │ │ │ réseau,     │   │
  │ │ iptables    │   │ │ │ iptables    │   │ │ │ iptables    │   │
  │ └─────────────┘   │ │ └─────────────┘   │ │ └─────────────┘   │
  │                   │ │                   │ │                   │
  │ ┌──Pod───────┐    │ │ ┌──Pod───────┐    │ │ ┌──Pod───────┐    │
  │ │ [api:1]    │    │ │ │ [api:2]    │    │ │ │ [api:3]    │    │
  │ └────────────┘    │ │ └────────────┘    │ │ └────────────┘    │
  │ ┌──Pod───────┐    │ │ ┌──Pod───────┐    │ │                   │
  │ │ [postgres] │    │ │ │ [redis]    │    │ │                   │
  │ └────────────┘    │ │ └────────────┘    │ │                   │
  └───────────────────┘ └───────────────────┘ └───────────────────┘

  COMPOSANTS DU CONTROL PLANE:
  ──────────────────────────────
  API Server     = La porte d'entrée. kubectl parle à l'API Server via HTTPS.
                   Tous les composants communiquent via l'API Server.
                   JAMAIS directement entre eux.

  etcd           = La base de données du cluster. Distribué, tolérant aux pannes.
                   Stocke l'état désiré ET l'état actuel de TOUT.
                   C'est le seul composant avec état (stateful) du Control Plane.

  Controller Mgr = Ensemble de contrôleurs qui surveillent l'état du cluster.
                   ReplicationController: "Il me faut 3 replicas -> il en manque 1 -> créer."
                   NodeController: "Le node 2 ne répond plus -> marquer comme Not Ready."
                   JobController: "Ce Job est terminé -> ne pas redémarrer."
                   ... et beaucoup d'autres.

  Scheduler      = Décide SUR QUEL NODE placer un nouveau Pod.
                   Critères: ressources disponibles, affinités, taints/tolerations,
                   spread topology, priorités. C'est un algorithme sophistiqué.

  COMPOSANTS DE CHAQUE WORKER NODE:
  ───────────────────────────────────
  kubelet        = Agent qui tourne sur chaque node.
                   Reçoit les PodSpecs de l'API Server.
                   Démarre/arrête les conteneurs via le Container Runtime.
                   Rapporte l'état des Pods à l'API Server.
                   Exécute les health checks (probes).

  kube-proxy     = Proxy réseau sur chaque node.
                   Maintient les règles iptables/ipvs pour le routage réseau.
                   Implémente les Services Kubernetes (ClusterIP, NodePort).

  Container Runtime = Docker, containerd, CRI-O.
                   Exécute vraiment les conteneurs.
                   Kubernetes parle au runtime via CRI (Container Runtime Interface).

────────────────────────────────────────────────────────────────────────────────
0.4 — LES RESSOURCES KUBERNETES: VOCABULAIRE ESSENTIEL
────────────────────────────────────────────────────────────────────────────────

  Tout dans Kubernetes est une RESSOURCE décrite dans un fichier YAML.
  Chaque ressource a:
  - apiVersion: quelle version de l'API (apps/v1, v1, networking.k8s.io/v1...)
  - kind: quel type de ressource (Pod, Deployment, Service, ConfigMap...)
  - metadata: nom, namespace, labels, annotations
  - spec: la description de l'état désiré
  - status: l'état actuel (rempli par Kubernetes, pas vous)

  EXEMPLE MINIMAL:
  ─────────────────
  apiVersion: v1          # Version de l'API qui définit ce type
  kind: Pod               # Ce que c'est
  metadata:               # Informations d'identification
    name: taskmanager-api # Nom unique dans le namespace
    namespace: default    # Dans quel espace de noms
    labels:               # Étiquettes (pour sélection, monitoring)
      app: taskmanager
  spec:                   # ÉTAT DÉSIRÉ — ce que vous voulez
    containers:
    - name: api
      image: taskmanager:1.0.0
  # status: (rempli par Kubernetes — ne pas éditer manuellement)

  TABLEAU DES RESSOURCES KUBERNETES:
  ────────────────────────────────────
  ┌──────────────────┬────────────────────────────────────────────────────┐
  │ Ressource        │ Rôle                                               │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ Pod              │ Unité de base. 1+ conteneurs partageant réseau/    │
  │                  │ stockage. Éphémère, ne pas gérer directement.      │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ ReplicaSet       │ Maintient N copies d'un Pod. Rarement utilisé      │
  │                  │ directement (géré par Deployment).                 │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ Deployment       │ Gère les ReplicaSets. Rolling updates, rollbacks.  │
  │                  │ Ressource la plus utilisée pour les apps stateless.│
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ StatefulSet      │ Comme Deployment mais pour apps STATEFUL (BDD).    │
  │                  │ Pods ont un nom stable et un stockage persistant.  │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ DaemonSet        │ 1 Pod PAR NODE. Pour agents de monitoring, log     │
  │                  │ collecteurs, proxies réseau.                        │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ Job              │ Tâche qui se termine (backup, migration de BDD).   │
  │                  │ Redémarre en cas d'échec jusqu'au succès.          │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ CronJob          │ Job planifié (comme cron Linux). Ex: nettoyage      │
  │                  │ nocturne, rapports quotidiens.                      │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ Service          │ Point d'entrée réseau stable pour un groupe de     │
  │                  │ Pods. Load balancing interne. IP virtuelle stable. │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ Ingress          │ Règles HTTP(S) pour exposer des Services à         │
  │                  │ l'extérieur. Routing par path ou hostname.         │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ ConfigMap        │ Configuration non-sensible en clé-valeur. Montée  │
  │                  │ comme volume ou injectée comme variables d'env.    │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ Secret           │ Comme ConfigMap mais pour données sensibles.       │
  │                  │ Encodé en base64 (pas chiffré! Utiliser Vault).   │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ PersistentVolume │ Volume de stockage provisionné (manuellement ou    │
  │ (PV)             │ dynamiquement). Existe indépendamment des Pods.    │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ PersistentVolume │ Demande de stockage par un Pod. "Je veux 10Gi".    │
  │ Claim (PVC)      │ Lié à un PV compatible.                            │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ StorageClass     │ Type de stockage dynamique (SSD, HDD, NFS...).     │
  │                  │ PVC -> StorageClass -> PV créé automatiquement.     │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ Namespace        │ Espace virtuel pour isoler des ressources.         │
  │                  │ dev, staging, prod dans le même cluster.           │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ ServiceAccount   │ Identité d'un Pod dans le cluster. Permet d'appeler│
  │                  │ l'API Kubernetes depuis l'intérieur d'un Pod.      │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ Role/ClusterRole │ Permissions RBAC (qui peut faire quoi sur quoi).   │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ RoleBinding      │ Attribue un Role à un User/ServiceAccount.         │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ HPA              │ Horizontal Pod Autoscaler. Ajoute/supprime des     │
  │                  │ Pods selon la charge CPU/mémoire/métriques custom. │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ VPA              │ Vertical Pod Autoscaler. Ajuste CPU/mémoire d'un  │
  │                  │ Pod (requests/limits) automatiquement.             │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ NetworkPolicy    │ Règles de pare-feu entre Pods. Par défaut: tout    │
  │                  │ le monde peut parler à tout le monde.              │
  ├──────────────────┼────────────────────────────────────────────────────┤
  │ PodDisruptionBdg │ Garantit un minimum de Pods disponibles lors de   │
  │                  │ maintenances (draining de nodes).                  │
  └──────────────────┴────────────────────────────────────────────────────┘

────────────────────────────────────────────────────────────────────────────────
0.5 — KUBECTL: L'OUTIL EN LIGNE DE COMMANDE
────────────────────────────────────────────────────────────────────────────────

  POURQUOI kubectl?
  ──────────────────
  kubectl = "Kubernetes Control" = votre télécommande du cluster.
  Il parle à l'API Server via HTTPS, en envoyant du JSON/YAML.
  TOUT ce que vous pouvez faire via l'interface web, vous pouvez le faire
  avec kubectl. Et beaucoup de choses que l'interface web ne permet pas.

  LA SYNTAXE GÉNÉRALE:
  ─────────────────────
  kubectl [commande] [TYPE] [NOM] [options]

  Commandes principales:
  apply      -> Créer ou mettre à jour une ressource depuis un fichier YAML
  get        -> Lister des ressources
  describe   -> Détails complets d'une ressource
  delete     -> Supprimer une ressource
  logs       -> Voir les logs d'un Pod/conteneur
  exec       -> Exécuter une commande dans un Pod
  port-forward -> Rediriger un port local vers un Pod
  scale      -> Changer le nombre de replicas
  rollout    -> Gérer les déploiements (status, history, undo)
  create     -> Créer depuis ligne de commande (plutôt qu'un fichier)

  TYPES DE RESSOURCES (abréviations):
  ─────────────────────────────────────
  pods (po)                   services (svc)
  deployments (deploy)        configmaps (cm)
  replicasets (rs)            secrets
  statefulsets (sts)          persistentvolumes (pv)
  daemonsets (ds)             persistentvolumeclaims (pvc)
  jobs                        namespaces (ns)
  cronjobs (cj)               nodes (no)
  ingresses (ing)             serviceaccounts (sa)
  horizontalpodautoscalers    networkpolicies (netpol)
  (hpa)

================================================================================
PARTIE 1 — INSTALLATION: KUBECTL, MINIKUBE, KIND, HELM
================================================================================

  PLAN D'INSTALLATION:
  ─────────────────────
  Alice, Bob et Claire ont besoin chacun de:
  1. kubectl          -> outil de commande (obligatoire)
  2. Minikube         -> cluster Kubernetes local (pour développement)
  3. kind             -> clusters K8s dans Docker (pour CI/CD, tests)
  4. Helm             -> gestionnaire de paquets K8s
  5. k9s              -> TUI (interface terminal) pour naviguer dans K8s

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.1 — ALICE INSTALLE KUBECTL (MAC)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI KUBECTL?
  ──────────────────
  Sans kubectl, vous ne pouvez pas interagir avec Kubernetes.
  C'est le seul outil dont TOUS les membres de l'équipe ont besoin.
  Il lit votre fichier ~/.kube/config pour savoir à quel cluster se connecter.

  INSTALLATION SUR MAC (Alice):
  ──────────────────────────────
  brew install kubectl

  VÉRIFICATION:
  ──────────────
  kubectl version --client

  CE QUE VOUS DEVEZ VOIR:
  ────────────────────────
  Client Version: v1.29.0
  Kustomize Version: v5.0.4-0.20230601165947-6ce0bf390ce3

  CONFIGURER L'AUTOCOMPLÉTION (très utile!):
  ───────────────────────────────────────────
  # Pour zsh (macOS par défaut):
  echo 'source <(kubectl completion zsh)' >> ~/.zshrc
  echo 'alias k=kubectl' >> ~/.zshrc
  echo 'complete -F __start_kubectl k' >> ~/.zshrc
  source ~/.zshrc

  # Test de l'alias:
  k version --client
  # -> Même résultat qu'avec kubectl

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.2 — BOB INSTALLE KUBECTL (UBUNTU)
────────────────────────────────────────────────────────────────────────────────

  # Télécharger la dernière version stable:
  curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"

  # Rendre exécutable et installer:
  chmod +x kubectl
  sudo mv kubectl /usr/local/bin/kubectl

  # Vérifier:
  kubectl version --client

  # Autocomplétion bash:
  echo 'source <(kubectl completion bash)' >> ~/.bashrc
  echo 'alias k=kubectl' >> ~/.bashrc
  echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc
  source ~/.bashrc

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.3 — CLAIRE INSTALLE KUBECTL (WINDOWS 11)
────────────────────────────────────────────────────────────────────────────────

  # Dans PowerShell (en tant qu'administrateur):

  # Méthode 1: via winget (Windows Package Manager):
  winget install Kubernetes.kubectl

  # Méthode 2: via Chocolatey:
  # choco install kubernetes-cli

  # Méthode 3: manuelle:
  # Télécharger: https://dl.k8s.io/release/v1.29.0/bin/windows/amd64/kubectl.exe
  # Placer dans C:\Windows\System32\kubectl.exe

  # Vérifier (dans PowerShell ou Git Bash):
  kubectl version --client

  # Autocomplétion PowerShell:
  kubectl completion powershell >> $PROFILE

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.4 — ALICE INSTALLE ET CONFIGURE MINIKUBE
────────────────────────────────────────────────────────────────────────────────

  POURQUOI MINIKUBE?
  ───────────────────
  Minikube crée un cluster Kubernetes COMPLET sur votre machine locale.
  Il tourne dans une VM ou dans Docker.
  Parfait pour apprendre et développer sans cluster cloud coûteux.

  QUAND UTILISER MINIKUBE?
  ─────────────────────────
  -> Développement local quotidien
  -> Tester des manifestes YAML avant de les pousser
  -> Apprendre Kubernetes sans frais cloud
  -> Démonstrations et formations

  INSTALLATION SUR MAC:
  ──────────────────────
  brew install minikube

  DÉMARRER MINIKUBE (avec le driver Docker):
  ──────────────────────────────────────────
  # POURQUOI --driver=docker?
  # Docker est déjà installé. C'est plus léger qu'une VM complète.
  # Minikube crée un conteneur Docker qui joue le rôle du cluster K8s.

  minikube start \
    --driver=docker \
    --cpus=4 \
    --memory=8192 \
    --disk-size=20g \
    --kubernetes-version=v1.29.0 \
    --addons=ingress,metrics-server,dashboard

  # Explication de chaque option:
  # --driver=docker       -> utiliser Docker comme hyperviseur
  # --cpus=4              -> 4 CPU alloués au cluster
  # --memory=8192         -> 8 Go RAM (8192 Mo)
  # --disk-size=20g       -> 20 Go disque
  # --kubernetes-version  -> version spécifique de K8s
  # --addons=...          -> activer des extensions dès le départ:
  #   ingress             -> NGINX Ingress Controller (pour exposer des Services)
  #   metrics-server      -> collecte CPU/mémoire (nécessaire pour HPA)
  #   dashboard           -> interface web K8s

  CE QUE VOUS VOYEZ (peut prendre 3-5 minutes la première fois):
  ─────────────────────────────────────────────────────────────────
  [SMILING_FACE_WITH_OPEN_MOUTH_AND_SMILING_EYES]  minikube v1.32.0 on Darwin 14.2
  *  Using the docker driver based on user choice
  [IMPORTANT]  Using Docker Desktop driver with root privileges
  [BIEN]  Starting control plane node minikube in cluster minikube
  [TRACTOR]  Pulling base image ...
  [HOT]  Creating docker container (CPUs=4, Memory=8192MB) ...
  [DOCKER]  Preparing Kubernetes v1.29.0 on Docker 25.0.0 ...
      - Generating certificates and keys ...
      - Booting up control plane ...
      - Configuring RBAC rules ...
  [LIEN]  Configuring bridge CNI (Container Networking Interface) ...
  [RECHERCHE]  Verifying Kubernetes components...
      - Using image gcr.io/k8s-minikube/storage-provisioner:v5
      - Using image registry.k8s.io/ingress-nginx/controller:v1.9.4
      - Using image registry.k8s.io/metrics-server/metrics-server:v0.6.4
  *  Enabled addons: storage-provisioner, default-storageclass,
      metrics-server, ingress
  [SURFER]  Done! kubectl is now configured to use "minikube" cluster and
      "default" namespace by default

  VÉRIFIER QUE LE CLUSTER FONCTIONNE:
  ──────────────────────────────────────
  kubectl cluster-info

  CE QUE VOUS VOYEZ:
  ───────────────────
  Kubernetes control plane is running at https://127.0.0.1:49832
  CoreDNS is running at https://127.0.0.1:49832/api/v1/namespaces/...

  kubectl get nodes

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME       STATUS   ROLES           AGE   VERSION
  minikube   Ready    control-plane   2m    v1.29.0

  ACCÉDER AU TABLEAU DE BORD:
  ────────────────────────────
  minikube dashboard
  # -> Ouvre automatiquement http://127.0.0.1:PORT/... dans votre navigateur
  # C'est une interface web complète pour visualiser votre cluster.

  COMMANDES MINIKUBE ESSENTIELLES:
  ─────────────────────────────────
  minikube status           # État du cluster
  minikube stop             # Arrêter le cluster (garde les données)
  minikube start            # Redémarrer le cluster
  minikube delete           # Supprimer le cluster complètement
  minikube ip               # IP du cluster (pour accéder aux NodePort)
  minikube ssh              # Se connecter au node minikube en SSH
  minikube tunnel           # Créer un tunnel pour les LoadBalancer
  minikube addons list      # Lister les addons disponibles
  minikube addons enable [nom]   # Activer un addon
  minikube image load [image]    # Charger une image Docker locale dans minikube

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.5 — ALICE INSTALLE KIND (KUBERNETES IN DOCKER)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI KIND EN PLUS DE MINIKUBE?
  ───────────────────────────────────
  Minikube = 1 seul node (Control Plane + Worker sur la même machine).
  kind     = multi-nodes (plusieurs Control Planes, plusieurs Workers) dans Docker.

  kind est utilisé pour:
  -> Tester des scénarios multi-nodes (affinity, taints, HA)
  -> Pipelines CI/CD (GitHub Actions lance un cluster kind pour les tests)
  -> Clusters éphémères (créé et supprimé en 1 minute)

  INSTALLATION SUR MAC:
  ──────────────────────
  brew install kind

  CRÉER UN CLUSTER MULTI-NODES AVEC KIND:
  ─────────────────────────────────────────
  # Créer le fichier de configuration du cluster:
  cat > /tmp/kind-cluster.yaml << 'EOF'
  kind: Cluster
  apiVersion: kind.x-k8s.io/v1alpha4
  name: taskmanager-cluster

  nodes:
  # Control Plane node:
  - role: control-plane
    kubeadmConfigPatches:
    - |
      kind: InitConfiguration
      nodeRegistration:
        kubeletExtraArgs:
          node-labels: "ingress-ready=true"
    extraPortMappings:
    - containerPort: 80
      hostPort: 80
      protocol: TCP
    - containerPort: 443
      hostPort: 443
      protocol: TCP

  # Worker nodes:
  - role: worker
    labels:
      environment: production
      tier: backend
  - role: worker
    labels:
      environment: production
      tier: backend
  - role: worker
    labels:
      environment: production
      tier: database
  EOF

  # Créer le cluster:
  kind create cluster --config /tmp/kind-cluster.yaml

  CE QUE VOUS VOYEZ:
  ───────────────────
  Creating cluster "taskmanager-cluster" ...
   [OK] Ensuring node image (kindest/node:v1.29.0) [FRAME_WITH_PICTURE]
   [OK] Preparing nodes [PACKAGE] [PACKAGE] [PACKAGE] [PACKAGE]
   [OK] Writing configuration [DOC]
   [OK] Starting control-plane [JOYSTICK]
   [OK] Installing CNI [PLUGIN]
   [OK] Installing StorageClass [SAUVEGARDE]
   [OK] Joining worker nodes [TRACTOR]
  Set kubectl context to "kind-taskmanager-cluster"
  You can now use your cluster with: kubectl cluster-info

  # Voir les nodes créés:
  kubectl get nodes

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME                              STATUS   ROLES           AGE   VERSION
  taskmanager-cluster-control-plane Ready    control-plane   2m    v1.29.0
  taskmanager-cluster-worker        Ready    <none>          90s   v1.29.0
  taskmanager-cluster-worker2       Ready    <none>          90s   v1.29.0
  taskmanager-cluster-worker3       Ready    <none>          90s   v1.29.0

  GÉRER PLUSIEURS CLUSTERS AVEC KUBECONFIG:
  ───────────────────────────────────────────
  # kubectl sait quel cluster utiliser grâce au fichier ~/.kube/config.
  # Ce fichier contient tous vos clusters ("contexts").

  kubectl config get-contexts
  # -> Montre tous les clusters configurés (minikube, kind, GKE, EKS...)

  kubectl config current-context
  # -> minikube (ou kind-taskmanager-cluster)

  kubectl config use-context minikube
  # -> Switched to context "minikube"

  kubectl config use-context kind-taskmanager-cluster
  # -> Switched to context "kind-taskmanager-cluster"

  # Astuce: installer kubectx pour changer de contexte facilement:
  brew install kubectx
  kubectx minikube                     # Changer de contexte
  kubens development                   # Changer de namespace par défaut

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.6 — ALICE INSTALLE HELM
────────────────────────────────────────────────────────────────────────────────

  POURQUOI HELM?
  ───────────────
  Déployer PostgreSQL dans Kubernetes = 15+ fichiers YAML complexes.
  Avec Helm: helm install postgres bitnami/postgresql -> 1 commande.

  Helm = gestionnaire de paquets pour Kubernetes.
  Un "Chart" Helm = ensemble de YAML templatisés pour une application.
  Le "Values" file = vos paramètres de configuration.

  Helm gère:
  -> Installation de logiciels complexes (PostgreSQL, Redis, Prometheus...)
  -> Mise à jour sans interruption avec gestion de versions
  -> Rollback en 1 commande si problème
  -> Templating pour multi-environnements

  INSTALLATION SUR MAC:
  ──────────────────────
  brew install helm

  VÉRIFICATION:
  ──────────────
  helm version
  # -> version.BuildInfo{Version:"v3.14.0", ...}

  AJOUTER LES DÉPÔTS ESSENTIELS:
  ────────────────────────────────
  # Bitnami: PostgreSQL, Redis, Nginx, MongoDB...
  helm repo add bitnami https://charts.bitnami.com/bitnami

  # Prometheus Community: Prometheus, Grafana...
  helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

  # Ingress-Nginx: Nginx Ingress Controller
  helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

  # ArgoCD: GitOps
  helm repo add argo https://argoproj.github.io/argo-helm

  # Elastic: EFK Stack
  helm repo add elastic https://helm.elastic.co

  # Mettre à jour les dépôts:
  helm repo update

  # Chercher un chart:
  helm search repo postgresql
  # -> bitnami/postgresql    14.0.0    ...

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.7 — ALICE INSTALLE K9S
────────────────────────────────────────────────────────────────────────────────

  POURQUOI K9S?
  ──────────────
  kubectl get pods -> liste statique.
  k9s -> interface interactive en temps réel, comme htop pour Kubernetes.
  Naviguer entre ressources, voir les logs, exec dans un Pod: tout au clavier.

  INSTALLATION SUR MAC:
  ──────────────────────
  brew install k9s

  UTILISATION:
  ─────────────
  k9s
  # -> Interface interactive s'ouvre dans le terminal.

  Navigation dans k9s:
  :pods          -> Voir les Pods
  :deployments   -> Voir les Deployments
  :services      -> Voir les Services
  :namespaces    -> Voir les Namespaces
  l              -> Logs du Pod sélectionné
  e              -> Exec dans le Pod sélectionné
  d              -> Describe le Pod sélectionné
  shift+d        -> Supprimer le Pod sélectionné
  /              -> Filtrer (search)
  Ctrl+C         -> Quitter k9s

================================================================================
PARTIE 2 — APPLICATION FLASK: CODE ET IMAGES DOCKER
================================================================================

  POURQUOI COMMENCER PAR L'APPLICATION?
  ──────────────────────────────────────
  Kubernetes orchestre des CONTENEURS.
  Avant de créer des manifestes K8s, il faut l'image Docker de l'application.
  Dans cette partie, Alice crée l'application complète et les images Docker.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.1 — STRUCTURE COMPLÈTE DU PROJET
────────────────────────────────────────────────────────────────────────────────

  ALICE CRÉE LA STRUCTURE:
  ─────────────────────────
  mkdir taskmanager-k8s && cd taskmanager-k8s

  # Structure complète:
  mkdir -p app tests kubernetes/{base,overlays/{development,staging,production}} \
    kubernetes/base/{api,postgres,redis,ingress,monitoring,rbac,storage} \
    helm/taskmanager/{templates,charts} \
    scripts docker argocd monitoring/{prometheus,grafana,elasticsearch}

  ── app/__init__.py ─────────────────────────────────────────────────────────
  import os
  import logging
  from flask import Flask
  from flask_sqlalchemy import SQLAlchemy
  from flask_redis import FlaskRedis
  from prometheus_flask_exporter import PrometheusMetrics

  db = SQLAlchemy()
  redis_client = FlaskRedis()

  def create_app(config_name=None):
      if config_name is None:
          config_name = os.environ.get('FLASK_ENV', 'production')

      app = Flask(__name__)

      # Charger la configuration:
      if config_name == 'testing':
          app.config.update(
              TESTING=True,
              SQLALCHEMY_DATABASE_URI='sqlite:///:memory:',
              SECRET_KEY='test-secret',
              REDIS_URL='redis://localhost:6379/0',
          )
      else:
          app.config.update(
              SECRET_KEY=os.environ.get('SECRET_KEY', 'change-me'),
              SQLALCHEMY_DATABASE_URI=os.environ.get('DATABASE_URL'),
              SQLALCHEMY_TRACK_MODIFICATIONS=False,
              SQLALCHEMY_POOL_SIZE=int(os.environ.get('DB_POOL_SIZE', '5')),
              SQLALCHEMY_MAX_OVERFLOW=int(os.environ.get('DB_MAX_OVERFLOW', '10')),
              SQLALCHEMY_POOL_TIMEOUT=int(os.environ.get('DB_POOL_TIMEOUT', '30')),
              REDIS_URL=os.environ.get('REDIS_URL', 'redis://redis:6379/0'),
              APP_VERSION=os.environ.get('APP_VERSION', '1.0.0'),
              APP_ENV=os.environ.get('APP_ENV', 'production'),
              LOG_LEVEL=os.environ.get('LOG_LEVEL', 'INFO'),
          )

      # Configurer les logs JSON (pour Fluentd/EFK):
      logging.basicConfig(
          level=getattr(logging, app.config.get('LOG_LEVEL', 'INFO')),
          format='%(asctime)s %(levelname)s %(name)s %(message)s'
      )

      # Initialiser les extensions:
      db.init_app(app)
      redis_client.init_app(app)

      # Métriques Prometheus (endpoint /metrics automatique):
      metrics = PrometheusMetrics(app)
      metrics.info('app_info', 'Application info',
                   version=app.config.get('APP_VERSION', '1.0.0'))

      # Enregistrer les blueprints:
      from app.routes.tasks import tasks_bp
      from app.routes.auth import auth_bp
      from app.routes.health import health_bp

      app.register_blueprint(tasks_bp, url_prefix='/api/v1')
      app.register_blueprint(auth_bp, url_prefix='/api/v1/auth')
      app.register_blueprint(health_bp)

      return app

  ── app/models.py ───────────────────────────────────────────────────────────
  from datetime import datetime
  import hashlib
  import os
  from app import db

  class User(db.Model):
      __tablename__ = 'users'
      id         = db.Column(db.Integer, primary_key=True)
      username   = db.Column(db.String(80),  unique=True, nullable=False, index=True)
      email      = db.Column(db.String(120), unique=True, nullable=False, index=True)
      password   = db.Column(db.String(256), nullable=False)
      is_active  = db.Column(db.Boolean, default=True, nullable=False)
      created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
      tasks      = db.relationship('Task', backref='owner', lazy='dynamic',
                                   cascade='all, delete-orphan')

      def set_password(self, password):
          salt = os.urandom(32)
          key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
          self.password = salt.hex() + ':' + key.hex()

      def check_password(self, password):
          if ':' not in self.password:
              return False
          salt_hex, key_hex = self.password.split(':', 1)
          salt = bytes.fromhex(salt_hex)
          new_key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
          return new_key.hex() == key_hex

      def to_dict(self):
          return {'id': self.id, 'username': self.username,
                  'email': self.email, 'created_at': self.created_at.isoformat() + 'Z'}

  class Task(db.Model):
      __tablename__ = 'tasks'
      PRIORITY_VALUES = ('low', 'medium', 'high')
      STATUS_VALUES   = ('todo', 'in_progress', 'done')

      id          = db.Column(db.Integer, primary_key=True)
      title       = db.Column(db.String(200), nullable=False)
      description = db.Column(db.Text,        nullable=True)
      status      = db.Column(db.String(20),  default='todo',   nullable=False, index=True)
      priority    = db.Column(db.String(10),  default='medium', nullable=False, index=True)
      tags        = db.Column(db.JSON,        default=list)
      user_id     = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
      due_date    = db.Column(db.DateTime, nullable=True)
      created_at  = db.Column(db.DateTime, default=datetime.utcnow, index=True)
      updated_at  = db.Column(db.DateTime, default=datetime.utcnow,
                               onupdate=datetime.utcnow)

      def to_dict(self):
          return {
              'id':          self.id,
              'title':       self.title,
              'description': self.description,
              'status':      self.status,
              'priority':    self.priority,
              'tags':        self.tags or [],
              'user_id':     self.user_id,
              'due_date':    self.due_date.isoformat() + 'Z' if self.due_date else None,
              'created_at':  self.created_at.isoformat() + 'Z',
              'updated_at':  self.updated_at.isoformat() + 'Z',
          }

  ── app/routes/health.py ────────────────────────────────────────────────────
  # Routes de santé pour les probes Kubernetes
  import os
  import time
  from flask import Blueprint, jsonify
  from app import db, redis_client

  health_bp = Blueprint('health', __name__)

  # Heure de démarrage de l'application (pour startup probe):
  _start_time = time.time()
  _is_ready = False   # Devient True quand l'app a fini son init

  @health_bp.route('/health', methods=['GET'])
  def health():
      """Vérification globale. Pour monitoring externe."""
      return jsonify({
          'status':   'healthy',
          'version':  os.environ.get('APP_VERSION', 'unknown'),
          'env':      os.environ.get('APP_ENV', 'unknown'),
      }), 200

  @health_bp.route('/live', methods=['GET'])
  def liveness():
      """
      Liveness probe: est-ce que l'app est vivante?
      Si ce retourne != 200 -> K8s redémarre le Pod.
      Ne pas inclure de vérification de dépendances (BDD, Redis)
      car si la BDD est down, le Pod serait tué en boucle.
      """
      return jsonify({'status': 'alive'}), 200

  @health_bp.route('/ready', methods=['GET'])
  def readiness():
      """
      Readiness probe: est-ce que l'app peut recevoir du trafic?
      Si ce retourne != 200 -> K8s ne route PLUS de trafic vers ce Pod.
      Vérifier les dépendances: BDD, Redis.
      """
      checks = {}
      overall = 'ready'

      # Vérifier PostgreSQL:
      try:
          db.session.execute(db.text('SELECT 1'))
          checks['database'] = 'ok'
      except Exception as e:
          checks['database'] = f'error: {str(e)}'
          overall = 'not_ready'

      # Vérifier Redis:
      try:
          redis_client.ping()
          checks['redis'] = 'ok'
      except Exception as e:
          checks['redis'] = f'error: {str(e)}'
          overall = 'not_ready'

      code = 200 if overall == 'ready' else 503
      return jsonify({'status': overall, 'checks': checks}), code

  @health_bp.route('/startup', methods=['GET'])
  def startup():
      """
      Startup probe: est-ce que l'app a fini de démarrer?
      K8s attend que ce retourne 200 avant de lancer liveness/readiness.
      Utile pour les apps lentes au démarrage (migrations BDD...).
      """
      uptime = time.time() - _start_time
      # L'app a eu au moins 10 secondes pour démarrer:
      if uptime > 10:
          return jsonify({'status': 'started', 'uptime': uptime}), 200
      return jsonify({'status': 'starting', 'uptime': uptime}), 503

  ── app/routes/tasks.py ─────────────────────────────────────────────────────
  import json
  from datetime import datetime
  from flask import Blueprint, jsonify, request, g
  from functools import wraps
  from app import db, redis_client
  from app.models import Task

  tasks_bp = Blueprint('tasks', __name__)

  CACHE_TTL = 300   # 5 minutes

  def require_auth(f):
      """Décorateur simple d'authentification JWT (simplifié)."""
      @wraps(f)
      def decorated(*args, **kwargs):
          token = request.headers.get('Authorization', '').replace('Bearer ', '')
          if not token:
              return jsonify({'error': 'Authentication required'}), 401
          # Décoder le token (simplifié — voir auth.py pour l'impl complète):
          import hashlib
          secret = 'jwt-secret-key'
          # Dans la vraie implémentation: vérification JWT avec PyJWT
          g.user_id = 1   # Simplifié pour l'exemple
          return f(*args, **kwargs)
      return decorated

  @tasks_bp.route('/tasks', methods=['GET'])
  @require_auth
  def get_tasks():
      """Lister les tâches avec filtres, pagination et cache Redis."""
      # Construire la clé de cache depuis les paramètres:
      cache_key = f"tasks:user:{g.user_id}:{request.query_string.decode()}"

      # Essayer le cache Redis:
      cached = redis_client.get(cache_key)
      if cached:
          return jsonify(json.loads(cached)), 200

      # Requête BDD:
      query = Task.query.filter_by(user_id=g.user_id)

      status   = request.args.get('status')
      priority = request.args.get('priority')
      search   = request.args.get('search')

      if status and status in Task.STATUS_VALUES:
          query = query.filter(Task.status == status)
      if priority and priority in Task.PRIORITY_VALUES:
          query = query.filter(Task.priority == priority)
      if search:
          query = query.filter(Task.title.ilike(f'%{search}%'))

      page     = request.args.get('page', 1, type=int)
      per_page = min(request.args.get('per_page', 20, type=int), 100)
      result   = query.order_by(Task.created_at.desc()).paginate(
                     page=page, per_page=per_page, error_out=False)

      response = {
          'tasks':    [t.to_dict() for t in result.items],
          'total':    result.total,
          'page':     result.page,
          'per_page': result.per_page,
          'pages':    result.pages,
      }

      # Mettre en cache pour 5 minutes:
      redis_client.setex(cache_key, CACHE_TTL, json.dumps(response))

      return jsonify(response), 200

  @tasks_bp.route('/tasks', methods=['POST'])
  @require_auth
  def create_task():
      data = request.get_json()
      if not data:
          return jsonify({'error': 'Request body must be JSON'}), 400
      if not data.get('title'):
          return jsonify({'error': 'title is required'}), 400

      task = Task(
          title=data['title'],
          description=data.get('description'),
          priority=data.get('priority', 'medium'),
          tags=data.get('tags', []),
          user_id=g.user_id,
      )
      db.session.add(task)
      db.session.commit()

      # Invalider le cache:
      pattern = f"tasks:user:{g.user_id}:*"
      for key in redis_client.scan_iter(pattern):
          redis_client.delete(key)

      return jsonify(task.to_dict()), 201

  @tasks_bp.route('/tasks/<int:task_id>', methods=['GET'])
  @require_auth
  def get_task(task_id):
      task = Task.query.filter_by(id=task_id, user_id=g.user_id).first_or_404()
      return jsonify(task.to_dict()), 200

  @tasks_bp.route('/tasks/<int:task_id>', methods=['PUT'])
  @require_auth
  def update_task(task_id):
      task = Task.query.filter_by(id=task_id, user_id=g.user_id).first_or_404()
      data = request.get_json()
      if not data:
          return jsonify({'error': 'Request body must be JSON'}), 400

      for field in ('title', 'description', 'status', 'priority', 'tags'):
          if field in data:
              setattr(task, field, data[field])

      db.session.commit()

      # Invalider le cache:
      pattern = f"tasks:user:{g.user_id}:*"
      for key in redis_client.scan_iter(pattern):
          redis_client.delete(key)

      return jsonify(task.to_dict()), 200

  @tasks_bp.route('/tasks/<int:task_id>', methods=['DELETE'])
  @require_auth
  def delete_task(task_id):
      task = Task.query.filter_by(id=task_id, user_id=g.user_id).first_or_404()
      db.session.delete(task)
      db.session.commit()
      return '', 204

  ── app/routes/auth.py ──────────────────────────────────────────────────────
  import os
  import jwt
  import time
  from flask import Blueprint, jsonify, request
  from app import db
  from app.models import User

  auth_bp = Blueprint('auth', __name__)
  JWT_SECRET     = os.environ.get('JWT_SECRET', 'change-this-jwt-secret')
  JWT_EXPIRY     = int(os.environ.get('JWT_EXPIRY_HOURS', '24'))

  @auth_bp.route('/register', methods=['POST'])
  def register():
      data = request.get_json()
      if not data or not data.get('username') or not data.get('email'):
          return jsonify({'error': 'username and email required'}), 400

      if User.query.filter_by(username=data['username']).first():
          return jsonify({'error': 'Username already taken'}), 409

      user = User(username=data['username'], email=data['email'])
      user.set_password(data.get('password', ''))
      db.session.add(user)
      db.session.commit()
      return jsonify(user.to_dict()), 201

  @auth_bp.route('/login', methods=['POST'])
  def login():
      data = request.get_json()
      user = User.query.filter_by(username=data.get('username')).first()
      if not user or not user.check_password(data.get('password', '')):
          return jsonify({'error': 'Invalid credentials'}), 401

      token = jwt.encode({
          'user_id': user.id,
          'exp':     time.time() + JWT_EXPIRY * 3600,
      }, JWT_SECRET, algorithm='HS256')

      return jsonify({'token': token, 'user': user.to_dict()}), 200

  ── requirements.txt ────────────────────────────────────────────────────────
  flask==3.0.0
  flask-sqlalchemy==3.1.1
  flask-redis==0.4.0
  psycopg2-binary==2.9.9
  gunicorn==21.2.0
  prometheus-flask-exporter==0.23.0
  PyJWT==2.8.0
  python-json-logger==2.0.7

  ── requirements-dev.txt ────────────────────────────────────────────────────
  pytest==7.4.3
  pytest-cov==4.1.0
  pytest-flask==1.3.0
  black==23.12.1
  flake8==7.0.0
  locust==2.20.0

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.2 — ALICE CRÉE LES DOCKERFILES
────────────────────────────────────────────────────────────────────────────────

  POURQUOI DES IMAGES DOCKER AVANT K8S?
  ──────────────────────────────────────
  Kubernetes ne "compile" pas votre code. Il télécharge et lance des images Docker.
  Pour déployer votre app dans K8s, il faut:
  1. Construire l'image Docker
  2. La pousser vers un Registry (Docker Hub, ghcr.io, ECR...)
  3. Dans le manifeste K8s: référencer l'image par son nom complet

  ── docker/Dockerfile ───────────────────────────────────────────────────────
  # Build multi-stage pour une image de production légère et sécurisée.
  # Stage 1: installer les dépendances dans un env temporaire.
  # Stage 2: image finale sans les outils de build.

  # ════════════════════════════════════════════════
  # STAGE 1 — Builder: installer les dépendances
  # ════════════════════════════════════════════════
  FROM python:3.11-slim AS builder

  # Variables de build:
  ARG BUILD_DATE
  ARG APP_VERSION=1.0.0

  # Installer les dépendances système pour psycopg2:
  RUN apt-get update && apt-get install -y --no-install-recommends \
      gcc \
      libpq-dev \
      && rm -rf /var/lib/apt/lists/*

  # Créer l'environnement virtuel:
  RUN python -m venv /opt/venv
  ENV PATH="/opt/venv/bin:$PATH"

  # Installer les dépendances Python:
  COPY requirements.txt .
  RUN pip install --no-cache-dir --upgrade pip \
   && pip install --no-cache-dir -r requirements.txt

  # ════════════════════════════════════════════════
  # STAGE 2 — Runtime: image finale de production
  # ════════════════════════════════════════════════
  FROM python:3.11-slim AS production

  ARG BUILD_DATE
  ARG APP_VERSION=1.0.0

  # Labels OCI standards (bonnes pratiques):
  LABEL org.opencontainers.image.created="${BUILD_DATE}"
  LABEL org.opencontainers.image.version="${APP_VERSION}"
  LABEL org.opencontainers.image.title="TaskManager API"
  LABEL org.opencontainers.image.description="API REST Flask TaskManager"
  LABEL org.opencontainers.image.source="https://github.com/equipe/taskmanager-k8s"

  # Installer seulement libpq (runtime de psycopg2):
  RUN apt-get update && apt-get install -y --no-install-recommends \
      libpq5 \
      curl \
      && rm -rf /var/lib/apt/lists/*

  # Copier le venv du builder:
  COPY --from=builder /opt/venv /opt/venv
  ENV PATH="/opt/venv/bin:$PATH"

  # Créer un utilisateur non-root (SÉCURITÉ: jamais root en prod!):
  RUN groupadd -r appuser && useradd -r -g appuser appuser

  # Répertoire de l'application:
  WORKDIR /app

  # Copier le code:
  COPY --chown=appuser:appuser app/ ./app/
  COPY --chown=appuser:appuser config.py run.py ./

  # Variables d'environnement de base:
  ENV FLASK_ENV=production
  ENV PYTHONDONTWRITEBYTECODE=1
  ENV PYTHONUNBUFFERED=1
  ENV APP_VERSION=${APP_VERSION}

  # Changer vers l'utilisateur non-root:
  USER appuser

  # Port exposé (documentation, pas binding réel):
  EXPOSE 5000

  # Healthcheck Docker (utilisé par docker-compose, pas K8s qui a ses probes):
  HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

  # Démarrer avec Gunicorn (serveur WSGI de production):
  # -w 4           = 4 workers (recommandé: 2*CPU+1)
  # -b 0.0.0.0:5000 = écouter sur toutes les interfaces
  # --timeout 120   = timeout des workers
  # --access-logfile - = logs d'accès vers stdout
  CMD ["gunicorn", \
       "--workers", "4", \
       "--bind", "0.0.0.0:5000", \
       "--timeout", "120", \
       "--access-logfile", "-", \
       "--error-logfile", "-", \
       "--log-level", "info", \
       "run:app"]

  ── docker/Dockerfile.dev ───────────────────────────────────────────────────
  # Image de développement avec hot-reload Flask
  FROM python:3.11-slim

  RUN apt-get update && apt-get install -y --no-install-recommends \
      gcc libpq-dev curl && rm -rf /var/lib/apt/lists/*

  WORKDIR /app

  COPY requirements.txt requirements-dev.txt ./
  RUN pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt

  # En dev, le code est monté en volume -> pas de COPY
  ENV FLASK_ENV=development
  ENV FLASK_DEBUG=1
  ENV PYTHONDONTWRITEBYTECODE=1
  ENV PYTHONUNBUFFERED=1

  EXPOSE 5000

  CMD ["flask", "--app", "run.py", "run", "--host", "0.0.0.0", "--port", "5000",
       "--reload"]

  ── .dockerignore ───────────────────────────────────────────────────────────
  venv/
  .venv/
  __pycache__/
  *.py[cod]
  .pytest_cache/
  .coverage
  .git/
  .gitignore
  .env
  .env.*
  !.env.example
  tests/
  kubernetes/
  helm/
  scripts/
  *.md
  Makefile
  docker-compose*.yml

  CONSTRUIRE ET POUSSER L'IMAGE:
  ─────────────────────────────────
  # Construire l'image:
  docker build \
    --file docker/Dockerfile \
    --build-arg APP_VERSION=1.0.0 \
    --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
    --tag ghcr.io/equipe/taskmanager-api:1.0.0 \
    --tag ghcr.io/equipe/taskmanager-api:latest \
    .

  # Tester l'image localement:
  docker run --rm -p 5000:5000 \
    -e DATABASE_URL=sqlite:///test.db \
    -e REDIS_URL=redis://localhost:6379/0 \
    ghcr.io/equipe/taskmanager-api:1.0.0

  # Pousser sur ghcr.io (GitHub Container Registry):
  echo $GITHUB_TOKEN | docker login ghcr.io -u alice-dev --password-stdin
  docker push ghcr.io/equipe/taskmanager-api:1.0.0
  docker push ghcr.io/equipe/taskmanager-api:latest

  # Charger l'image dans minikube (pour développement local sans push):
  minikube image load ghcr.io/equipe/taskmanager-api:1.0.0
  # Ou construire directement dans minikube:
  # eval $(minikube docker-env) && docker build ...

================================================================================
PARTIE 3 — LES PODS: L'UNITÉ DE BASE DE KUBERNETES
================================================================================

  POURQUOI COMPRENDRE LES PODS?
  ──────────────────────────────
  Tout dans Kubernetes tourne dans des Pods.
  Un Pod = la plus petite unité déployable dans K8s.
  Un Pod contient 1 ou plusieurs conteneurs qui:
  - Partagent le même réseau (même IP, même ports)
  - Partagent le même stockage (volumes)
  - Sont toujours schedulés ensemble sur le même node

  POURQUOI 1+ CONTENEURS PAR POD?
  ──────────────────────────────────
  Cas principal (95%): 1 conteneur par Pod.
  Cas "sidecar" (5%): 2+ conteneurs qui doivent être co-localisés.

  Exemples de sidecar patterns:
  - API Flask + agent Fluentd (collecte les logs)
  - API Flask + envoy proxy (service mesh)
  - Conteneur "init" qui fait les migrations BDD avant l'app principale

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 3.1 — ALICE CRÉE UN POD SIMPLE (POUR COMPRENDRE)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI CRÉER UN POD DIRECTEMENT?
  ────────────────────────────────────
  En production, on ne crée jamais de Pods directement (on utilise Deployment).
  Mais pour apprendre, créer un Pod seul montre comment ça fonctionne
  sans la complexité d'un Deployment.

  ── kubernetes/base/api/pod-example.yaml ────────────────────────────────────
  # AVERTISSEMENT: Ceci est un exemple pédagogique uniquement.
  # En production, utilisez TOUJOURS un Deployment (Partie 4).
  # Un Pod seul n'est pas relancé s'il crashe!

  apiVersion: v1         # Les Pods font partie de l'API "core" (v1)
  kind: Pod
  metadata:
    name: taskmanager-api-pod     # Nom unique dans le namespace
    namespace: default            # Namespace (espace de noms)
    labels:                       # Étiquettes pour sélection et monitoring
      app: taskmanager            # Label principal (utilisé par Services)
      version: "1.0.0"            # Version de l'app
      tier: backend               # Niveau architectural
    annotations:                  # Métadonnées non-sélectionnables (documentation)
      description: "Pod de démonstration - utiliser Deployment en prod"
      contact: "alice@equipe.com"
  spec:
    # ── Sécurité au niveau du Pod ──────────────────────────────────────────
    securityContext:
      runAsNonRoot: true          # Jamais lancer en root (sécurité)
      runAsUser: 1000             # UID de l'utilisateur appuser
      runAsGroup: 1000            # GID du groupe appuser
      fsGroup: 1000               # GID pour les volumes montés

    # ── Conteneurs ────────────────────────────────────────────────────────
    containers:
    - name: api                               # Nom du conteneur dans le Pod
      image: ghcr.io/equipe/taskmanager-api:1.0.0  # Image Docker à utiliser
      imagePullPolicy: IfNotPresent           # Politique de téléchargement:
      # Always        = toujours télécharger (bon pour :latest mais lent)
      # IfNotPresent  = utiliser le cache local si disponible (recommandé)
      # Never         = jamais télécharger (pour images locales uniquement)

      ports:
      - containerPort: 5000       # Port sur lequel le conteneur écoute
        name: http                # Nom du port (référencé dans Service)
        protocol: TCP

      # ── Variables d'environnement ────────────────────────────────────────
      env:
      - name: FLASK_ENV
        value: "production"
      - name: APP_VERSION
        value: "1.0.0"
      # Référencer une valeur depuis un Secret:
      - name: DATABASE_URL
        valueFrom:
          secretKeyRef:
            name: taskmanager-secrets     # Nom du Secret
            key: database-url             # Clé dans le Secret
      - name: SECRET_KEY
        valueFrom:
          secretKeyRef:
            name: taskmanager-secrets
            key: secret-key
      # Référencer une valeur depuis un ConfigMap:
      - name: LOG_LEVEL
        valueFrom:
          configMapKeyRef:
            name: taskmanager-config      # Nom du ConfigMap
            key: log-level                # Clé dans le ConfigMap
      # Variables auto-injectées par Kubernetes (informations sur le Pod):
      - name: POD_NAME
        valueFrom:
          fieldRef:
            fieldPath: metadata.name
      - name: POD_NAMESPACE
        valueFrom:
          fieldRef:
            fieldPath: metadata.namespace
      - name: NODE_NAME
        valueFrom:
          fieldRef:
            fieldPath: spec.nodeName

      # ── Ressources (CPU et mémoire) ───────────────────────────────────────
      # POURQUOI définir des ressources?
      # Sans limits: un Pod peut consommer TOUTE la RAM/CPU du node
      # et affamer les autres Pods. Catastrophique en prod.
      # requests = ce que le Scheduler réserve pour ce Pod
      # limits   = le maximum absolu que le Pod peut utiliser
      resources:
        requests:
          cpu: "100m"             # 100 millicores = 0.1 vCPU
          memory: "128Mi"         # 128 Mégaoctets
        limits:
          cpu: "500m"             # 500 millicores = 0.5 vCPU
          memory: "512Mi"         # 512 Mégaoctets

      # ── Probes de santé ───────────────────────────────────────────────────
      livenessProbe:
        httpGet:
          path: /live             # Route de liveness dans notre API
          port: 5000
        initialDelaySeconds: 30   # Attendre 30s avant la 1ère vérification
        periodSeconds: 10         # Vérifier toutes les 10 secondes
        failureThreshold: 3       # 3 échecs -> redémarrer le Pod
        timeoutSeconds: 5         # Timeout de 5 secondes par vérification

      readinessProbe:
        httpGet:
          path: /ready            # Route de readiness
          port: 5000
        initialDelaySeconds: 15
        periodSeconds: 5
        failureThreshold: 3
        successThreshold: 1       # 1 succès suffit pour être "ready"
        timeoutSeconds: 5

      startupProbe:
        httpGet:
          path: /startup
          port: 5000
        initialDelaySeconds: 10
        periodSeconds: 5
        failureThreshold: 12      # 12 * 5s = 60s maximum pour démarrer
        # Tant que startup échoue: liveness et readiness ne sont pas lancées.
        # Protège les apps lentes (migrations BDD, warm-up de cache...)

      # ── Sécurité au niveau du conteneur ──────────────────────────────────
      securityContext:
        allowPrivilegeEscalation: false   # Interdit sudo, setuid...
        readOnlyRootFilesystem: true      # Système de fichiers en lecture seule
        capabilities:
          drop:
          - ALL                   # Retirer TOUTES les capabilities Linux

      # ── Volumes montés ────────────────────────────────────────────────────
      volumeMounts:
      - name: tmp-dir
        mountPath: /tmp           # Gunicorn a besoin d'écrire dans /tmp
      - name: app-config
        mountPath: /app/config    # Configurer depuis un ConfigMap
        readOnly: true

    # ── Volumes du Pod ────────────────────────────────────────────────────
    volumes:
    - name: tmp-dir
      emptyDir: {}                # Volume temporaire en mémoire
    - name: app-config
      configMap:
        name: taskmanager-config  # Monter un ConfigMap comme volume

    # ── Politique de redémarrage ──────────────────────────────────────────
    restartPolicy: Always
    # Always       = toujours redémarrer (défaut, pour apps longues)
    # OnFailure    = redémarrer seulement sur erreur (pour Jobs)
    # Never        = jamais redémarrer (pour debug)

    # ── ServiceAccount ────────────────────────────────────────────────────
    serviceAccountName: taskmanager-sa
    # Identité du Pod pour accéder à l'API K8s.
    # Si omis: utilise le ServiceAccount "default" du namespace.

    # ── Termination Grace Period ──────────────────────────────────────────
    terminationGracePeriodSeconds: 30
    # K8s envoie SIGTERM, attend 30s, puis envoie SIGKILL.
    # Pendant ces 30s: l'app doit terminer ses requêtes en cours.
    # Si votre app est plus lente: augmenter cette valeur.

  ALICE APPLIQUE ET EXPLORE LE POD:
  ────────────────────────────────────
  # Appliquer le manifeste:
  kubectl apply -f kubernetes/base/api/pod-example.yaml

  # Voir l'état du Pod:
  kubectl get pod taskmanager-api-pod

  CE QUE VOUS DEVEZ VOIR:
  ────────────────────────
  NAME                    READY   STATUS    RESTARTS   AGE
  taskmanager-api-pod     0/1     Pending   0          2s

  # "Pending" = K8s essaie de le placer sur un node.
  # Après quelques secondes:
  NAME                    READY   STATUS    RESTARTS   AGE
  taskmanager-api-pod     1/1     Running   0          15s

  # "1/1" = 1 conteneur sur 1 est prêt (readiness probe OK).

  # Voir les détails complets (TRÈS utile pour déboguer!):
  kubectl describe pod taskmanager-api-pod

  CE QUE VOUS VOYEZ (extrait):
  ─────────────────────────────
  Name:             taskmanager-api-pod
  Namespace:        default
  Priority:         0
  Node:             minikube/192.168.49.2
  Start Time:       Mon, 10 Jan 2024 10:00:00 +0100
  Labels:           app=taskmanager
                    tier=backend
                    version=1.0.0
  Annotations:      contact: alice@equipe.com
  Status:           Running
  IP:               10.244.0.4
  Containers:
    api:
      Container ID:   docker://abc123...
      Image:          ghcr.io/equipe/taskmanager-api:1.0.0
      Image ID:       ...
      Port:           5000/TCP
      State:          Running
        Started:      Mon, 10 Jan 2024 10:00:10 +0100
      Ready:          True
      Restart Count:  0
      Limits:
        cpu:     500m
        memory:  512Mi
      Requests:
        cpu:      100m
        memory:   128Mi
      Liveness:   http-get http://:5000/live delay=30s timeout=5s period=10s
      Readiness:  http-get http://:5000/ready delay=15s timeout=5s period=5s
      Environment:
        FLASK_ENV:     production
        DATABASE_URL:  <set to the key 'database-url' in secret 'taskmanager-secrets'>
        ...
  Events:
    Type    Reason     Age   From               Message
    ────    ──────     ───   ────               ───────
    Normal  Scheduled  30s   default-scheduler  Successfully assigned default/...
    Normal  Pulling    29s   kubelet            Pulling image "ghcr.io/..."
    Normal  Pulled     15s   kubelet            Successfully pulled image
    Normal  Created    15s   kubelet            Created container api
    Normal  Started    15s   kubelet            Started container api

  # La section "Events" est LA première chose à regarder pour déboguer!

  # Voir les logs du Pod:
  kubectl logs taskmanager-api-pod

  # Voir les logs en temps réel (comme tail -f):
  kubectl logs taskmanager-api-pod -f

  # Voir les logs du conteneur nommé "api":
  kubectl logs taskmanager-api-pod -c api

  # Voir les logs du Pod précédent (si le Pod a redémarré):
  kubectl logs taskmanager-api-pod --previous

  # Entrer dans le Pod (comme SSH):
  kubectl exec -it taskmanager-api-pod -- /bin/bash
  # -> Vous êtes maintenant dans le conteneur!
  # -> ls, cat, curl, ping, etc. fonctionnent normalement.

  # Copier un fichier depuis/vers le Pod:
  kubectl cp taskmanager-api-pod:/app/config.py ./config-from-pod.py
  kubectl cp ./my-script.py taskmanager-api-pod:/tmp/my-script.py

  # Port-forward pour accéder au Pod depuis votre machine:
  kubectl port-forward pod/taskmanager-api-pod 8080:5000
  # -> curl http://localhost:8080/health fonctionne maintenant!

  # Supprimer le Pod:
  kubectl delete pod taskmanager-api-pod

  # IMPORTANT: un Pod supprimé ne revient PAS (pas de Deployment).
  # C'est pour ça qu'on utilise Deployment en production.

================================================================================
PARTIE 4 — LES CONTROLLERS: DEPLOYMENT, STATEFULSET, DAEMONSET
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.1 — ALICE CRÉE LE CONFIGMAP ET LES SECRETS (PRÉREQUIS)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI CONFIGMAP ET SECRET AVANT LE DEPLOYMENT?
  ──────────────────────────────────────────────────
  Le Deployment référence des ConfigMaps et Secrets.
  Ils doivent exister avant que le Deployment essaie de les monter.
  Ordre de création recommandé: Namespace -> ConfigMap/Secret -> PVC -> Deployment -> Service

  ── kubernetes/base/api/configmap.yaml ──────────────────────────────────────
  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: taskmanager-config
    namespace: production          # Namespace de production
    labels:
      app: taskmanager
      component: config
  data:
    # Données de configuration non-sensibles:
    log-level: "INFO"
    app-env: "production"
    db-pool-size: "10"
    db-max-overflow: "20"
    db-pool-timeout: "30"
    cache-ttl: "300"
    max-page-size: "100"
    gunicorn-workers: "4"
    gunicorn-timeout: "120"

    # On peut aussi stocker des fichiers entiers dans un ConfigMap:
    gunicorn.conf.py: |
      # Gunicorn configuration file
      bind = "0.0.0.0:5000"
      workers = 4
      worker_class = "gthread"
      threads = 2
      timeout = 120
      keepalive = 5
      max_requests = 1000
      max_requests_jitter = 50
      accesslog = "-"
      errorlog = "-"
      loglevel = "info"
      forwarded_allow_ips = "*"
      proxy_allow_ips = "*"

    nginx.conf: |
      upstream api {
          server taskmanager-api:5000;
          keepalive 32;
      }
      server {
          listen 80;
          location /api/ {
              proxy_pass http://api/;
              proxy_set_header Host $host;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          }
          location /health {
              proxy_pass http://api/health;
          }
      }

  POURQUOI NE PAS METTRE LES SECRETS DANS CONFIGMAP?
  ────────────────────────────────────────────────────
  ConfigMap = données en clair dans etcd et dans les logs K8s.
  Quiconque peut lire les ConfigMaps peut voir les valeurs.
  Les mots de passe, clés JWT, tokens -> TOUJOURS dans des Secrets.

  ── kubernetes/base/api/secret.yaml ─────────────────────────────────────────
  # IMPORTANT: Ce fichier NE doit PAS être commité dans Git avec de vraies valeurs!
  # En production: utiliser External Secrets Operator ou Vault CSI.
  # Ici: valeurs base64 encodées (PAS chiffrées!).

  apiVersion: v1
  kind: Secret
  metadata:
    name: taskmanager-secrets
    namespace: production
    labels:
      app: taskmanager
      component: secrets
  type: Opaque    # Type générique (vs kubernetes.io/tls pour les certificats)

  # COMMENT ENCODER EN BASE64?
  # echo -n "ma-valeur" | base64
  # COMMENT DÉCODER?
  # echo "bWEtdmFsZXVy" | base64 --decode

  data:
    # Exemples de valeurs encodées:
    # echo -n "postgresql://taskmanager:mdp@postgres:5432/taskmanager" | base64
    database-url: cG9zdGdyZXNxbDovL3Rhc2ttYW5hZ2VyOm1kcEBwb3N0Z3JlczoqNDMyL3Rhc2ttYW5hZ2Vy
    # echo -n "super-secret-random-key-64-chars" | base64
    secret-key: c3VwZXItc2VjcmV0LXJhbmRvbS1rZXktNjQtY2hhcnM=
    # echo -n "jwt-secret-for-tokens" | base64
    jwt-secret: and0LXNlY3JldC1mb3ItdG9rZW5z
    # echo -n "redis-password-here" | base64
    redis-password: cmVkaXMtcGFzc3dvcmQtaGVyZQ==

  # OU UTILISER stringData (K8s encode automatiquement en base64):
  # stringData:
  #   database-url: "postgresql://taskmanager:mdp@postgres:5432/taskmanager"
  #   secret-key: "super-secret-random-key-64-chars"
  # (Plus lisible mais attention à ne JAMAIS committer!)

  CRÉER LE SECRET DEPUIS LA LIGNE DE COMMANDE (méthode recommandée):
  ────────────────────────────────────────────────────────────────────
  # Évite d'écrire les valeurs en clair dans un fichier YAML:
  kubectl create secret generic taskmanager-secrets \
    --namespace=production \
    --from-literal=database-url="postgresql://taskmanager:$(openssl rand -hex 16)@postgres:5432/taskmanager" \
    --from-literal=secret-key="$(openssl rand -hex 32)" \
    --from-literal=jwt-secret="$(openssl rand -hex 32)" \
    --from-literal=redis-password="$(openssl rand -hex 16)"

  # Créer un Secret depuis un fichier:
  kubectl create secret generic tls-secret \
    --namespace=production \
    --from-file=tls.crt=./cert.pem \
    --from-file=tls.key=./key.pem

  # Voir les Secrets (les valeurs sont masquées):
  kubectl get secrets -n production
  kubectl describe secret taskmanager-secrets -n production
  # -> Les valeurs apparaissent comme "[6 bytes]" — protégé!

  # Décoder une valeur (pour vérification):
  kubectl get secret taskmanager-secrets -n production \
    -o jsonpath='{.data.secret-key}' | base64 --decode

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.2 — ALICE CRÉE LE DEPLOYMENT DE L'API FLASK
────────────────────────────────────────────────────────────────────────────────

  POURQUOI UN DEPLOYMENT ET PAS UN POD DIRECT?
  ─────────────────────────────────────────────
  Un Pod seul:
  -> Si le Pod crashe -> il reste mort (aucune récupération automatique)
  -> Impossible de faire un rolling update (nouvelle version)
  -> Impossible de scaler (avoir N copies)

  Un Deployment:
  -> Gère un ReplicaSet qui maintient N Pods en permanence
  -> Si un Pod crashe -> recréé automatiquement
  -> Rolling update: remplace les Pods un par un (zéro downtime)
  -> Rollback: revenir à la version précédente en 1 commande
  -> Scaling: kubectl scale -> N copies en quelques secondes

  ── kubernetes/base/api/deployment.yaml ─────────────────────────────────────
  apiVersion: apps/v1          # Deployments sont dans "apps/v1"
  kind: Deployment
  metadata:
    name: taskmanager-api
    namespace: production
    labels:
      app: taskmanager
      component: api
      version: "1.0.0"
    annotations:
      deployment.kubernetes.io/revision: "1"
      kubernetes.io/change-cause: "Initial deployment v1.0.0"
      # "change-cause" s'affiche dans: kubectl rollout history deployment/taskmanager-api
  spec:
    # ── Nombre de replicas ───────────────────────────────────────────────────
    replicas: 3
    # POURQUOI 3?
    # - 1 replica: pas de HA (si le Pod crashe, downtime pendant le redémarrage)
    # - 2 replicas: si 1 crashe pendant un rolling update, il en reste 0 (si mal configuré)
    # - 3 replicas: minimum recommandé pour la production
    # Le HPA (Partie 9) ajustera ce nombre selon la charge.

    # ── Sélecteur ────────────────────────────────────────────────────────────
    selector:
      matchLabels:
        app: taskmanager
        component: api
    # Le Deployment gère tous les Pods qui ont CES labels.
    # ATTENTION: ne JAMAIS changer selector après création (immutable).

    # ── Stratégie de mise à jour ─────────────────────────────────────────────
    strategy:
      type: RollingUpdate         # Remplacer progressivement (alternative: Recreate)
      rollingUpdate:
        maxSurge: 1               # Créer au maximum 1 Pod EN PLUS pendant la MAJ
        maxUnavailable: 0         # 0 Pod indisponible pendant la MAJ = zéro downtime
    # Avec replicas=3, maxSurge=1, maxUnavailable=0:
    # - K8s crée 1 nouveau Pod (total: 4)
    # - Quand le nouveau est "Ready": supprime 1 ancien (total: 3)
    # - Répète jusqu'à ce que tous les anciens soient remplacés.

    # ── Template du Pod ──────────────────────────────────────────────────────
    # C'est exactement la spec d'un Pod, avec metadata et spec.
    template:
      metadata:
        labels:
          app: taskmanager        # DOIT correspondre au selector ci-dessus
          component: api
          version: "1.0.0"
        annotations:
          prometheus.io/scrape: "true"     # Prometheus doit scraper ce Pod
          prometheus.io/port: "5000"       # Sur quel port
          prometheus.io/path: "/metrics"   # Sur quel path

      spec:
        # ── Init Container ────────────────────────────────────────────────────
        # Les init containers s'exécutent AVANT les conteneurs principaux.
        # Ils doivent tous réussir (exit 0) avant que les conteneurs démarrent.
        # POURQUOI? Attendre que PostgreSQL soit prêt, faire les migrations, etc.
        initContainers:
        - name: wait-for-postgres
          image: busybox:1.36
          command:
          - sh
          - -c
          - |
            until nc -z postgres-service 5432; do
              echo "Waiting for PostgreSQL...";
              sleep 2;
            done;
            echo "PostgreSQL is ready!"
          # Ce conteneur init attend que PostgreSQL accepte des connexions
          # avant de lancer l'API Flask.

        - name: wait-for-redis
          image: busybox:1.36
          command:
          - sh
          - -c
          - |
            until nc -z redis-service 6379; do
              echo "Waiting for Redis...";
              sleep 2;
            done;
            echo "Redis is ready!"

        - name: run-migrations
          image: ghcr.io/equipe/taskmanager-api:1.0.0
          command:
          - python
          - -c
          - |
            from app import create_app, db
            app = create_app('production')
            with app.app_context():
                db.create_all()
                print("Database migrations complete!")
          env:
          - name: DATABASE_URL
            valueFrom:
              secretKeyRef:
                name: taskmanager-secrets
                key: database-url

        # ── Conteneurs principaux ─────────────────────────────────────────────
        containers:
        - name: api
          image: ghcr.io/equipe/taskmanager-api:1.0.0
          imagePullPolicy: IfNotPresent

          ports:
          - containerPort: 5000
            name: http

          # ── Toutes les variables d'env depuis ConfigMap et Secrets ────────
          env:
          - name: FLASK_ENV
            value: "production"
          - name: APP_VERSION
            value: "1.0.0"
          - name: DATABASE_URL
            valueFrom:
              secretKeyRef:
                name: taskmanager-secrets
                key: database-url
          - name: SECRET_KEY
            valueFrom:
              secretKeyRef:
                name: taskmanager-secrets
                key: secret-key
          - name: JWT_SECRET
            valueFrom:
              secretKeyRef:
                name: taskmanager-secrets
                key: jwt-secret
          - name: REDIS_URL
            value: "redis://:$(REDIS_PASSWORD)@redis-service:6379/0"
          - name: REDIS_PASSWORD
            valueFrom:
              secretKeyRef:
                name: taskmanager-secrets
                key: redis-password
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: POD_NAMESPACE
            valueFrom:
              fieldRef:
                fieldPath: metadata.namespace
          - name: NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName

          # ── Charger TOUTES les clés d'un ConfigMap comme variables ─────────
          envFrom:
          - configMapRef:
              name: taskmanager-config

          # ── Ressources ────────────────────────────────────────────────────
          resources:
            requests:
              cpu: "200m"         # 0.2 vCPU réservé
              memory: "256Mi"
            limits:
              cpu: "1000m"        # 1 vCPU maximum
              memory: "1Gi"

          # ── Probes ────────────────────────────────────────────────────────
          startupProbe:
            httpGet:
              path: /startup
              port: 5000
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 12

          livenessProbe:
            httpGet:
              path: /live
              port: 5000
            initialDelaySeconds: 0    # 0 car startupProbe gère le démarrage
            periodSeconds: 10
            failureThreshold: 3
            timeoutSeconds: 5

          readinessProbe:
            httpGet:
              path: /ready
              port: 5000
            initialDelaySeconds: 0
            periodSeconds: 5
            failureThreshold: 3
            successThreshold: 1
            timeoutSeconds: 5

          # ── Sécurité ──────────────────────────────────────────────────────
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            runAsUser: 1000
            capabilities:
              drop:
              - ALL

          # ── Volumes ──────────────────────────────────────────────────────
          volumeMounts:
          - name: tmp-dir
            mountPath: /tmp
          - name: gunicorn-config
            mountPath: /app/gunicorn.conf.py
            subPath: gunicorn.conf.py
            readOnly: true

        # ── Sidecar: Fluentd pour la collecte de logs ──────────────────────
        # (Optionnel mais montré ici pour illustration du pattern sidecar)
        - name: fluentd-sidecar
          image: fluent/fluentd:v1.16-1
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "100m"
              memory: "128Mi"
          volumeMounts:
          - name: app-logs
            mountPath: /var/log/app
          - name: fluentd-config
            mountPath: /fluentd/etc

        # ── Volumes du Pod ────────────────────────────────────────────────
        volumes:
        - name: tmp-dir
          emptyDir: {}
        - name: app-logs
          emptyDir: {}
        - name: gunicorn-config
          configMap:
            name: taskmanager-config
            items:
            - key: gunicorn.conf.py
              path: gunicorn.conf.py
        - name: fluentd-config
          configMap:
            name: fluentd-config

        # ── Security Context du Pod ────────────────────────────────────────
        securityContext:
          runAsNonRoot: true
          runAsUser: 1000
          fsGroup: 1000

        serviceAccountName: taskmanager-sa
        terminationGracePeriodSeconds: 60

        # ── Politique de téléchargement d'image ───────────────────────────
        imagePullSecrets:
        - name: ghcr-secret      # Secret contenant les credentials du registry

  ALICE APPLIQUE LE DEPLOYMENT:
  ────────────────────────────────
  kubectl apply -f kubernetes/base/api/deployment.yaml

  # Suivre le déploiement en temps réel:
  kubectl rollout status deployment/taskmanager-api -n production

  CE QUE VOUS VOYEZ:
  ───────────────────
  Waiting for deployment "taskmanager-api" rollout to finish:
  0 of 3 updated replicas are available...
  1 of 3 updated replicas are available...
  2 of 3 updated replicas are available...
  deployment "taskmanager-api" successfully rolled out

  # Voir les Pods créés:
  kubectl get pods -n production -l app=taskmanager

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME                               READY   STATUS    RESTARTS   AGE
  taskmanager-api-7d8f9c5b4-2xk9p   2/2     Running   0          2m
  taskmanager-api-7d8f9c5b4-8hm3r   2/2     Running   0          2m
  taskmanager-api-7d8f9c5b4-xq7n2   2/2     Running   0          2m

  # "2/2" = 2 conteneurs sur 2 sont prêts (api + fluentd-sidecar).
  # Le suffixe "7d8f9c5b4" = hash du ReplicaSet.
  # Le suffixe final "2xk9p" = hash du Pod.

  SCÉNARIO BOB — DEPLOYER UNE NOUVELLE VERSION:
  ──────────────────────────────────────────────
  # Bob a développé la v1.1.0 avec une nouvelle feature.
  # Il a buildé et poussé l'image ghcr.io/equipe/taskmanager-api:1.1.0.

  # Méthode 1: modifier le YAML et appliquer:
  # (Changer image: ...api:1.0.0 -> ...api:1.1.0 dans deployment.yaml)
  kubectl apply -f kubernetes/base/api/deployment.yaml
  # Ajouter --record est déprécié, utiliser annotations:
  kubectl annotate deployment taskmanager-api \
    kubernetes.io/change-cause="Deployment v1.1.0 — add task status field" \
    -n production

  # Méthode 2: set image directement (plus rapide pour des tests):
  kubectl set image deployment/taskmanager-api \
    api=ghcr.io/equipe/taskmanager-api:1.1.0 \
    -n production

  # Suivre le rolling update:
  kubectl rollout status deployment/taskmanager-api -n production

  # Voir l'historique des déploiements:
  kubectl rollout history deployment/taskmanager-api -n production
  CE QUE VOUS VOYEZ:
  REVISION  CHANGE-CAUSE
  1         Initial deployment v1.0.0
  2         Deployment v1.1.0 — add task status field

  # Si la v1.1.0 a un bug: rollback en 1 commande!
  kubectl rollout undo deployment/taskmanager-api -n production
  # Revient à la REVISION précédente (1 = v1.0.0)

  # Revenir à une révision spécifique:
  kubectl rollout undo deployment/taskmanager-api \
    --to-revision=1 -n production

  # Scaler manuellement (avant d'avoir HPA):
  kubectl scale deployment/taskmanager-api --replicas=5 -n production

  # Mettre en pause le déploiement (pendant des maintenances):
  kubectl rollout pause deployment/taskmanager-api -n production
  # Puis reprendre:
  kubectl rollout resume deployment/taskmanager-api -n production

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.3 — ALICE CRÉE LE STATEFULSET POUR POSTGRESQL
────────────────────────────────────────────────────────────────────────────────

  POURQUOI STATEFULSET POUR POSTGRESQL?
  ──────────────────────────────────────
  PostgreSQL est une application STATEFUL: elle stocke des données sur disque.
  Un Deployment crée des Pods identiques et interchangeables.
  Un StatefulSet crée des Pods avec:
  - Des noms stables et prévisibles: postgres-0, postgres-1, postgres-2
  - Des volumes persistants DÉDIÉS à chaque Pod (postgres-0 garde toujours son volume)
  - Un ordre de démarrage/arrêt garanti (postgres-0 démarre avant postgres-1)
  - Un nom DNS stable: postgres-0.postgres-headless.production.svc.cluster.local

  DIFFÉRENCE DEPLOYMENT vs STATEFULSET:
  ──────────────────────────────────────
  Deployment:
  - Pod-xyz123 -> Pod-abc456 (recréé avec un nouveau nom)
  - Même volume partagé (ou pas de volume persistant)
  - Aucun ordre de démarrage/arrêt

  StatefulSet:
  - postgres-0 reste postgres-0 (nom stable même après redémarrage)
  - postgres-0 garde TOUJOURS son propre volume (PVC dédié)
  - postgres-0 démarre avant postgres-1

  QUAND UTILISER STATEFULSET?
  ─────────────────────────────
  -> Bases de données: PostgreSQL, MySQL, MongoDB
  -> Message queues: Kafka, RabbitMQ
  -> Applications de coordination distribuée: ZooKeeper, etcd
  -> Toute app qui a besoin d'un stockage persistant ET d'une identité stable

  ── kubernetes/base/postgres/statefulset.yaml ───────────────────────────────
  apiVersion: apps/v1
  kind: StatefulSet
  metadata:
    name: postgres
    namespace: production
    labels:
      app: postgres
      component: database
  spec:
    serviceName: postgres-headless   # Headless Service requis pour StatefulSet
    replicas: 1                      # 1 pour dev, 3 pour HA avec réplication
    selector:
      matchLabels:
        app: postgres
    template:
      metadata:
        labels:
          app: postgres
          component: database
      spec:
        securityContext:
          fsGroup: 999               # GID du groupe postgres dans l'image officielle

        initContainers:
        - name: init-permissions
          image: busybox:1.36
          command:
          - sh
          - -c
          - |
            chmod 700 /var/lib/postgresql/data
            chown 999:999 /var/lib/postgresql/data
          volumeMounts:
          - name: postgres-data
            mountPath: /var/lib/postgresql/data

        containers:
        - name: postgres
          image: postgres:15-alpine
          ports:
          - containerPort: 5432
            name: postgres

          env:
          - name: POSTGRES_DB
            valueFrom:
              configMapKeyRef:
                name: postgres-config
                key: postgres-db
          - name: POSTGRES_USER
            valueFrom:
              configMapKeyRef:
                name: postgres-config
                key: postgres-user
          - name: POSTGRES_PASSWORD
            valueFrom:
              secretKeyRef:
                name: postgres-secrets
                key: postgres-password
          - name: PGDATA
            value: /var/lib/postgresql/data/pgdata

          resources:
            requests:
              cpu: "250m"
              memory: "512Mi"
            limits:
              cpu: "2000m"
              memory: "2Gi"

          livenessProbe:
            exec:
              command:
              - pg_isready
              - -U
              - taskmanager
              - -d
              - taskmanager
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3

          readinessProbe:
            exec:
              command:
              - pg_isready
              - -U
              - taskmanager
              - -d
              - taskmanager
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3

          volumeMounts:
          - name: postgres-data
            mountPath: /var/lib/postgresql/data
          - name: postgres-config-files
            mountPath: /docker-entrypoint-initdb.d
            readOnly: true
          - name: tmp
            mountPath: /tmp

          securityContext:
            runAsUser: 999
            runAsGroup: 999
            allowPrivilegeEscalation: false

        volumes:
        - name: postgres-config-files
          configMap:
            name: postgres-init-scripts
        - name: tmp
          emptyDir: {}

    # ── Volume Claim Templates ────────────────────────────────────────────────
    # SPÉCIFIQUE aux StatefulSets: chaque Pod reçoit son propre PVC.
    # postgres-0 -> postgres-data-postgres-0
    # postgres-1 -> postgres-data-postgres-1 (si replicas=3)
    volumeClaimTemplates:
    - metadata:
        name: postgres-data
        labels:
          app: postgres
      spec:
        accessModes:
        - ReadWriteOnce              # 1 seul node peut lire et écrire (pour BDD)
        storageClassName: standard   # Classe de stockage (voir Partie 6)
        resources:
          requests:
            storage: 20Gi           # 20 Go de stockage persistant

  ── kubernetes/base/postgres/configmap.yaml ─────────────────────────────────
  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: postgres-config
    namespace: production
  data:
    postgres-db: "taskmanager"
    postgres-user: "taskmanager"
    shared-buffers: "256MB"
    max-connections: "100"

  ---
  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: postgres-init-scripts
    namespace: production
  data:
    # Script SQL d'initialisation de la base de données:
    01-init.sql: |
      -- Créer les extensions PostgreSQL:
      CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
      CREATE EXTENSION IF NOT EXISTS "pg_trgm";  -- Pour la recherche full-text

      -- Index pour les recherches fréquentes:
      CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_user_id
        ON tasks(user_id);
      CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_status
        ON tasks(status);
      CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_created_at
        ON tasks(created_at DESC);

      -- Recherche full-text sur les titres:
      CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_title_trgm
        ON tasks USING gin(title gin_trgm_ops);

    02-seed-dev.sql: |
      -- Données de test (seulement en développement):
      INSERT INTO users (username, email, password, created_at)
      VALUES
        ('alice', 'alice@equipe.com', 'hashed_password', NOW()),
        ('bob',   'bob@equipe.com',   'hashed_password', NOW())
      ON CONFLICT DO NOTHING;

  ── kubernetes/base/postgres/service.yaml ───────────────────────────────────
  # Service "Headless" pour le StatefulSet (DNS stable par Pod):
  apiVersion: v1
  kind: Service
  metadata:
    name: postgres-headless
    namespace: production
    labels:
      app: postgres
  spec:
    clusterIP: None        # "None" = Headless (pas d'IP virtuelle)
    # Headless -> les requêtes DNS retournent les IPs des Pods directement.
    # Permet d'accéder à postgres-0.postgres-headless.production.svc.cluster.local
    selector:
      app: postgres
    ports:
    - port: 5432
      targetPort: 5432
      name: postgres

  ---
  # Service "normal" pour que l'API accède à PostgreSQL:
  apiVersion: v1
  kind: Service
  metadata:
    name: postgres-service
    namespace: production
    labels:
      app: postgres
  spec:
    selector:
      app: postgres
    ports:
    - port: 5432
      targetPort: 5432
      name: postgres

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.4 — ALICE CRÉE LE DAEMONSET POUR FLUENTD
────────────────────────────────────────────────────────────────────────────────

  POURQUOI DAEMONSET POUR FLUENTD?
  ──────────────────────────────────
  Fluentd collecte les logs de TOUS les Pods sur un node.
  Il doit tourner sur CHAQUE node du cluster.
  DaemonSet garantit exactement 1 Pod Fluentd par node.
  Quand un nouveau node rejoint le cluster: Fluentd y est déployé automatiquement.

  AUTRES USAGES DU DAEMONSET:
  ────────────────────────────
  -> Agents de monitoring (Prometheus Node Exporter, Datadog Agent)
  -> Agents de stockage distribué (Ceph, GlusterFS)
  -> Proxies réseau
  -> Agents de sécurité (Falco)

  ── kubernetes/base/monitoring/fluentd-daemonset.yaml ───────────────────────
  apiVersion: apps/v1
  kind: DaemonSet
  metadata:
    name: fluentd
    namespace: logging           # Namespace dédié aux logs
    labels:
      app: fluentd
      component: logging
  spec:
    selector:
      matchLabels:
        app: fluentd

    updateStrategy:
      type: RollingUpdate        # Mettre à jour les nodes un par un
      rollingUpdate:
        maxUnavailable: 1

    template:
      metadata:
        labels:
          app: fluentd
          component: logging
        annotations:
          prometheus.io/scrape: "true"
          prometheus.io/port: "24231"
      spec:
        serviceAccountName: fluentd-sa

        # Toleration pour tourner sur les nodes "tainted" (master):
        tolerations:
        - key: node-role.kubernetes.io/control-plane
          effect: NoSchedule
        - key: node-role.kubernetes.io/master
          effect: NoSchedule

        containers:
        - name: fluentd
          image: fluent/fluentd-kubernetes-daemonset:v1.16-debian-elasticsearch8-1
          env:
          - name: K8S_NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName
          - name: FLUENT_ELASTICSEARCH_HOST
            value: "elasticsearch-service"
          - name: FLUENT_ELASTICSEARCH_PORT
            value: "9200"
          - name: FLUENT_ELASTICSEARCH_SCHEME
            value: "http"

          resources:
            requests:
              cpu: "100m"
              memory: "200Mi"
            limits:
              cpu: "200m"
              memory: "500Mi"

          # Accès aux logs des Pods sur ce node:
          volumeMounts:
          - name: varlog
            mountPath: /var/log
            readOnly: true
          - name: dockercontainerlogpath
            mountPath: /var/lib/docker/containers
            readOnly: true
          - name: fluentd-config
            mountPath: /fluentd/etc
            readOnly: true

          securityContext:
            privileged: false       # Fluentd n'a pas besoin d'être root
            readOnlyRootFilesystem: false
            # Note: certains DaemonSets de monitoring ont besoin de plus de privil.

        # Volumes host (accès direct au système de fichiers du node):
        volumes:
        - name: varlog
          hostPath:
            path: /var/log
            type: Directory
        - name: dockercontainerlogpath
          hostPath:
            path: /var/lib/docker/containers
            type: DirectoryOrCreate
        - name: fluentd-config
          configMap:
            name: fluentd-config

================================================================================
PARTIE 5 — SERVICES ET NETWORKING
================================================================================

  POURQUOI LES SERVICES?
  ───────────────────────
  Les Pods ont des IPs ÉPHÉMÈRES: quand un Pod redémarre, il change d'IP.
  Si l'API Flask accède à PostgreSQL via 10.244.0.5 et que le Pod PostgreSQL
  redémarre avec l'IP 10.244.0.8: la connexion est perdue.

  Un Service est une IP virtuelle STABLE qui pointe vers un groupe de Pods.
  L'IP du Service ne change jamais (tant que le Service existe).
  Le kube-proxy maintient les règles iptables pour router vers les Pods.

  4 TYPES DE SERVICES:
  ─────────────────────
  ClusterIP    = IP interne seulement (default). Accessible seulement depuis le cluster.
  NodePort     = Ouvre un port sur chaque node (30000-32767). Accessible de l'extérieur.
  LoadBalancer = Crée un load balancer cloud (AWS ALB, GCP LB...). IP externe.
  ExternalName = Alias DNS vers un service externe (db.equipe.com).

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.1 — ALICE CRÉE LES SERVICES
────────────────────────────────────────────────────────────────────────────────

  ── kubernetes/base/api/service.yaml ────────────────────────────────────────
  # Service ClusterIP pour l'API Flask (accès interne seulement):
  apiVersion: v1
  kind: Service
  metadata:
    name: taskmanager-api
    namespace: production
    labels:
      app: taskmanager
      component: api
    annotations:
      # Pour Prometheus: scraper les métriques de ce Service:
      prometheus.io/scrape: "true"
      prometheus.io/port: "5000"
      prometheus.io/path: "/metrics"
  spec:
    type: ClusterIP          # Accessible seulement depuis l'intérieur du cluster
    selector:
      app: taskmanager       # Sélectionne les Pods avec app=taskmanager
      component: api
    ports:
    - name: http
      port: 80               # Port du Service (ce que les autres Pods appellent)
      targetPort: 5000       # Port du conteneur (ce que Flask écoute)
      protocol: TCP
    - name: metrics
      port: 9090
      targetPort: 5000
      protocol: TCP
    sessionAffinity: None    # None = round-robin (ClientIP = sticky sessions)

  # COMMENT LE DNS FONCTIONNE-T-IL?
  # Dans le cluster, depuis n'importe quel namespace:
  # taskmanager-api.production.svc.cluster.local -> IP du Service
  # Depuis le même namespace:
  # taskmanager-api -> résout automatiquement
  # Depuis un autre namespace:
  # taskmanager-api.production -> résout automatiquement

  ---
  # Service Redis:
  apiVersion: v1
  kind: Service
  metadata:
    name: redis-service
    namespace: production
    labels:
      app: redis
  spec:
    type: ClusterIP
    selector:
      app: redis
    ports:
    - name: redis
      port: 6379
      targetPort: 6379

  ── kubernetes/base/api/service-nodeport.yaml ───────────────────────────────
  # NodePort pour accéder depuis l'extérieur du cluster (dev, tests):
  apiVersion: v1
  kind: Service
  metadata:
    name: taskmanager-api-nodeport
    namespace: production
    labels:
      app: taskmanager
  spec:
    type: NodePort
    selector:
      app: taskmanager
      component: api
    ports:
    - name: http
      port: 80
      targetPort: 5000
      nodePort: 30080        # Port sur chaque node (30000-32767)
      # Accessible via: http://[NODE-IP]:30080
      # Sur minikube: http://$(minikube ip):30080

  # POURQUOI NODEPORT EN DEV?
  # Pas d'Ingress ni de LoadBalancer nécessaire pour les tests.
  # Accéder directement via l'IP du node.
  # Simple mais pas adapté à la production (pas de HTTPS, port non-standard).

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.2 — ALICE CONFIGURE L'INGRESS (NGINX)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI L'INGRESS?
  ────────────────────
  LoadBalancer: crée 1 IP externe par Service. En prod avec 10 services = 10 IPs.
  Coûteux (chaque LoadBalancer cloud coûte de l'argent) et difficile à gérer.

  Ingress: 1 seul LoadBalancer, routing basé sur le path ou le hostname.
  - api.equipe.com -> Service taskmanager-api
  - api.equipe.com/docs -> Service swagger-ui
  - monitoring.equipe.com -> Service grafana

  Ingress = reverse proxy géré par Kubernetes.
  L'Ingress Controller (NGINX, Traefik, HAProxy...) lit les règles Ingress
  et configure le proxy en conséquence.

  ACTIVER L'INGRESS CONTROLLER SUR MINIKUBE:
  ──────────────────────────────────────────
  minikube addons enable ingress
  kubectl get pods -n ingress-nginx

  ── kubernetes/base/ingress/ingress.yaml ────────────────────────────────────
  apiVersion: networking.k8s.io/v1
  kind: Ingress
  metadata:
    name: taskmanager-ingress
    namespace: production
    labels:
      app: taskmanager
    annotations:
      kubernetes.io/ingress.class: nginx

      # Rediriger HTTP -> HTTPS automatiquement:
      nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
      nginx.ingress.kubernetes.io/ssl-redirect: "true"

      # Rate limiting: max 100 requêtes par minute par IP:
      nginx.ingress.kubernetes.io/limit-rps: "100"
      nginx.ingress.kubernetes.io/limit-connections: "20"

      # Timeout:
      nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
      nginx.ingress.kubernetes.io/proxy-send-timeout: "120"
      nginx.ingress.kubernetes.io/proxy-connect-timeout: "30"

      # Taille max du body (pour upload de fichiers):
      nginx.ingress.kubernetes.io/proxy-body-size: "10m"

      # CORS (si frontend séparé):
      nginx.ingress.kubernetes.io/enable-cors: "true"
      nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.equipe.com"
      nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, OPTIONS"
      nginx.ingress.kubernetes.io/cors-allow-headers: "Authorization, Content-Type"

      # Cert-manager pour HTTPS automatique (Let's Encrypt):
      cert-manager.io/cluster-issuer: "letsencrypt-prod"

  spec:
    # TLS: HTTPS avec certificat Let's Encrypt:
    tls:
    - hosts:
      - api.taskmanager.equipe.com
      secretName: taskmanager-tls   # Secret créé automatiquement par cert-manager
      # Le Secret contient tls.crt et tls.key

    rules:
    # Règle pour l'API:
    - host: api.taskmanager.equipe.com
      http:
        paths:
        - path: /api/v1
          pathType: Prefix          # Prefix = /api/v1/* correspond
          backend:
            service:
              name: taskmanager-api
              port:
                name: http

        - path: /health
          pathType: Exact           # Exact = exactement /health
          backend:
            service:
              name: taskmanager-api
              port:
                name: http

        - path: /metrics
          pathType: Exact
          backend:
            service:
              name: taskmanager-api
              port:
                name: http

    # Règle pour monitoring (sous-domaine différent):
    - host: monitoring.taskmanager.equipe.com
      http:
        paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: grafana
              port:
                number: 3000

  TESTER L'INGRESS SUR MINIKUBE:
  ────────────────────────────────
  # Obtenir l'IP de l'Ingress:
  kubectl get ingress -n production

  CE QUE VOUS VOYEZ:
  NAME                    CLASS   HOSTS                        ADDRESS         PORTS     AGE
  taskmanager-ingress     nginx   api.taskmanager.equipe.com   192.168.49.2    80, 443   5m

  # Ajouter une entrée dans /etc/hosts (sur votre machine):
  echo "192.168.49.2 api.taskmanager.equipe.com" | sudo tee -a /etc/hosts

  # Tester:
  curl http://api.taskmanager.equipe.com/health
  # -> {"status": "healthy", "version": "1.0.0"}

================================================================================
PARTIE 6 — STOCKAGE: PERSISTENTVOLUME, PVC, STORAGECLASS
================================================================================

  POURQUOI LE STOCKAGE PERSISTANT?
  ──────────────────────────────────
  Les conteneurs sont ÉPHÉMÈRES: toutes les données sur le disque local
  disparaissent quand le conteneur est supprimé.
  Pour PostgreSQL: les données DOIVENT survivre au redémarrage du Pod.

  Les 3 niveaux de stockage Kubernetes:
  1. PersistentVolume (PV)     = le stockage physique (SSD, NFS, EBS...)
  2. PersistentVolumeClaim (PVC) = la demande de stockage par un Pod
  3. StorageClass              = le type de stockage disponible

  ANALOGIE:
  ─────────
  StorageClass = le catalogue de votre fournisseur d'hébergement
  PVC          = votre commande ("Je veux 20Go de SSD")
  PV           = le serveur effectivement alloué

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 6.1 — ALICE CONFIGURE LE STOCKAGE
────────────────────────────────────────────────────────────────────────────────

  ── kubernetes/base/storage/storageclass.yaml ───────────────────────────────
  # StorageClass pour du stockage SSD rapide:
  apiVersion: storage.k8s.io/v1
  kind: StorageClass
  metadata:
    name: fast-ssd
    annotations:
      storageclass.kubernetes.io/is-default-class: "false"
  provisioner: docker.io/hostpath    # Sur minikube
  # Sur GKE: kubernetes.io/gce-pd
  # Sur AWS: kubernetes.io/aws-ebs
  # Sur Azure: kubernetes.io/azure-disk
  parameters:
    type: pd-ssd                     # Type de disque (GCP)
  reclaimPolicy: Retain              # Garder le PV quand le PVC est supprimé
  # Delete = supprimer le PV quand le PVC est supprimé (danger!)
  # Retain = garder les données (récupération manuelle si besoin)
  volumeBindingMode: WaitForFirstConsumer
  # WaitForFirstConsumer = créer le PV seulement quand un Pod le demande
  # (sur le même node que le Pod, pour la performance)
  allowVolumeExpansion: true         # Permettre d'augmenter la taille plus tard

  ---
  # StorageClass standard (HDD, moins cher):
  apiVersion: storage.k8s.io/v1
  kind: StorageClass
  metadata:
    name: standard
    annotations:
      storageclass.kubernetes.io/is-default-class: "true"  # Classe par défaut
  provisioner: docker.io/hostpath
  parameters:
    type: pd-standard
  reclaimPolicy: Delete
  volumeBindingMode: Immediate
  allowVolumeExpansion: true

  ── kubernetes/base/storage/pvc-postgres.yaml ───────────────────────────────
  # PersistentVolumeClaim pour les sauvegardes PostgreSQL:
  # (Le PVC du StatefulSet est défini dans volumeClaimTemplates)
  apiVersion: v1
  kind: PersistentVolumeClaim
  metadata:
    name: postgres-backup-pvc
    namespace: production
    labels:
      app: postgres
      component: backup
  spec:
    accessModes:
    - ReadWriteOnce              # Un seul node à la fois
    # ReadWriteMany  = plusieurs nodes simultanément (NFS, GlusterFS)
    # ReadOnlyMany   = plusieurs nodes en lecture seule
    storageClassName: fast-ssd   # Utiliser le stockage SSD
    resources:
      requests:
        storage: 50Gi            # 50 Go pour les sauvegardes

  # Voir les PVC et leur état:
  kubectl get pvc -n production

  CE QUE VOUS VOYEZ:
  ─────────────────
  NAME                      STATUS   VOLUME                              CAPACITY
  postgres-data-postgres-0  Bound    pvc-abc123-xyz456-...               20Gi
  postgres-backup-pvc       Bound    pvc-def456-uvw789-...               50Gi

  # "Bound" = le PVC a trouvé un PV compatible et est lié.
  # "Pending" = pas encore de PV disponible.

  # Voir les PV créés automatiquement:
  kubectl get pv

  CE QUE VOUS VOYEZ:
  ─────────────────
  NAME                             CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS
  pvc-abc123-xyz456-...            20Gi       RWO            Delete           Bound
  pvc-def456-uvw789-...            50Gi       RWO            Retain           Bound

================================================================================
PARTIE 7 — NAMESPACES ET ISOLATION
================================================================================

  POURQUOI LES NAMESPACES?
  ──────────────────────────
  Sans namespaces: toutes les ressources de tous les environnements
  (dev, staging, prod) seraient dans le même espace -> chaos total.

  Avec namespaces:
  -> Isolation logique: dev ne voit pas prod
  -> Quotas de ressources par environnement
  -> Politiques réseau par namespace
  -> RBAC par namespace (Bob ne peut modifier que dev)
  -> Un même nom peut exister dans 2 namespaces (postgres dans dev ET prod)

  NAMESPACES SYSTÈME (créés par K8s, ne pas modifier):
  ──────────────────────────────────────────────────────
  default          -> Namespace par défaut (éviter de l'utiliser en prod)
  kube-system      -> Composants système K8s (kube-dns, metrics-server...)
  kube-public      -> Données accessibles publiquement (ConfigMap cluster-info)
  kube-node-lease  -> Objets Lease pour la détection de pannes de nodes

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 7.1 — ALICE CRÉE LES NAMESPACES
────────────────────────────────────────────────────────────────────────────────

  ── kubernetes/base/namespaces.yaml ─────────────────────────────────────────
  apiVersion: v1
  kind: Namespace
  metadata:
    name: development
    labels:
      env: development
      team: backend
    annotations:
      contact: "alice@equipe.com"
      description: "Environnement de développement - données non-critiques"

  ---
  apiVersion: v1
  kind: Namespace
  metadata:
    name: staging
    labels:
      env: staging
      team: backend
    annotations:
      contact: "alice@equipe.com"
      description: "Environnement de staging - préproduction"

  ---
  apiVersion: v1
  kind: Namespace
  metadata:
    name: production
    labels:
      env: production
      team: backend
    annotations:
      contact: "alice@equipe.com"
      description: "Environnement de production - données critiques"

  ---
  apiVersion: v1
  kind: Namespace
  metadata:
    name: monitoring
    labels:
      team: platform
    annotations:
      description: "Prometheus, Grafana, Alertmanager"

  ---
  apiVersion: v1
  kind: Namespace
  metadata:
    name: logging
    labels:
      team: platform
    annotations:
      description: "Elasticsearch, Fluentd, Kibana"

  CRÉER LES NAMESPACES:
  ──────────────────────
  kubectl apply -f kubernetes/base/namespaces.yaml

  # Voir tous les namespaces:
  kubectl get namespaces

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME              STATUS   AGE
  default           Active   1d
  development       Active   5s
  kube-node-lease   Active   1d
  kube-public       Active   1d
  kube-system       Active   1d
  logging           Active   5s
  monitoring        Active   5s
  production        Active   5s
  staging           Active   5s

  CONFIGURER DES LIMITES DE RESSOURCES PAR NAMESPACE:
  ─────────────────────────────────────────────────────
  # ResourceQuota: limite totale du namespace
  # LimitRange: valeurs par défaut et min/max par Pod

  ── kubernetes/base/resource-quotas.yaml ────────────────────────────────────
  apiVersion: v1
  kind: ResourceQuota
  metadata:
    name: development-quota
    namespace: development
  spec:
    hard:
      # Limites de compute:
      requests.cpu: "4"             # Total CPU demandé par tous les Pods: max 4
      requests.memory: 8Gi          # Total mémoire demandée: max 8Gi
      limits.cpu: "8"               # Total CPU limité: max 8
      limits.memory: 16Gi

      # Limites d'objets:
      pods: "20"                    # Max 20 Pods dans ce namespace
      services: "10"
      persistentvolumeclaims: "5"
      secrets: "20"
      configmaps: "20"

      # Limites de stockage:
      requests.storage: 100Gi       # Total stockage demandé: max 100Gi

  ---
  apiVersion: v1
  kind: LimitRange
  metadata:
    name: development-limits
    namespace: development
  spec:
    limits:
    # Valeurs par défaut si le Pod ne les spécifie pas:
    - type: Container
      default:                      # limits par défaut
        cpu: "500m"
        memory: "512Mi"
      defaultRequest:               # requests par défaut
        cpu: "100m"
        memory: "128Mi"
      max:                          # maximum autorisé
        cpu: "2"
        memory: "4Gi"
      min:                          # minimum requis
        cpu: "50m"
        memory: "64Mi"

    - type: Pod
      max:
        cpu: "4"
        memory: "8Gi"

    - type: PersistentVolumeClaim
      max:
        storage: 20Gi
      min:
        storage: 1Gi

================================================================================
PARTIE 8 — RBAC: RÔLES, BINDINGS, SERVICEACCOUNTS
================================================================================

  POURQUOI RBAC?
  ───────────────
  Sans RBAC: tous les utilisateurs et Pods ont accès à TOUT dans le cluster.
  Bob pourrait accidentellement supprimer les Pods de production.
  Un Pod compromis pourrait lire tous les Secrets du cluster.

  RBAC = Role-Based Access Control.
  Définit QUI peut faire QUOI sur QUELLES ressources.

  LES 4 RESSOURCES RBAC:
  ────────────────────────
  Role            = Permissions dans UN namespace spécifique
  ClusterRole     = Permissions dans TOUT le cluster (ou ressources non-namespaced)
  RoleBinding     = Attribue un Role (ou ClusterRole) à une entité dans UN namespace
  ClusterRoleBinding = Attribue un ClusterRole dans TOUT le cluster

  ENTITÉS QUI PEUVENT AVOIR DES PERMISSIONS:
  ────────────────────────────────────────────
  User          = Utilisateur humain (alice, bob, claire)
  Group         = Groupe d'utilisateurs (developers, ops)
  ServiceAccount = Identité d'un Pod (pour accéder à l'API K8s)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 8.1 — ALICE CONFIGURE LES RÔLES
────────────────────────────────────────────────────────────────────────────────

  ── kubernetes/base/rbac/serviceaccounts.yaml ───────────────────────────────
  # ServiceAccount pour l'API Flask:
  apiVersion: v1
  kind: ServiceAccount
  metadata:
    name: taskmanager-sa
    namespace: production
    labels:
      app: taskmanager
    annotations:
      description: "ServiceAccount pour l'API TaskManager"
  automountServiceAccountToken: false
  # false = Ne pas monter automatiquement le token dans le Pod.
  # Par défaut: true, ce qui expose le token à n'importe quelle app dans le Pod.
  # Mettre false sauf si le Pod a besoin d'appeler l'API K8s.

  ---
  # ServiceAccount pour les Jobs de backup PostgreSQL:
  apiVersion: v1
  kind: ServiceAccount
  metadata:
    name: postgres-backup-sa
    namespace: production

  ---
  # ServiceAccount pour Fluentd (besoin d'accès aux Pods pour collecter les logs):
  apiVersion: v1
  kind: ServiceAccount
  metadata:
    name: fluentd-sa
    namespace: logging

  ── kubernetes/base/rbac/roles.yaml ─────────────────────────────────────────
  # Role pour les développeurs dans le namespace development:
  apiVersion: rbac.authorization.k8s.io/v1
  kind: Role
  metadata:
    name: developer-role
    namespace: development
  rules:
  # Permissions sur les ressources de base:
  - apiGroups: [""]                  # "" = core API group
    resources: ["pods", "pods/log", "pods/exec", "services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  # Interdire: les développeurs ne peuvent PAS supprimer des Deployments:
  # (Ne pas lister "delete" dans verbs)
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get", "list"]           # Lecture seulement pour les Secrets

  ---
  # Role en lecture seule pour le namespace staging:
  apiVersion: rbac.authorization.k8s.io/v1
  kind: Role
  metadata:
    name: readonly-role
    namespace: staging
  rules:
  - apiGroups: ["", "apps", "networking.k8s.io"]
    resources: ["pods", "deployments", "services", "ingresses",
                "configmaps", "replicasets", "statefulsets"]
    verbs: ["get", "list", "watch"]
  # Pas de "create", "update", "patch", "delete" = lecture seule

  ---
  # ClusterRole pour Fluentd (besoin d'accès cluster-wide aux Pods):
  apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRole
  metadata:
    name: fluentd-clusterrole
  rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log", "namespaces", "nodes"]
    verbs: ["get", "list", "watch"]

  ---
  # ClusterRole pour le monitoring Prometheus:
  apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRole
  metadata:
    name: prometheus-clusterrole
  rules:
  - apiGroups: [""]
    resources: ["nodes", "nodes/proxy", "services", "endpoints",
                "pods", "configmaps"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["extensions", "networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["get", "list", "watch"]
  - nonResourceURLs: ["/metrics"]    # Accès aux endpoints /metrics non-namespaced
    verbs: ["get"]

  ── kubernetes/base/rbac/bindings.yaml ──────────────────────────────────────
  # RoleBinding: Bob peut développer dans le namespace development:
  apiVersion: rbac.authorization.k8s.io/v1
  kind: RoleBinding
  metadata:
    name: bob-developer-binding
    namespace: development
  subjects:
  - kind: User
    name: bob                        # Nom de l'utilisateur dans le kubeconfig
    apiGroup: rbac.authorization.k8s.io
  roleRef:
    kind: Role
    name: developer-role
    apiGroup: rbac.authorization.k8s.io

  ---
  # RoleBinding: Claire peut voir le staging:
  apiVersion: rbac.authorization.k8s.io/v1
  kind: RoleBinding
  metadata:
    name: claire-readonly-staging-binding
    namespace: staging
  subjects:
  - kind: User
    name: claire
    apiGroup: rbac.authorization.k8s.io
  roleRef:
    kind: Role
    name: readonly-role
    apiGroup: rbac.authorization.k8s.io

  ---
  # ClusterRoleBinding pour Fluentd:
  apiVersion: rbac.authorization.k8s.io/v1
  kind: ClusterRoleBinding
  metadata:
    name: fluentd-clusterrolebinding
  subjects:
  - kind: ServiceAccount
    name: fluentd-sa
    namespace: logging
  roleRef:
    kind: ClusterRole
    name: fluentd-clusterrole
    apiGroup: rbac.authorization.k8s.io

  VÉRIFIER LES PERMISSIONS:
  ──────────────────────────
  # Est-ce que Bob peut supprimer un Pod en production?
  kubectl auth can-i delete pods --namespace=production --as=bob
  # -> no

  # Est-ce que Bob peut créer un Deployment en development?
  kubectl auth can-i create deployments --namespace=development --as=bob
  # -> yes

  # Lister toutes les permissions d'une entité:
  kubectl auth can-i --list --namespace=development --as=bob

================================================================================
PARTIE 9 — AUTOSCALING: HPA, VPA, KEDA
================================================================================

  POURQUOI L'AUTOSCALING?
  ────────────────────────
  Trafic variable = avoir toujours le bon nombre de Pods:
  - Trop peu -> l'API est surchargée, réponses lentes
  - Trop de -> gaspillage de ressources (coût cloud élevé)

  TYPES D'AUTOSCALING:
  ─────────────────────
  HPA (Horizontal Pod Autoscaler) = Ajouter/supprimer des PODS
  VPA (Vertical Pod Autoscaler)   = Ajuster CPU/RAM d'UN Pod
  KEDA (Kubernetes Event-driven Autoscaling) = Scaler selon des événements (Kafka, SQS...)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 9.1 — ALICE CRÉE LE HPA POUR L'API FLASK
────────────────────────────────────────────────────────────────────────────────

  PRÉREQUIS: metrics-server doit être installé:
  minikube addons enable metrics-server

  ── kubernetes/base/api/hpa.yaml ────────────────────────────────────────────
  apiVersion: autoscaling/v2              # v2 supporte les métriques custom
  kind: HorizontalPodAutoscaler
  metadata:
    name: taskmanager-api-hpa
    namespace: production
    labels:
      app: taskmanager
  spec:
    # ── Cible du HPA ──────────────────────────────────────────────────────────
    scaleTargetRef:
      apiVersion: apps/v1
      kind: Deployment
      name: taskmanager-api

    # ── Limites de scaling ─────────────────────────────────────────────────────
    minReplicas: 3              # Minimum de Pods (HA: pas en dessous de 3)
    maxReplicas: 20             # Maximum de Pods (pour contrôler les coûts)

    # ── Métriques pour déclencher le scaling ──────────────────────────────────
    metrics:
    # Scaler selon l'utilisation CPU:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70     # Si CPU > 70%: scaler UP
          # Quand CPU < 70%: scaler DOWN (après cooldown)

    # Scaler selon l'utilisation mémoire:
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80     # Si RAM > 80%: scaler UP

    # Scaler selon des métriques Prometheus custom (requests/s):
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second   # Nom de la métrique custom
        target:
          type: AverageValue
          averageValue: 100           # Max 100 req/s par Pod

    # ── Comportement de scaling ───────────────────────────────────────────────
    behavior:
      # Scaler UP: agressif (ajouter des capacités vite):
      scaleUp:
        stabilizationWindowSeconds: 60     # Attendre 60s avant de re-scaler
        policies:
        - type: Percent
          value: 100                        # Doubler le nombre de Pods max
          periodSeconds: 60
        - type: Pods
          value: 4                          # Ajouter max 4 Pods à la fois
          periodSeconds: 60
        selectPolicy: Max                   # Utiliser la politique la plus agressive

      # Scaler DOWN: conservateur (ne pas supprimer trop vite):
      scaleDown:
        stabilizationWindowSeconds: 300     # Attendre 5 minutes avant de réduire
        # Évite le "flapping": le cluster oscille entre 5 et 3 Pods.
        policies:
        - type: Percent
          value: 50                          # Réduire de max 50% à la fois
          periodSeconds: 60
        - type: Pods
          value: 2                           # Supprimer max 2 Pods à la fois
          periodSeconds: 60
        selectPolicy: Min                    # Utiliser la politique la plus conservative

  SURVEILLER LE HPA:
  ───────────────────
  kubectl get hpa -n production -w

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME                    REFERENCE                     TARGETS         MINPODS   MAXPODS   REPLICAS
  taskmanager-api-hpa     Deployment/taskmanager-api    45%/70%         3         20        3

  # "45%/70%" = CPU actuel 45%, seuil 70%.
  # Le HPA garde 3 Pods tant que CPU < 70%.

  # Simuler un pic de trafic (test de charge):
  kubectl run -it --rm load-test \
    --image=busybox \
    --restart=Never \
    -- sh -c "while true; do wget -q -O- http://taskmanager-api/health; done"

  # Observer le scaling en temps réel:
  watch kubectl get hpa,pods -n production

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 9.2 — ALICE CONFIGURE LE VPA
────────────────────────────────────────────────────────────────────────────────

  POURQUOI VPA?
  ──────────────
  Les requests/limits que vous définissez sont souvent des estimations.
  VPA observe la vraie consommation et recommande les bonnes valeurs.
  Il peut aussi les appliquer automatiquement (avec redémarrage des Pods).

  INSTALLATION DU VPA:
  ─────────────────────
  git clone https://github.com/kubernetes/autoscaler.git
  cd autoscaler/vertical-pod-autoscaler
  ./hack/vpa-install.sh

  ── kubernetes/base/api/vpa.yaml ────────────────────────────────────────────
  apiVersion: autoscaling.k8s.io/v1
  kind: VerticalPodAutoscaler
  metadata:
    name: taskmanager-api-vpa
    namespace: production
  spec:
    targetRef:
      apiVersion: apps/v1
      kind: Deployment
      name: taskmanager-api

    updatePolicy:
      updateMode: "Off"
      # "Off"    = seulement recommander, ne pas changer (mode apprentissage)
      # "Auto"   = appliquer automatiquement (nécessite redémarrage des Pods)
      # "Initial"= appliquer seulement à la création des Pods

    resourcePolicy:
      containerPolicies:
      - containerName: api
        minAllowed:
          cpu: "100m"
          memory: "128Mi"
        maxAllowed:
          cpu: "4"
          memory: "4Gi"
        controlledResources: ["cpu", "memory"]

  # Voir les recommandations du VPA:
  kubectl describe vpa taskmanager-api-vpa -n production

  # Vous verrez:
  #   Recommendation:
  #     Container Recommendations:
  #       Container Name: api
  #       Lower Bound:    cpu: 180m, memory: 218Mi
  #       Target:         cpu: 250m, memory: 350Mi
  #       Upper Bound:    cpu: 500m, memory: 700Mi
  # -> "Target" = valeurs recommandées par VPA basées sur l'historique réel.

================================================================================
PARTIE 10 — JOBS ET CRONJOBS
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 10.1 — BOB CRÉE UN JOB DE MIGRATION DE BASE DE DONNÉES
────────────────────────────────────────────────────────────────────────────────

  POURQUOI UN JOB ET PAS UN DEPLOYMENT?
  ──────────────────────────────────────
  Un Deployment tourne EN PERMANENCE.
  Un Job tourne JUSQU'À COMPLÉTION puis s'arrête.
  Une migration de BDD = tâche unique qui doit réussir une fois.

  ── kubernetes/base/api/job-migration.yaml ──────────────────────────────────
  apiVersion: batch/v1
  kind: Job
  metadata:
    name: taskmanager-db-migration-v1-1-0
    namespace: production
    labels:
      app: taskmanager
      component: migration
      version: "1.1.0"
    annotations:
      description: "Migration BDD pour la v1.1.0 — ajout colonne status"
  spec:
    # ── Configuration du Job ──────────────────────────────────────────────────
    completions: 1              # Le Job est terminé quand 1 Pod réussit
    parallelism: 1              # Exécuter 1 Pod à la fois (séquentiel)
    backoffLimit: 3             # Réessayer 3 fois en cas d'échec
    # POURQUOI backoffLimit: 3?
    # Les migrations peuvent échouer si la BDD est momentanément indisponible.
    # 3 tentatives = tolérance aux pannes transitoires.

    activeDeadlineSeconds: 600  # Timeout: si pas terminé en 10 min -> échec

    # Que faire des Pods terminés:
    ttlSecondsAfterFinished: 3600   # Supprimer le Pod 1h après la fin du Job
    # Évite l'accumulation de Pods "Completed" dans le namespace.

    template:
      metadata:
        labels:
          app: taskmanager
          component: migration
      spec:
        restartPolicy: OnFailure    # Redémarrer seulement en cas d'échec
        # Never     = ne jamais redémarrer (utiliser backoffLimit pour les retry)
        # OnFailure = redémarrer le conteneur (PAS recréer le Pod) si exit != 0

        containers:
        - name: migration
          image: ghcr.io/equipe/taskmanager-api:1.1.0
          command:
          - python
          - -c
          - |
            import sys
            from app import create_app, db
            from sqlalchemy import text

            app = create_app('production')
            with app.app_context():
                print("Starting database migration...")

                # Vérifier si la migration a déjà été exécutée:
                try:
                    result = db.session.execute(
                        text("SELECT column_name FROM information_schema.columns "
                             "WHERE table_name='tasks' AND column_name='status'")
                    ).fetchone()
                    if result:
                        print("Migration already applied. Exiting.")
                        sys.exit(0)
                except Exception as e:
                    print(f"Check failed: {e}")

                # Appliquer la migration:
                try:
                    db.session.execute(
                        text("ALTER TABLE tasks ADD COLUMN IF NOT EXISTS "
                             "status VARCHAR(20) DEFAULT 'todo' NOT NULL")
                    )
                    db.session.execute(
                        text("CREATE INDEX IF NOT EXISTS idx_tasks_status "
                             "ON tasks(status)")
                    )
                    db.session.commit()
                    print("Migration v1.1.0 completed successfully!")
                except Exception as e:
                    db.session.rollback()
                    print(f"Migration failed: {e}")
                    sys.exit(1)

          env:
          - name: DATABASE_URL
            valueFrom:
              secretKeyRef:
                name: taskmanager-secrets
                key: database-url

          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"

  BOB LANCE LE JOB:
  ──────────────────
  kubectl apply -f kubernetes/base/api/job-migration.yaml

  # Suivre le Job:
  kubectl get job taskmanager-db-migration-v1-1-0 -n production -w

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME                                 COMPLETIONS   DURATION   AGE
  taskmanager-db-migration-v1-1-0      0/1           2s         2s
  taskmanager-db-migration-v1-1-0      1/1           8s         10s
  # "1/1 COMPLETIONS" = le Job a réussi [OK]

  # Voir les logs du Job:
  kubectl logs job/taskmanager-db-migration-v1-1-0 -n production

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 10.2 — ALICE CRÉE UN CRONJOB DE BACKUP POSTGRESQL
────────────────────────────────────────────────────────────────────────────────

  ── kubernetes/base/postgres/cronjob-backup.yaml ────────────────────────────
  apiVersion: batch/v1
  kind: CronJob
  metadata:
    name: postgres-backup
    namespace: production
    labels:
      app: postgres
      component: backup
  spec:
    # Syntaxe cron: minute heure jour-du-mois mois jour-de-semaine
    # Ici: tous les jours à 2h du matin UTC
    schedule: "0 2 * * *"

    # Conserver les 3 derniers Jobs réussis:
    successfulJobsHistoryLimit: 3

    # Conserver seulement 1 Job échoué (pour déboguer):
    failedJobsHistoryLimit: 1

    # Si le Job précédent est encore en cours quand le suivant doit démarrer:
    concurrencyPolicy: Forbid
    # Allow   = autoriser les exécutions parallèles (dangereux pour backup!)
    # Forbid  = ignorer le nouveau si l'ancien tourne encore
    # Replace = supprimer l'ancien et démarrer le nouveau

    # Ne pas lancer les Jobs manqués depuis plus de 100 secondes:
    startingDeadlineSeconds: 100

    jobTemplate:
      spec:
        backoffLimit: 2
        ttlSecondsAfterFinished: 86400   # Garder les logs 24h
        template:
          metadata:
            labels:
              app: postgres
              component: backup
          spec:
            restartPolicy: OnFailure
            serviceAccountName: postgres-backup-sa

            containers:
            - name: pg-backup
              image: postgres:15-alpine
              command:
              - sh
              - -c
              - |
                set -e
                TIMESTAMP=$(date +%Y%m%d_%H%M%S)
                BACKUP_FILE="/backups/taskmanager_${TIMESTAMP}.sql"

                echo "Starting backup at ${TIMESTAMP}..."

                # Faire le backup:
                pg_dump \
                  --host=$POSTGRES_HOST \
                  --port=5432 \
                  --username=$POSTGRES_USER \
                  --dbname=$POSTGRES_DB \
                  --format=custom \
                  --compress=9 \
                  --file=$BACKUP_FILE

                echo "Backup completed: ${BACKUP_FILE}"
                ls -lh ${BACKUP_FILE}

                # Nettoyer les vieux backups (garder 7 jours):
                find /backups -name "*.sql" -mtime +7 -delete
                echo "Old backups cleaned."

              env:
              - name: POSTGRES_HOST
                value: "postgres-service"
              - name: POSTGRES_USER
                valueFrom:
                  configMapKeyRef:
                    name: postgres-config
                    key: postgres-user
              - name: POSTGRES_DB
                valueFrom:
                  configMapKeyRef:
                    name: postgres-config
                    key: postgres-db
              - name: PGPASSWORD           # pg_dump lit cette variable d'env
                valueFrom:
                  secretKeyRef:
                    name: postgres-secrets
                    key: postgres-password

              resources:
                requests:
                  cpu: "100m"
                  memory: "256Mi"
                limits:
                  cpu: "500m"
                  memory: "512Mi"

              volumeMounts:
              - name: backup-storage
                mountPath: /backups

            volumes:
            - name: backup-storage
              persistentVolumeClaim:
                claimName: postgres-backup-pvc

  # Voir les CronJobs:
  kubectl get cronjob -n production

  # Déclencher manuellement le backup (sans attendre l'heure planifiée):
  kubectl create job postgres-backup-manual \
    --from=cronjob/postgres-backup \
    -n production

  # Voir les jobs créés par le CronJob:
  kubectl get jobs -n production -l app=postgres

================================================================================
PARTIE 11 — NETWORKPOLICIES: SÉCURITÉ RÉSEAU
================================================================================

  POURQUOI LES NETWORKPOLICIES?
  ──────────────────────────────
  Par défaut dans Kubernetes: TOUT pod peut parler à TOUT pod.
  Un attaquant qui compromet un Pod frontend pourrait directement
  accéder à la base de données.

  NetworkPolicy = pare-feu entre les Pods.
  Principe du "least privilege": chaque Pod ne communique qu'avec
  ceux avec qui il a besoin de communiquer.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 11.1 — CLAIRE CONFIGURE LES NETWORKPOLICIES
────────────────────────────────────────────────────────────────────────────────

  ── kubernetes/base/network-policies.yaml ───────────────────────────────────
  # Règle 1: Bloquer tout le trafic par défaut dans le namespace production:
  apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    name: default-deny-all
    namespace: production
  spec:
    podSelector: {}             # s'applique à TOUS les Pods du namespace
    policyTypes:
    - Ingress                   # Bloquer tout trafic entrant
    - Egress                    # Bloquer tout trafic sortant
    # Résultat: aucun Pod ne peut communiquer avec personne.
    # On ajoute ensuite des règles permissives au cas par cas.

  ---
  # Règle 2: Autoriser l'API à recevoir du trafic de l'Ingress Controller:
  apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    name: allow-ingress-to-api
    namespace: production
  spec:
    podSelector:
      matchLabels:
        app: taskmanager
        component: api
    policyTypes:
    - Ingress
    ingress:
    - from:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: ingress-nginx
        podSelector:
          matchLabels:
            app.kubernetes.io/name: ingress-nginx
      ports:
      - protocol: TCP
        port: 5000

  ---
  # Règle 3: Autoriser l'API à accéder à PostgreSQL:
  apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    name: allow-api-to-postgres
    namespace: production
  spec:
    podSelector:
      matchLabels:
        app: postgres
    policyTypes:
    - Ingress
    ingress:
    - from:
      - podSelector:
          matchLabels:
            app: taskmanager     # Seulement l'API peut accéder à la BDD
      ports:
      - protocol: TCP
        port: 5432

  ---
  # Règle 4: Autoriser l'API à accéder à Redis:
  apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    name: allow-api-to-redis
    namespace: production
  spec:
    podSelector:
      matchLabels:
        app: redis
    policyTypes:
    - Ingress
    ingress:
    - from:
      - podSelector:
          matchLabels:
            app: taskmanager
      ports:
      - protocol: TCP
        port: 6379

  ---
  # Règle 5: Autoriser l'API à faire des requêtes DNS (sortant vers kube-dns):
  apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    name: allow-api-dns-egress
    namespace: production
  spec:
    podSelector:
      matchLabels:
        app: taskmanager
    policyTypes:
    - Egress
    egress:
    # DNS interne Kubernetes:
    - ports:
      - protocol: UDP
        port: 53
      - protocol: TCP
        port: 53
    # Accès à PostgreSQL:
    - to:
      - podSelector:
          matchLabels:
            app: postgres
      ports:
      - protocol: TCP
        port: 5432
    # Accès à Redis:
    - to:
      - podSelector:
          matchLabels:
            app: redis
      ports:
      - protocol: TCP
        port: 6379

  ---
  # Règle 6: Autoriser Prometheus à scraper les métriques de l'API:
  apiVersion: networking.k8s.io/v1
  kind: NetworkPolicy
  metadata:
    name: allow-prometheus-scrape
    namespace: production
  spec:
    podSelector:
      matchLabels:
        app: taskmanager
    policyTypes:
    - Ingress
    ingress:
    - from:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: monitoring
        podSelector:
          matchLabels:
            app: prometheus
      ports:
      - protocol: TCP
        port: 5000

================================================================================
PARTIE 12 — PODDISRUPTIONBUDGET ET AVANCÉ
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 12.1 — ALICE CONFIGURE LE PDB (PodDisruptionBudget)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI PDB?
  ──────────────
  Lors d'une maintenance (kubectl drain pour mettre à jour un node),
  K8s peut supprimer TOUS les Pods d'un node en même temps.
  Si l'API n'a que 3 Pods et qu'ils sont tous sur le même node:
  -> Pendant la maintenance: 0 Pods disponibles -> downtime complet!

  PodDisruptionBudget garantit un MINIMUM de Pods disponibles
  pendant les disruptions volontaires (maintenance, drain de node).

  ── kubernetes/base/api/pdb.yaml ────────────────────────────────────────────
  apiVersion: policy/v1
  kind: PodDisruptionBudget
  metadata:
    name: taskmanager-api-pdb
    namespace: production
    labels:
      app: taskmanager
  spec:
    # Option 1: minAvailable = minimum de Pods qui doivent rester disponibles
    minAvailable: 2             # Toujours au moins 2 Pods opérationnels
    # Option 2 (alternative): maxUnavailable = maximum de Pods indisponibles
    # maxUnavailable: 1         # Pas plus d'1 Pod hors service à la fois

    selector:
      matchLabels:
        app: taskmanager
        component: api

  # EXEMPLE CONCRET:
  # Vous avez 3 replicas. minAvailable: 2.
  # Alice drain un node pour maintenance.
  # K8s veut supprimer les Pods de ce node.
  # Si cela ferait tomber à 1 Pod disponible -> K8s ATTEND
  # jusqu'à ce que le Pod soit reschedule ailleurs avant de supprimer.

  # Voir le PDB:
  kubectl get pdb -n production

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME                    MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
  taskmanager-api-pdb     2               N/A               1                     5m
  # "ALLOWED DISRUPTIONS: 1" = 1 Pod peut être supprimé maintenant.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 12.2 — ALICE CONFIGURE AFFINITY ET ANTI-AFFINITY
────────────────────────────────────────────────────────────────────────────────

  POURQUOI AFFINITY?
  ───────────────────
  Contrôler SUR QUEL NODE un Pod est schedulé.

  TYPES D'AFFINITY:
  ──────────────────
  nodeAffinity         = Je veux être sur des nodes avec certains labels
  podAffinity          = Je veux être sur le même node qu'un autre Pod
  podAntiAffinity      = Je ne veux PAS être sur le même node qu'un autre Pod

  POURQUOI ANTI-AFFINITY POUR L'API?
  ──────────────────────────────────
  Si les 3 replicas de l'API sont tous sur le même node:
  -> Node tombe -> 0 Pods disponibles -> downtime.
  Anti-affinity force la répartition sur des nodes différents.

  DANS LE DEPLOYMENT (spec.template.spec):
  ─────────────────────────────────────────

  # Dans kubernetes/base/api/deployment.yaml, ajouter dans spec.template.spec:
  affinity:
    # Anti-affinité entre les Pods de l'API:
    podAntiAffinity:
      # "Required" = obligation (le Pod ne sera PAS schedulé si la règle ne peut pas être respectée):
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values: ["taskmanager"]
          - key: component
            operator: In
            values: ["api"]
        topologyKey: "kubernetes.io/hostname"
        # "topologyKey: hostname" = ne pas mettre 2 Pods API sur le même node.
        # "topologyKey: zone" = ne pas mettre 2 Pods dans la même zone cloud.

      # "Preferred" = préférence (le Pod SERA schedulé même si la règle ne peut pas être respectée):
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: taskmanager
          topologyKey: "topology.kubernetes.io/zone"
          # Préférer répartir sur des zones cloud différentes.

    # L'API préfère tourner sur des nodes "fast-ssd":
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 80
        preference:
          matchExpressions:
          - key: disk-type
            operator: In
            values: ["ssd"]

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 12.3 — ALICE CONFIGURE LES TAINTS ET TOLERATIONS
────────────────────────────────────────────────────────────────────────────────

  POURQUOI TAINTS ET TOLERATIONS?
  ─────────────────────────────────
  Taint = "Ce node est réservé à un type particulier de Pods."
  Toleration = "Ce Pod accepte d'être schedulé sur un node avec cette taint."

  Exemples d'usage:
  -> Réserver des nodes GPU pour les Pods ML
  -> Réserver des nodes haute-mémoire pour PostgreSQL
  -> Empêcher les workloads utilisateurs sur les nodes du Control Plane

  CRÉER UNE TAINT SUR UN NODE:
  ──────────────────────────────
  # Réserver le node "worker3" pour les bases de données:
  kubectl taint nodes worker3 dedicated=database:NoSchedule
  # "NoSchedule" = les Pods sans toleration ne seront PAS schedulés ici.
  # "PreferNoSchedule" = essaie de ne pas scheduler mais peut si nécessaire.
  # "NoExecute" = expulse les Pods existants sans toleration ET refuse les nouveaux.

  # Voir les taints d'un node:
  kubectl describe node worker3 | grep Taints

  # Supprimer une taint:
  kubectl taint nodes worker3 dedicated=database:NoSchedule-

  AJOUTER UNE TOLERATION AU STATEFULSET POSTGRES:
  ─────────────────────────────────────────────────
  # Dans kubernetes/base/postgres/statefulset.yaml, dans spec.template.spec:
  tolerations:
  - key: "dedicated"
    operator: "Equal"
    value: "database"
    effect: "NoSchedule"
  # Ce Pod peut maintenant être schedulé sur les nodes taintés "dedicated=database".

================================================================================
PARTIE 13 — HELM: GESTIONNAIRE DE PAQUETS KUBERNETES
================================================================================

  POURQUOI HELM POUR DÉPLOYER L'APPLICATION?
  ────────────────────────────────────────────
  Sans Helm: 15 fichiers YAML séparés, impossible de paramétrer pour
  dev/staging/prod sans duplication.

  Avec Helm:
  -> 1 seul "Chart" (template) paramétrable
  -> values.yaml pour dev, values-prod.yaml pour prod
  -> Versionning des releases (helm rollback en 1 commande)
  -> Dépendances gérées (PostgreSQL, Redis comme subcharts)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 13.1 — ALICE CRÉE UN CHART HELM
────────────────────────────────────────────────────────────────────────────────

  STRUCTURE D'UN CHART HELM:
  ───────────────────────────
  helm/taskmanager/
  ├── Chart.yaml              # Métadonnées du chart
  ├── values.yaml             # Valeurs par défaut
  ├── values-development.yaml # Override pour dev
  ├── values-staging.yaml     # Override pour staging
  ├── values-production.yaml  # Override pour prod
  └── templates/              # YAML templatisés
      ├── _helpers.tpl        # Fonctions helper Go templates
      ├── deployment.yaml     # Deployment templatisé
      ├── service.yaml
      ├── configmap.yaml
      ├── secret.yaml
      ├── ingress.yaml
      ├── hpa.yaml
      ├── pdb.yaml
      ├── serviceaccount.yaml
      └── NOTES.txt           # Message affiché après l'installation

  ── helm/taskmanager/Chart.yaml ─────────────────────────────────────────────
  apiVersion: v2           # Helm 3 API version
  name: taskmanager
  description: >
    TaskManager API REST — Flask + PostgreSQL + Redis
    Déploiement Kubernetes pour l'équipe backend.
  type: application
  version: 0.1.0           # Version du Chart (pas de l'application)
  appVersion: "1.0.0"      # Version de l'application

  keywords:
  - flask
  - rest-api
  - python
  - taskmanager

  maintainers:
  - name: Alice Martin
    email: alice@equipe.com

  # Dépendances (subcharts):
  dependencies:
  - name: postgresql
    version: "14.0.0"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled     # Activer/désactiver via values.yaml
  - name: redis
    version: "18.0.0"
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled

  ── helm/taskmanager/values.yaml ────────────────────────────────────────────
  # Valeurs par défaut (environnement de développement)

  # ── Image ────────────────────────────────────────────────────────────────
  image:
    repository: ghcr.io/equipe/taskmanager-api
    tag: "1.0.0"
    pullPolicy: IfNotPresent

  imagePullSecrets:
  - name: ghcr-secret

  # ── Déploiement ───────────────────────────────────────────────────────────
  replicaCount: 1

  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

  # ── Service ───────────────────────────────────────────────────────────────
  service:
    type: ClusterIP
    port: 80
    targetPort: 5000

  # ── Ingress ───────────────────────────────────────────────────────────────
  ingress:
    enabled: false           # Désactivé en développement
    className: nginx
    host: api.taskmanager.local
    tls: false
    annotations: {}

  # ── Ressources ────────────────────────────────────────────────────────────
  resources:
    requests:
      cpu: "100m"
      memory: "128Mi"
    limits:
      cpu: "500m"
      memory: "512Mi"

  # ── Autoscaling ───────────────────────────────────────────────────────────
  autoscaling:
    enabled: false
    minReplicas: 1
    maxReplicas: 5
    targetCPUUtilizationPercentage: 70

  # ── PodDisruptionBudget ───────────────────────────────────────────────────
  podDisruptionBudget:
    enabled: false

  # ── Configuration ─────────────────────────────────────────────────────────
  config:
    flaskEnv: development
    logLevel: DEBUG
    dbPoolSize: "5"
    cacheTimeout: "60"

  # ── Secrets (dans un contexte réel: utiliser External Secrets) ────────────
  secrets:
    databaseUrl: ""          # Obligatoire
    secretKey: ""            # Obligatoire
    jwtSecret: ""            # Obligatoire

  # ── Probes ────────────────────────────────────────────────────────────────
  livenessProbe:
    enabled: true
    initialDelaySeconds: 30
    periodSeconds: 10
    failureThreshold: 3

  readinessProbe:
    enabled: true
    initialDelaySeconds: 15
    periodSeconds: 5
    failureThreshold: 3

  # ── PostgreSQL (subchart bitnami) ─────────────────────────────────────────
  postgresql:
    enabled: true
    auth:
      username: taskmanager
      password: dev-password
      database: taskmanager
    primary:
      resources:
        requests:
          cpu: "100m"
          memory: "256Mi"

  # ── Redis (subchart bitnami) ──────────────────────────────────────────────
  redis:
    enabled: true
    auth:
      enabled: false          # Pas de mot de passe en dev
    master:
      resources:
        requests:
          cpu: "50m"
          memory: "64Mi"

  # ── Labels supplémentaires ────────────────────────────────────────────────
  extraLabels: {}
  extraAnnotations: {}

  # ── ServiceAccount ────────────────────────────────────────────────────────
  serviceAccount:
    create: true
    name: ""                  # Si vide: généré automatiquement
    automountToken: false

  ── helm/taskmanager/values-production.yaml ─────────────────────────────────
  # Override pour la production — remplace les valeurs de values.yaml

  replicaCount: 3             # 3 replicas en prod (vs 1 en dev)

  image:
    tag: "1.0.0"              # Version figée en prod
    pullPolicy: Always

  ingress:
    enabled: true             # Activer l'Ingress en prod
    host: api.taskmanager.equipe.com
    tls: true
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod
      nginx.ingress.kubernetes.io/force-ssl-redirect: "true"

  resources:
    requests:
      cpu: "200m"
      memory: "256Mi"
    limits:
      cpu: "1000m"
      memory: "1Gi"

  autoscaling:
    enabled: true
    minReplicas: 3
    maxReplicas: 20
    targetCPUUtilizationPercentage: 70

  podDisruptionBudget:
    enabled: true
    minAvailable: 2

  config:
    flaskEnv: production
    logLevel: INFO
    dbPoolSize: "10"
    cacheTimeout: "300"

  postgresql:
    enabled: false            # PostgreSQL géré séparément en prod (StatefulSet)

  redis:
    enabled: false            # Redis géré séparément en prod

  ── helm/taskmanager/templates/_helpers.tpl ─────────────────────────────────
  {{/*
  Fonctions helper Go templates pour le chart.
  Ces fonctions sont appelées depuis les autres templates.
  */}}

  {{/* Nom complet de l'app */}}
  {{- define "taskmanager.fullname" -}}
  {{- if .Values.fullnameOverride }}
  {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
  {{- else }}
  {{- $name := default .Chart.Name .Values.nameOverride }}
  {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
  {{- end }}
  {{- end }}

  {{/* Labels communs (pour TOUTES les ressources du chart) */}}
  {{- define "taskmanager.labels" -}}
  helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
  app.kubernetes.io/name: {{ .Chart.Name }}
  app.kubernetes.io/instance: {{ .Release.Name }}
  app.kubernetes.io/version: {{ .Values.image.tag | quote }}
  app.kubernetes.io/managed-by: {{ .Release.Service }}
  {{- end }}

  {{/* Selector labels (stables dans le temps — ne pas changer!) */}}
  {{- define "taskmanager.selectorLabels" -}}
  app.kubernetes.io/name: {{ .Chart.Name }}
  app.kubernetes.io/instance: {{ .Release.Name }}
  {{- end }}

  {{/* Nom du ServiceAccount */}}
  {{- define "taskmanager.serviceAccountName" -}}
  {{- if .Values.serviceAccount.create }}
  {{- default (include "taskmanager.fullname" .) .Values.serviceAccount.name }}
  {{- else }}
  {{- default "default" .Values.serviceAccount.name }}
  {{- end }}
  {{- end }}

  ── helm/taskmanager/templates/deployment.yaml ──────────────────────────────
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: {{ include "taskmanager.fullname" . }}
    namespace: {{ .Release.Namespace }}
    labels:
      {{- include "taskmanager.labels" . | nindent 4 }}
      {{- with .Values.extraLabels }}
      {{- toYaml . | nindent 4 }}
      {{- end }}
    annotations:
      kubernetes.io/change-cause: "Helm release {{ .Release.Name }} v{{ .Chart.AppVersion }}"
      {{- with .Values.extraAnnotations }}
      {{- toYaml . | nindent 4 }}
      {{- end }}
  spec:
    {{- if not .Values.autoscaling.enabled }}
    replicas: {{ .Values.replicaCount }}
    {{- end }}
    selector:
      matchLabels:
        {{- include "taskmanager.selectorLabels" . | nindent 6 }}
    strategy:
      {{- toYaml .Values.strategy | nindent 4 }}
    template:
      metadata:
        labels:
          {{- include "taskmanager.labels" . | nindent 8 }}
          {{- include "taskmanager.selectorLabels" . | nindent 8 }}
        annotations:
          prometheus.io/scrape: "true"
          prometheus.io/port: "5000"
          prometheus.io/path: "/metrics"
          # Force le redémarrage si le ConfigMap/Secret change:
          checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
          checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
      spec:
        {{- with .Values.imagePullSecrets }}
        imagePullSecrets:
          {{- toYaml . | nindent 8 }}
        {{- end }}
        serviceAccountName: {{ include "taskmanager.serviceAccountName" . }}
        securityContext:
          runAsNonRoot: true
          runAsUser: 1000
          fsGroup: 1000

        containers:
        - name: api
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
          - containerPort: 5000
            name: http

          env:
          - name: FLASK_ENV
            value: {{ .Values.config.flaskEnv | quote }}
          - name: APP_VERSION
            value: {{ .Values.image.tag | quote }}
          - name: LOG_LEVEL
            value: {{ .Values.config.logLevel | quote }}
          - name: DATABASE_URL
            valueFrom:
              secretKeyRef:
                name: {{ include "taskmanager.fullname" . }}-secrets
                key: database-url
          - name: SECRET_KEY
            valueFrom:
              secretKeyRef:
                name: {{ include "taskmanager.fullname" . }}-secrets
                key: secret-key

          {{- if .Values.livenessProbe.enabled }}
          livenessProbe:
            httpGet:
              path: /live
              port: 5000
            initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
            periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
            failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
          {{- end }}

          {{- if .Values.readinessProbe.enabled }}
          readinessProbe:
            httpGet:
              path: /ready
              port: 5000
            initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
            periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
            failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
          {{- end }}

          resources:
            {{- toYaml .Values.resources | nindent 12 }}

          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
              - ALL

          volumeMounts:
          - name: tmp
            mountPath: /tmp

        volumes:
        - name: tmp
          emptyDir: {}

  ALICE DÉPLOIE AVEC HELM:
  ─────────────────────────
  # Télécharger les dépendances (PostgreSQL, Redis):
  helm dependency update helm/taskmanager/

  # Prévisualiser les YAML générés (sans déployer):
  helm template taskmanager-release helm/taskmanager/ \
    --values helm/taskmanager/values.yaml \
    --namespace development

  # Valider le chart (lint):
  helm lint helm/taskmanager/

  # Déployer en développement:
  helm install taskmanager-dev helm/taskmanager/ \
    --namespace development \
    --create-namespace \
    --values helm/taskmanager/values.yaml \
    --set secrets.databaseUrl="postgresql://taskmanager:devpass@localhost:5432/taskmanager" \
    --set secrets.secretKey="$(openssl rand -hex 32)" \
    --set secrets.jwtSecret="$(openssl rand -hex 32)"

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME: taskmanager-dev
  LAST DEPLOYED: Mon Jan 10 10:00:00 2024
  NAMESPACE: development
  STATUS: deployed
  REVISION: 1
  NOTES:
    TaskManager API is deployed!
    Access the API:
      kubectl port-forward svc/taskmanager-dev-taskmanager 8080:80 -n development
      curl http://localhost:8080/health

  # Déployer en production:
  helm install taskmanager-prod helm/taskmanager/ \
    --namespace production \
    --create-namespace \
    --values helm/taskmanager/values.yaml \
    --values helm/taskmanager/values-production.yaml \
    --set "secrets.databaseUrl=${PROD_DB_URL}" \
    --set "secrets.secretKey=$(openssl rand -hex 32)" \
    --set "secrets.jwtSecret=$(openssl rand -hex 32)"

  # Voir les releases Helm:
  helm list --all-namespaces

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME              NAMESPACE    REVISION  STATUS    CHART              APP VERSION
  taskmanager-dev   development  1         deployed  taskmanager-0.1.0  1.0.0
  taskmanager-prod  production   1         deployed  taskmanager-0.1.0  1.0.0

  # Mettre à jour une release (nouvelle version de l'image):
  helm upgrade taskmanager-prod helm/taskmanager/ \
    --namespace production \
    --values helm/taskmanager/values.yaml \
    --values helm/taskmanager/values-production.yaml \
    --set image.tag=1.1.0 \
    --set "secrets.databaseUrl=${PROD_DB_URL}" \
    --atomic              # Rollback automatique si le déploiement échoue
    --timeout 5m

  # Rollback en cas de problème:
  helm rollback taskmanager-prod 1 --namespace production
  # Revient à la revision 1 (la version précédente).

  # Historique des releases:
  helm history taskmanager-prod --namespace production

  CE QUE VOUS VOYEZ:
  ───────────────────
  REVISION  STATUS     CHART              APP VERSION  DESCRIPTION
  1         superseded taskmanager-0.1.0  1.0.0        Install complete
  2         deployed   taskmanager-0.1.0  1.1.0        Upgrade complete

shooting, Cheatsheet
text

================================================================================
PARTIE 14 — KUSTOMIZE: CONFIGURATION MULTI-ENVIRONNEMENT
================================================================================

  POURQUOI KUSTOMIZE EN PLUS DE HELM?
  ─────────────────────────────────────
  Helm = templating complet avec fonctions Go, dépendances, packaging.
  Kustomize = overlays simples: partir d'une base YAML et surcharger.

  Kustomize est intégré dans kubectl: kubectl apply -k.
  Kustomize convient pour:
  -> Des YAML simples sans templating complexe
  -> Gérer dev/staging/prod sans dupliquer les fichiers
  -> Des transformations simples (patches, images, replicas)

  STRUCTURE KUSTOMIZE:
  ─────────────────────
  kubernetes/
  ├── base/                      # Configuration commune
  │   ├── kustomization.yaml     # Fichier de définition Kustomize
  │   ├── api/
  │   │   ├── deployment.yaml
  │   │   ├── service.yaml
  │   │   └── configmap.yaml
  │   └── postgres/
  │       ├── statefulset.yaml
  │       └── service.yaml
  └── overlays/
      ├── development/           # Surcharges pour dev
      │   ├── kustomization.yaml
      │   ├── patch-replicas.yaml
      │   └── patch-resources.yaml
      ├── staging/
      │   ├── kustomization.yaml
      │   └── patch-image.yaml
      └── production/
          ├── kustomization.yaml
          ├── patch-replicas.yaml
          ├── patch-resources.yaml
          └── patch-ingress.yaml

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 14.1 — ALICE CONFIGURE KUSTOMIZE
────────────────────────────────────────────────────────────────────────────────

  ── kubernetes/base/kustomization.yaml ──────────────────────────────────────
  apiVersion: kustomize.config.k8s.io/v1beta1
  kind: Kustomization

  # Namespace par défaut pour toutes les ressources:
  namespace: production

  # Labels communs ajoutés à toutes les ressources:
  commonLabels:
    app: taskmanager
    managed-by: kustomize

  # Annotations communes:
  commonAnnotations:
    team: backend
    contact: alice@equipe.com

  # Ressources à inclure:
  resources:
  - api/deployment.yaml
  - api/service.yaml
  - api/configmap.yaml
  - api/hpa.yaml
  - api/pdb.yaml
  - api/serviceaccount.yaml
  - postgres/statefulset.yaml
  - postgres/service.yaml
  - postgres/configmap.yaml
  - redis/deployment.yaml
  - redis/service.yaml
  - ingress/ingress.yaml
  - rbac/serviceaccounts.yaml
  - rbac/roles.yaml
  - rbac/bindings.yaml

  # Images: centraliser les versions d'images:
  images:
  - name: ghcr.io/equipe/taskmanager-api
    newTag: "1.0.0"
  - name: postgres
    newTag: "15-alpine"
  - name: redis
    newTag: "7-alpine"

  ── kubernetes/overlays/development/kustomization.yaml ──────────────────────
  apiVersion: kustomize.config.k8s.io/v1beta1
  kind: Kustomization

  # Hériter de la base:
  bases:
  - ../../base

  # Changer le namespace:
  namespace: development

  # Modifier l'image pour dev (tag latest ou dev):
  images:
  - name: ghcr.io/equipe/taskmanager-api
    newTag: "latest"

  # Patches strategiques (modifier des parties spécifiques d'un YAML):
  patches:
  # Réduire les replicas en dev:
  - patch: |
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: taskmanager-api
      spec:
        replicas: 1
  # Réduire les ressources en dev:
  - path: patch-resources.yaml
    target:
      kind: Deployment
      name: taskmanager-api

  # Désactiver le HPA en dev:
  - patch: |
      $patch: delete
      apiVersion: autoscaling/v2
      kind: HorizontalPodAutoscaler
      metadata:
        name: taskmanager-api-hpa

  # Désactiver le PDB en dev:
  - patch: |
      $patch: delete
      apiVersion: policy/v1
      kind: PodDisruptionBudget
      metadata:
        name: taskmanager-api-pdb

  # Préfixer les noms pour différencier de la prod:
  namePrefix: dev-

  # Ajouter des labels spécifiques à dev:
  commonLabels:
    environment: development

  ── kubernetes/overlays/development/patch-resources.yaml ────────────────────
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: taskmanager-api
  spec:
    template:
      spec:
        containers:
        - name: api
          resources:
            requests:
              cpu: "50m"          # Moins en dev
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "256Mi"

  ── kubernetes/overlays/production/kustomization.yaml ───────────────────────
  apiVersion: kustomize.config.k8s.io/v1beta1
  kind: Kustomization

  bases:
  - ../../base

  namespace: production

  images:
  - name: ghcr.io/equipe/taskmanager-api
    newTag: "1.0.0"              # Version figée en prod

  patches:
  # 3 replicas en prod:
  - patch: |
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: taskmanager-api
      spec:
        replicas: 3
  - path: patch-resources-prod.yaml
    target:
      kind: Deployment
      name: taskmanager-api

  configMapGenerator:
  - name: taskmanager-config-extra
    namespace: production
    literals:
    - log-level=INFO
    - max-connections=100

  secretGenerator:
  - name: taskmanager-secrets-extra
    namespace: production
    literals:
    - extra-key=extra-value
    type: Opaque

  commonLabels:
    environment: production

  DÉPLOYER AVEC KUSTOMIZE:
  ─────────────────────────
  # Prévisualiser ce qui sera déployé:
  kubectl kustomize kubernetes/overlays/development/

  # Déployer en développement:
  kubectl apply -k kubernetes/overlays/development/

  # Déployer en production:
  kubectl apply -k kubernetes/overlays/production/

  # Supprimer un environnement:
  kubectl delete -k kubernetes/overlays/development/

================================================================================
PARTIE 15 — MONITORING: PROMETHEUS ET GRAFANA
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 15.1 — ALICE DÉPLOIE LA STACK PROMETHEUS
────────────────────────────────────────────────────────────────────────────────

  POURQUOI PROMETHEUS ET GRAFANA?
  ─────────────────────────────────
  Sans monitoring: vous savez que votre API est lente SEULEMENT quand
  les utilisateurs se plaignent. Trop tard.

  Avec Prometheus + Grafana:
  -> Voir le nombre de requêtes/seconde en temps réel
  -> Alertes automatiques: "CPU > 90% depuis 5 minutes"
  -> Histogramme des temps de réponse (p50, p95, p99)
  -> Utilisation de la mémoire, garbage collector Python...

  INSTALLER VIA HELM (kube-prometheus-stack inclut tout):
  ─────────────────────────────────────────────────────────
  # Ce chart installe: Prometheus + Alertmanager + Grafana + Node Exporter
  # + kube-state-metrics + nombreux dashboards prêts à l'emploi.

  helm install prometheus prometheus-community/kube-prometheus-stack \
    --namespace monitoring \
    --create-namespace \
    --values monitoring/prometheus-values.yaml

  ── monitoring/prometheus/prometheus-values.yaml ────────────────────────────
  # Configuration de kube-prometheus-stack:

  # ── Grafana ────────────────────────────────────────────────────────────────
  grafana:
    enabled: true
    adminPassword: "changeme-in-prod"

    # Ingress pour accéder à Grafana:
    ingress:
      enabled: true
      annotations:
        kubernetes.io/ingress.class: nginx
      hosts:
      - monitoring.taskmanager.equipe.com
      tls:
      - hosts:
        - monitoring.taskmanager.equipe.com
        secretName: grafana-tls

    # Dashboards prédéfinis à importer:
    dashboardsConfigMaps:
      taskmanager: taskmanager-grafana-dashboard

    # Sources de données (Prometheus déjà configuré automatiquement):
    datasources:
      datasources.yaml:
        apiVersion: 1
        datasources:
        - name: Prometheus
          type: prometheus
          url: http://prometheus-kube-prometheus-prometheus:9090
          isDefault: true

    persistence:
      enabled: true
      size: 5Gi

  # ── Prometheus ─────────────────────────────────────────────────────────────
  prometheus:
    prometheusSpec:
      retention: 30d             # Garder 30 jours de métriques
      retentionSize: 10GB

      # Scraper automatiquement tous les Services avec l'annotation:
      # prometheus.io/scrape: "true"
      serviceMonitorSelectorNilUsesHelmValues: false
      podMonitorSelectorNilUsesHelmValues: false

      storageSpec:
        volumeClaimTemplate:
          spec:
            accessModes: ["ReadWriteOnce"]
            storageClassName: fast-ssd
            resources:
              requests:
                storage: 20Gi

      resources:
        requests:
          cpu: "250m"
          memory: "512Mi"
        limits:
          cpu: "1000m"
          memory: "2Gi"

  # ── Alertmanager ───────────────────────────────────────────────────────────
  alertmanager:
    enabled: true
    config:
      global:
        slack_api_url: "https://hooks.slack.com/services/VOTRE_WEBHOOK"

      route:
        group_by: ["alertname", "severity"]
        group_wait: 30s
        group_interval: 5m
        repeat_interval: 4h
        receiver: "slack-critical"
        routes:
        - match:
            severity: critical
          receiver: "slack-critical"
        - match:
            severity: warning
          receiver: "slack-warning"

      receivers:
      - name: "slack-critical"
        slack_configs:
        - channel: "#alerts-prod"
          title: "[ALERTE] ALERTE CRITIQUE: {{ .GroupLabels.alertname }}"
          text: |
            *Résumé:* {{ range .Alerts }}{{ .Annotations.summary }}{{ end }}
            *Détail:* {{ range .Alerts }}{{ .Annotations.description }}{{ end }}
          send_resolved: true
      - name: "slack-warning"
        slack_configs:
        - channel: "#alerts-dev"
          title: "[ATTENTION] Avertissement: {{ .GroupLabels.alertname }}"
          send_resolved: true

  ── monitoring/prometheus/servicemonitor.yaml ───────────────────────────────
  # ServiceMonitor: dit à Prometheus quoi scraper pour notre API
  apiVersion: monitoring.coreos.com/v1
  kind: ServiceMonitor
  metadata:
    name: taskmanager-api-monitor
    namespace: monitoring
    labels:
      app: taskmanager
      release: prometheus    # Doit correspondre au label du release Prometheus
  spec:
    namespaceSelector:
      matchNames:
      - production
    selector:
      matchLabels:
        app: taskmanager
        component: api
    endpoints:
    - port: http              # Nom du port dans le Service
      path: /metrics          # Endpoint Prometheus
      interval: 15s           # Scraper toutes les 15 secondes
      scrapeTimeout: 10s

  ── monitoring/prometheus/alertingrules.yaml ─────────────────────────────────
  # Règles d'alerte Prometheus pour l'API TaskManager
  apiVersion: monitoring.coreos.com/v1
  kind: PrometheusRule
  metadata:
    name: taskmanager-alerts
    namespace: monitoring
    labels:
      app: taskmanager
      release: prometheus
  spec:
    groups:
    - name: taskmanager.api
      interval: 30s
      rules:

      # Alerte: l'API est indisponible:
      - alert: TaskManagerApiDown
        expr: |
          up{job="taskmanager-api", namespace="production"} == 0
        for: 1m
        labels:
          severity: critical
          service: taskmanager-api
        annotations:
          summary: "TaskManager API est indisponible"
          description: |
            L'API TaskManager dans le namespace production est DOWN depuis 1 minute.
            Pod: {{ $labels.pod }}. Vérifier les logs: kubectl logs {{ $labels.pod }} -n production

      # Alerte: erreurs 5xx élevées:
      - alert: TaskManagerHighErrorRate
        expr: |
          rate(flask_http_request_total{namespace="production",status=~"5.."}[5m]) /
          rate(flask_http_request_total{namespace="production"}[5m]) > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Taux d'erreurs 5xx > 5%"
          description: "Le taux d'erreurs est {{ $value | humanizePercentage }} sur les 5 dernières minutes."

      # Alerte: latence élevée:
      - alert: TaskManagerHighLatency
        expr: |
          histogram_quantile(0.95,
            rate(flask_http_request_duration_seconds_bucket{namespace="production"}[5m])
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Latence p95 > 2 secondes"
          description: "Le p95 de latence est {{ $value }}s (seuil: 2s)."

      # Alerte: CPU élevé:
      - alert: TaskManagerHighCPU
        expr: |
          rate(container_cpu_usage_seconds_total{
            namespace="production",
            container="api"
          }[5m]) * 100 > 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CPU > 80% sur l'API TaskManager"

      # Alerte: mémoire élevée:
      - alert: TaskManagerHighMemory
        expr: |
          container_memory_usage_bytes{
            namespace="production",
            container="api"
          } / container_spec_memory_limit_bytes{
            namespace="production",
            container="api"
          } > 0.85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Mémoire > 85% de la limite"

      # Alerte: Pod redémarre souvent (crashloopbackoff imminent):
      - alert: TaskManagerPodRestarting
        expr: |
          increase(kube_pod_container_status_restarts_total{
            namespace="production",
            container="api"
          }[1h]) > 3
        labels:
          severity: warning
        annotations:
          summary: "Un Pod a redémarré plus de 3 fois en 1 heure"
          description: "Pod: {{ $labels.pod }}"

  ── monitoring/grafana/dashboard-taskmanager.json ───────────────────────────
  # Dashboard Grafana pour l'API TaskManager (JSON complet)
  # À importer dans Grafana ou via un ConfigMap Kubernetes.
  # Ce dashboard montre:
  # - Requêtes/seconde par endpoint
  # - Taux d'erreurs (2xx, 4xx, 5xx)
  # - Latence p50/p95/p99
  # - Replicas actifs
  # - CPU et mémoire par Pod
  # - Cache Redis hit rate

  # Pour créer ce fichier:
  # 1. Configurer Grafana manuellement avec les panels souhaités
  # 2. Dashboard settings -> JSON Model -> copier le JSON
  # 3. Sauvegarder dans ce fichier
  # Ce fichier peut ensuite être chargé via un ConfigMap:

  ── monitoring/grafana/configmap-dashboard.yaml ─────────────────────────────
  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: taskmanager-grafana-dashboard
    namespace: monitoring
    labels:
      grafana_dashboard: "1"    # Label détecté par Grafana pour import auto
  data:
    taskmanager.json: |
      {
        "title": "TaskManager API",
        "uid": "taskmanager-main",
        "panels": [
          {
            "title": "Requests per Second",
            "type": "graph",
            "targets": [
              {
                "expr": "rate(flask_http_request_total{namespace='production'}[1m])",
                "legendFormat": "{{method}} {{endpoint}} {{status}}"
              }
            ]
          },
          {
            "title": "Error Rate",
            "type": "stat",
            "targets": [
              {
                "expr": "rate(flask_http_request_total{namespace='production',status=~'5..'}[5m]) / rate(flask_http_request_total{namespace='production'}[5m]) * 100"
              }
            ]
          }
        ],
        "refresh": "30s",
        "time": {"from": "now-1h", "to": "now"}
      }

================================================================================
PARTIE 16 — LOGGING: STACK EFK (ELASTICSEARCH, FLUENTD, KIBANA)
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 16.1 — ALICE DÉPLOIE LA STACK EFK
────────────────────────────────────────────────────────────────────────────────

  POURQUOI EFK?
  ──────────────
  kubectl logs = voir les logs d'un Pod à la fois, perdu si le Pod est supprimé.
  EFK = centraliser TOUS les logs de TOUS les Pods dans Elasticsearch.
  Kibana = interface pour chercher, filtrer, visualiser les logs.
  Fluentd = agent DaemonSet qui collecte et envoie les logs.

  INSTALLER ELASTICSEARCH VIA HELM:
  ───────────────────────────────────
  helm install elasticsearch elastic/elasticsearch \
    --namespace logging \
    --values monitoring/elasticsearch/values.yaml

  helm install kibana elastic/kibana \
    --namespace logging \
    --values monitoring/elasticsearch/kibana-values.yaml

  ── monitoring/elasticsearch/values.yaml ────────────────────────────────────
  # Configuration Elasticsearch pour un cluster de dev (minimal):
  replicas: 1                  # 3 pour la production (HA)
  minimumMasterNodes: 1

  resources:
    requests:
      cpu: "500m"
      memory: "1Gi"
    limits:
      cpu: "2000m"
      memory: "2Gi"

  volumeClaimTemplate:
    resources:
      requests:
        storage: 20Gi
    storageClassName: standard

  esConfig:
    elasticsearch.yml: |
      cluster.name: taskmanager-logs
      network.host: 0.0.0.0
      discovery.type: single-node    # Seulement en dev (pas pour la prod HA)
      xpack.security.enabled: false   # Désactivé pour simplifier (activer en prod)

  ── monitoring/elasticsearch/kibana-values.yaml ──────────────────────────────
  elasticsearchHosts: "http://elasticsearch-master:9200"

  resources:
    requests:
      cpu: "250m"
      memory: "512Mi"
    limits:
      cpu: "1000m"
      memory: "1Gi"

  ingress:
    enabled: true
    annotations:
      kubernetes.io/ingress.class: nginx
    hosts:
    - host: logs.taskmanager.equipe.com
      paths:
      - path: /

  ── monitoring/elasticsearch/fluentd-configmap.yaml ──────────────────────────
  # Configuration Fluentd pour parser et router les logs
  apiVersion: v1
  kind: ConfigMap
  metadata:
    name: fluentd-config
    namespace: logging
  data:
    fluent.conf: |
      # ── Source: lire les logs des conteneurs Docker/containerd ──────────
      <source>
        @type tail
        path /var/log/containers/*.log
        pos_file /var/log/fluentd-containers.log.pos
        tag kubernetes.*
        read_from_head true
        <parse>
          @type multi_format
          <pattern>
            format json
            time_key time
            time_type string
            time_format "%Y-%m-%dT%H:%M:%S.%NZ"
          </pattern>
          <pattern>
            format /^(?<time>.+) (?<stream>stdout|stderr) [^ ]* (?<log>.*)$/
            time_format %Y-%m-%dT%H:%M:%S.%N%:z
          </pattern>
        </parse>
      </source>

      # ── Filter: enrichir avec les métadonnées K8s ───────────────────────
      <filter kubernetes.**>
        @type kubernetes_metadata
        @id filter_kube_metadata
        kubernetes_url "#{ENV['FLUENT_FILTER_KUBERNETES_URL']||'https://'+ENV.fetch('KUBERNETES_SERVICE_HOST')+':'+ENV.fetch('KUBERNETES_SERVICE_PORT')+'/api'}"
        verify_ssl "#{ENV['KUBERNETES_VERIFY_SSL']||'true'}"
        ca_file "#{ENV['KUBERNETES_CA_FILE']||''}"
      </filter>

      # ── Filter: parser les logs JSON de l'API Flask ─────────────────────
      <filter kubernetes.**taskmanager**>
        @type parser
        key_name log
        reserve_data true
        <parse>
          @type json
          json_parser json
        </parse>
      </filter>

      # ── Output: envoyer à Elasticsearch ────────────────────────────────
      <match kubernetes.**>
        @type elasticsearch
        host "#{ENV['FLUENT_ELASTICSEARCH_HOST']}"
        port "#{ENV['FLUENT_ELASTICSEARCH_PORT']}"
        scheme "#{ENV['FLUENT_ELASTICSEARCH_SCHEME']||'http'}"
        logstash_format true
        logstash_prefix k8s
        logstash_dateformat %Y.%m.%d
        include_tag_key true
        type_name _doc
        <buffer>
          @type file
          path /var/log/fluentd-buffers/kubernetes.system.buffer
          flush_mode interval
          retry_type exponential_backoff
          flush_interval 5s
          retry_forever true
          retry_max_interval 30
          chunk_limit_size 2M
          queue_limit_length 8
          overflow_action block
        </buffer>
      </match>

================================================================================
PARTIE 17 — GITOPS AVEC ARGOCD
================================================================================

  POURQUOI GITOPS?
  ──────────────────
  Approche traditionnelle: Alice SSH sur le serveur ou lance kubectl manuellement.
  Problème: pas d'audit trail, risque d'erreur humaine, pas de rollback simple.

  GitOps: la source de vérité est le dépôt Git.
  ArgoCD surveille le dépôt et SYNCHRONISE automatiquement le cluster.
  Changement dans le dépôt -> ArgoCD déploie automatiquement.
  Rollback = git revert -> ArgoCD synchronise le rollback.

  AVANTAGES GITOPS:
  ──────────────────
  -> Audit trail complet: qui a changé quoi et quand (git log)
  -> Rollback en 1 commande (git revert)
  -> "Drift detection": si quelqu'un modifie le cluster manuellement,
    ArgoCD le détecte et peut automatiquement revenir à l'état Git
  -> PR review avant déploiement (même processus que le code)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 17.1 — ALICE INSTALLE ET CONFIGURE ARGOCD
────────────────────────────────────────────────────────────────────────────────

  INSTALLER ARGOCD:
  ──────────────────
  kubectl create namespace argocd

  helm install argocd argo/argo-cd \
    --namespace argocd \
    --values argocd/values.yaml

  ── argocd/values.yaml ──────────────────────────────────────────────────────
  global:
    domain: argocd.taskmanager.equipe.com

  server:
    ingress:
      enabled: true
      annotations:
        kubernetes.io/ingress.class: nginx
        cert-manager.io/cluster-issuer: letsencrypt-prod
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
        nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
      hosts:
      - argocd.taskmanager.equipe.com
      tls:
      - hosts:
        - argocd.taskmanager.equipe.com
        secretName: argocd-tls

  # RBAC ArgoCD: qui peut faire quoi dans ArgoCD:
  configs:
    rbac:
      policy.default: role:readonly
      policy.csv: |
        p, role:admin, applications, *, */*, allow
        p, role:admin, clusters, *, *, allow
        p, role:developer, applications, sync, */*, allow
        p, role:developer, applications, get, */*, allow
        g, alice, role:admin
        g, bob, role:developer
        g, claire, role:developer

  ACCÉDER À ARGOCD:
  ──────────────────
  # Mot de passe initial admin:
  kubectl -n argocd get secret argocd-initial-admin-secret \
    -o jsonpath="{.data.password}" | base64 -d

  # Login via CLI:
  argocd login argocd.taskmanager.equipe.com \
    --username admin \
    --password [MOT_DE_PASSE]

  ── argocd/application-production.yaml ──────────────────────────────────────
  # Application ArgoCD pour déployer l'API en production depuis Git:
  apiVersion: argoproj.io/v1alpha1
  kind: Application
  metadata:
    name: taskmanager-production
    namespace: argocd
    labels:
      app: taskmanager
      environment: production
    finalizers:
    - resources-finalizer.argocd.argoproj.io  # Supprimer les ressources K8s si l'App ArgoCD est supprimée
  spec:
    # ── Projet ArgoCD (contrôle d'accès) ──────────────────────────────────────
    project: taskmanager

    # ── Source: le dépôt Git ──────────────────────────────────────────────────
    source:
      repoURL: https://github.com/equipe/taskmanager-k8s.git
      targetRevision: main              # Branche, tag ou commit hash
      path: kubernetes/overlays/production    # Chemin des manifestes dans le dépôt

      # Si utilisation de Helm (au lieu de Kustomize):
      # helm:
      #   valueFiles:
      #   - values-production.yaml
      #   parameters:
      #   - name: image.tag
      #     value: "1.0.0"

      # Si utilisation de Kustomize (détection automatique):
      kustomize:
        images:
        - ghcr.io/equipe/taskmanager-api:1.0.0

    # ── Destination: le cluster et le namespace ───────────────────────────────
    destination:
      server: https://kubernetes.default.svc    # Cluster courant
      namespace: production

    # ── Politique de synchronisation ──────────────────────────────────────────
    syncPolicy:
      automated:
        prune: true               # Supprimer les ressources supprimées dans Git
        selfHeal: true            # Corriger les dérives (drift correction)
        allowEmpty: false         # Ne pas supprimer tout si le dossier est vide

      syncOptions:
      - CreateNamespace=true      # Créer le namespace s'il n'existe pas
      - PrunePropagationPolicy=foreground
      - PruneLast=true            # Supprimer les anciennes ressources en dernier

      retry:
        limit: 5                  # Réessayer 5 fois si synchronisation échoue
        backoff:
          duration: 5s
          factor: 2
          maxDuration: 3m

    # ── Ignorance des champs qui changent dynamiquement ────────────────────────
    ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
      - /spec/replicas              # Le HPA change les replicas -> ignorer la différence
    - group: autoscaling
      kind: HorizontalPodAutoscaler
      jsonPointers:
      - /spec/metrics               # Métriques custom ajoutées dynamiquement

    # ── Health checks custom ──────────────────────────────────────────────────
    info:
    - name: url
      value: "https://api.taskmanager.equipe.com"
    - name: slack-channel
      value: "#deployments"

  ── argocd/application-development.yaml ─────────────────────────────────────
  apiVersion: argoproj.io/v1alpha1
  kind: Application
  metadata:
    name: taskmanager-development
    namespace: argocd
  spec:
    project: taskmanager
    source:
      repoURL: https://github.com/equipe/taskmanager-k8s.git
      targetRevision: develop           # Branche develop pour l'env de dev
      path: kubernetes/overlays/development
    destination:
      server: https://kubernetes.default.svc
      namespace: development
    syncPolicy:
      automated:
        prune: true
        selfHeal: true
      syncOptions:
      - CreateNamespace=true

  ── argocd/project.yaml ─────────────────────────────────────────────────────
  # AppProject: définit les permissions ArgoCD (quels dépôts, quels namespaces)
  apiVersion: argoproj.io/v1alpha1
  kind: AppProject
  metadata:
    name: taskmanager
    namespace: argocd
  spec:
    description: "Projet TaskManager — API REST Flask"

    # Dépôts Git autorisés:
    sourceRepos:
    - "https://github.com/equipe/taskmanager-k8s.git"
    - "https://charts.bitnami.com/bitnami"

    # Clusters de destination autorisés:
    destinations:
    - namespace: "development"
      server: "https://kubernetes.default.svc"
    - namespace: "staging"
      server: "https://kubernetes.default.svc"
    - namespace: "production"
      server: "https://kubernetes.default.svc"

    # Ressources K8s que ce projet peut créer:
    clusterResourceWhitelist:
    - group: ""
      kind: Namespace

    # Rôles dans le projet:
    roles:
    - name: developer
      description: "Développeurs (Bob, Claire) — peuvent synchroniser mais pas supprimer"
      policies:
      - p, proj:taskmanager:developer, applications, sync, taskmanager/*, allow
      - p, proj:taskmanager:developer, applications, get, taskmanager/*, allow
      groups:
      - developers

  WORKFLOW GITOPS COMPLET (Comment Bob déploie):
  ─────────────────────────────────────────────────
  # Étape 1: Bob développe sa feature sur sa branche Git
  git checkout -b feature/add-task-status
  # ... développe le code et les manifestes K8s ...

  # Étape 2: Bob ouvre une PR vers develop
  # Alice et Claire font la review

  # Étape 3: Après merge dans develop:
  # ArgoCD détecte le changement dans le dépôt
  # ArgoCD synchronise automatiquement le namespace "development"
  # Bob peut voir le déploiement dans l'interface ArgoCD

  # Étape 4: Tests en staging
  # Alice crée un PR de develop vers main
  # Après review et merge: ArgoCD déploie en production automatiquement!

================================================================================
PARTIE 18 — SCÉNARIOS COMPLETS: ALICE, BOB ET CLAIRE
================================================================================

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 1 — ALICE: MIGRATION D'UNE APPLICATION DOCKER VERS KUBERNETES
Durée: Jour 1 complet
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  L'application tourne avec docker-compose. Alice doit la migrer vers Kubernetes.
  Plan de la journée:
  - 9h:  Créer le cluster Minikube et les namespaces
  - 10h: Créer les secrets et configmaps
  - 11h: Déployer PostgreSQL (StatefulSet) et Redis
  - 13h: Déployer l'API Flask (Deployment + Service + Ingress)
  - 14h: Configurer le monitoring (Prometheus + Grafana)
  - 15h: Configurer RBAC pour Bob et Claire
  - 16h: Configurer ArgoCD pour le GitOps
  - 17h: Tester et documenter

  ALICE — ÉTAPE 1: Préparer le cluster:
  ──────────────────────────────────────
  # Démarrer Minikube:
  minikube start --driver=docker --cpus=4 --memory=8192 \
    --addons=ingress,metrics-server,dashboard

  # Créer les namespaces:
  kubectl apply -f kubernetes/base/namespaces.yaml

  # Créer les quotas:
  kubectl apply -f kubernetes/base/resource-quotas.yaml

  # Vérifier:
  kubectl get namespaces
  kubectl get resourcequota -A

  ALICE — ÉTAPE 2: Secrets et ConfigMaps:
  ─────────────────────────────────────────
  # Créer les secrets PostgreSQL:
  kubectl create secret generic postgres-secrets \
    --namespace=production \
    --from-literal=postgres-password="$(openssl rand -hex 16)"

  # Créer les secrets de l'API:
  kubectl create secret generic taskmanager-secrets \
    --namespace=production \
    --from-literal=database-url="postgresql://taskmanager:$(kubectl get secret postgres-secrets -n production -o jsonpath='{.data.postgres-password}' | base64 --decode)@postgres-service:5432/taskmanager" \
    --from-literal=secret-key="$(openssl rand -hex 32)" \
    --from-literal=jwt-secret="$(openssl rand -hex 32)" \
    --from-literal=redis-password="$(openssl rand -hex 16)"

  # Créer les ConfigMaps:
  kubectl apply -f kubernetes/base/api/configmap.yaml
  kubectl apply -f kubernetes/base/postgres/configmap.yaml

  ALICE — ÉTAPE 3: Déployer PostgreSQL:
  ──────────────────────────────────────
  kubectl apply -f kubernetes/base/storage/storageclass.yaml
  kubectl apply -f kubernetes/base/postgres/statefulset.yaml
  kubectl apply -f kubernetes/base/postgres/service.yaml

  # Attendre que PostgreSQL soit prêt:
  kubectl rollout status statefulset/postgres -n production
  kubectl wait --for=condition=ready pod/postgres-0 -n production --timeout=120s

  # Vérifier:
  kubectl exec -it postgres-0 -n production -- psql -U taskmanager -d taskmanager -c "\l"

  ALICE — ÉTAPE 4: Déployer Redis:
  ─────────────────────────────────
  # (Pour simplifier, Alice utilise Helm pour Redis)
  helm install redis bitnami/redis \
    --namespace production \
    --set auth.enabled=true \
    --set auth.password="$(kubectl get secret taskmanager-secrets -n production -o jsonpath='{.data.redis-password}' | base64 --decode)" \
    --set master.resources.requests.cpu=50m \
    --set master.resources.requests.memory=64Mi

  ALICE — ÉTAPE 5: Déployer l'API Flask:
  ─────────────────────────────────────────
  # Charger l'image dans Minikube (pas besoin de registry externe):
  minikube image load ghcr.io/equipe/taskmanager-api:1.0.0

  # Créer les RBAC:
  kubectl apply -f kubernetes/base/rbac/serviceaccounts.yaml
  kubectl apply -f kubernetes/base/rbac/roles.yaml
  kubectl apply -f kubernetes/base/rbac/bindings.yaml

  # Déployer l'API:
  kubectl apply -f kubernetes/base/api/deployment.yaml
  kubectl apply -f kubernetes/base/api/service.yaml

  # Suivre le déploiement:
  kubectl rollout status deployment/taskmanager-api -n production

  # Configurer l'Ingress:
  kubectl apply -f kubernetes/base/ingress/ingress.yaml

  # Tester via port-forward:
  kubectl port-forward svc/taskmanager-api 8080:80 -n production &
  curl http://localhost:8080/health
  # -> {"status": "healthy", "version": "1.0.0"}  [OK]

  ALICE — ÉTAPE 6: Configurer le monitoring:
  ────────────────────────────────────────────
  helm install prometheus prometheus-community/kube-prometheus-stack \
    --namespace monitoring \
    --create-namespace \
    --values monitoring/prometheus/prometheus-values.yaml

  kubectl apply -f monitoring/prometheus/servicemonitor.yaml
  kubectl apply -f monitoring/prometheus/alertingrules.yaml
  kubectl apply -f monitoring/grafana/configmap-dashboard.yaml

  # Accéder à Grafana:
  kubectl port-forward svc/prometheus-grafana 3000:80 -n monitoring &
  # -> http://localhost:3000 (admin/changeme-in-prod)

  ALICE — ÉTAPE 7: Configurer ArgoCD:
  ──────────────────────────────────────
  helm install argocd argo/argo-cd \
    --namespace argocd \
    --create-namespace \
    --values argocd/values.yaml

  kubectl apply -f argocd/project.yaml
  kubectl apply -f argocd/application-production.yaml
  kubectl apply -f argocd/application-development.yaml

  # ArgoCD synchronise maintenant automatiquement le cluster depuis Git!

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 2 — BOB: DÉVELOPPER ET DÉPLOYER UNE NOUVELLE FEATURE
Durée: 2 jours | Feature: Ajout des tags de tâches avec filtrage
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Bob développe le filtrage par tags pour les tâches.
  Il doit modifier le code Python, les tests, ET les manifestes Kubernetes.

  BOB — ÉTAPE 1: Préparer l'environnement de développement:
  ──────────────────────────────────────────────────────────
  # Bob travaille sur le namespace "development":
  kubectl config set-context --current --namespace=development

  # Voir l'état actuel du namespace development:
  kubectl get all -n development

  # Bob lance l'API en local pour développer:
  # (Il utilise docker-compose en local, K8s pour les tests d'intégration)
  docker-compose up -d

  BOB — ÉTAPE 2: Développer la feature (code Python):
  ──────────────────────────────────────────────────────
  # Bob modifie app/routes/tasks.py pour ajouter le filtre par tags
  # Bob ajoute des tests dans tests/test_tasks.py
  # Bob fait ses commits Git (voir guide Git)

  BOB — ÉTAPE 3: Construire et pousser la nouvelle image:
  ─────────────────────────────────────────────────────────
  docker build \
    --file docker/Dockerfile \
    --build-arg APP_VERSION=1.1.0 \
    --tag ghcr.io/equipe/taskmanager-api:1.1.0 \
    .

  docker push ghcr.io/equipe/taskmanager-api:1.1.0

  # Pour tester en local sur Minikube sans pousser:
  minikube image load ghcr.io/equipe/taskmanager-api:1.1.0

  BOB — ÉTAPE 4: Mettre à jour les manifestes Kubernetes:
  ──────────────────────────────────────────────────────────
  # Bob met à jour kubernetes/base/kustomization.yaml:
  # Changer: newTag: "1.0.0" -> newTag: "1.1.0"

  # Bob crée un Job de migration pour le nouveau champ:
  # kubernetes/base/api/job-migration-v1-1-0.yaml (voir Partie 10)

  BOB — ÉTAPE 5: Déployer en développement pour tester:
  ────────────────────────────────────────────────────────
  # Appliquer en développement avec Kustomize:
  kubectl apply -k kubernetes/overlays/development/

  # Lancer la migration:
  kubectl apply -f kubernetes/base/api/job-migration-v1-1-0.yaml -n development

  # Attendre la fin de la migration:
  kubectl wait --for=condition=complete job/taskmanager-db-migration-v1-1-0 \
    -n development --timeout=120s

  # Voir le résultat:
  kubectl get pods -n development
  kubectl logs job/taskmanager-db-migration-v1-1-0 -n development

  BOB — ÉTAPE 6: Tester la feature déployée:
  ───────────────────────────────────────────
  kubectl port-forward svc/taskmanager-api 8080:80 -n development &

  # Créer une tâche avec des tags:
  curl -X POST http://localhost:8080/api/v1/tasks \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer TOKEN" \
    -d '{"title": "Fix Redis cache", "tags": ["backend", "redis", "urgent"]}'

  # Filtrer par tag:
  curl "http://localhost:8080/api/v1/tasks?tag=redis"

  BOB — ÉTAPE 7: Ouvrir une PR et laisser ArgoCD déployer en staging:
  ─────────────────────────────────────────────────────────────────────
  # Bob ouvre une PR: feature/add-tag-filtering -> develop
  # Alice et Claire font la review
  # Après merge dans develop: ArgoCD déploie automatiquement en development!
  # Bob vérifie dans l'interface ArgoCD que tout est vert.
  # Bob demande à Alice de préparer la release pour production.

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 3 — CLAIRE: AJOUTER DES TESTS DE CHARGE ET OBSERVER LE HPA
Durée: 1 journée | Test: Simuler 1000 utilisateurs simultanés
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Claire veut vérifier que le HPA fonctionne correctement.
  Elle simule une charge élevée et observe l'autoscaling.

  CLAIRE — ÉTAPE 1: Préparer les tests de charge avec Locust:
  ────────────────────────────────────────────────────────────
  # Claire crée un fichier de test Locust:
  cat > tests/locustfile.py << 'EOF'
  from locust import HttpUser, task, between
  import random

  class TaskManagerUser(HttpUser):
      wait_time = between(0.1, 0.5)    # 0.1-0.5s entre les requêtes
      token = None

      def on_start(self):
          """Se connecter au début de chaque session utilisateur."""
          response = self.client.post("/api/v1/auth/login", json={
              "username": f"user{random.randint(1, 100)}",
              "password": "testpassword"
          })
          if response.status_code == 200:
              self.token = response.json()["token"]

      @task(5)    # Poids 5: cette tâche est 5x plus fréquente
      def list_tasks(self):
          self.client.get(
              "/api/v1/tasks?page=1&per_page=20",
              headers={"Authorization": f"Bearer {self.token}"}
          )

      @task(3)
      def list_tasks_filtered(self):
          priority = random.choice(["low", "medium", "high"])
          self.client.get(
              f"/api/v1/tasks?priority={priority}&page=1",
              headers={"Authorization": f"Bearer {self.token}"}
          )

      @task(2)
      def create_task(self):
          self.client.post(
              "/api/v1/tasks",
              json={
                  "title": f"Test task {random.randint(1, 10000)}",
                  "priority": random.choice(["low", "medium", "high"]),
                  "tags": ["test", "locust"]
              },
              headers={"Authorization": f"Bearer {self.token}"}
          )

      @task(1)
      def health_check(self):
          self.client.get("/health")
  EOF

  CLAIRE — ÉTAPE 2: Déployer Locust dans Kubernetes:
  ──────────────────────────────────────────────────
  # Créer un ConfigMap avec le fichier Locust:
  kubectl create configmap locust-config \
    --from-file=locustfile.py=tests/locustfile.py \
    -n development

  # Créer un Deployment Locust:
  cat > /tmp/locust-deployment.yaml << 'EOF'
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: locust-master
    namespace: development
  spec:
    replicas: 1
    selector:
      matchLabels:
        app: locust
        role: master
    template:
      metadata:
        labels:
          app: locust
          role: master
      spec:
        containers:
        - name: locust
          image: locustio/locust:2.20.0
          command: ["locust", "--master", "--host=http://taskmanager-api"]
          ports:
          - containerPort: 8089
            name: web
          - containerPort: 5557
            name: master-bind
          volumeMounts:
          - name: locust-config
            mountPath: /home/locust
        volumes:
        - name: locust-config
          configMap:
            name: locust-config
  EOF

  kubectl apply -f /tmp/locust-deployment.yaml

  # Accéder à l'interface Locust:
  kubectl port-forward deployment/locust-master 8089:8089 -n development &
  # -> http://localhost:8089

  CLAIRE — ÉTAPE 3: Observer le HPA pendant le test de charge:
  ────────────

─────────────────────────────────────────────────────────────────────
  # Dans un autre terminal, observer l'autoscaling en temps réel:
  watch -n 2 "kubectl get hpa,pods -n production"

  CE QUE VOUS VOYEZ (pendant le test de charge):
  ────────────────────────────────────────────────
  # Avant le test (faible charge):
  NAME                                    REFERENCE                     TARGETS    MINPODS  MAXPODS  REPLICAS
  horizontalpodautoscaler/taskmanager-api Deployment/taskmanager-api    12%/70%    3        20       3

  # Pendant le test (forte charge - quelques minutes après):
  horizontalpodautoscaler/taskmanager-api Deployment/taskmanager-api    87%/70%    3        20       5
  # -> CPU > 70%: le HPA crée 2 nouveaux Pods!

  # Après stabilisation (plus de requêtes):
  horizontalpodautoscaler/taskmanager-api Deployment/taskmanager-api    95%/70%    3        20       8
  # -> Encore plus de Pods créés pour absorber la charge!

  # Observer dans k9s:
  k9s
  # Aller sur :hpa -> voir les métriques en direct

  CLAIRE — ÉTAPE 4: Analyser les résultats dans Grafana:
  ────────────────────────────────────────────────────────
  kubectl port-forward svc/prometheus-grafana 3000:80 -n monitoring &
  # -> http://localhost:3000
  # Dashboard "TaskManager API":
  # - Requests/s: 800 req/s au pic
  # - p95 latency: 380ms (objectif < 500ms [OK])
  # - Error rate: 0.2% (objectif < 1% [OK])
  # - Pod count: de 3 à 8 (autoscaling fonctionne [OK])

  CLAIRE — ÉTAPE 5: Écrire le rapport de test:
  ─────────────────────────────────────────────
  # Claire ouvre une issue GitHub avec les résultats:
  # Titre: [PERF] Résultats test de charge v1.0.0
  # Contenu:
  # - Charge testée: 1000 utilisateurs, ramp-up 2 minutes
  # - Pic observé: 820 req/s
  # - Latence p50: 45ms | p95: 380ms | p99: 820ms
  # - Taux d'erreur: 0.2%
  # - Autoscaling: 3 -> 8 Pods en 3 minutes
  # - Scale-down: retour à 3 Pods après 8 minutes (cooldown 5 min)
  # Recommandation: augmenter replica minimum à 5 pour les heures de pointe

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 4 — ALICE: GÉRER UNE PANNE MAJEURE (NODE DOWN)
Durée: 1h | Incident: Node worker2 est tombé en panne
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  À 15h, Alice reçoit une alerte Slack: "TaskManagerApiDown — Pod en erreur".
  Elle doit diagnostiquer et résoudre l'incident rapidement.

  ALICE — ÉTAPE 1: Diagnostiquer (triage initial):
  ──────────────────────────────────────────────────
  # Vue d'ensemble immédiate:
  kubectl get nodes
  CE QUE VOUS VOYEZ:
  NAME       STATUS     ROLES    AGE   VERSION
  worker1    Ready      <none>   2d    v1.29.0
  worker2    NotReady   <none>   2d    v1.29.0  <- PROBLÈME ICI!
  worker3    Ready      <none>   2d    v1.29.0

  # Voir les Pods affectés:
  kubectl get pods -n production -o wide
  CE QUE VOUS VOYEZ:
  NAME                           READY   STATUS        NODE      AGE
  taskmanager-api-xxx-aaa       2/2     Running       worker1   2d
  taskmanager-api-xxx-bbb       2/2     Unknown       worker2   2d    <- NODE DOWN
  taskmanager-api-xxx-ccc       2/2     Running       worker3   2d
  postgres-0                    1/1     Unknown       worker2   2d    <- NODE DOWN

  # Détailler le node down:
  kubectl describe node worker2

  CE QUE VOUS VOYEZ (extrait):
  ─────────────────────────────
  Conditions:
    Type                Status  Reason
    ─────               ──────  ──────
    MemoryPressure      False   KubeletHasSufficientMemory
    DiskPressure        False   KubeletHasNoDiskPressure
    PIDPressure         False   KubeletHasSufficientPID
    Ready               False   KubeletNotReady     <- !
  Events:
    Normal  NodeNotReady    5m  node-controller  Node worker2 status is now: NodeNotReady

  ALICE — ÉTAPE 2: Vérifier l'impact sur l'application:
  ──────────────────────────────────────────────────────
  # L'API a encore 2 Pods Running (worker1 et worker3) -> pas de downtime.
  # Mais PostgreSQL (postgres-0) est sur worker2 -> CRITIQUE!

  # Vérifier l'état de PostgreSQL:
  kubectl get pod postgres-0 -n production -o wide
  # -> STATUS: Unknown (node down)

  # Tester si l'API répond encore:
  curl http://api.taskmanager.equipe.com/health
  # -> {"status": "unhealthy", "checks": {"database": "error: connection refused"}}
  # L'API est en vie mais ne peut pas accéder à la BDD!

  ALICE — ÉTAPE 3: Forcer le reschedule de PostgreSQL:
  ──────────────────────────────────────────────────────
  # K8s attend 5 minutes par défaut avant de reschedule un Pod sur un node "Unknown".
  # Alice ne peut pas attendre -> forcer manuellement.

  # Supprimer le Pod (StatefulSet le recrée sur un autre node):
  kubectl delete pod postgres-0 -n production --grace-period=0 --force
  # --force = ne pas attendre SIGTERM (node inaccessible de toute façon)

  # Observer le reschedule:
  kubectl get pod postgres-0 -n production -w
  CE QUE VOUS VOYEZ:
  NAME         READY   STATUS    RESTARTS   AGE   NODE
  postgres-0   0/1     Pending   0          5s    <none>    <- En attente de scheduling
  postgres-0   0/1     Pending   0          8s    worker3   <- Schedulé sur worker3
  postgres-0   0/1     Init:0/1  0          10s   worker3   <- Init container en cours
  postgres-0   1/1     Running   0          45s   worker3   <- En cours d'exécution [OK]

  # Vérifier la santé:
  curl http://api.taskmanager.equipe.com/health
  # -> {"status": "healthy"}  [OK]

  ALICE — ÉTAPE 4: Drainer le node pour maintenance:
  ────────────────────────────────────────────────────
  # Drainer = évacuer tous les Pods du node avant maintenance.
  # K8s respecte le PodDisruptionBudget pendant cette opération.

  kubectl drain worker2 \
    --ignore-daemonsets \          # Ignorer les DaemonSet (normal qu'ils soient là)
    --delete-emptydir-data \       # Supprimer les Pods avec emptyDir volumes
    --grace-period=60              # 60s pour que les Pods terminent proprement

  CE QUE VOUS VOYEZ:
  ───────────────────
  node/worker2 cordoned
  evicting pod production/taskmanager-api-xxx-bbb
  evicting pod logging/fluentd-xxx
  pod/taskmanager-api-xxx-bbb evicted
  pod/fluentd-xxx evicted
  node/worker2 drained

  # Le node est maintenant "cordoned" (plus aucun nouveau Pod ne peut y être schedulé):
  kubectl get nodes
  NAME     STATUS                     ROLES
  worker2  Ready,SchedulingDisabled   <none>    <- Cordonné

  ALICE — ÉTAPE 5: Après la réparation du node:
  ──────────────────────────────────────────────
  # Remettre le node en service:
  kubectl uncordon worker2
  # Le Scheduler peut à nouveau placer des Pods sur worker2.
  kubectl get nodes
  # -> worker2: Ready

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 5 — BOB: ROLLBACK D'UN DÉPLOIEMENT DÉFAILLANT
Durée: 15 minutes | Situation: La v1.2.0 a un bug critique
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Bob a déployé la v1.2.0 en production. Immédiatement après:
  - Les erreurs 500 explosent dans Grafana
  - L'alerte TaskManagerHighErrorRate se déclenche sur Slack
  - Bob doit rollback immédiatement.

  BOB — ÉTAPE 1: Identifier le problème:
  ─────────────────────────────────────────
  # Voir les Pods et leur état:
  kubectl get pods -n production -l app=taskmanager

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME                               READY   STATUS             RESTARTS   AGE
  taskmanager-api-8f7d6c5b4-abc11    2/2     Running            0          2m
  taskmanager-api-8f7d6c5b4-def22    0/2     CrashLoopBackOff   3          2m
  taskmanager-api-8f7d6c5b4-ghi33    0/2     CrashLoopBackOff   2          1m
  # CrashLoopBackOff = le conteneur démarre, crashe, K8s attend, réessaie...

  # Voir les logs du Pod qui crashe:
  kubectl logs taskmanager-api-8f7d6c5b4-def22 -n production -c api
  CE QUE VOUS VOYEZ:
  Traceback (most recent call last):
    File "/app/app/__init__.py", line 28, in create_app
      from app.routes.tasks import tasks_bp, UNDEFINED_FUNCTION
  ImportError: cannot import name 'UNDEFINED_FUNCTION' from 'app.routes.tasks'
  # Bug évident: import manquant dans la v1.2.0!

  # Voir l'historique des déploiements:
  kubectl rollout history deployment/taskmanager-api -n production
  REVISION  CHANGE-CAUSE
  1         Initial deployment v1.0.0
  2         Deploy v1.1.0 — add tag filtering
  3         Deploy v1.2.0 — add task comments

  BOB — ÉTAPE 2: Rollback immédiat:
  ───────────────────────────────────
  # Rollback vers la révision précédente (revision 2 = v1.1.0):
  kubectl rollout undo deployment/taskmanager-api -n production

  # Suivre le rollback:
  kubectl rollout status deployment/taskmanager-api -n production

  CE QUE VOUS VOYEZ:
  Waiting for deployment "taskmanager-api" rollout to finish:
  1 out of 3 new replicas have been updated...
  2 out of 3 new replicas have been updated...
  3 out of 3 new replicas have been updated...
  deployment "taskmanager-api" successfully rolled out

  # Vérifier que les Pods sont sains:
  kubectl get pods -n production -l app=taskmanager
  NAME                               READY   STATUS    RESTARTS
  taskmanager-api-7d8f9c5b4-abc44    2/2     Running   0
  taskmanager-api-7d8f9c5b4-def55    2/2     Running   0
  taskmanager-api-7d8f9c5b4-ghi66    2/2     Running   0
  # [OK] Les 3 Pods sont Running avec la v1.1.0!

  # Tester:
  curl http://api.taskmanager.equipe.com/health
  # -> {"status": "healthy", "version": "1.1.0"}  [OK]

  BOB — ÉTAPE 3: Corriger le bug et redéployer:
  ───────────────────────────────────────────────
  # Bob corrige le bug dans le code Python
  # Rebuilde l'image: 1.2.1 (patch version)
  # Ouvre une PR avec le fix
  # Après review et merge: ArgoCD redéploie automatiquement la v1.2.1

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 6 — CLAIRE: CONFIGURER LES NETWORKPOLICIES ET TESTER
Durée: 1 journée | Objectif: Sécuriser les communications inter-services
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Claire doit implémenter le principe du "least privilege" réseau.
  Elle applique les NetworkPolicies et vérifie qu'elles fonctionnent.

  CLAIRE — ÉTAPE 1: Vérifier l'état actuel (tout ouvert):
  ─────────────────────────────────────────────────────────
  # Avant les NetworkPolicies: tout pod peut parler à tout pod.
  # Claire teste depuis un Pod temporaire:
  kubectl run test-pod --image=busybox --rm -it \
    -n development \
    -- sh -c "nc -z postgres-service.production.svc.cluster.local 5432 && echo CONNECTED"
  # -> CONNECTED (le Pod development peut accéder à postgres en production!)
  # Ce n'est PAS ce qu'on veut en production.

  CLAIRE — ÉTAPE 2: Appliquer les NetworkPolicies:
  ──────────────────────────────────────────────────
  kubectl apply -f kubernetes/base/network-policies.yaml

  # Vérifier les policies appliquées:
  kubectl get networkpolicies -n production

  CE QUE VOUS VOYEZ:
  ───────────────────
  NAME                     POD-SELECTOR         AGE
  default-deny-all         <none>               10s
  allow-ingress-to-api     app=taskmanager      10s
  allow-api-to-postgres    app=postgres         10s
  allow-api-to-redis       app=redis            10s
  allow-api-dns-egress     app=taskmanager      10s
  allow-prometheus-scrape  app=taskmanager      10s

  CLAIRE — ÉTAPE 3: Tester que les NetworkPolicies fonctionnent:
  ────────────────────────────────────────────────────────────────
  # Test 1: Un Pod quelconque NE DOIT PLUS accéder à postgres depuis dev:
  kubectl run test-blocked --image=busybox --rm -it \
    -n development \
    -- sh -c "timeout 5 nc -z postgres-service.production.svc.cluster.local 5432 && echo CONNECTED || echo BLOCKED"
  # -> BLOCKED  [OK]

  # Test 2: L'API DOIT toujours pouvoir accéder à postgres:
  kubectl exec -it $(kubectl get pod -n production -l app=taskmanager -o name | head -1) \
    -n production -c api \
    -- python -c "
  from app import create_app, db
  app = create_app('production')
  with app.app_context():
      result = db.session.execute(db.text('SELECT 1')).fetchone()
      print('DB accessible:', result)
  "
  # -> DB accessible: (1,)  [OK]

  # Test 3: Prometheus DOIT pouvoir scraper l'API:
  kubectl exec -it $(kubectl get pod -n monitoring -l app=prometheus -o name | head -1) \
    -n monitoring \
    -- wget -q -O- http://taskmanager-api.production.svc.cluster.local:5000/metrics | head -5
  # -> # HELP flask_http_request_total ...  [OK]

  # Test 4: Un Pod prod NE DOIT PAS accéder à dev:
  kubectl run test-prod-to-dev --image=busybox --rm -it \
    -n production \
    -- sh -c "timeout 3 nc -z taskmanager-api.development.svc.cluster.local 80 && echo OK || echo BLOCKED"
  # -> BLOCKED  [OK]

  CLAIRE — ÉTAPE 4: Documenter les flux réseau autorisés:
  ─────────────────────────────────────────────────────────
  # Claire crée un diagramme dans le Wiki GitHub:
  #
  #  Internet
  #    v
  #  Ingress (nginx) <- namespace: ingress-nginx
  #    v
  #  API Flask <- namespace: production (port 5000)
  #    v              v
  #  PostgreSQL     Redis <- namespace: production
  #    (5432)       (6379)
  #
  #  Prometheus <- namespace: monitoring
  #    v scrape (5000/metrics)
  #  API Flask
  #
  # TOUT LE RESTE EST BLOQUÉ PAR default-deny-all.

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 7 — ALICE: MISE À JOUR DU CLUSTER (VERSION KUBERNETES)
Durée: 2h | Objectif: Migrer de K8s 1.28 à 1.29 sans interruption
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Le fournisseur cloud annonce que K8s 1.28 arrive en fin de support.
  Alice doit migrer vers K8s 1.29. Sur un cluster cloud (GKE/EKS/AKS),
  cette procédure est semi-automatisée. Sur un cluster bare-metal: manuelle.

  ALICE — ÉTAPE 1: Vérifier la compatibilité:
  ────────────────────────────────────────────
  # Vérifier les APIs dépréciées qui seraient supprimées dans 1.29:
  kubectl api-resources --verbs=list --namespaced -o name | head -20

  # Utiliser pluto pour détecter les manifestes utilisant des APIs obsolètes:
  # Installation: brew install FairwindsOps/tap/pluto
  pluto detect-files -d kubernetes/

  # Vérifier les YAML du cluster:
  pluto detect-api-resources -ojson | jq

  ALICE — ÉTAPE 2: Préparer la mise à jour:
  ───────────────────────────────────────────
  # Vérifier l'état actuel:
  kubectl get nodes
  kubectl cluster-info
  kubectl version

  # S'assurer que le PodDisruptionBudget est correctement configuré:
  kubectl get pdb -A

  ALICE — ÉTAPE 3: Mettre à jour le Control Plane (sur GKE):
  ────────────────────────────────────────────────────────────
  # Sur GKE (Google Kubernetes Engine):
  gcloud container clusters upgrade production-cluster \
    --master \
    --cluster-version=1.29

  # Sur EKS (AWS):
  # eksctl upgrade cluster --name=production-cluster --version=1.29

  # Sur minikube:
  # minikube start --kubernetes-version=v1.29.0

  ALICE — ÉTAPE 4: Mettre à jour les nodes un par un:
  ──────────────────────────────────────────────────────
  # Sur GKE: node pool rolling update
  gcloud container node-pools update default-pool \
    --cluster=production-cluster \
    --node-version=1.29 \
    --region=europe-west1

  # Sur un cluster bare-metal:
  for NODE in worker1 worker2 worker3; do
    echo "Upgrading $NODE..."
    # Cordonner le node:
    kubectl cordon $NODE
    # Drainer (respecte le PDB):
    kubectl drain $NODE --ignore-daemonsets --delete-emptydir-data
    # Mettre à jour kubeadm, kubelet, kubectl sur le node (via SSH):
    # ssh $NODE "apt-get update && apt-get install -y kubeadm=1.29.0 kubelet=1.29.0 kubectl=1.29.0"
    # ssh $NODE "kubeadm upgrade node && systemctl restart kubelet"
    # Remettre le node en service:
    kubectl uncordon $NODE
    # Attendre que le node soit Ready:
    kubectl wait --for=condition=ready node/$NODE --timeout=120s
    echo "$NODE upgraded [OK]"
    sleep 30   # Attendre que les Pods se redistribuent avant le prochain node
  done

================================================================================
PARTIE 19 — TROUBLESHOOTING COMPLET
================================================================================

  Cette partie couvre TOUS les problèmes courants et leurs solutions.
  C'est votre guide de dépannage de référence.

────────────────────────────────────────────────────────────────────────────────
PROBLÈME 1 — Pod en état "Pending" (ne démarre pas)
────────────────────────────────────────────────────────────────────────────────

  SYMPTÔME: kubectl get pods -> STATUS: Pending

  DIAGNOSTIC:
  ────────────
  kubectl describe pod [NOM] -n [NAMESPACE]
  # Regarder la section "Events:" en bas de l'output.

  CAS 1 — "0/3 nodes are available: 3 Insufficient cpu"
  ──────────────────────────────────────────────────────
  CAUSE: Les requests.cpu du Pod dépassent les CPU disponibles sur les nodes.
  FIX:
  # Voir les ressources disponibles sur chaque node:
  kubectl describe nodes | grep -A5 "Allocated resources"
  # Ou:
  kubectl top nodes    # (nécessite metrics-server)

  # Solution A: Réduire les requests du Pod:
  # Dans deployment.yaml: resources.requests.cpu: "50m" (au lieu de "2")

  # Solution B: Ajouter des nodes au cluster.

  CAS 2 — "0/3 nodes are available: 3 node(s) had untolerated taint"
  ────────────────────────────────────────────────────────────────────
  CAUSE: Les nodes ont des taints que le Pod ne tolère pas.
  FIX:
  # Voir les taints:
  kubectl get nodes -o custom-columns=NODE:.metadata.name,TAINTS:.spec.taints

  # Solution A: Ajouter une toleration au Pod.
  # Solution B: Supprimer la taint du node.
  kubectl taint nodes [NODE] [KEY]:[EFFECT]-

  CAS 3 — "pod has unbound immediate PersistentVolumeClaims"
  ────────────────────────────────────────────────────────────
  CAUSE: Le PVC ne trouve pas de PV compatible.
  FIX:
  kubectl get pvc -n [NAMESPACE]
  kubectl describe pvc [NOM-PVC] -n [NAMESPACE]
  # Vérifier:
  # - La StorageClass existe: kubectl get storageclass
  # - Le provisioner de la StorageClass fonctionne
  # - Les accessModes correspondent
  # - Il y a assez de stockage disponible

  CAS 4 — "Unschedulable" avec message sur node affinity
  ──────────────────────────────────────────────────────────
  CAUSE: Les règles nodeAffinity/podAntiAffinity ne peuvent pas être satisfaites.
  FIX:
  # Vérifier les labels des nodes:
  kubectl get nodes --show-labels
  # Assouplir les règles: "required" -> "preferred"

────────────────────────────────────────────────────────────────────────────────
PROBLÈME 2 — Pod en état "CrashLoopBackOff"
────────────────────────────────────────────────────────────────────────────────

  SYMPTÔME: kubectl get pods -> STATUS: CrashLoopBackOff
  Le Pod démarre, crashe, K8s attend (1s, 2s, 4s, 8s... backoff exponentiel)
  et réessaie. Le délai augmente jusqu'à 5 minutes entre les tentatives.

  DIAGNOSTIC:
  ────────────
  # Voir les logs du crash (avant le redémarrage):
  kubectl logs [POD] -n [NAMESPACE] --previous
  # "--previous" = logs du dernier run (avant le crash actuel)

  # Voir les logs du run actuel:
  kubectl logs [POD] -n [NAMESPACE]

  # Voir le code de sortie:
  kubectl describe pod [POD] -n [NAMESPACE] | grep "Exit Code"

  CAS 1 — ImportError / ModuleNotFoundError
  ─────────────────────────────────────────────
  CAUSE: Dépendance Python manquante dans requirements.txt.
  LOGS: "ImportError: No module named 'xxx'"
  FIX: Ajouter le module à requirements.txt, rebuilder l'image.

  CAS 2 — Variable d'environnement manquante
  ────────────────────────────────────────────
  CAUSE: Un Secret ou ConfigMap référencé n'existe pas.
  LOGS: "KeyError: 'DATABASE_URL'" ou l'app crashe au démarrage.
  FIX:
  # Vérifier les Secrets et ConfigMaps:
  kubectl get secrets -n [NAMESPACE]
  kubectl get configmaps -n [NAMESPACE]
  # Vérifier les noms dans le deployment.yaml correspondent!

  CAS 3 — OOMKilled (Out of Memory)
  ───────────────────────────────────
  CAUSE: Le conteneur a dépassé sa limits.memory -> K8s le tue.
  SYMPTÔME: Exit Code: 137, Reason: OOMKilled
  FIX:
  # Voir la consommation mémoire:
  kubectl top pods -n [NAMESPACE]
  # Augmenter limits.memory dans le deployment.yaml
  # Ou utiliser VPA pour avoir des recommandations.

  CAS 4 — Liveness probe échoue trop tôt
  ────────────────────────────────────────
  CAUSE: L'app prend du temps à démarrer mais la liveness probe échoue
         avant qu'elle soit prête -> K8s tue le Pod -> boucle infinie.
  FIX:
  # Augmenter initialDelaySeconds ou utiliser une startupProbe.
  startupProbe:
    httpGet:
      path: /startup
      port: 5000
    failureThreshold: 30   # 30 * 5s = 150s pour démarrer
    periodSeconds: 5

  CAS 5 — Permission denied (fichier ou socket)
  ──────────────────────────────────────────────
  CAUSE: L'image tourne en non-root mais essaie d'écrire dans un dossier root.
  LOGS: "PermissionError: [Errno 13] Permission denied: '/var/lib/xxx'"
  FIX:
  # Dans le Dockerfile: chown les dossiers nécessaires au bon utilisateur.
  # RUN chown -R appuser:appuser /var/lib/xxx

────────────────────────────────────────────────────────────────────────────────
PROBLÈME 3 — Pod "Running" mais service inaccessible
────────────────────────────────────────────────────────────────────────────────

  SYMPTÔME: Les Pods sont Running mais curl retourne "Connection refused".

  DIAGNOSTIC:
  ────────────
  # Étape 1: Le Service a-t-il des Endpoints?
  kubectl get endpoints [SERVICE] -n [NAMESPACE]

  CE QUE VOUS VOYEZ SI LE SERVICE FONCTIONNE:
  taskmanager-api   10.244.0.4:5000,10.244.0.5:5000,10.244.0.6:5000   5m
  # ^ Il y a des IPs -> le Service trouve les Pods.

  CE QUE VOUS VOYEZ SI LE SERVICE NE FONCTIONNE PAS:
  taskmanager-api   <none>   5m
  # ^ Aucun Endpoint -> le selector du Service ne correspond à aucun Pod!

  FIX — Endpoints vides:
  ───────────────────────
  # Vérifier les labels du Pod:
  kubectl get pods -n [NAMESPACE] --show-labels
  # Comparer avec le selector du Service:
  kubectl get service [SERVICE] -n [NAMESPACE] -o yaml | grep -A5 selector

  # Les labels du Pod DOIVENT correspondre EXACTEMENT au selector du Service.
  # Ex: selector: app=taskmanager,component=api
  #     Labels Pod: app=taskmanager,component=api [OK]

  FIX — Service accessible mais Pod ne répond pas:
  ──────────────────────────────────────────────────
  # Tester directement le Pod (bypasser le Service):
  kubectl port-forward pod/[POD] 8080:5000 -n [NAMESPACE]
  curl http://localhost:8080/health
  # Si ça marche: le problème est dans le Service (targetPort wrong?)
  # Si ça ne marche pas: le problème est dans l'app

  # Vérifier le targetPort du Service:
  # Le Service doit pointer vers le PORT DU CONTENEUR (pas le port du Service):
  # spec.ports.port       = port du Service (ce que les clients appellent)
  # spec.ports.targetPort = port du conteneur (ce que l'app écoute)

────────────────────────────────────────────────────────────────────────────────
PROBLÈME 4 — Ingress ne fonctionne pas
────────────────────────────────────────────────────────────────────────────────

  SYMPTÔME: curl vers le domaine retourne 404 ou connection refused.

  DIAGNOSTIC:
  ────────────
  # Vérifier l'Ingress:
  kubectl get ingress -n [NAMESPACE]
  kubectl describe ingress [NOM] -n [NAMESPACE]

  # Vérifier que l'Ingress Controller tourne:
  kubectl get pods -n ingress-nginx

  # Vérifier les logs de l'Ingress Controller:
  kubectl logs -n ingress-nginx \
    $(kubectl get pod -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx -o name)

  CAS 1 — ADDRESS vide dans "kubectl get ingress"
  ──────────────────────────────────────────────────
  CAUSE: L'Ingress Controller n'est pas installé ou pas prêt.
  FIX:
  minikube addons enable ingress
  # Ou:
  helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace

  CAS 2 — 404 Not Found retourné par nginx
  ──────────────────────────────────────────
  CAUSE: Le path ou host ne correspond à aucune règle Ingress.
  FIX:
  # Vérifier les règles dans l'Ingress:
  kubectl get ingress -n [NAMESPACE] -o yaml
  # Le host dans la règle doit correspondre exactement au Host header HTTP.
  # Vérifier /etc/hosts sur votre machine.

  CAS 3 — 502 Bad Gateway
  ──────────────────────────
  CAUSE: L'Ingress Controller ne peut pas atteindre le Pod backend.
  FIX:
  # Vérifier que le Service backend est accessible:
  kubectl get endpoints [SERVICE] -n [NAMESPACE]
  # Vérifier les NetworkPolicies (bloquent-elles l'Ingress Controller?)

────────────────────────────────────────────────────────────────────────────────
PROBLÈME 5 — Secret ou ConfigMap non monté
────────────────────────────────────────────────────────────────────────────────

  SYMPTÔME: Le Pod démarre mais les variables d'env sont vides ou manquantes.

  DIAGNOSTIC:
  ────────────
  # Voir les variables d'env dans un Pod en cours:
  kubectl exec -it [POD] -n [NAMESPACE] -- env | sort
  # Ou pour une variable spécifique:
  kubectl exec -it [POD] -n [NAMESPACE] -- printenv DATABASE_URL

  # Voir si le Secret existe et contient la bonne clé:
  kubectl get secret [SECRET] -n [NAMESPACE] -o yaml
  # Vérifier que le nom de la clé correspond exactement (case-sensitive!).

  # Pod Events pour les erreurs de montage:
  kubectl describe pod [POD] -n [NAMESPACE] | grep -A 20 Events

  CE QUE VOUS VOYEZ SI LE SECRET EST MANQUANT:
  ─────────────────────────────────────────────
  Events:
    Warning  Failed     2m   kubelet  Error: secret "taskmanager-secrets" not found
  # -> Le Secret n'existe pas dans ce namespace!

  FIX:
  ─────
  # Vérifier le namespace: les Secrets sont namespaced!
  kubectl get secrets -n production
  # Un Secret dans "default" n'est PAS accessible depuis "production".

────────────────────────────────────────────────────────────────────────────────
PROBLÈME 6 — HPA ne scale pas
────────────────────────────────────────────────────────────────────────────────

  SYMPTÔME: CPU > 90% mais le HPA ne crée pas de nouveaux Pods.

  DIAGNOSTIC:
  ────────────
  kubectl describe hpa [NOM] -n [NAMESPACE]

  CE QUE VOUS VOYEZ:
  ───────────────────
  Conditions:
    Type         Status  Reason              Message
    ─────        ──────  ──────              ───────
    AbleToScale  True    ReadyForNewScale    ready for scaling
    ScalingLimited True  TooFewReplicas      the desired replica count is less than the minimum replica count
  # -> Le HPA VEUT scaler mais est bloqué par minReplicas ou maxReplicas

  # Autre message courant:
  # "unable to fetch metrics from resource metrics API: the server is currently unable to handle the request"
  # -> metrics-server n'est pas installé!
  kubectl get pods -n kube-system | grep metrics-server
  minikube addons enable metrics-server

  # Autre: "unknown" dans la colonne TARGETS:
  # NAME   REFERENCE   TARGETS     MINPODS  MAXPODS  REPLICAS
  # hpa    Deploy/api  <unknown>/70%  3     20       3
  # -> Les requests CPU ne sont pas définies dans le Pod!
  # HPA ne peut pas calculer le pourcentage sans "requests.cpu" défini.

  FIX:
  ─────
  # S'assurer que resources.requests.cpu est défini dans le Deployment.
  # Sans requests: le HPA ne peut pas calculer le % d'utilisation.

────────────────────────────────────────────────────────────────────────────────
PROBLÈME 7 — PVC en état "Pending"
────────────────────────────────────────────────────────────────────────────────

  SYMPTÔME: kubectl get pvc -> STATUS: Pending

  DIAGNOSTIC:
  ────────────
  kubectl describe pvc [NOM] -n [NAMESPACE]

  CAS 1 — "no persistent volumes available"
  ──────────────────────────────────────────
  CAUSE: Aucun PV existant ne correspond aux critères du PVC.
  FIX:
  # Voir les PV disponibles:
  kubectl get pv
  # Vérifier: accessModes, storageClassName, capacity
  # Solution: créer un PV manuellement ou configurer un provisioner dynamique.

  CAS 2 — "waiting for first consumer to be created before binding"
  ──────────────────────────────────────────────────────────────────
  CAUSE: StorageClass avec volumeBindingMode: WaitForFirstConsumer.
  EXPLICATION: Normal! Le PV est créé seulement quand un Pod utilise le PVC.
  FIX: Pas d'action nécessaire — déployer le Pod qui utilise le PVC.

────────────────────────────────────────────────────────────────────────────────
PROBLÈME 8 — Ressources "Unknown" après panne de node
────────────────────────────────────────────────────────────────────────────────

  SYMPTÔME: Des Pods restent en "Unknown" longtemps après la panne d'un node.

  EXPLICATION:
  ─────────────
  K8s attend 5 minutes (pod-eviction-timeout) avant de reschedule.
  Il espère que le node va revenir. C'est intentionnel (éviter les split-brain).

  FIX URGENT (forcer le reschedule):
  ────────────────────────────────────
  # Supprimer le Pod manuellement (StatefulSet/Deployment le recréera):
  kubectl delete pod [NOM] -n [NAMESPACE] --grace-period=0 --force

  # Ou drainer le node entier (meilleure pratique):
  kubectl drain [NODE] --ignore-daemonsets --delete-emptydir-data --force

────────────────────────────────────────────────────────────────────────────────
COMMANDES DE DÉBOGAGE ESSENTIELLES
────────────────────────────────────────────────────────────────────────────────

  # Vue d'ensemble complète d'un namespace:
  kubectl get all -n [NAMESPACE]

  # Voir TOUS les événements (triés par date):
  kubectl get events -n [NAMESPACE] --sort-by='.lastTimestamp'

  # Voir seulement les événements Warning:
  kubectl get events -n [NAMESPACE] --field-selector=type=Warning

  # Décrire une ressource (la commande la plus utile pour déboguer!):
  kubectl describe [TYPE] [NOM] -n [NAMESPACE]

  # Voir les logs de tous les Pods d'un déploiement:
  kubectl logs -l app=taskmanager -n [NAMESPACE] --all-containers

  # Exec interactif dans un Pod:
  kubectl exec -it [POD] -n [NAMESPACE] -- /bin/bash

  # Exec avec un conteneur spécifique (Pod multi-conteneurs):
  kubectl exec -it [POD] -n [NAMESPACE] -c api -- /bin/bash

  # Lancer un Pod temporaire pour tester le réseau:
  kubectl run debug --image=busybox --rm -it -n [NAMESPACE] -- sh
  # Dans le Pod: wget, nc, nslookup, ping...

  # Tester la résolution DNS depuis le cluster:
  kubectl run dns-test --image=busybox --rm -it -n [NAMESPACE] \
    -- nslookup taskmanager-api.production.svc.cluster.local

  # Voir les ressources utilisées par les Pods:
  kubectl top pods -n [NAMESPACE]
  kubectl top pods -n [NAMESPACE] --containers    # Par conteneur

  # Voir les ressources utilisées par les nodes:
  kubectl top nodes

  # Voir les manifestes YAML des ressources créées:
  kubectl get deployment [NOM] -n [NAMESPACE] -o yaml

  # Voir seulement un champ spécifique (jsonpath):
  kubectl get pod [POD] -n [NAMESPACE] -o jsonpath='{.status.phase}'
  kubectl get pod [POD] -n [NAMESPACE] -o jsonpath='{.spec.containers[0].image}'

  # Comparer ce qui est dans K8s vs le fichier local:
  kubectl diff -f kubernetes/base/api/deployment.yaml

================================================================================
PARTIE 20 — REDIS DEPLOYMENT ET FINALISATION DE L'APPLICATION
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 20.1 — MANIFESTES REDIS COMPLETS
────────────────────────────────────────────────────────────────────────────────

  ── kubernetes/base/redis/deployment.yaml ───────────────────────────────────
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: redis
    namespace: production
    labels:
      app: redis
      component: cache
  spec:
    replicas: 1
    selector:
      matchLabels:
        app: redis
    strategy:
      type: Recreate    # Redis avec 1 replica: Recreate évite 2 instances simultanées
    template:
      metadata:
        labels:
          app: redis
          component: cache
      spec:
        securityContext:
          runAsNonRoot: true
          runAsUser: 999         # UID redis dans l'image officielle
          fsGroup: 999

        containers:
        - name: redis
          image: redis:7-alpine
          command:
          - redis-server
          - --maxmemory
          - 256mb
          - --maxmemory-policy
          - allkeys-lru           # Éviction LRU quand mémoire pleine
          - --requirepass
          - $(REDIS_PASSWORD)     # Mot de passe depuis Secret

          ports:
          - containerPort: 6379
            name: redis

          env:
          - name: REDIS_PASSWORD
            valueFrom:
              secretKeyRef:
                name: taskmanager-secrets
                key: redis-password

          resources:
            requests:
              cpu: "50m"
              memory: "128Mi"
            limits:
              cpu: "200m"
              memory: "384Mi"    # Un peu plus que maxmemory pour Redis overhead

          livenessProbe:
            exec:
              command:
              - redis-cli
              - -a
              - $(REDIS_PASSWORD)
              - ping
            initialDelaySeconds: 15
            periodSeconds: 10

          readinessProbe:
            exec:
              command:
              - redis-cli
              - -a
              - $(REDIS_PASSWORD)
              - ping
            initialDelaySeconds: 5
            periodSeconds: 5

          volumeMounts:
          - name: redis-data
            mountPath: /data    # Persistance des données Redis (optionnel)

          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: false   # Redis a besoin d'écrire dans /data

        volumes:
        - name: redis-data
          emptyDir: {}           # En production: utiliser un PVC pour la persistance

  ── kubernetes/base/redis/service.yaml ──────────────────────────────────────
  apiVersion: v1
  kind: Service
  metadata:
    name: redis-service
    namespace: production
    labels:
      app: redis
  spec:
    type: ClusterIP
    selector:
      app: redis
    ports:
    - name: redis
      port: 6379
      targetPort: 6379

================================================================================
PARTIE 21 — CHEATSHEET EXHAUSTIF KUBERNETES ET KUBECTL
================================================================================

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
KUBECTL — COMMANDES ESSENTIELLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ─── APPLIQUER / CRÉER / SUPPRIMER ──────────────────────────────────────────
  kubectl apply -f fichier.yaml                 # Créer ou MAJ depuis fichier
  kubectl apply -f dossier/                     # Appliquer tous les YAML d'un dossier
  kubectl apply -k overlays/production/         # Kustomize
  kubectl delete -f fichier.yaml               # Supprimer les ressources d'un fichier
  kubectl delete pod [NOM] -n [NS]             # Supprimer un Pod (recréé par Deployment)
  kubectl delete pod [NOM] --grace-period=0 --force  # Forcer la suppression
  kubectl create secret generic [NOM] --from-literal=key=value  # Créer un Secret
  kubectl create configmap [NOM] --from-file=fichier.conf       # ConfigMap depuis fichier

  ─── LISTER / VOIR ──────────────────────────────────────────────────────────
  kubectl get pods                              # Pods du namespace courant
  kubectl get pods -A                           # Pods de TOUS les namespaces
  kubectl get pods -n [NS]                      # Pods d'un namespace spécifique
  kubectl get pods -o wide                      # + colonnes IP et NODE
  kubectl get pods --show-labels               # + colonne labels
  kubectl get pods -l app=taskmanager          # Filtrer par label
  kubectl get pods -w                           # Watch (actualisation auto)
  kubectl get all -n [NS]                       # Toutes les ressources d'un NS
  kubectl get all -A                            # Toutes les ressources de tous NS
  kubectl get [TYPE] -o yaml                   # Format YAML complet
  kubectl get [TYPE] -o json                   # Format JSON
  kubectl get [TYPE] -o jsonpath='{.spec.xxx}' # Extraire un champ
  kubectl get nodes --show-labels              # Labels des nodes

  ─── DÉCRIRE / INSPECTER ────────────────────────────────────────────────────
  kubectl describe pod [NOM] -n [NS]           # Détail complet d'un Pod
  kubectl describe node [NOM]                  # Détail d'un node
  kubectl describe deployment [NOM] -n [NS]    # Détail d'un Deployment
  kubectl events -n [NS]                       # Événements du namespace
  kubectl events -n [NS] --for pod/[POD]       # Événements d'un Pod spécifique

  ─── LOGS ────────────────────────────────────────────────────────────────────
  kubectl logs [POD] -n [NS]                   # Logs du conteneur
  kubectl logs [POD] -n [NS] -f                # Suivre les logs en live
  kubectl logs [POD] -n [NS] --previous        # Logs du run précédent (après crash)
  kubectl logs [POD] -n [NS] -c [CONTAINER]    # Logs d'un conteneur spécifique
  kubectl logs -l app=taskmanager -n [NS]      # Logs de tous les Pods matching
  kubectl logs [POD] --tail=100 -n [NS]        # 100 dernières lignes

  ─── EXÉCUTER ────────────────────────────────────────────────────────────────
  kubectl exec -it [POD] -n [NS] -- bash       # Shell interactif
  kubectl exec -it [POD] -n [NS] -c [C] -- sh # Shell dans conteneur spécifique
  kubectl exec [POD] -n [NS] -- env            # Lister les variables d'env
  kubectl exec [POD] -n [NS] -- cat /app/config.py  # Lire un fichier

  ─── PORT-FORWARD ────────────────────────────────────────────────────────────
  kubectl port-forward pod/[POD] 8080:5000 -n [NS]     # Pod -> local
  kubectl port-forward svc/[SVC] 8080:80 -n [NS]       # Service -> local
  kubectl port-forward deploy/[DEPLOY] 8080:80 -n [NS] # Deployment -> local

  ─── SCALING ─────────────────────────────────────────────────────────────────
  kubectl scale deployment [NOM] --replicas=5 -n [NS]  # Scaler manuellement
  kubectl rollout status deployment/[NOM] -n [NS]       # Suivre un déploiement
  kubectl rollout history deployment/[NOM] -n [NS]      # Historique
  kubectl rollout undo deployment/[NOM] -n [NS]         # Rollback
  kubectl rollout undo deployment/[NOM] --to-revision=2 # Rollback à révision 2
  kubectl rollout pause deployment/[NOM] -n [NS]        # Pauser
  kubectl rollout resume deployment/[NOM] -n [NS]       # Reprendre
  kubectl set image deployment/[NOM] api=image:tag -n [NS]  # Changer l'image

  ─── CONTEXTS ET NAMESPACES ──────────────────────────────────────────────────
  kubectl config get-contexts                  # Lister les clusters configurés
  kubectl config current-context               # Contexte actuel
  kubectl config use-context [CTX]             # Changer de cluster
  kubectl config set-context --current --namespace=[NS]  # Changer de NS par défaut
  kubectl config view                          # Voir kubeconfig complet

  ─── NODES ───────────────────────────────────────────────────────────────────
  kubectl get nodes                            # Lister les nodes
  kubectl get nodes -o wide                    # + IP externe
  kubectl describe node [NOM]                  # Détail du node
  kubectl cordon [NODE]                        # Interdire nouveaux Pods
  kubectl uncordon [NODE]                      # Réautoriser les Pods
  kubectl drain [NODE] --ignore-daemonsets --delete-emptydir-data  # Évacuer
  kubectl taint nodes [NODE] key=value:NoSchedule  # Ajouter une taint
  kubectl taint nodes [NODE] key:NoSchedule-   # Supprimer une taint

  ─── DEBUG ───────────────────────────────────────────────────────────────────
  kubectl top pods -n [NS]                     # Ressources par Pod
  kubectl top pods -n [NS] --containers        # Ressources par conteneur
  kubectl top nodes                            # Ressources par node
  kubectl diff -f fichier.yaml                 # Différences avant apply
  kubectl dry-run=client -f fichier.yaml       # Simuler sans appliquer
  kubectl auth can-i delete pods -n [NS]       # Vérifier ses permissions
  kubectl auth can-i delete pods -n [NS] --as=bob  # Vérifier les permissions de bob

  ─── RBAC ────────────────────────────────────────────────────────────────────
  kubectl get roles -n [NS]                    # Rôles dans un namespace
  kubectl get clusterroles                     # ClusterRoles
  kubectl get rolebindings -n [NS]             # Bindings dans un namespace
  kubectl auth can-i --list -n [NS]            # Ses propres permissions
  kubectl auth can-i --list -n [NS] --as=bob   # Permissions de bob

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HELM — COMMANDES ESSENTIELLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  helm repo add bitnami https://charts.bitnami.com/bitnami  # Ajouter un dépôt
  helm repo update                                           # MAJ les dépôts
  helm search repo postgresql                                # Chercher un chart
  helm show values bitnami/postgresql                        # Voir les values d'un chart

  helm install [RELEASE] [CHART] -n [NS] \
    --create-namespace \
    --values values.yaml                         # Installer

  helm install [RELEASE] [CHART] -n [NS] \
    --set key=value \
    --set image.tag=1.0.0                        # Installer avec valeurs inline

  helm upgrade [RELEASE] [CHART] -n [NS] \
    --values values.yaml \
    --atomic                                     # MAJ avec rollback auto si échec

  helm rollback [RELEASE] 1 -n [NS]             # Rollback à la révision 1
  helm list -A                                   # Lister toutes les releases
  helm status [RELEASE] -n [NS]                  # Statut d'une release
  helm history [RELEASE] -n [NS]                 # Historique d'une release
  helm get values [RELEASE] -n [NS]              # Values utilisées
  helm get manifest [RELEASE] -n [NS]            # YAML générés et déployés
  helm uninstall [RELEASE] -n [NS]               # Désinstaller
  helm template [RELEASE] [CHART] --values values.yaml  # Prévisualiser les YAML
  helm lint [CHART_DIR]                          # Valider un chart
  helm dependency update [CHART_DIR]             # Télécharger les dépendances
  helm package [CHART_DIR]                       # Empaqueter un chart en .tgz

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MINIKUBE — COMMANDES ESSENTIELLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  minikube start --driver=docker --cpus=4 --memory=8192  # Démarrer
  minikube stop                              # Arrêter (données conservées)
  minikube delete                            # Supprimer complètement
  minikube status                            # État du cluster
  minikube ip                                # IP du node minikube
  minikube ssh                               # SSH dans le node
  minikube dashboard                         # Ouvrir le tableau de bord
  minikube tunnel                            # Tunnel pour LoadBalancer services
  minikube addons list                       # Lister les addons
  minikube addons enable metrics-server      # Activer un addon
  minikube image load [IMAGE]                # Charger une image Docker locale
  minikube image ls                          # Lister les images chargées
  eval $(minikube docker-env)               # Utiliser le daemon Docker de minikube

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STRUCTURE DES MANIFESTES KUBERNETES (RAPPEL RAPIDE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  CHAMPS COMMUNS À TOUTES LES RESSOURCES:
  ─────────────────────────────────────────
  apiVersion: [group/]version   # v1, apps/v1, networking.k8s.io/v1...
  kind: [Type]                  # Pod, Deployment, Service, ConfigMap...
  metadata:
    name: [nom]                 # Nom unique dans le namespace
    namespace: [ns]             # Namespace (si omis: namespace courant)
    labels:                     # Étiquettes pour sélection
      key: value
    annotations:                # Métadonnées pour outils tiers
      key: value
  spec:                         # L'état désiré (différent selon le kind)
    ...
  status:                       # L'état actuel (rempli par K8s, ne pas éditer)
    ...

  APIVERSION PAR KIND:
  ─────────────────────
  Pod, Service, ConfigMap, Secret, PVC, PV, Namespace, ServiceAccount, Endpoints
                                              -> apiVersion: v1
  Deployment, ReplicaSet, StatefulSet, DaemonSet, ControllerRevision
                                              -> apiVersion: apps/v1
  Job, CronJob                                -> apiVersion: batch/v1
  Ingress, NetworkPolicy                      -> apiVersion: networking.k8s.io/v1
  HPA                                         -> apiVersion: autoscaling/v2
  StorageClass                                -> apiVersion: storage.k8s.io/v1
  Role, ClusterRole, RoleBinding, ClusterRoleBinding
                                              -> apiVersion: rbac.authorization.k8s.io/v1
  PodDisruptionBudget                         -> apiVersion: policy/v1
  VPA                                         -> apiVersion: autoscaling.k8s.io/v1

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
UNITÉS DE RESSOURCES KUBERNETES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  CPU:
  1000m = 1 vCPU (1 core)
  500m  = 0.5 vCPU
  100m  = 0.1 vCPU
  10m   = 0.01 vCPU (minimum raisonnable)

  MÉMOIRE:
  1Ki = 1024 octets (kibibyte)
  1Mi = 1024 Ki = 1,048,576 octets (mebibyte) <- UTILISÉ dans K8s
  1Gi = 1024 Mi (gibibyte)
  1K  = 1000 octets (kilobyte)
  1M  = 1000 K (megabyte)
  1G  = 1000 M (gigabyte)

  RECOMMANDATIONS REQUESTS/LIMITS:
  ──────────────────────────────────
  ┌────────────────────┬──────────────────┬──────────────────┐
  │ Type de service    │ Requests         │ Limits           │
  ├────────────────────┼──────────────────┼──────────────────┤
  │ API Flask (dev)    │ 100m / 128Mi     │ 500m / 512Mi     │
  │ API Flask (prod)   │ 200m / 256Mi     │ 1000m / 1Gi      │
  │ PostgreSQL         │ 250m / 512Mi     │ 2000m / 2Gi      │
  │ Redis              │ 50m  / 128Mi     │ 200m / 384Mi     │
  │ Nginx              │ 50m  / 64Mi      │ 100m / 128Mi     │
  │ Fluentd            │ 100m / 200Mi     │ 200m / 500Mi     │
  │ Prometheus         │ 250m / 512Mi     │ 1000m / 2Gi      │
  │ Grafana            │ 100m / 256Mi     │ 500m / 512Mi     │
  └────────────────────┴──────────────────┴──────────────────┘

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CYCLE DE VIE D'UN POD: LES ÉTATS POSSIBLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Pending           -> Schedulé mais pas encore démarré (téléchargement image...)
  Init:0/2          -> Init containers en cours (0 sur 2 terminés)
  PodInitializing   -> Init containers terminés, conteneurs principaux démarrent
  Running           -> Au moins 1 conteneur en cours d'exécution
  Completed         -> Tous les conteneurs ont terminé avec succès (exit 0) [Job]
  Error             -> Un conteneur a terminé avec une erreur (exit != 0)
  CrashLoopBackOff  -> Le conteneur crashe en boucle (backoff exponentiel)
  OOMKilled         -> Tué par le kernel (mémoire insuffisante)
  Terminating       -> En cours de suppression (SIGTERM envoyé)
  Unknown           -> K8s ne peut plus contacter le node
  ImagePullBackOff  -> Impossible de télécharger l'image (mauvais tag, auth...)
  ErrImagePull      -> Erreur lors du téléchargement de l'image
  ContainerCreating -> L'image est téléchargée, le conteneur est en création

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ORDRE DE DÉPLOIEMENT RECOMMANDÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1.  Namespaces
  2.  StorageClasses
  3.  RBAC (ServiceAccounts, Roles, Bindings)
  4.  ConfigMaps
  5.  Secrets
  6.  PersistentVolumeClaims
  7.  StatefulSets (bases de données)
  8.  Services (Headless pour StatefulSets)
  9.  Deployments (applications)
  10. Services (pour les Deployments)
  11. Jobs de migration
  12. Ingress
  13. HPA
  14. PodDisruptionBudgets
  15. NetworkPolicies (activer en dernier pour éviter de bloquer le déploiement)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DNS KUBERNETES: NOMMAGE DES SERVICES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Format complet: [service].[namespace].svc.cluster.local
  Format court (même namespace): [service]
  Format moyen (autre namespace): [service].[namespace]

  Exemples pour le projet TaskManager:
  postgres-service.production.svc.cluster.local  -> PostgreSQL depuis n'importe où
  postgres-service.production                    -> PostgreSQL depuis autre NS
  postgres-service                               -> PostgreSQL depuis production

  Pour les StatefulSets (pods individuels):
  postgres-0.postgres-headless.production.svc.cluster.local -> Pod postgres-0 spécifique
  postgres-1.postgres-headless.production.svc.cluster.local -> Pod postgres-1 spécifique

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LES 15 RÈGLES D'OR KUBERNETES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1. JAMAIS modifier directement les Pods — toujours via Deployment/StatefulSet.
     -> git push -> ArgoCD -> kubectl apply, jamais kubectl edit pod

  2. TOUJOURS définir resources.requests ET resources.limits.
     -> Sans requests: le Scheduler ne peut pas placer le Pod intelligemment.
     -> Sans limits: un Pod peut affamer le node entier.

  3. TOUJOURS utiliser des tags d'image spécifiques (jamais :latest en prod).
     -> :latest = image non reproductible, débogage impossible.
     -> :1.2.3  = version déterministe, rollback possible.

  4. JAMAIS stocker de secrets en clair dans les YAML commités dans Git.
     -> Utiliser External Secrets Operator, Vault CSI, ou Sealed Secrets.

  5. TOUJOURS configurer les 3 probes pour les apps en production.
     -> startupProbe: protection au démarrage
     -> livenessProbe: redémarrage automatique si l'app plante
     -> readinessProbe: pas de trafic vers les Pods non prêts

  6. TOUJOURS utiliser un non-root user dans les conteneurs.
     -> securityContext.runAsNonRoot: true
     -> runAsUser: 1000 (ou l'UID approprié)

  7. TOUJOURS avoir au moins 3 replicas pour les services critiques.
     -> 1 replica: pas de HA, downtime au moindre problème.
     -> 3 replicas: tolérance à 1 panne + rolling update sans downtime.

  8. TOUJOURS configurer podAntiAffinity pour répartir les Pods sur plusieurs nodes.
     -> Évite que tous les Pods se retrouvent sur le même node.

  9. TOUJOURS utiliser des namespaces pour isoler les environnements.
     -> dev, staging, production dans le même cluster = namespaces séparés.

  10. TOUJOURS configurer des ResourceQuota par namespace.
      -> Évite qu'un namespace consomme toutes les ressources du cluster.

  11. TOUJOURS configurer les NetworkPolicies en production.
      -> Default: deny all, puis ouvrir au cas par cas.
      -> Principe du least privilege réseau.

  12. TOUJOURS configurer PodDisruptionBudget pour les apps critiques.
      -> Garantit la disponibilité pendant les maintenances de nodes.

  13. TOUJOURS sauvegarder etcd régulièrement.
      -> etcd = mémoire du cluster. Perdu = cluster perdu.
      -> CronJob de backup quotidien vers un stockage externe.

  14. JAMAIS utiliser hostPath volumes en production sauf pour les DaemonSets.
      -> hostPath = couplage fort au node, anti-pattern K8s.
      -> Utiliser PVC + StorageClass pour le stockage persistant.

  15. TOUJOURS tester les rollbacks avant d'en avoir besoin.
      -> Vérifier régulièrement que kubectl rollout undo fonctionne.
      -> Automatiser les smoke tests post-déploiement.

================================================================================
TABLEAU DE BORD DES RESPONSABILITÉS PAR RÔLE
================================================================================

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  ALICE — Lead DevOps / Administratrice du cluster                       │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  FAIT UNE SEULE FOIS (setup initial):                                   │
  │  [OK] Installe et configure Minikube/kind                                  │
  │  [OK] Crée les namespaces et ResourceQuotas                               │
  │  [OK] Crée les StorageClasses                                             │
  │  [OK] Configure les Secrets (credentials BDD, JWT...)                     │
  │  [OK] Déploie PostgreSQL (StatefulSet) et Redis                           │
  │  [OK] Configure le RBAC (roles, bindings, serviceaccounts)               │
  │  [OK] Installe Prometheus + Grafana + Alertmanager                        │
  │  [OK] Installe la stack EFK (logs)                                        │
  │  [OK] Installe et configure ArgoCD                                        │
  │  [OK] Configure les NetworkPolicies                                       │
  │  [OK] Configure PodDisruptionBudgets                                      │
  │  [OK] Configure l'Ingress Controller et les certificats TLS              │
  │  [OK] Crée le Chart Helm de l'application                                 │
  │  [OK] Crée la structure Kustomize (base + overlays)                      │
  │                                                                          │
  │  FAIT À CHAQUE RELEASE:                                                 │
  │  [OK] Review les manifestes K8s des PRs de Bob et Claire                 │
  │  [OK] Lance les migrations de base de données (Jobs)                     │
  │  [OK] Surveille le déploiement en production via ArgoCD et Grafana       │
  │  [OK] Gère les incidents (node down, rollback, scaling manuel)           │
  │  [OK] Met à jour les dépendances Helm (bitnami/postgresql...)            │
  │                                                                          │
  │  COMMANDES LES PLUS UTILISÉES:                                         │
  │  kubectl get all -A                                                    │
  │  kubectl describe pod [POD] -n [NS]                                   │
  │  kubectl drain/uncordon [NODE]                                        │
  │  helm upgrade [RELEASE] [CHART] --atomic                              │
  │  kubectl rollout undo deployment/[NOM] -n production                  │
  │  kubectl top nodes && kubectl top pods -A                             │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  BOB — Développeur backend / Features métier                           │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  FAIT UNE SEULE FOIS (setup):                                           │
  │  [OK] Installe kubectl, Helm, Minikube                                    │
  │  [OK] Configure son kubeconfig (~/.kube/config)                          │
  │  [OK] Se familiarise avec k9s pour naviguer dans le cluster              │
  │                                                                          │
  │  FAIT À CHAQUE FEATURE:                                                 │
  │  [OK] Développe le code Python et les tests                              │
  │  [OK] Build l'image Docker avec le bon tag (1.x.x)                       │
  │  [OK] Met à jour le tag dans kustomization.yaml ou values.yaml           │
  │  [OK] Crée les Jobs de migration si nécessaire                           │
  │  [OK] Teste en local: kubectl apply -k overlays/development/             │
  │  [OK] Port-forward pour tester: kubectl port-forward svc/api 8080:80    │
  │  [OK] Ouvre une PR (code + manifestes K8s ensemble)                      │
  │  [OK] Surveille ArgoCD après le merge pour confirmer le déploiement      │
  │                                                                          │
  │  COMMANDES LES PLUS UTILISÉES:                                         │
  │  minikube image load ghcr.io/equipe/taskmanager-api:1.x.x            │
  │  kubectl apply -k kubernetes/overlays/development/                    │
  │  kubectl get pods -n development -w                                   │
  │  kubectl logs -l app=taskmanager -n development -f                    │
  │  kubectl port-forward svc/taskmanager-api 8080:80 -n development     │
  │  kubectl rollout status deployment/taskmanager-api -n development     │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  CLAIRE — Développeuse backend / QA / Observabilité                   │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  RESPONSABILITÉS SPÉCIFIQUES K8S:                                       │
  │  [OK] Maintenir et améliorer les PrometheusRules (alertes)               │
  │  [OK] Créer et maintenir les dashboards Grafana                          │
  │  [OK] Configurer et tester les NetworkPolicies                           │
  │  [OK] Lancer les tests de charge (Locust) et analyser les résultats      │
  │  [OK] Vérifier que le HPA fonctionne correctement                       │
  │  [OK] S'assurer que les probes sont correctement configurées             │
  │  [OK] Review les manifestes K8s des PRs de Bob                          │
  │                                                                          │
  │  COMMANDES LES PLUS UTILISÉES:                                         │
  │  kubectl get hpa -n production -w                                     │
  │  kubectl top pods -n production                                       │
  │  kubectl get networkpolicies -n production                            │
  │  kubectl logs [POD] -n production -f                                  │
  │  kubectl run debug --image=busybox --rm -it -n production -- sh      │
  │  helm upgrade prometheus prometheus-community/kube-prometheus-stack   │
  └──────────────────────────────────────────────────────────────────────────┘

================================================================================
RÉCAPITULATIF: CE QUE CE GUIDE A COUVERT
================================================================================

  [OK] Concepts fondamentaux:
     - Architecture cluster (Control Plane, Worker Nodes, etcd)
     - Les 3 zones Git et leur équivalent K8s
     - Les ressources K8s et leurs rôles
     - kubectl et sa syntaxe

  [OK] Installation complète:
     - kubectl (Mac, Ubuntu, Windows)
     - Minikube avec addons
     - kind (multi-nodes)
     - Helm et les dépôts essentiels
     - k9s

  [OK] Application Flask complète:
     - Code Python (Flask, SQLAlchemy, Redis, JWT, Prometheus)
     - Routes: tasks, auth, health, metrics, liveness, readiness, startup
     - Dockerfiles multi-stage (dev et production)
     - Tests de charge Locust

  [OK] Toutes les ressources Kubernetes avec YAML complets:
     - Pod (avec toutes les options: probes, sécurité, volumes)
     - Deployment (rolling update, init containers, sidecar)
     - StatefulSet (PostgreSQL avec volumes persistants)
     - DaemonSet (Fluentd)
     - Service (ClusterIP, NodePort, Headless)
     - Ingress (NGINX, TLS, rate limiting, CORS)
     - ConfigMap et Secret (toutes les méthodes de montage)
     - PersistentVolume, PVC, StorageClass
     - Namespace et ResourceQuota/LimitRange
     - RBAC (Role, ClusterRole, RoleBinding, ServiceAccount)
     - HPA (métriques CPU, mémoire, custom)
     - VPA
     - Job (migration BDD)
     - CronJob (backup PostgreSQL)
     - NetworkPolicy (default-deny + règles permissives)
     - PodDisruptionBudget
     - Affinity, AntiAffinity
     - Taints et Tolerations

  [OK] Helm:
     - Créer un Chart complet avec templates Go
     - values.yaml multi-environnement
     - Subcharts (PostgreSQL, Redis)
     - Déploiement, upgrade, rollback

  [OK] Kustomize:
     - Structure base + overlays
     - Patches strategiques
     - namePrefix, images, commonLabels

  [OK] Monitoring:
     - kube-prometheus-stack (Prometheus + Grafana + Alertmanager)
     - ServiceMonitor
     - PrometheusRule (alertes complètes)
     - Dashboard Grafana

  [OK] Logging:
     - Stack EFK (Elasticsearch + Fluentd + Kibana)
     - Configuration Fluentd complète

  [OK] GitOps:
     - ArgoCD installation
     - Application ArgoCD (sync automatique depuis Git)
     - AppProject (RBAC ArgoCD)
     - Workflow GitOps complet

  [OK] Scénarios complets:
     1. Alice migre l'app de docker-compose vers K8s
     2. Bob développe et déploie une feature
     3. Claire teste la charge et observe le HPA
     4. Alice gère une panne de node
     5. Bob rollback un déploiement défaillant
     6. Claire configure et teste les NetworkPolicies
     7. Alice met à jour la version de Kubernetes

  [OK] Troubleshooting exhaustif:
     - Pod Pending (8 cas)
     - CrashLoopBackOff (5 cas)
     - Service inaccessible
     - Ingress défaillant
     - Secret/ConfigMap non monté
     - HPA qui ne scale pas
     - PVC Pending
     - Ressources Unknown après panne

  [OK] Cheatsheet complet:
     - kubectl (toutes les commandes catégorisées)
     - Helm (installation, upgrade, rollback)
     - Minikube
     - Unités de ressources
     - Ordre de déploiement
     - DNS K8s
     - 15 règles d'or

================================================================================
FIN DU GUIDE KUBERNETES
================================================================================