================================================================================
  ████████╗███████╗██████╗ ██████╗  █████╗ ███████╗ ██████╗ ██████╗ ███╗   ███╗
     ██║   ██╔════╝██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔═══██╗██╔══██╗████╗ ████║
     ██║   █████╗  ██████╔╝██████╔╝███████║█████╗  ██║   ██║██████╔╝██╔████╔██║
     ██║   ██╔══╝  ██╔══██╗██╔══██╗██╔══██║██╔══╝  ██║   ██║██╔══██╗██║╚██╔╝██║
     ██║   ███████╗██║  ██║██║  ██║██║  ██║██║     ╚██████╔╝██║  ██║██║ ╚═╝ ██║
     ╚═╝   ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚═╝      ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝
  ╔═╗╔╗╔╔═╗╦╔╗ ╦  ╔═╗  ╦ ╦╦╦  ╦╔═╗╦  ╔═╗
  ╠═╣║║║╚═╗║╠╩╗║  ║╣   ╠╣╠║║  ║╠═╣║  ║╣
  ╩ ╩╝╚╝╚═╝╩╚═╝╩═╝╚═╝  ╩╚╝╩╩═╝╩╩ ╩╩═╝╚═╝
================================================================================
  TERRAFORM & ANSIBLE EN ÉQUIPE DE 3 DÉVELOPPEURS — APPLICATION FLASK COMPLÈTE
  Guide ultra-détaillé pour GRAND DÉBUTANT
  Toutes les fonctionnalités explorées sans exception
  Chaque action: POURQUOI? COMMENT? QUAND? QUI? RÉSULTAT ATTENDU À L'ÉCRAN
================================================================================

[BLACK_RIGHT-POINTING_TRIANGLE] À QUI S'ADRESSE CE GUIDE?
  Ce guide est pour quelqu'un qui:
  - N'a jamais entendu parler de Terraform ou Ansible (ou très peu)
  - Se demande pourquoi ces outils existent alors qu'on a déjà Docker
  - Veut comprendre AVANT de taper des commandes
  - Travaille en équipe sur une API Flask Python
  - Veut un déploiement professionnel, reproductible et automatisé

[BLACK_RIGHT-POINTING_TRIANGLE] COMMENT LIRE CE GUIDE?
  Chaque section suit ce format:
  ┌─ POURQUOI? ──── Pourquoi cet outil / cette commande existe-t-il?
  ├─ QU'EST-CE?  ── Explication claire du concept en français simple
  ├─ QUAND? ─────── Dans quelle situation l'utiliser?
  ├─ COMMENT? ───── Actions concrètes, une par une, avec sorties attendues
  └─ RÉSULTAT ───── Ce que vous devez voir à l'écran exactement

[BLACK_RIGHT-POINTING_TRIANGLE] L'ÉQUIPE (même équipe que le guide Git & GitHub)
  Alice  — Lead dev + DevOps. Responsable Terraform, Ansible, déploiement.
           Machine: MacBook Pro macOS. Rôle IaC: écrit tout, forme Bob et Claire.

  Bob    — Développeur backend Flask.
           Machine: PC Ubuntu 22.04. Rôle IaC: utilise les envs créés, teste.

  Claire — Développeuse + responsable qualité.
           Machine: PC Windows 11 + WSL2. Rôle IaC: valide les déploiements.

[BLACK_RIGHT-POINTING_TRIANGLE] L'APPLICATION DÉPLOYÉE DANS CE GUIDE
  Nom: TaskManager API
  Stack: Python 3.11 + Flask + PostgreSQL 15 + Redis 7 + Nginx + Gunicorn
  Endpoints: GET/POST/PUT/DELETE /tasks — POST /auth/login — GET /health
  Déployée sur: serveurs Ubuntu 22.04 (staging + production)
  Cloud: DigitalOcean (concepts identiques sur AWS / GCP / Azure)

[BLACK_RIGHT-POINTING_TRIANGLE] INFRASTRUCTURE FINALE CONSTRUITE DANS CE GUIDE

  INTERNET
     │
  [DNS: api-staging.equipe.com -> 164.90.x.x]  <- Terraform crée l'enregistrement
     │
  [Pare-feu DigitalOcean: ports 22/80/443 ouverts, 5432/6379 VPC only]
     │                                          <- Terraform crée les règles
  ┌──────────────────── Ubuntu 22.04 ────────────────────────────────────────┐
  │  [Nginx :80/:443]         <- Ansible installe + configure + SSL           │
  │       │                                                                   │
  │  [Gunicorn/Flask :5000]   <- Ansible déploie le code + .env + service    │
  │       │                                                                   │
  │  [PostgreSQL :5432]       <- Ansible installe + crée DB/user + volume    │
  │  [Redis :6379]            <- Ansible installe + configure maxmemory      │
  │  [Node Exporter :9100]    <- Ansible installe le monitoring               │
  └──────────────────────────────────────────────────────────────────────────┘

================================================================================
PARTIE 0 — COMPRENDRE TERRAFORM ET ANSIBLE AVANT DE TOUCHER UN TERMINAL
================================================================================

Avant de taper la moindre commande, il faut comprendre CE QUE FONT ces outils.
Des débutants copient des commandes sans comprendre -> ils créent des serveurs
fantômes qui coûtent de l'argent, ou suppriment la base de données de production.

────────────────────────────────────────────────────────────────────────────────
0.1 — LE PROBLÈME SANS CES OUTILS: LA VIE DE L'ÉQUIPE AVANT TERRAFORM/ANSIBLE
────────────────────────────────────────────────────────────────────────────────

  CONTEXTE:
  ──────────
  L'équipe vient de terminer le développement de TaskManager API.
  Alice doit déployer l'application sur un serveur staging et un serveur prod.
  Elle fait tout à la main via SSH. Voici ce qu'elle tape sur chaque serveur:

  ── LES 28 COMMANDES QU'ALICE TAPE À LA MAIN (PAR SERVEUR) ──────────────────
  ssh ubuntu@164.90.10.1
  sudo apt-get update && sudo apt-get upgrade -y
  sudo apt-get install -y python3.11 python3.11-venv python3-pip
  sudo apt-get install -y nginx postgresql-15 redis-server
  sudo apt-get install -y git curl wget unzip fail2ban ufw
  sudo -u postgres psql -c "CREATE DATABASE taskmanager;"
  sudo -u postgres psql -c "CREATE USER taskmanager WITH PASSWORD 'abc123';"
  sudo -u postgres psql -c "GRANT ALL ON DATABASE taskmanager TO taskmanager;"
  sudo useradd -m -s /bin/bash deploy
  sudo mkdir -p /opt/taskmanager
  sudo chown deploy:deploy /opt/taskmanager
  sudo -u deploy git clone git@github.com:equipe/taskmanager.git /opt/taskmanager
  cd /opt/taskmanager && sudo -u deploy python3.11 -m venv venv
  sudo -u deploy venv/bin/pip install -r requirements.txt
  sudo nano /opt/taskmanager/.env                      # Écrire les vars à la main
  sudo nano /etc/nginx/sites-available/taskmanager     # Configurer Nginx à la main
  sudo ln -s /etc/nginx/sites-available/taskmanager /etc/nginx/sites-enabled/
  sudo nginx -t && sudo systemctl reload nginx
  sudo nano /etc/systemd/system/taskmanager.service    # Créer le service à la main
  sudo systemctl daemon-reload
  sudo systemctl enable taskmanager
  sudo systemctl start taskmanager
  sudo ufw allow 22 && sudo ufw allow 80 && sudo ufw allow 443
  sudo ufw enable
  sudo systemctl enable fail2ban && sudo systemctl start fail2ban
  sudo timedatectl set-timezone Europe/Paris
  curl http://localhost:5000/health
  sudo systemctl status taskmanager
  # -> 28 commandes. ~45 minutes. Et c'est sans les corrections d'erreurs.

  ── LES 5 PROBLÈMES QUE ÇA GÉNÈRE ───────────────────────────────────────────

  PROBLÈME 1 — REPRODUCTIBILITÉ:
  ────────────────────────────────
  Alice refait ces 28 commandes sur le serveur de production.
  Elle oublie: sudo -u postgres psql -c "GRANT ALL ON DATABASE..."
  L'API plante au premier appel: "FATAL: permission denied for database taskmanager"
  "Mais ça marchait en staging!" crie Alice.
  -> Cause: configuration manuelle = erreurs humaines inévitables.

  PROBLÈME 2 — DÉPENDANCE BLOQUANTE:
  ────────────────────────────────────
  Bob veut un serveur de test pour sa feature filtrage-dates.
  Il demande à Alice. Alice est en réunion jusqu'à 17h.
  Bob attend 6 heures. Sa journée est bloquée.
  -> Cause: seule Alice connaît les 28 commandes.

  PROBLÈME 3 — REPRISE APRÈS PANNE:
  ────────────────────────────────────
  Le disque du serveur staging est plein. DigitalOcean le remet à zéro.
  Alice doit refaire les 28 commandes. 45 minutes de travail répété.
  Pire: elle ne se souvient plus exactement de l'étape 14.
  Staging et production divergent imperceptiblement sur la config Nginx.
  -> Cause: la configuration n'est nulle part écrite de façon versionnable.

  PROBLÈME 4 — TRAÇABILITÉ ZÉRO:
  ────────────────────────────────
  "Quelle version de PostgreSQL est en production?" -> Personne ne sait.
  "Qui a changé la config Nginx mardi?" -> Personne ne sait.
  "Pourquoi le serveur est en UTC au lieu de Europe/Paris?" -> Mystère.
  -> Cause: les configurations SSH ne sont pas dans Git.

  PROBLÈME 5 — SCALING DOULOUREUX:
  ──────────────────────────────────
  Le client veut un serveur dans une région différente (London).
  Alice refait les 28 commandes. 45 minutes. Avec des erreurs possibles.
  Si 5 clients veulent chacun leur environnement: 5 × 45 min = 3h45.
  -> Cause: pas de réutilisation, tout est ad hoc.

  ── LA SOLUTION AVEC TERRAFORM ET ANSIBLE ────────────────────────────────────
  Alice écrit du code UNE SEULE FOIS. Ensuite:

  Pour créer le serveur staging (infrastructure):
  terraform apply          -> 9 ressources créées en 90 secondes. Automatiquement.

  Pour configurer le serveur (logiciels + code):
  ansible-playbook site.yml -> tout installé et configuré en 5 minutes.

  Bob veut un serveur de test?
  -> Bob copie 1 dossier, change 2 variables, tape "terraform apply".
  -> Serveur prêt en 90 secondes. SANS Alice. Coût: ~0.04$/heure.

  Serveur qui tombe?
  -> "terraform apply" recrée le même serveur en 90 secondes.
  -> "ansible-playbook site.yml" reconfigure identiquement en 5 minutes.

  Nouveau client dans London?
  -> Changer 1 ligne: region = "lon1" -> terraform apply -> 90 secondes.

────────────────────────────────────────────────────────────────────────────────
0.2 — TERRAFORM: QU'EST-CE QUE C'EST?
────────────────────────────────────────────────────────────────────────────────

  DÉFINITION SIMPLE:
  ───────────────────
  Terraform = outil "Infrastructure as Code" (IaC).
  Il crée et gère des ressources cloud (serveurs, réseaux, DNS...)
  en lisant des fichiers texte (.tf) que vous écrivez.
  Ces fichiers décrivent l'ÉTAT FINAL DÉSIRÉ de votre infrastructure.
  Terraform compare cet état désiré à l'état réel et fait les ajustements.

  ANALOGIE:
  ──────────
  Vous êtes architecte. Vous dessinez un plan: "maison de 3 pièces, porte rouge."
  Terraform = l'entreprise qui LIT votre plan et CONSTRUIT.
  Si vous changez le plan (porte bleue), Terraform repeint la porte.
  Si vous supprimez une pièce du plan, Terraform démolit cette pièce.

  CE QUE TERRAFORM PEUT CRÉER ET GÉRER (liste complète):
  ──────────────────────────────────────────────────────
  Catégorie         Exemples concrets
  ──────────────    ──────────────────────────────────────────────────────────
  Serveurs          VPS DigitalOcean (Droplets), EC2 AWS, VM GCP, Azure VMs
  Réseau            VPC, sous-réseaux, tables de routage, peering
  Sécurité          Pare-feux, groupes de sécurité, ACLs réseau
  DNS               Enregistrements A, CNAME, MX, TXT (nom de domaine)
  Stockage          Volumes de données, buckets S3/GCS, disques attachés
  Base de données   PostgreSQL managé, MySQL, Redis managé (cloud)
  Load balancing    Répartiteur de charge entre plusieurs serveurs
  SSL/TLS           Certificats Let's Encrypt, AWS ACM (auto-renouvelés)
  Clés SSH          Enregistrement et gestion des clés publiques sur le cloud
  Alertes           Notification email/Slack si CPU > 80%, disque > 90%
  CDN               CloudFlare, distribution géographique du contenu
  Kubernetes        Clusters K8s managés (EKS, GKE, AKS)
  Secrets           HashiCorp Vault, AWS Secrets Manager
  Monitoring        Dashboards, métriques, logs centralisés

  CONCEPTS CLÉS — GLOSSAIRE ILLUSTRÉ:
  ────────────────────────────────────
  ┌──────────────┬─────────────────────────────────────────────────────────┐
  │ Terme        │ Définition simple + exemple concret                     │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Provider     │ Plugin qui parle à un cloud spécifique.                 │
  │              │ provider "digitalocean" -> Terraform parle à l'API DO.  │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Resource     │ Une ressource cloud à créer.                            │
  │              │ resource "digitalocean_droplet" "api" { ... }           │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Data source  │ Lire une ressource existante (pas en créer une).        │
  │              │ data "digitalocean_ssh_key" "alice" { name = "..." }    │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Variable     │ Paramètre configurable (région, taille serveur...).     │
  │              │ variable "region" { default = "fra1" }                  │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Output       │ Valeur exportée après création (IP, ID...).             │
  │              │ output "ip" { value = droplet.api.ipv4_address }        │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ State        │ Fichier JSON: la mémoire de Terraform.                  │
  │              │ Sait que "serveur 987654 = resource droplet.api".       │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Plan         │ Aperçu de ce que Terraform VA faire. SANS agir.         │
  │              │ "Je vais créer 9 ressources, modifier 0, détruire 0."   │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Apply        │ Exécution réelle: crée/modifie/détruit les ressources.  │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Destroy      │ Supprime TOUTES les ressources gérées.                  │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Module       │ Groupe de .tf réutilisables = fonction avec paramètres. │
  │              │ module "server" { source = "./modules/server" }         │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Backend      │ Où stocker le state. Local = votre machine.             │
  │              │ Remote = S3 ou Terraform Cloud = partagé en équipe.     │
  └──────────────┴─────────────────────────────────────────────────────────┘

  CE QUE TERRAFORM NE FAIT PAS:
  ────────────────────────────────
  Terraform crée un serveur Ubuntu VIDE. Il ne sait pas qu'on veut y mettre
  Flask, Python, Nginx. Il ne gère pas les services systemd, les fichiers .env,
  les configurations applicatives. C'est le rôle d'Ansible.

────────────────────────────────────────────────────────────────────────────────
0.3 — ANSIBLE: QU'EST-CE QUE C'EST?
────────────────────────────────────────────────────────────────────────────────

  DÉFINITION SIMPLE:
  ───────────────────
  Ansible configure des serveurs existants via SSH.
  Il lit des fichiers YAML (playbooks) qui décrivent ce qui doit être installé
  et configuré, puis exécute ces instructions dans l'ordre sur les serveurs.

  ANALOGIE:
  ──────────
  Terraform a construit la maison (murs, toit, électricité brute).
  Ansible = l'équipe d'aménagement: ils installent les meubles (Python, Flask),
  branchent les appareils (services systemd), peignent les murs (config Nginx),
  et s'assurent que la cuisine est fonctionnelle (l'API répond sur /health).

  3 FORCES MAJEURES D'ANSIBLE:
  ─────────────────────────────
  1. AGENTLESS: aucun logiciel à installer sur les serveurs cibles.
     Ansible utilise SSH qui est déjà présent sur tout serveur Ubuntu.
     Comparaison: Chef et Puppet nécessitent un agent sur chaque serveur.

  2. IDEMPOTENT: exécuter le même playbook 10 fois = résultat identique.
     "Installe Python 3.11" -> s'il est déjà installé: ne fait rien, pas d'erreur.
     "Crée l'utilisateur deploy" -> s'il existe déjà: vérifie les droits, continue.
     Relancer un playbook après une panne partielle: sûr à 100%.

  3. DÉCLARATIF: vous décrivez l'ÉTAT FINAL, pas les étapes pour y arriver.
     Déclaratif: "Le service taskmanager doit être démarré et activé au boot."
     Ansible vérifie l'état actuel et agit en conséquence. Vous ne gérez pas le "si/sinon".

  CONCEPTS CLÉS — GLOSSAIRE ILLUSTRÉ:
  ────────────────────────────────────
  ┌──────────────┬─────────────────────────────────────────────────────────┐
  │ Terme        │ Définition simple + exemple concret                     │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Control node │ Votre machine (Alice) d'où on lance Ansible.            │
  │ Managed node │ Le serveur distant configuré par Ansible.               │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Inventory    │ Liste des serveurs avec IPs, groupes, variables.        │
  │              │ [web]\n164.90.154.23 ansible_user=ubuntu                │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Playbook     │ Fichier YAML = recette de configuration.                │
  │              │ - name: Installer Flask\n  pip: name=flask              │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Task         │ Action unitaire: installer paquet, copier fichier,      │
  │              │ créer utilisateur, démarrer service.                    │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Role         │ Ensemble de tasks organisées = bibliothèque réutilis.   │
  │              │ role "nginx" = toutes les tasks pour installer Nginx.   │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Handler      │ Task déclenchée SEULEMENT si une autre a changé qqch.   │
  │              │ "Redémarre Nginx seulement si nginx.conf a changé."     │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Template     │ Fichier de config avec variables Jinja2.                │
  │              │ server_name {{ domain }}; -> variable remplacée runtime. │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Variable     │ Valeur configurable dans les playbooks.                 │
  │              │ app_port: 5000, db_pass: "{{ vault_db_password }}"      │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Vault        │ Chiffrement AES-256 des secrets Ansible dans Git.       │
  │              │ Les mots de passe sont chiffrés: $ANSIBLE_VAULT;1.2;... │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Module       │ Plugin Ansible pour une action: apt, pip, template,     │
  │              │ service, git, copy, user, file, uri... (3000+ modules)  │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Facts        │ Infos auto-collectées sur le serveur cible.             │
  │              │ ansible_os_family: Debian, ansible_memtotal_mb: 3944    │
  ├──────────────┼─────────────────────────────────────────────────────────┤
  │ Galaxy       │ Dépôt de roles communautaires (= PyPI pour Ansible).    │
  │              │ ansible-galaxy install geerlingguy.nginx                │
  └──────────────┴─────────────────────────────────────────────────────────┘

────────────────────────────────────────────────────────────────────────────────
0.4 — LE FLUX COMPLET: TERRAFORM PUIS ANSIBLE
────────────────────────────────────────────────────────────────────────────────

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  TERRAFORM                          ANSIBLE                             │
  │  ─────────────────────────────────  ─────────────────────────────────  │
  │  Rôle: créer l'infra cloud          Rôle: configurer les serveurs       │
  │  Agit sur: APIs cloud (HTTP)        Agit sur: serveurs via SSH           │
  │  Quand: AVANT qu'un serveur existe  Quand: APRÈS qu'il existe           │
  │  Langage: HCL                       Langage: YAML                       │
  │  Mémoire: terraform.tfstate         Mémoire: vérifie à chaque fois      │
  │  Exemple: créer serveur Ubuntu      Exemple: installer Nginx dessus     │
  └──────────────────────────────────────────────────────────────────────────┘

  FLUX EN 5 ÉTAPES:

  ÉTAPE 1 -> Alice tape: terraform apply
            Terraform lit main.tf -> appelle l'API DigitalOcean
            DigitalOcean crée un VPC, un pare-feu, un serveur Ubuntu 22.04
            Outputs: server_ip = "164.90.154.23", ssh_command = "ssh ubuntu@..."

  ÉTAPE 2 -> Le script d'inventaire dynamique lit terraform output
            inventory/staging.ini: taskmanager-staging ansible_host=164.90.154.23

  ÉTAPE 3 -> Alice tape: ansible-playbook site.yml -i inventory/staging.ini
            Ansible se connecte en SSH à 164.90.154.23

  ÉTAPE 4 -> Ansible exécute les roles dans l'ordre:
            common    -> timezone Europe/Paris, paquets système, fail2ban, ufw
            python    -> Python 3.11, pip, venv
            postgresql -> installe PG15, crée DB "taskmanager" et user, configure
            redis     -> installe Redis7, bind 127.0.0.1, maxmemory 256mb
            flask_app -> clone repo, pip install, fichier .env, service systemd
            nginx     -> installe, configure virtual host, redémarre
            ssl       -> Let's Encrypt, renouvellement automatique (prod only)
            monitoring -> Node Exporter pour Prometheus

  ÉTAPE 5 -> Vérification:
            curl https://api-staging.equipe.com/health
            -> {"status": "healthy", "database": "ok", "redis": "ok"}  [OK]

================================================================================
PARTIE 1 — ALICE INSTALLE LES OUTILS ET PRÉPARE LE PROJET (JOUR 1, MATIN)
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.1 — INSTALLER TERRAFORM
────────────────────────────────────────────────────────────────────────────────

  POURQUOI INSTALLER TERRAFORM EN LOCAL?
  ────────────────────────────────────────
  Terraform s'exécute depuis VOTRE machine (ou depuis GitHub Actions en CI/CD).
  Il communique avec les APIs cloud via HTTPS.
  Vous n'avez PAS besoin d'être sur le serveur pour l'utiliser.

  QUAND?
  ───────
  Une seule fois sur chaque machine de développement.
  Alice l'installe maintenant. Bob et Claire l'installeront pour leurs tests.

  ALICE — MAC:
  ─────────────
  brew tap hashicorp/tap
  brew install hashicorp/tap/terraform

  terraform --version

  CE QUE VOUS VOYEZ:
  ───────────────────
  Terraform v1.7.4
  on darwin_amd64

  SI "command not found": ajouter /usr/local/bin au PATH dans ~/.zshrc.

  BOB — UBUNTU:
  ──────────────
  sudo apt-get update && sudo apt-get install -y gnupg software-properties-common curl
  curl -fsSL https://apt.releases.hashicorp.com/gpg | \
    sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
  echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
    https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
    sudo tee /etc/apt/sources.list.d/hashicorp.list
  sudo apt-get update && sudo apt-get install -y terraform
  terraform --version

  CLAIRE — WINDOWS 11 (dans WSL2):
  ──────────────────────────────────
  # Ouvrir WSL2 Ubuntu, puis mêmes commandes que Bob.
  # Vérifier WSL2: wsl --list --verbose (dans PowerShell)
  # Installer WSL2 si absent: wsl --install (PowerShell admin)

  ACTIVER L'AUTO-COMPLÉTION (Mac/Linux):
  ────────────────────────────────────────
  terraform -install-autocomplete
  source ~/.zshrc
  # terraform pl<TAB> -> terraform plan  /  terraform ap<TAB> -> terraform apply

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.2 — INSTALLER ANSIBLE
────────────────────────────────────────────────────────────────────────────────

  POURQUOI ANSIBLE SEULEMENT SUR LA MACHINE D'ALICE (CONTROL NODE)?
  ──────────────────────────────────────────────────────────────────
  Ansible est "agentless": il s'installe UNIQUEMENT sur la machine qui LANCE
  les playbooks. Les serveurs cibles n'ont besoin que de SSH + Python3.
  Bob et Claire peuvent aussi l'installer pour lancer eux-mêmes des playbooks.

  ALICE — MAC:
  ─────────────
  pip3 install ansible ansible-lint
  # ou via Homebrew: brew install ansible

  ansible --version

  CE QUE VOUS VOYEZ:
  ───────────────────
  ansible [core 2.16.5]
    python version = 3.11.9
    jinja version = 3.1.4

  BOB — UBUNTU:
  ──────────────
  sudo apt-add-repository --yes --update ppa:ansible/ansible
  sudo apt-get install -y ansible
  ansible --version

  CLAIRE — WINDOWS 11 (dans WSL2):
  ──────────────────────────────────
  # Ansible ne tourne PAS nativement sur Windows. WSL2 est obligatoire.
  # Dans WSL2 Ubuntu: mêmes commandes que Bob.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.3 — OUTILS COMPLÉMENTAIRES
────────────────────────────────────────────────────────────────────────────────

  # tflint: linter Terraform (erreurs de syntaxe, arguments dépréciés)
  brew install tflint           # Mac
  curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

  # tfsec: scanner de sécurité (ports ouverts au monde, chiffrement manquant)
  brew install tfsec            # Mac
  go install github.com/aquasecurity/tfsec/cmd/tfsec@latest   # Linux

  # checkov: scanner sécurité multi-outils (Terraform + Ansible + Docker)
  pip3 install checkov

  # ansible-lint: linter playbooks (bonnes pratiques, erreurs)
  pip3 install ansible-lint

  # terraform-docs: génère la doc des modules automatiquement
  brew install terraform-docs   # Mac

  # Vérifier tout:
  terraform --version && ansible --version && tflint --version && checkov --version

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 1.4 — ALICE OBTIENT UN TOKEN API DIGITALOCEAN
────────────────────────────────────────────────────────────────────────────────

  POURQUOI UN TOKEN?
  ───────────────────
  Terraform doit s'authentifier auprès de l'API DigitalOcean pour créer
  des ressources en votre nom. Le token = votre clé d'accès à l'API.

  PROCÉDURE:
  ───────────
  1. cloud.digitalocean.com -> API -> Personal access tokens
  2. "Generate New Token"
  3. Name: "terraform-taskmanager"  /  Expiration: No expiry  /  Write: [OK]
  4. COPIER le token immédiatement (visible une seule fois!)

  LE TOKEN RESSEMBLE À:
  ──────────────────────
  dop_v1_abc123def456ghi789jkl012mno345pqr678stu901vwx234

  STOCKER SANS JAMAIS COMMITTER DANS GIT:
  ─────────────────────────────────────────
  # Dans ~/.zshrc (Mac) ou ~/.bashrc (Linux/WSL):
  echo 'export TF_VAR_do_token="dop_v1_abc123..."' >> ~/.zshrc
  source ~/.zshrc

  # Vérification:
  echo $TF_VAR_do_token
  # -> dop_v1_abc123...   [OK]

  AJOUTER LES CLÉS SSH SUR DIGITALOCEAN:
  ────────────────────────────────────────
  # 1. cloud.digitalocean.com -> Settings -> Security -> SSH Keys -> Add SSH Key
  # 2. Coller le contenu de: cat ~/.ssh/id_ed25519_github.pub
  # 3. Name: "alice-macbook-2024"
  # 4. Répéter pour la clé GitHub Actions:
  ssh-keygen -t ed25519 -C "github-actions@equipe.com" -f ~/.ssh/deploy_key -N ""
  # Ajouter deploy_key.pub -> DigitalOcean avec name "github-actions-deploy"
  # Ajouter le contenu de deploy_key (privée) dans les secrets GitHub Actions

================================================================================
PARTIE 2 — STRUCTURE DU PROJET (ALICE L'ORGANISE)
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 2.1 — ALICE CRÉE L'ARBORESCENCE DU PROJET INFRASTRUCTURE
────────────────────────────────────────────────────────────────────────────────

  POURQUOI SÉPARER L'INFRA DU CODE FLASK?
  ─────────────────────────────────────────
  Code Flask = change souvent (chaque feature). Infra = change rarement.
  Les mélanger brouille les responsabilités et pollue l'historique Git.
  Structure séparée = commits d'infra distincts des commits de code.

  ALICE DANS SON TERMINAL:
  ─────────────────────────
  cd taskmanager/   # Racine du projet (existant depuis le guide Git)

  mkdir -p infrastructure/terraform/environments/{staging,production}
  mkdir -p infrastructure/terraform/modules/{server,network,database,monitoring}
  mkdir -p infrastructure/ansible/inventory/dynamic
  mkdir -p infrastructure/ansible/roles/{common,python,flask_app,postgresql,redis,nginx,ssl,monitoring}
  mkdir -p infrastructure/ansible/{group_vars,host_vars,vars,templates,files}

  ARBORESCENCE RÉSULTANTE:
  ─────────────────────────
  taskmanager/
  ├── app/                                <- Code Flask (inchangé)
  ├── tests/                              <- Tests (inchangés)
  ├── docker/                             <- Dockerfiles (inchangés)
  ├── .github/workflows/                  <- CI/CD (modifiés)
  └── infrastructure/
      ├── terraform/
      │   ├── environments/
      │   │   ├── staging/
      │   │   │   ├── main.tf             <- Ressources cloud staging
      │   │   │   ├── variables.tf        <- Déclaration variables
      │   │   │   ├── outputs.tf          <- Valeurs exportées
      │   │   │   ├── terraform.tfvars    <- Valeurs concrètes (NON commité)
      │   │   │   └── terraform.tfvars.example  <- Template (commité)
      │   │   └── production/
      │   │       ├── main.tf
      │   │       ├── variables.tf
      │   │       ├── outputs.tf
      │   │       └── terraform.tfvars.example
      │   └── modules/
      │       ├── server/                 <- Module réutilisable: créer un VPS
      │       │   ├── main.tf
      │       │   ├── variables.tf
      │       │   └── outputs.tf
      │       ├── network/                <- Module: VPC + pare-feu
      │       │   ├── main.tf
      │       │   ├── variables.tf
      │       │   └── outputs.tf
      │       └── monitoring/             <- Module: alertes CPU/RAM/disque
      │           ├── main.tf
      │           ├── variables.tf
      │           └── outputs.tf
      └── ansible/
          ├── ansible.cfg                 <- Configuration Ansible
          ├── site.yml                    <- Playbook principal (tout)
          ├── deploy.yml                  <- Déploiement code seul
          ├── rollback.yml                <- Rollback version précédente
          ├── database.yml                <- Backup + migrations DB
          ├── security.yml                <- Durcissement sécurité
          ├── inventory/
          │   ├── staging.ini             <- Inventaire statique staging
          │   ├── production.ini          <- Inventaire statique production
          │   └── dynamic/
          │       └── terraform.py        <- Inventaire dynamique (lit TF outputs)
          ├── roles/
          │   ├── common/
          │   │   ├── tasks/main.yml
          │   │   ├── handlers/main.yml
          │   │   └── defaults/main.yml
          │   ├── python/
          │   │   └── tasks/main.yml
          │   ├── flask_app/
          │   │   ├── tasks/main.yml
          │   │   ├── handlers/main.yml
          │   │   ├── defaults/main.yml
          │   │   └── templates/
          │   │       ├── env.j2
          │   │       └── taskmanager.service.j2
          │   ├── postgresql/
          │   │   ├── tasks/main.yml
          │   │   ├── handlers/main.yml
          │   │   └── templates/
          │   │       └── pg_hba.conf.j2
          │   ├── redis/
          │   │   ├── tasks/main.yml
          │   │   ├── handlers/main.yml
          │   │   └── templates/
          │   │       └── redis.conf.j2
          │   ├── nginx/
          │   │   ├── tasks/main.yml
          │   │   ├── handlers/main.yml
          │   │   └── templates/
          │   │       ├── taskmanager.conf.j2
          │   │       └── taskmanager-ssl.conf.j2
          │   ├── ssl/
          │   │   └── tasks/main.yml
          │   └── monitoring/
          │       └── tasks/main.yml
          ├── group_vars/
          │   ├── all.yml                 <- Variables communes à TOUS
          │   ├── staging.yml             <- Variables spécifiques staging
          │   └── production.yml          <- Variables spécifiques production
          ├── host_vars/
          │   └── taskmanager-prod.yml    <- Variables pour 1 serveur précis
          ├── vars/
          │   ├── staging_secrets.yml     <- Secrets chiffrés Vault staging
          │   └── prod_secrets.yml        <- Secrets chiffrés Vault production
          └── templates/                  <- Templates partagés entre roles

  COMMITER LA STRUCTURE:
  ───────────────────────
  git add infrastructure/
  git commit -m "chore(infra): add Terraform and Ansible project structure

  - terraform/environments/staging and production
  - terraform/modules/server, network, monitoring
  - ansible/roles: common, python, flask_app, postgresql, redis, nginx, ssl
  - ansible/group_vars, host_vars, vars, templates"

================================================================================
PARTIE 3 — TERRAFORM: CRÉER L'INFRASTRUCTURE STAGING (ALICE — JOUR 1, APRÈS-MIDI)
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 3.1 — main.tf: LE CŒUR DE L'INFRASTRUCTURE
────────────────────────────────────────────────────────────────────────────────

  ALICE DANS SON ÉDITEUR:
  ───────────────────────
  nano infrastructure/terraform/environments/staging/main.tf

  CONTENU COMPLET (chaque ligne commentée):
  ──────────────────────────────────────────

  # ═══════════════════════════════════════════════════════════════════════════
  # BLOC terraform: méta-configuration de Terraform
  # ═══════════════════════════════════════════════════════════════════════════
  terraform {
    # Version minimale: "~> 1.7" = accepter 1.7.x mais pas 2.0.
    # Protège l'équipe contre comportements imprévus d'une version trop récente.
    required_version = "~> 1.7"

    required_providers {
      digitalocean = {
        source  = "digitalocean/digitalocean"
        version = "~> 2.0"
        # Terraform télécharge ce plugin depuis registry.terraform.io lors de init.
      }
    }

    # BACKEND: où stocker le state terraform.
    # CRUCIAL en équipe: le state doit être PARTAGÉ et VERROUILLÉ.
    # Si le state est local: Bob fait apply sans savoir ce qu'Alice a déjà créé -> double création!
    #
    # Option A — Terraform Cloud (gratuit pour petites équipes, recommandé):
    backend "remote" {
      organization = "equipe-taskmanager"
      workspaces {
        name = "taskmanager-staging"
      }
    }
    # Option B — Backend S3 + DynamoDB (si déjà sur AWS):
    # backend "s3" {
    #   bucket         = "equipe-tf-state"
    #   key            = "staging/terraform.tfstate"
    #   region         = "eu-west-3"
    #   encrypt        = true
    #   dynamodb_table = "terraform-state-lock"  <- verrou distribué
    # }
    # Option C — Local (seulement pour dev solo, JAMAIS en équipe):
    # backend "local" { path = "terraform.tfstate" }
  }

  # ═══════════════════════════════════════════════════════════════════════════
  # PROVIDER: authentification DigitalOcean
  # ═══════════════════════════════════════════════════════════════════════════
  provider "digitalocean" {
    token = var.do_token
    # var.do_token est fourni via: export TF_VAR_do_token="dop_v1_..."
    # JAMAIS écrire le token en dur ici!
  }

  # ═══════════════════════════════════════════════════════════════════════════
  # DATA SOURCES: lire des ressources EXISTANTES sans les créer
  # ═══════════════════════════════════════════════════════════════════════════
  # "data" = interroger l'API DigitalOcean pour récupérer l'ID d'une ressource existante.
  # Ici: récupérer l'ID des clés SSH déjà enregistrées sur DigitalOcean.

  data "digitalocean_ssh_key" "alice" {
    name = "alice-macbook-2024"
    # Doit correspondre exactement au nom dans DigitalOcean -> Security -> SSH Keys.
  }

  data "digitalocean_ssh_key" "deploy" {
    name = "github-actions-deploy"
    # Clé SSH pour les déploiements automatiques depuis GitHub Actions CI/CD.
  }

  # ═══════════════════════════════════════════════════════════════════════════
  # VPC: réseau privé virtuel
  # ═══════════════════════════════════════════════════════════════════════════
  # POURQUOI UN VPC?
  # Sans VPC: PostgreSQL et Redis seraient sur l'adresse publique du serveur.
  # Avec VPC: ils n'ont que des IPs privées (10.0.x.x), inaccessibles depuis Internet.
  # Le pare-feu n'autorise les ports 5432/6379 QUE depuis le VPC (10.0.0.0/16).

  resource "digitalocean_vpc" "main" {
    name     = "${var.project_name}-${var.environment}-vpc"
    region   = var.region
    ip_range = "10.0.0.0/16"
    # 10.0.0.0/16 = IPs privées de 10.0.0.0 à 10.0.255.255 = 65534 serveurs possibles.
  }

  # ═══════════════════════════════════════════════════════════════════════════
  # PARE-FEU: règles réseau au niveau cloud (avant d'atteindre l'OS)
  # ═══════════════════════════════════════════════════════════════════════════
  # POURQUOI UN PARE-FEU CLOUD ET PAS JUSTE ufw SUR LE SERVEUR?
  # Le pare-feu cloud est au niveau réseau (avant même d'atteindre l'OS).
  # ufw est sur l'OS (si l'OS est compromis, ufw peut être désactivé).
  # Les deux ensemble = défense en profondeur.

  resource "digitalocean_firewall" "main" {
    name = "${var.project_name}-${var.environment}-fw"
    # Ce pare-feu s'applique à TOUS les Droplets ayant ce tag.
    tags = ["${var.project_name}-${var.environment}"]

    # ── TRAFIC ENTRANT ──────────────────────────────────────────────────────
    # SSH: autorisé depuis partout (staging). En prod: restreindre aux IPs connues.
    inbound_rule {
      protocol         = "tcp"
      port_range       = "22"
      source_addresses = ["0.0.0.0/0", "::/0"]
    }
    # HTTP: autorisé depuis partout (trafic web).
    inbound_rule {
      protocol         = "tcp"
      port_range       = "80"
      source_addresses = ["0.0.0.0/0", "::/0"]
    }
    # HTTPS: autorisé depuis partout.
    inbound_rule {
      protocol         = "tcp"
      port_range       = "443"
      source_addresses = ["0.0.0.0/0", "::/0"]
    }
    # PostgreSQL: SEULEMENT depuis le VPC privé.
    # Depuis Internet: connexion REFUSÉE même avec le bon mot de passe.
    inbound_rule {
      protocol         = "tcp"
      port_range       = "5432"
      source_addresses = ["10.0.0.0/16"]
    }
    # Redis: SEULEMENT depuis le VPC privé.
    inbound_rule {
      protocol         = "tcp"
      port_range       = "6379"
      source_addresses = ["10.0.0.0/16"]
    }
    # Node Exporter (Prometheus monitoring): VPC seulement.
    inbound_rule {
      protocol         = "tcp"
      port_range       = "9100"
      source_addresses = ["10.0.0.0/16"]
    }
    # ICMP: autoriser le ping (vérifier que le serveur répond).
    inbound_rule {
      protocol         = "icmp"
      source_addresses = ["0.0.0.0/0", "::/0"]
    }

    # ── TRAFIC SORTANT ──────────────────────────────────────────────────────
    # Tout autoriser: le serveur peut télécharger paquets apt, contacter des APIs.
    outbound_rule {
      protocol              = "tcp"
      port_range            = "1-65535"
      destination_addresses = ["0.0.0.0/0", "::/0"]
    }
    outbound_rule {
      protocol              = "udp"
      port_range            = "1-65535"
      destination_addresses = ["0.0.0.0/0", "::/0"]
    }
    outbound_rule {
      protocol              = "icmp"
      destination_addresses = ["0.0.0.0/0", "::/0"]
    }
  }

  # ═══════════════════════════════════════════════════════════════════════════
  # DROPLET: le serveur Ubuntu (machine virtuelle)
  # ═══════════════════════════════════════════════════════════════════════════
  resource "digitalocean_droplet" "api" {
    name   = "${var.project_name}-${var.environment}"  # "taskmanager-staging"
    region = var.region        # "fra1" = Frankfurt
    size   = var.droplet_size  # "s-2vcpu-4gb" = 2 CPU, 4 GB RAM
    image  = "ubuntu-22-04-x64"

    # Placer dans le VPC privé.
    vpc_uuid = digitalocean_vpc.main.id

    # Clés SSH autorisées à se connecter.
    ssh_keys = [
      data.digitalocean_ssh_key.alice.id,
      data.digitalocean_ssh_key.deploy.id,
    ]

    # Tags: ce Droplet recevra le pare-feu configuré plus haut.
    tags = ["${var.project_name}-${var.environment}", var.environment, "flask"]

    # Activer métriques DigitalOcean (CPU, RAM, bande passante).
    monitoring = true

    # Sauvegardes automatiques hebdomadaires (false=staging, true=prod).
    backups = var.enable_backups

    ipv6 = true

    # user_data: script bash exécuté UNE SEULE FOIS à la création.
    # Prépare le serveur pour Ansible (qui a besoin de Python3).
    user_data = <<-USERDATA
      #!/bin/bash
      export DEBIAN_FRONTEND=noninteractive
      apt-get update -y
      apt-get upgrade -y -o Dpkg::Options::="--force-confdef"
      apt-get install -y python3 python3-pip
      useradd -m -s /bin/bash deploy
      cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys 2>/dev/null || true
      chown -R deploy:deploy /home/deploy/.ssh 2>/dev/null || true
      echo "user_data complete" >> /var/log/user-data.log
    USERDATA
  }

  # ═══════════════════════════════════════════════════════════════════════════
  # VOLUME: stockage persistant pour PostgreSQL
  # ═══════════════════════════════════════════════════════════════════════════
  # POURQUOI UN VOLUME SÉPARÉ?
  # Le disque interne du Droplet disparaît si on terraform destroy.
  # Le volume attaché PERSISTE: même si on recrée le serveur, les données survivent.

  resource "digitalocean_volume" "data" {
    region                  = var.region
    name                    = "${var.project_name}-${var.environment}-data"
    size                    = var.data_volume_size_gb
    initial_filesystem_type = "ext4"
    description             = "Données PostgreSQL ${var.environment}"
  }

  resource "digitalocean_volume_attachment" "data" {
    droplet_id = digitalocean_droplet.api.id
    volume_id  = digitalocean_volume.data.id
    # Disponible comme /dev/sda sur le serveur. Ansible le montera.
  }

  # ═══════════════════════════════════════════════════════════════════════════
  # DNS: enregistrement A -> IP du serveur
  # ═══════════════════════════════════════════════════════════════════════════
  # Prérequis: le domaine "equipe.com" est géré par DigitalOcean DNS.
  resource "digitalocean_record" "api" {
    domain = var.domain
    type   = "A"
    name   = "api-${var.environment}"   # -> api-staging.equipe.com
    value  = digitalocean_droplet.api.ipv4_address
    ttl    = 300
  }

  # ═══════════════════════════════════════════════════════════════════════════
  # ALERTES: notifications si le serveur est en difficulté
  # ═══════════════════════════════════════════════════════════════════════════
  resource "digitalocean_monitor_alert" "cpu" {
    alerts { email = [var.alert_email] }
    window      = "5m"
    type        = "v1/insights/droplet/cpu"
    compare     = "GreaterThan"
    value       = 85   # Alerte si CPU > 85% pendant 5 minutes
    enabled     = true
    entities    = [digitalocean_droplet.api.id]
    description = "CPU ${var.environment} > 85%"
  }

  resource "digitalocean_monitor_alert" "memory" {
    alerts { email = [var.alert_email] }
    window      = "5m"
    type        = "v1/insights/droplet/memory_utilization_percent"
    compare     = "GreaterThan"
    value       = 90
    enabled     = true
    entities    = [digitalocean_droplet.api.id]
    description = "RAM ${var.environment} > 90%"
  }

  resource "digitalocean_monitor_alert" "disk" {
    alerts { email = [var.alert_email] }
    window      = "5m"
    type        = "v1/insights/droplet/disk_utilization_percent"
    compare     = "GreaterThan"
    value       = 80
    enabled     = true
    entities    = [digitalocean_droplet.api.id]
    description = "Disque ${var.environment} > 80%"
  }

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 3.2 — variables.tf: DÉCLARATION DES PARAMÈTRES
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/terraform/environments/staging/variables.tf

  variable "do_token" {
    description = "Token API DigitalOcean. Fournir via TF_VAR_do_token."
    type        = string
    sensitive   = true   # sensitive = masque la valeur dans les logs Terraform
  }

  variable "region" {
    description = "Région DigitalOcean (fra1=Frankfurt, ams3=Amsterdam, lon1=London)"
    type        = string
    default     = "fra1"
    validation {
      condition     = contains(["fra1","ams3","nyc1","lon1","sgp1","blr1"], var.region)
      error_message = "Régions valides: fra1 ams3 nyc1 lon1 sgp1 blr1."
    }
  }

  variable "droplet_size" {
    description = "Taille Droplet DigitalOcean (CPU+RAM+SSD)"
    type        = string
    default     = "s-2vcpu-4gb"
    # s-1vcpu-2gb  = test    (~12$/mois)
    # s-2vcpu-4gb  = staging (~24$/mois)
    # s-4vcpu-8gb  = prod    (~48$/mois)
  }

  variable "environment" {
    description = "Environnement: staging | production | test"
    type        = string
    default     = "staging"
    validation {
      condition     = contains(["staging","production","test"], var.environment)
      error_message = "Environnements valides: staging production test."
    }
  }

  variable "project_name" {
    description = "Nom du projet (préfixe de toutes les ressources)"
    type        = string
    default     = "taskmanager"
    validation {
      condition     = can(regex("^[a-z][a-z0-9-]{2,20}$", var.project_name))
      error_message = "Minuscules, chiffres, tirets, 3-21 caractères."
    }
  }

  variable "domain" {
    description = "Domaine principal géré par DigitalOcean DNS"
    type        = string
    default     = "equipe.com"
  }

  variable "data_volume_size_gb" {
    description = "Taille volume PostgreSQL (GB)"
    type        = number
    default     = 20
    validation {
      condition     = var.data_volume_size_gb >= 10 && var.data_volume_size_gb <= 500
      error_message = "Taille entre 10 et 500 GB."
    }
  }

  variable "enable_backups" {
    description = "Activer les sauvegardes auto DigitalOcean (+20% coût)"
    type        = bool
    default     = false   # false=staging, true=production
  }

  variable "alert_email" {
    description = "Email pour les alertes de monitoring"
    type        = string
    default     = "alice@equipe.com"
  }

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 3.3 — outputs.tf: VALEURS EXPORTÉES APRÈS CRÉATION
────────────────────────────────────────────────────────────────────────────────

  POURQUOI LES OUTPUTS?
  ──────────────────────
  1. Affichés à l'écran après apply -> Alice connaît l'IP sans aller sur DigitalOcean.
  2. Lus par le script d'inventaire Ansible -> inventaire mis à jour automatiquement.
  3. Consommables par d'autres modules Terraform via module.staging.server_ip.
  4. Accessibles plus tard: terraform output -raw server_ip

  nano infrastructure/terraform/environments/staging/outputs.tf

  output "server_ip" {
    description = "IP publique du serveur"
    value       = digitalocean_droplet.api.ipv4_address
  }

  output "server_ip_private" {
    description = "IP privée (VPC) du serveur"
    value       = digitalocean_droplet.api.ipv4_address_private
  }

  output "server_id" {
    description = "ID du Droplet"
    value       = digitalocean_droplet.api.id
  }

  output "server_url" {
    description = "URL de l'API"
    value       = "https://api-${var.environment}.${var.domain}/health"
  }

  output "ssh_command" {
    description = "Commande SSH prête à l'emploi"
    value       = "ssh ubuntu@${digitalocean_droplet.api.ipv4_address}"
  }

  output "vpc_id" {
    description = "ID du VPC"
    value       = digitalocean_vpc.main.id
  }

  output "environment" {
    description = "Environnement déployé"
    value       = var.environment
  }

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 3.4 — terraform.tfvars: VALEURS CONCRÈTES (NON COMMITÉ)
────────────────────────────────────────────────────────────────────────────────

  # Template commité dans Git:
  nano infrastructure/terraform/environments/staging/terraform.tfvars.example

  # Copier: cp terraform.tfvars.example terraform.tfvars
  # JAMAIS committer terraform.tfvars!
  do_token            = "VOTRE_TOKEN_DIGITALOCEAN"
  region              = "fra1"
  droplet_size        = "s-2vcpu-4gb"
  environment         = "staging"
  project_name        = "taskmanager"
  domain              = "equipe.com"
  data_volume_size_gb = 20
  enable_backups      = false
  alert_email         = "alice@equipe.com"

  # AJOUTER DANS .gitignore:
  echo '
  # Terraform — fichiers sensibles et générés
  infrastructure/terraform/**/*.tfvars
  !infrastructure/terraform/**/*.tfvars.example
  infrastructure/terraform/**/.terraform/
  infrastructure/terraform/**/terraform.tfstate*
  infrastructure/terraform/**/crash.log
  # NE PAS ignorer .terraform.lock.hcl (doit être commité)
  ' >> .gitignore

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 3.5 — ALICE EXÉCUTE TERRAFORM: INIT -> VALIDATE -> FMT -> PLAN -> APPLY
────────────────────────────────────────────────────────────────────────────────

  ALICE DANS SON TERMINAL:
  ─────────────────────────
  cd infrastructure/terraform/environments/staging/

  ── TERRAFORM INIT ──────────────────────────────────────────────────────────

  POURQUOI?
  ─────────
  Initialise le dossier de travail Terraform:
  -> Télécharge le plugin provider digitalocean v2.x
  -> Configure le backend (Terraform Cloud)
  -> Installe les modules locaux déclarés
  QUAND: une fois au premier setup, et après chaque ajout de provider/module.

  terraform init

  CE QUE VOUS VOYEZ:
  ───────────────────
  Initializing the backend...
  Successfully configured the backend "remote"!

  Initializing provider plugins...
  - Finding digitalocean/digitalocean versions matching "~> 2.0"...
  - Installing digitalocean/digitalocean v2.36.0...
  - Installed digitalocean/digitalocean v2.36.0 (signed by HashiCorp)

  Terraform has been successfully initialized!

  # .terraform/ créé (ignoré par Git)
  # .terraform.lock.hcl créé (COMMITER dans Git — fixe les versions providers)

  SI "Error: Failed to query available provider packages":
  ─────────────────────────────────────────────────────────
  -> Problème réseau. Vérifier la connexion.
  -> Essayer: terraform init -upgrade

  ── TERRAFORM VALIDATE ──────────────────────────────────────────────────────

  POURQUOI?
  ─────────
  Vérifie la syntaxe HCL sans contacter DigitalOcean.
  Détecte: attributs inconnus, types incorrects, références circulaires.
  QUAND: après chaque modification de fichier .tf. Dans la CI/CD (GitHub Actions).

  terraform validate

  CE QUE VOUS VOYEZ (OK):
  ────────────────────────
  Success! The configuration is valid.

  CE QUE VOUS VOYEZ (erreur):
  ────────────────────────────
  Error: Unsupported argument
    on main.tf line 89:
    89: regoin = "fra1"
  An argument named "regoin" is not expected here. Did you mean "region"?

  # Corriger la faute de frappe, relancer terraform validate jusqu'à "Success!".

  ── TERRAFORM FMT ───────────────────────────────────────────────────────────

  POURQUOI?
  ─────────
  Formate automatiquement les fichiers .tf selon le style officiel HashiCorp.
  Aligne les "=" dans les blocs, corrige l'indentation (2 espaces).
  QUAND: avant chaque commit. En CI: terraform fmt -check pour vérifier.

  terraform fmt -recursive   # Formater tous les .tf (dossier et sous-dossiers)

  CE QUE VOUS VOYEZ (fichiers modifiés):
  ──────────────────────────────────────
  main.tf
  variables.tf

  terraform fmt -check        # Vérifier sans modifier (CI/CD)
  # -> exit code 0: OK  /  exit code 1: fichiers mal formatés

  ── TERRAFORM PLAN ──────────────────────────────────────────────────────────

  POURQUOI?
  ─────────
  Affiche EXACTEMENT ce que Terraform va faire. NE TOUCHE À RIEN.
  RÈGLE D'OR: lire le plan entier avant d'appliquer.
  Symboles:  + create  ~ update  - destroy  -/+ replace (recréer = interruption!)
  QUAND: avant chaque apply. Partager le plan en PR avec l'équipe.

  export TF_VAR_do_token="dop_v1_abc123..."  # Si pas dans ~/.zshrc
  terraform plan

  CE QUE VOUS VOYEZ:
  ───────────────────
  Terraform used the selected providers to generate the following execution plan.

    # digitalocean_vpc.main will be created
    + resource "digitalocean_vpc" "main" {
        + ip_range = "10.0.0.0/16"
        + name     = "taskmanager-staging-vpc"
        + region   = "fra1"
      }

    # digitalocean_firewall.main will be created
    + resource "digitalocean_firewall" "main" {
        + name = "taskmanager-staging-fw"
        + tags = ["taskmanager-staging"]
        # [règles inbound/outbound...]
      }

    # digitalocean_droplet.api will be created
    + resource "digitalocean_droplet" "api" {
        + backups      = false
        + image        = "ubuntu-22-04-x64"
        + ipv4_address = (known after apply)
        + name         = "taskmanager-staging"
        + region       = "fra1"
        + size         = "s-2vcpu-4gb"
      }

    # digitalocean_volume.data will be created
    # digitalocean_volume_attachment.data will be created
    # digitalocean_record.api will be created
    # digitalocean_monitor_alert.cpu will be created
    # digitalocean_monitor_alert.memory will be created
    # digitalocean_monitor_alert.disk will be created

  Plan: 9 to add, 0 to change, 0 to destroy.

  Changes to Outputs:
    + server_ip   = (known after apply)
    + server_url  = "https://api-staging.equipe.com/health"
    + ssh_command = (known after apply)

  # Alice lit: "9 to add, 0 to change, 0 to destroy". Tout est création.
  # 0 to destroy = aucun risque de perte de données.

  SAUVEGARDER LE PLAN (bonne pratique):
  ──────────────────────────────────────
  terraform plan -out=staging.tfplan     # Sauvegarder le plan
  terraform apply staging.tfplan         # Appliquer EXACTEMENT ce plan sauvegardé

  ── TERRAFORM APPLY ─────────────────────────────────────────────────────────

  POURQUOI?
  ─────────
  Exécute le plan: appelle les APIs DigitalOcean, crée les ressources réelles.
  QUAND: après avoir vérifié et approuvé le plan.

  terraform apply

  CE QUE VOUS VOYEZ:
  ───────────────────
  Do you want to perform these actions?
    Only 'yes' will be accepted to approve.

  Enter a value: yes

  digitalocean_vpc.main: Creating...
  digitalocean_vpc.main: Creation complete after 3s [id=a1b2c3d4]
  digitalocean_firewall.main: Creating...
  digitalocean_firewall.main: Creation complete after 4s
  digitalocean_droplet.api: Creating...
  digitalocean_droplet.api: Still creating... [10s elapsed]
  digitalocean_droplet.api: Still creating... [30s elapsed]
  digitalocean_droplet.api: Still creating... [1m0s elapsed]
  digitalocean_droplet.api: Creation complete after 1m22s [id=987654321]
  digitalocean_volume.data: Creating...
  digitalocean_volume.data: Creation complete after 5s [id=vol-abc]
  digitalocean_volume_attachment.data: Creating...
  digitalocean_volume_attachment.data: Creation complete after 2s
  digitalocean_record.api: Creating...
  digitalocean_record.api: Creation complete after 2s
  digitalocean_monitor_alert.cpu: Creation complete after 2s
  digitalocean_monitor_alert.memory: Creation complete after 2s
  digitalocean_monitor_alert.disk: Creation complete after 2s

  Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

  Outputs:
  environment  = "staging"
  server_ip    = "164.90.154.23"
  server_url   = "https://api-staging.equipe.com/health"
  ssh_command  = "ssh ubuntu@164.90.154.23"
  vpc_id       = "a1b2c3d4-..."

  # Vérification SSH immédiate:
  ssh ubuntu@164.90.154.23
  # -> Welcome to Ubuntu 22.04.3 LTS...   [OK]

  SI "Error: Error creating Droplet":
  ─────────────────────────────────────
  -> Vérifier TF_VAR_do_token: echo $TF_VAR_do_token
  -> Tester l'API: curl -H "Authorization: Bearer $TF_VAR_do_token" \
    https://api.digitalocean.com/v2/account | python3 -m json.tool

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 3.6 — TOUTES LES COMMANDES TERRAFORM EN DÉTAIL
────────────────────────────────────────────────────────────────────────────────

  INSPECTER L'ÉTAT (STATE):
  ──────────────────────────
  terraform state list
  # Liste toutes les ressources dans le state:
  # digitalocean_droplet.api
  # digitalocean_firewall.main
  # digitalocean_monitor_alert.cpu
  # digitalocean_record.api
  # digitalocean_volume.data
  # digitalocean_volume_attachment.data
  # digitalocean_vpc.main

  terraform state show digitalocean_droplet.api
  # Affiche tous les attributs du serveur:
  # id = "987654321"
  # ipv4_address = "164.90.154.23"
  # size = "s-2vcpu-4gb"
  # status = "active"
  # ...

  terraform output                   # Afficher tous les outputs
  terraform output server_ip         # Afficher un output spécifique
  terraform output -raw server_ip    # Sans guillemets (pour scripts bash)
  # -> 164.90.154.23

  MODIFIER UNE RESSOURCE EXISTANTE:
  ───────────────────────────────────
  # Cas: Alice veut augmenter la RAM: s-2vcpu-4gb -> s-4vcpu-8gb.
  # Elle modifie terraform.tfvars: droplet_size = "s-4vcpu-8gb"

  terraform plan

  CE QUE VOUS VOYEZ:
  ───────────────────
  # digitalocean_droplet.api must be replaced
  -/+ resource "digitalocean_droplet" "api" {
        ~ size = "s-2vcpu-4gb" -> "s-4vcpu-8gb"   # forces replacement
      }
  Plan: 1 to add, 0 to change, 1 to destroy.

  # -/+ = DigitalOcean doit RECRÉER le serveur pour changer la taille.
  # 2-3 minutes d'indisponibilité. Alice prévient l'équipe avant d'appliquer en prod.

  IMPORTER UNE RESSOURCE EXISTANTE DANS LE STATE:
  ─────────────────────────────────────────────────
  # Cas: un serveur a été créé manuellement (pas via Terraform).
  # Alice veut le gérer avec Terraform sans le recréer.

  terraform import digitalocean_droplet.api 987654321
  # Terraform ajoute ce serveur dans son state.
  # Désormais terraform plan inclut ce serveur.

  SUPPRIMER UNE RESSOURCE DU STATE SANS LA DÉTRUIRE:
  ────────────────────────────────────────────────────
  terraform state rm digitalocean_droplet.api
  # Terraform "oublie" ce serveur. Il existe toujours sur DigitalOcean.
  # Utile: migrer entre states, exclure temporairement.

  DÉPLACER UNE RESSOURCE DANS LE STATE:
  ────────────────────────────────────────
  # Cas: Alice refactorise main.tf pour utiliser un module.
  # Sans state mv: Terraform destroy l'ancienne + create la nouvelle.
  # Avec state mv: simple renommage dans le state, pas de recréation.
  terraform state mv \
    digitalocean_droplet.api \
    module.server.digitalocean_droplet.this

  RAFRAÎCHIR LE STATE DEPUIS LE CLOUD:
  ──────────────────────────────────────
  # Cas: quelqu'un a modifié une ressource directement sur DigitalOcean.
  # Le state est désynchronisé.
  terraform refresh   # Ou: terraform apply -refresh-only

  VERROUILLAGE DU STATE EN ÉQUIPE:
  ──────────────────────────────────
  # Terraform Cloud et le backend S3+DynamoDB implémentent un verrou automatique.
  # Si Alice fait terraform apply, Bob voit:
  # Error: Error locking state
  # State is currently locked by alice (started 2 minutes ago).
  # Bob attend qu'Alice finisse.

  TERRAFORM TAINT ET UNTAINT (forcer la recréation):
  ────────────────────────────────────────────────────
  # Cas: le serveur est "cassé" (corrompu, mauvaise config OS).
  # Alice veut forcer Terraform à le recréer même si rien n'a changé dans main.tf.

  terraform taint digitalocean_droplet.api
  # Marque le Droplet comme "tainted" (à recréer).
  terraform plan
  # -> -/+ resource "digitalocean_droplet" "api" (tainted, must be replaced)
  terraform apply   # -> Supprime et recrée le serveur.

  terraform untaint digitalocean_droplet.api   # Annuler le taint.

  TERRAFORM GRAPH (visualiser les dépendances):
  ──────────────────────────────────────────────
  terraform graph | dot -Tsvg > graph.svg
  # Génère un graphe SVG des dépendances entre ressources.
  # Utile pour comprendre l'ordre de création.

  TERRAFORM CONSOLE (tester des expressions):
  ─────────────────────────────────────────────
  terraform console
  > var.region
  "fra1"
  > "taskmanager-${var.environment}"
  "taskmanager-staging"
  > digitalocean_droplet.api.ipv4_address
  "164.90.154.23"
  > exit

  TERRAFORM DESTROY (supprimer toutes les ressources):
  ──────────────────────────────────────────────────────
  # ATTENTION: supprime TOUT ce que Terraform gère dans ce dossier!
  terraform destroy

  CE QUE VOUS VOYEZ:
  ───────────────────
  Do you really want to destroy all resources?
    Terraform will destroy all your managed infrastructure, as shown above.
    There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

  digitalocean_monitor_alert.disk: Destroying...
  digitalocean_record.api: Destroying...
  digitalocean_volume_attachment.data: Destroying...
  digitalocean_droplet.api: Destroying...
  digitalocean_droplet.api: Still destroying... [30s elapsed]
  digitalocean_droplet.api: Destruction complete after 55s
  digitalocean_volume.data: Destroying...
  digitalocean_vpc.main: Destroying...

  Destroy complete! Resources: 9 destroyed.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 3.7 — MODULE TERRAFORM RÉUTILISABLE (modules/server/)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI UN MODULE?
  ────────────────────
  staging/main.tf et production/main.tf sont quasi-identiques.
  Sans module: code dupliqué -> 2 fichiers à maintenir -> divergence inévitable.
  Avec module: code écrit UNE FOIS, appelé avec des paramètres différents.
  Analogie: une fonction Python réutilisable avec des arguments différents.

  nano infrastructure/terraform/modules/server/main.tf

  terraform {
    required_providers {
      digitalocean = { source = "digitalocean/digitalocean", version = "~> 2.0" }
    }
  }

  resource "digitalocean_vpc" "this" {
    name     = "${var.name}-vpc"
    region   = var.region
    ip_range = var.vpc_ip_range
  }

  resource "digitalocean_firewall" "this" {
    name = "${var.name}-fw"
    tags = [var.name]
    inbound_rule {
      protocol         = "tcp"; port_range = "22"
      source_addresses = var.ssh_allowed_ips
    }
    inbound_rule {
      protocol         = "tcp"; port_range = "80"
      source_addresses = ["0.0.0.0/0", "::/0"]
    }
    inbound_rule {
      protocol         = "tcp"; port_range = "443"
      source_addresses = ["0.0.0.0/0", "::/0"]
    }
    inbound_rule {
      protocol         = "tcp"; port_range = "5432"
      source_addresses = [var.vpc_ip_range]
    }
    inbound_rule {
      protocol         = "tcp"; port_range = "6379"
      source_addresses = [var.vpc_ip_range]
    }
    outbound_rule {
      protocol = "tcp"; port_range = "1-65535"
      destination_addresses = ["0.0.0.0/0", "::/0"]
    }
    outbound_rule {
      protocol = "udp"; port_range = "1-65535"
      destination_addresses = ["0.0.0.0/0", "::/0"]
    }
  }

  resource "digitalocean_droplet" "this" {
    name       = var.name
    region     = var.region
    size       = var.size
    image      = "ubuntu-22-04-x64"
    vpc_uuid   = digitalocean_vpc.this.id
    ssh_keys   = var.ssh_key_ids
    monitoring = true
    backups    = var.enable_backups
    ipv6       = true
    tags       = concat([var.name, var.environment], var.extra_tags)
    user_data  = var.user_data
  }

  resource "digitalocean_volume" "data" {
    count                   = var.data_volume_size_gb > 0 ? 1 : 0
    region                  = var.region
    name                    = "${var.name}-data"
    size                    = var.data_volume_size_gb
    initial_filesystem_type = "ext4"
  }

  resource "digitalocean_volume_attachment" "data" {
    count      = var.data_volume_size_gb > 0 ? 1 : 0
    droplet_id = digitalocean_droplet.this.id
    volume_id  = digitalocean_volume.data[0].id
  }

  nano infrastructure/terraform/modules/server/variables.tf

  variable "name"                { type = string }
  variable "region"              { type = string; default = "fra1" }
  variable "size"                { type = string; default = "s-2vcpu-4gb" }
  variable "environment"         { type = string }
  variable "ssh_key_ids"         { type = list(string) }
  variable "vpc_ip_range"        { type = string; default = "10.0.0.0/16" }
  variable "ssh_allowed_ips"     { type = list(string); default = ["0.0.0.0/0", "::/0"] }
  variable "extra_tags"          { type = list(string); default = [] }
  variable "enable_backups"      { type = bool; default = false }
  variable "data_volume_size_gb" { type = number; default = 20 }
  variable "user_data"           { type = string; default = "" }

  nano infrastructure/terraform/modules/server/outputs.tf

  output "ip"         { value = digitalocean_droplet.this.ipv4_address }
  output "ip_private" { value = digitalocean_droplet.this.ipv4_address_private }
  output "id"         { value = digitalocean_droplet.this.id }
  output "vpc_id"     { value = digitalocean_vpc.this.id }

  UTILISER LE MODULE DEPUIS STAGING:
  ────────────────────────────────────
  # Dans staging/main.tf — version modulaire (remplace tout le code répété):
  module "server" {
    source              = "../../modules/server"
    name                = "taskmanager-staging"
    region              = var.region
    size                = "s-2vcpu-4gb"
    environment         = "staging"
    ssh_key_ids         = [data.digitalocean_ssh_key.alice.id]
    ssh_allowed_ips     = ["0.0.0.0/0", "::/0"]
    enable_backups      = false
    data_volume_size_gb = 20
  }
  output "server_ip" { value = module.server.ip }

  # Dans production/main.tf — MÊME module, paramètres différents:
  module "server" {
    source              = "../../modules/server"
    name                = "taskmanager-production"
    region              = "fra1"
    size                = "s-4vcpu-8gb"        # Plus puissant
    environment         = "production"
    ssh_key_ids         = [data.digitalocean_ssh_key.alice.id]
    ssh_allowed_ips     = ["88.99.10.0/24"]    # Restreindre à l'IP du bureau
    enable_backups      = true                 # Sauvegardes activées
    data_volume_size_gb = 50                   # Plus d'espace
  }

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 1 — BOB: CRÉE UN SERVEUR DE TEST TEMPORAIRE
Durée: 30 minutes | Objectif: tester la feature filtrage-dates en isolation
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Bob développe la feature #3 (filtrage par dates).
  Il veut tester sur un vrai serveur Ubuntu avant d'ouvrir la PR.
  Il ne veut pas "polluer" le staging partagé avec du code non terminé.
  Il ne veut pas attendre Alice (qui est en réunion).

  POURQUOI TERRAFORM EST PARFAIT ICI?
  ─────────────────────────────────────
  Bob crée son propre serveur en 90 secondes, teste, puis le détruit.
  Coût: ~0.03$/heure (négligeable pour 30 min de test).
  Totale autonomie sans dépendre d'Alice.

────────────────────────────────────────────────────────────────────────────────
  BOB — ÉTAPE 1: Préparer son environnement de test
────────────────────────────────────────────────────────────────────────────────

  cd infrastructure/terraform/environments/
  cp -r staging bob-test-feature-3     # Copier le dossier staging
  cd bob-test-feature-3/

  # Créer son tfvars:
  cp terraform.tfvars.example terraform.tfvars
  nano terraform.tfvars
  # Modifier:
  droplet_size = "s-1vcpu-2gb"    # Plus petit = moins cher
  environment  = "test"
  do_token     = "dop_v1_bob_token_ici"

  # Utiliser un backend local pour ne pas polluer le state partagé:
  # (Modifier temporairement main.tf: remplacer backend "remote" par:)
  # backend "local" { path = "terraform.tfstate" }

  terraform init
  terraform plan  # Vérifier: "3 to add, 0 to change, 0 to destroy"
  terraform apply -auto-approve

  CE QUE VOUS VOYEZ:
  ───────────────────
  Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
  Outputs:
  server_ip   = "164.90.200.77"
  ssh_command = "ssh ubuntu@164.90.200.77"

────────────────────────────────────────────────────────────────────────────────
  BOB — ÉTAPE 2: Configurer avec Ansible et tester la feature
────────────────────────────────────────────────────────────────────────────────

  # Mettre à jour l'inventaire Ansible:
  cd ../../ansible/
  # Ajouter temporairement dans inventory/staging.ini:
  # [test]
  # bob-test ansible_host=164.90.200.77

  ansible-playbook site.yml -i inventory/staging.ini --limit bob-test

  # Déployer sa branche feature:
  ansible-playbook deploy.yml -i inventory/staging.ini \
    --limit bob-test \
    --extra-vars "app_version=feature/3-filtrage-dates-tri"

  # Tester l'API:
  curl "http://164.90.200.77/tasks?from=2024-01-01&to=2024-01-31"
  # -> {"tasks": [...], "total": 3}   [OK]

────────────────────────────────────────────────────────────────────────────────
  BOB — ÉTAPE 3: Détruire le serveur de test
────────────────────────────────────────────────────────────────────────────────

  cd ../../terraform/environments/bob-test-feature-3/
  terraform destroy -auto-approve

  CE QUE VOUS VOYEZ:
  ───────────────────
  Destroy complete! Resources: 6 destroyed.

  # Nettoyer:
  cd .. && rm -rf bob-test-feature-3/

  # Coût total: ~0.03$ pour 30 minutes de test.   [OK]
  # Bob ouvre sa PR avec confiance: ça fonctionne sur un vrai serveur Ubuntu.
  
================================================================================
PARTIE 4 — ANSIBLE: CONFIGURER LES SERVEURS (ALICE — JOUR 1, FIN D'APRÈS-MIDI)
================================================================================

Le serveur staging existe maintenant (IP: 164.90.154.23).
Terraform a fait son travail. C'est maintenant au tour d'Ansible de configurer
tout ce qui tourne à l'intérieur: Python, Flask, PostgreSQL, Redis, Nginx.

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.1 — ANSIBLE.CFG: FICHIER DE CONFIGURATION GLOBALE
────────────────────────────────────────────────────────────────────────────────

  POURQUOI ansible.cfg?
  ──────────────────────
  Sans ce fichier: Ansible cherche sa config dans /etc/ansible/ansible.cfg (global).
  Avec ce fichier dans le dossier projet: configuration spécifique au projet,
  versionnée dans Git, identique pour Alice, Bob et Claire.

  nano infrastructure/ansible/ansible.cfg

  [defaults]
  # Inventaire par défaut (relatif à ce fichier)
  inventory          = inventory/staging.ini

  # Utilisateur SSH pour se connecter aux serveurs
  remote_user        = ubuntu

  # Clé SSH privée à utiliser
  private_key_file   = ~/.ssh/id_ed25519_github

  # Ne pas vérifier la clé d'hôte SSH (pratique en dev, activer en prod)
  host_key_checking  = False

  # Parallélisme: nombre de serveurs configurés simultanément
  forks              = 5

  # Répertoire des roles (relatif à ansible.cfg)
  roles_path         = roles

  # Fichier de log Ansible
  log_path           = /tmp/ansible-taskmanager.log

  # Activer les couleurs dans le terminal
  force_color        = True

  # Stratégie d'exécution:
  # linear = une task sur tous les hôtes, puis la suivante (défaut)
  # free   = chaque hôte avance à son propre rythme (plus rapide)
  strategy           = linear

  # Timeout connexion SSH (secondes)
  timeout            = 30

  # Cache des facts (infos OS collectées automatiquement)
  fact_caching             = jsonfile
  fact_caching_connection  = /tmp/ansible-facts-cache
  fact_caching_timeout     = 3600

  [privilege_escalation]
  # Ansible se connecte en SSH en tant qu'ubuntu, puis fait "sudo" pour les tâches root
  become      = True
  become_method = sudo
  become_user = root
  become_ask_pass = False

  [ssh_connection]
  # Réutiliser les connexions SSH (beaucoup plus rapide)
  ssh_args   = -o ControlMaster=auto -o ControlPersist=60s -o ForwardAgent=yes
  pipelining = True   # Exécute plusieurs commandes via 1 connexion SSH (~50% plus rapide)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.2 — INVENTAIRES: LISTE DES SERVEURS
────────────────────────────────────────────────────────────────────────────────

  POURQUOI UN INVENTAIRE?
  ────────────────────────
  Ansible doit savoir sur QUELS serveurs travailler.
  L'inventaire = la liste avec IPs, groupes, utilisateurs, variables par hôte.

  INVENTAIRE STATIQUE STAGING:
  ─────────────────────────────
  nano infrastructure/ansible/inventory/staging.ini

  # ═══════════════════════════════════════════════════════════════════════════
  # Inventaire Ansible — STAGING
  # IP mise à jour après chaque terraform apply via:
  # IP=$(terraform -chdir=../terraform/environments/staging output -raw server_ip)
  # sed -i "s/ansible_host=.*/ansible_host=$IP/" inventory/staging.ini
  # ═══════════════════════════════════════════════════════════════════════════

  # Groupe "web": serveurs Flask + Nginx
  [web]
  taskmanager-staging ansible_host=164.90.154.23

  # Groupe "db": serveurs base de données
  # En staging: même serveur que web (économie)
  # En production: serveur séparé
  [db]
  taskmanager-staging ansible_host=164.90.154.23

  # Groupe parent "staging" = web + db
  [staging:children]
  web
  db

  # Variables communes à tous les hôtes
  [all:vars]
  ansible_user              = ubuntu
  ansible_python_interpreter = /usr/bin/python3

  # Variables spécifiques au groupe web
  [web:vars]
  app_port  = 5000
  nginx_port = 80

  # Variables spécifiques au groupe db
  [db:vars]
  postgres_port = 5432
  redis_port    = 6379

  INVENTAIRE DYNAMIQUE (LIT LES OUTPUTS TERRAFORM):
  ───────────────────────────────────────────────────
  POURQUOI DYNAMIQUE?
  L'IP change à chaque terraform destroy/apply.
  Mettre l'IP en dur = obligation de mettre à jour manuellement.
  Un inventaire dynamique lit l'IP en temps réel depuis terraform output.

  nano infrastructure/ansible/inventory/dynamic/terraform.py

  #!/usr/bin/env python3
  """
  Inventaire dynamique Ansible: lit les outputs Terraform.
  Usage: ansible-playbook -i inventory/dynamic/terraform.py site.yml
  """
  import json, subprocess, sys

  def tf_outputs(env):
      r = subprocess.run(
          ["terraform", "output", "-json"],
          cwd=f"../../terraform/environments/{env}",
          capture_output=True, text=True
      )
      return json.loads(r.stdout) if r.returncode == 0 else {}

  def build():
      inv = {
          "staging":    {"hosts": [], "vars": {"app_env": "staging"}},
          "production": {"hosts": [], "vars": {"app_env": "production"}},
          "_meta": {"hostvars": {}}
      }
      for env in ["staging", "production"]:
          out = tf_outputs(env)
          if "server_ip" in out:
              host = f"taskmanager-{env}"
              inv[env]["hosts"].append(host)
              inv["_meta"]["hostvars"][host] = {
                  "ansible_host": out["server_ip"]["value"],
                  "ansible_user": "ubuntu",
              }
      return inv

  if __name__ == "__main__":
      print(json.dumps(build(), indent=2))

  chmod +x infrastructure/ansible/inventory/dynamic/terraform.py

  # Test:
  python3 infrastructure/ansible/inventory/dynamic/terraform.py
  # -> {"staging": {"hosts": ["taskmanager-staging"], ...}}

  # Utiliser avec Ansible:
  ansible-playbook -i inventory/dynamic/terraform.py site.yml

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.3 — GROUP_VARS: VARIABLES PAR GROUPE DE SERVEURS
────────────────────────────────────────────────────────────────────────────────

  POURQUOI GROUP_VARS?
  ─────────────────────
  Mettre des variables en dur dans les playbooks = problème.
  group_vars/ = variables communes à un groupe de serveurs.
  Ansible charge automatiquement le bon fichier selon le groupe de l'hôte cible.

  nano infrastructure/ansible/group_vars/all.yml

  # ─── VARIABLES COMMUNES À TOUS LES SERVEURS ────────────────────────────────

  # Application
  app_name:       taskmanager
  app_user:       deploy
  app_group:      deploy
  app_dir:        /opt/taskmanager
  app_repo:       git@github.com:equipe/taskmanager.git

  # Python
  python_version: "3.11"
  python_bin:     /usr/bin/python3
  venv_dir:       "{{ app_dir }}/venv"

  # Flask / Gunicorn
  flask_port:          5000
  gunicorn_workers:    3
  gunicorn_timeout:    120
  gunicorn_bind:       "127.0.0.1:{{ flask_port }}"

  # PostgreSQL
  postgres_version: "15"
  postgres_port:    5432
  postgres_db:      taskmanager
  postgres_user:    taskmanager
  # postgres_password: dans vars/staging_secrets.yml (chiffré Vault)

  # Redis
  redis_port:              6379
  redis_bind:              "127.0.0.1"
  redis_maxmemory:         "256mb"
  redis_maxmemory_policy:  "allkeys-lru"

  # Nginx
  nginx_port:              80
  nginx_ssl_port:          443
  nginx_worker_processes:  auto
  nginx_worker_connections: 1024
  nginx_client_max_body:   "10m"

  # Timezone et locale
  timezone: "Europe/Paris"
  locale:   "fr_FR.UTF-8"

  # Paquets système installés sur tous les serveurs
  common_packages:
    - git
    - curl
    - wget
    - vim
    - htop
    - unzip
    - python3
    - python3-pip
    - python3-venv
    - fail2ban
    - ufw
    - acl      # Gestion des permissions Unix (requis par Ansible become)
    - ntp      # Synchronisation de l'heure

  nano infrastructure/ansible/group_vars/staging.yml

  # ─── VARIABLES SPÉCIFIQUES STAGING ─────────────────────────────────────────
  flask_env:           staging
  flask_debug:         "1"
  domain:              "api-staging.equipe.com"
  log_level:           DEBUG
  gunicorn_workers:    2
  enable_ssl:          false
  backup_enabled:      false
  monitoring_enabled:  false
  app_version:         develop
  db_name:             taskmanager_staging
  nginx_server_name:   "api-staging.equipe.com"

  nano infrastructure/ansible/group_vars/production.yml

  # ─── VARIABLES SPÉCIFIQUES PRODUCTION ──────────────────────────────────────
  flask_env:           production
  flask_debug:         "0"
  domain:              "api.equipe.com"
  log_level:           WARNING
  gunicorn_workers:    4
  enable_ssl:          true
  backup_enabled:      true
  monitoring_enabled:  true
  app_version:         "{{ lookup('env', 'APP_VERSION') | default('main') }}"
  db_name:             taskmanager
  nginx_server_name:   "api.equipe.com"
  ssl_cert:            "/etc/letsencrypt/live/api.equipe.com/fullchain.pem"
  ssl_key:             "/etc/letsencrypt/live/api.equipe.com/privkey.pem"

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.4 — ANSIBLE VAULT: CHIFFRER LES SECRETS
────────────────────────────────────────────────────────────────────────────────

  POURQUOI ANSIBLE VAULT?
  ────────────────────────
  Les mots de passe (PostgreSQL, SECRET_KEY Flask) ne doivent PAS être
  en clair dans Git. Ansible Vault chiffre ces fichiers avec AES-256.
  Ils peuvent être committés dans Git: illisibles sans le mot de passe Vault.

  ALICE CRÉE ET CHIFFRE LES SECRETS STAGING:
  ────────────────────────────────────────────
  nano infrastructure/ansible/vars/staging_secrets.yml

  # Contenu EN CLAIR avant chiffrement:
  vault_db_password:   "staging-db-pass-ultra-secure-2024"
  vault_secret_key:    "staging-flask-secret-key-generee-avec-openssl-rand-hex-32"
  vault_redis_password: ""    # Redis sans auth en staging

  # Chiffrer le fichier:
  ansible-vault encrypt infrastructure/ansible/vars/staging_secrets.yml

  CE QUE VOUS VOYEZ:
  ───────────────────
  New Vault password: ████████████  (Alice tape son mot de passe Vault)
  Confirm New Vault password: ████████████
  Encryption successful

  # Le fichier est maintenant chiffré:
  cat infrastructure/ansible/vars/staging_secrets.yml

  CE QUE VOUS VOYEZ:
  ───────────────────
  $ANSIBLE_VAULT;1.1;AES256
  66386438653661626637653136326363323134363466623365363163623431333632383934363239
  6663663539616135633436343861393933623137363230360a376531666665303462336631633665
  63323135613232326435373465316463376139373564383363313663363034366533623939363535
  6562383933373464390a613539373433633131313065356432323131323031343435303562396230
  ...   (chiffrement AES-256)

  # Alice commite ce fichier chiffré dans Git sans risque.

  VOIR LE CONTENU D'UN FICHIER VAULT:
  ─────────────────────────────────────
  ansible-vault view infrastructure/ansible/vars/staging_secrets.yml
  # -> demande le mot de passe -> affiche le contenu déchiffré

  ÉDITER UN FICHIER VAULT:
  ──────────────────────────
  ansible-vault edit infrastructure/ansible/vars/staging_secrets.yml
  # -> ouvre vim/nano avec le contenu déchiffré -> sauvegarder = rechiffrer auto

  DÉCHIFFRER UN FICHIER (temporairement):
  ─────────────────────────────────────────
  ansible-vault decrypt infrastructure/ansible/vars/staging_secrets.yml
  # -> ATTENTION: le fichier est maintenant en clair! Ne pas commiter ainsi.

  CHANGER LE MOT DE PASSE VAULT:
  ────────────────────────────────
  ansible-vault rekey infrastructure/ansible/vars/staging_secrets.yml
  # -> demande l'ancien mot de passe, puis le nouveau

  UTILISER VAULT DANS LES PLAYBOOKS:
  ────────────────────────────────────
  # Dans le playbook, référencer la variable chiffrée:
  - name: Créer l'utilisateur PostgreSQL
    postgresql_user:
      name: "{{ postgres_user }}"
      password: "{{ vault_db_password }}"   # <- variable du fichier Vault
    no_log: true   # Ne pas afficher le mot de passe dans les logs

  # Lancer le playbook avec le mot de passe Vault:
  ansible-playbook site.yml --ask-vault-pass
  # Ou depuis un fichier (pour CI/CD):
  echo "mon-mot-de-passe-vault" > ~/.vault_pass
  chmod 600 ~/.vault_pass
  ansible-playbook site.yml --vault-password-file ~/.vault_pass

  VAULT AVEC ID (plusieurs mots de passe Vault):
  ─────────────────────────────────────────────────
  # Cas: secrets staging et production chiffrés avec des mots de passe DIFFÉRENTS.
  ansible-vault encrypt vars/prod_secrets.yml --vault-id prod@prompt
  # prod = label  /  prompt = demander le mot de passe interactivement
  ansible-playbook site.yml --vault-id staging@~/.vault_staging --vault-id prod@~/.vault_prod

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.5 — ROLE common: CONFIGURATION DE BASE DU SYSTÈME
────────────────────────────────────────────────────────────────────────────────

  POURQUOI UN ROLE "common"?
  ───────────────────────────
  Tout serveur dans l'équipe doit avoir: timezone correcte, paquets de base,
  utilisateur applicatif, fail2ban (protection SSH), ufw (pare-feu OS),
  synchronisation NTP. Ce role s'applique à TOUS les serveurs.

  nano infrastructure/ansible/roles/common/tasks/main.yml

  ---
  # ═══════════════════════════════════════════════════════════════════════════
  # ROLE common — Configuration de base du système
  # ═══════════════════════════════════════════════════════════════════════════

  # TASK 1: Mettre à jour le cache apt
  # Le module "apt" gère les paquets sur Ubuntu/Debian.
  # update_cache: true = équivalent de "apt-get update" mais idempotent.
  - name: Mettre à jour le cache apt
    apt:
      update_cache: yes
      cache_valid_time: 3600   # Ne pas re-télécharger si cache < 1 heure
    tags: [common, packages]

  # TASK 2: Installer les paquets de base
  # "loop" applique la task pour chaque item de la liste.
  # "{{ item }}" = variable de boucle.
  - name: Installer les paquets système communs
    apt:
      name: "{{ common_packages }}"
      state: present    # "present" = installer si absent. "absent" = désinstaller.
    tags: [common, packages]

  # TASK 3: Configurer le timezone
  - name: Configurer le timezone
    timezone:
      name: "{{ timezone }}"   # "Europe/Paris"
    notify: Redémarrer systemd-timesyncd   # Déclenche le handler
    tags: [common, timezone]

  # TASK 4: Activer et configurer NTP
  - name: Activer la synchronisation NTP
    service:
      name: ntp
      state: started
      enabled: yes
    tags: [common, ntp]

  # TASK 5: Créer l'utilisateur applicatif "deploy"
  # L'application Flask tournera sous cet utilisateur (pas root!).
  - name: Créer l'utilisateur deploy
    user:
      name:    "{{ app_user }}"       # "deploy"
      group:   "{{ app_group }}"
      shell:   /bin/bash
      home:    /home/deploy
      system:  no
      create_home: yes
      state:   present
    tags: [common, users]

  # TASK 6: Créer le répertoire de l'application
  - name: Créer le répertoire de l'application
    file:
      path:    "{{ app_dir }}"        # "/opt/taskmanager"
      owner:   "{{ app_user }}"
      group:   "{{ app_group }}"
      mode:    "0755"
      state:   directory
    tags: [common, app]

  # TASK 7: Configurer ufw (pare-feu OS, en complément du pare-feu cloud)
  - name: Configurer ufw — autoriser SSH
    ufw:
      rule:  allow
      port:  "22"
      proto: tcp
    tags: [common, firewall]

  - name: Configurer ufw — autoriser HTTP
    ufw:
      rule:  allow
      port:  "80"
      proto: tcp

  - name: Configurer ufw — autoriser HTTPS
    ufw:
      rule:  allow
      port:  "443"
      proto: tcp

  - name: Activer ufw
    ufw:
      state:   enabled
      policy:  deny   # Bloquer tout par défaut, n'autoriser que les règles ci-dessus
    tags: [common, firewall]

  # TASK 8: Configurer fail2ban (protection contre les attaques SSH brute-force)
  - name: Activer fail2ban
    service:
      name:    fail2ban
      state:   started
      enabled: yes
    tags: [common, security]

  # TASK 9: Monter le volume de données si attaché
  # Le volume DigitalOcean est disponible sur /dev/sda après attach.
  - name: Monter le volume de données PostgreSQL
    mount:
      path:   /opt/taskmanager/data
      src:    /dev/sda
      fstype: ext4
      state:  mounted
      opts:   "defaults,noatime"
    when: ansible_devices.sda is defined   # Seulement si le volume est attaché
    tags: [common, storage]

  nano infrastructure/ansible/roles/common/handlers/main.yml

  ---
  # HANDLERS: tâches déclenchées seulement si une task notifie
  # "notify: Redémarrer systemd-timesyncd" -> ce handler s'exécute à la fin

  - name: Redémarrer systemd-timesyncd
    service:
      name:  systemd-timesyncd
      state: restarted

  nano infrastructure/ansible/roles/common/defaults/main.yml

  ---
  # Valeurs par défaut (surchargées par group_vars ou host_vars)
  timezone:     "UTC"
  app_user:     deploy
  app_group:    deploy
  app_dir:      /opt/taskmanager

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.6 — ROLE python: INSTALLER PYTHON 3.11
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/ansible/roles/python/tasks/main.yml

  ---
  - name: Ajouter le PPA Python 3.11
    apt_repository:
      repo: ppa:deadsnakes/ppa
      state: present
    tags: [python]

  - name: Installer Python 3.11 et ses dépendances
    apt:
      name:
        - python3.11
        - python3.11-venv
        - python3.11-dev
        - python3-pip
        - build-essential
        - libpq-dev     # Requis pour psycopg2 (driver PostgreSQL Python)
      state: present
      update_cache: yes
    tags: [python]

  - name: Vérifier la version Python
    command: python3.11 --version
    register: python_version_output
    changed_when: false   # Cette task ne "modifie" rien -> ne pas la marquer changed

  - name: Afficher la version Python installée
    debug:
      msg: "Python installé: {{ python_version_output.stdout }}"
    tags: [python]

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.7 — ROLE postgresql: INSTALLER ET CONFIGURER LA BASE DE DONNÉES
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/ansible/roles/postgresql/tasks/main.yml

  ---
  # ═══════════════════════════════════════════════════════════════════════════
  # ROLE postgresql — Installer et configurer PostgreSQL 15
  # ═══════════════════════════════════════════════════════════════════════════

  - name: Ajouter la clé GPG PostgreSQL
    apt_key:
      url: https://www.postgresql.org/media/keys/ACCC4CF8.asc
      state: present
    tags: [postgresql]

  - name: Ajouter le dépôt PostgreSQL 15
    apt_repository:
      repo: "deb http://apt.postgresql.org/pub/repos/apt {{ ansible_distribution_release }}-pgdg main"
      state: present
    tags: [postgresql]

  - name: Installer PostgreSQL 15
    apt:
      name:
        - postgresql-15
        - postgresql-contrib-15
        - python3-psycopg2    # Requis par les modules Ansible postgresql_*
      state: present
      update_cache: yes
    notify: Démarrer PostgreSQL
    tags: [postgresql]

  - name: S'assurer que PostgreSQL est démarré et activé au boot
    service:
      name:    postgresql
      state:   started
      enabled: yes
    tags: [postgresql]

  - name: Créer l'utilisateur PostgreSQL de l'application
    become_user: postgres   # Exécuter cette task en tant que l'utilisateur postgres
    postgresql_user:
      name:     "{{ postgres_user }}"
      password: "{{ vault_db_password }}"   # Variable chiffrée Vault
      role_attr_flags: NOSUPERUSER,NOCREATEDB,NOCREATEROLE
      state: present
    no_log: true   # Ne pas afficher le mot de passe dans les logs Ansible
    tags: [postgresql]

  - name: Créer la base de données
    become_user: postgres
    postgresql_db:
      name:     "{{ postgres_db }}"
      owner:    "{{ postgres_user }}"
      encoding: UTF8
      lc_collate: fr_FR.UTF-8
      lc_ctype:   fr_FR.UTF-8
      template: template0
      state: present
    tags: [postgresql]

  - name: Accorder tous les droits sur la base de données
    become_user: postgres
    postgresql_privs:
      database: "{{ postgres_db }}"
      role:     "{{ postgres_user }}"
      type:     database
      privs:    ALL
      state:    present
    tags: [postgresql]

  - name: Accorder les droits sur toutes les tables (futures)
    become_user: postgres
    postgresql_query:
      db: "{{ postgres_db }}"
      query: |
        ALTER DEFAULT PRIVILEGES IN SCHEMA public
        GRANT ALL ON TABLES TO {{ postgres_user }};
        ALTER DEFAULT PRIVILEGES IN SCHEMA public
        GRANT ALL ON SEQUENCES TO {{ postgres_user }};
    tags: [postgresql]

  - name: Copier la configuration pg_hba.conf
    template:
      src:    pg_hba.conf.j2
      dest:   /etc/postgresql/15/main/pg_hba.conf
      owner:  postgres
      group:  postgres
      mode:   "0640"
    notify: Recharger PostgreSQL
    tags: [postgresql]

  - name: Configurer postgresql.conf (écouter sur 127.0.0.1 seulement)
    lineinfile:
      path: /etc/postgresql/15/main/postgresql.conf
      regexp: "^#?listen_addresses"
      line:   "listen_addresses = '127.0.0.1'"
    notify: Recharger PostgreSQL
    tags: [postgresql]

  nano infrastructure/ansible/roles/postgresql/handlers/main.yml

  ---
  - name: Démarrer PostgreSQL
    service: name=postgresql state=started enabled=yes

  - name: Recharger PostgreSQL
    service: name=postgresql state=reloaded

  - name: Redémarrer PostgreSQL
    service: name=postgresql state=restarted

  nano infrastructure/ansible/roles/postgresql/templates/pg_hba.conf.j2

  # PostgreSQL Client Authentication Configuration File
  # Généré par Ansible — NE PAS MODIFIER MANUELLEMENT

  # TYPE  DATABASE        USER            ADDRESS                 METHOD
  local   all             postgres                                peer
  local   all             all                                     peer
  host    {{ postgres_db }} {{ postgres_user }} 127.0.0.1/32    md5
  host    {{ postgres_db }} {{ postgres_user }} 10.0.0.0/16     md5
  # Interdire toutes les autres connexions (inclus depuis Internet)

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.8 — ROLE redis: INSTALLER ET CONFIGURER LE CACHE
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/ansible/roles/redis/tasks/main.yml

  ---
  - name: Installer Redis 7
    apt:
      name: redis-server
      state: present
    tags: [redis]

  - name: Copier la configuration Redis
    template:
      src:   redis.conf.j2
      dest:  /etc/redis/redis.conf
      owner: redis
      group: redis
      mode:  "0640"
    notify: Redémarrer Redis
    tags: [redis]

  - name: Démarrer Redis et l'activer au boot
    service:
      name:    redis-server
      state:   started
      enabled: yes
    tags: [redis]

  - name: Vérifier que Redis répond
    command: redis-cli ping
    register: redis_ping
    changed_when: false
    failed_when: redis_ping.stdout != "PONG"
    tags: [redis]

  nano infrastructure/ansible/roles/redis/templates/redis.conf.j2

  # Redis configuration — Généré par Ansible

  # Écouter SEULEMENT sur 127.0.0.1 (jamais sur l'interface publique!)
  bind {{ redis_bind }} 127.0.0.1 ::1
  port {{ redis_port }}

  # Protection des données
  protected-mode yes

  # Limite de mémoire (évite que Redis mange toute la RAM)
  maxmemory {{ redis_maxmemory }}
  maxmemory-policy {{ redis_maxmemory_policy }}

  # Persistance (sauvegarde sur disque)
  save 900 1      # Sauvegarder si 1 clé changée en 15 min
  save 300 10     # Sauvegarder si 10 clés changées en 5 min
  save 60 10000   # Sauvegarder si 10000 clés changées en 1 min
  dbfilename dump.rdb
  dir /var/lib/redis

  # Logs
  loglevel notice
  logfile /var/log/redis/redis-server.log

  # Performance
  tcp-keepalive 300
  timeout 0

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.9 — ROLE flask_app: DÉPLOYER L'APPLICATION
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/ansible/roles/flask_app/tasks/main.yml

  ---
  # ═══════════════════════════════════════════════════════════════════════════
  # ROLE flask_app — Déployer TaskManager API
  # ═══════════════════════════════════════════════════════════════════════════

  # TASK 1: Cloner ou mettre à jour le code depuis GitHub
  # Le module "git" gère les dépôts Git de façon idempotente.
  # Si le dépôt est absent: clone. Si présent: git pull + checkout version.
  - name: Cloner/mettre à jour le code de l'application
    become_user: "{{ app_user }}"
    git:
      repo:    "{{ app_repo }}"        # git@github.com:equipe/taskmanager.git
      dest:    "{{ app_dir }}"         # /opt/taskmanager
      version: "{{ app_version }}"     # "main", "v1.0.1", "feature/3-..."
      force:   yes                     # Forcer même si des modifications locales existent
      accept_hostkey: yes              # Accepter la clé d'hôte GitHub automatiquement
    notify: Redémarrer TaskManager
    tags: [flask_app, deploy]

  # TASK 2: Créer le virtualenv Python
  - name: Créer le virtualenv Python
    become_user: "{{ app_user }}"
    command: "python3.11 -m venv {{ venv_dir }}"
    args:
      creates: "{{ venv_dir }}/bin/activate"   # Ne rien faire si déjà créé
    tags: [flask_app, python]

  # TASK 3: Installer les dépendances Python
  - name: Installer les dépendances Python
    become_user: "{{ app_user }}"
    pip:
      requirements: "{{ app_dir }}/requirements.txt"
      virtualenv:   "{{ venv_dir }}"
      state:        present
      extra_args:   "--upgrade"
    notify: Redémarrer TaskManager
    tags: [flask_app, python]

  # TASK 4: Créer le fichier .env depuis le template Jinja2
  # Le template env.j2 contient les variables avec {{ }} qui seront remplacées.
  - name: Créer le fichier .env de l'application
    template:
      src:   env.j2                   # Template dans roles/flask_app/templates/
      dest:  "{{ app_dir }}/.env"
      owner: "{{ app_user }}"
      group: "{{ app_group }}"
      mode:  "0600"                   # Lisible seulement par l'utilisateur deploy
    notify: Redémarrer TaskManager
    no_log: true                      # Ne pas afficher le contenu du .env dans les logs
    tags: [flask_app, config]

  # TASK 5: Exécuter les migrations de base de données
  - name: Exécuter les migrations Flask-Migrate
    become_user: "{{ app_user }}"
    command: "{{ venv_dir }}/bin/flask db upgrade"
    args:
      chdir: "{{ app_dir }}"
    environment:
      FLASK_APP: run.py
      FLASK_ENV: "{{ flask_env }}"
    register: migrate_output
    changed_when: "'Running upgrade' in migrate_output.stdout"
    tags: [flask_app, database]

  - name: Afficher la sortie des migrations
    debug:
      msg: "{{ migrate_output.stdout_lines }}"
    when: migrate_output.stdout_lines | length > 0
    tags: [flask_app, database]

  # TASK 6: Créer le service systemd pour Flask/Gunicorn
  - name: Créer le service systemd TaskManager
    template:
      src:  taskmanager.service.j2
      dest: /etc/systemd/system/taskmanager.service
      mode: "0644"
    notify:
      - Recharger systemd
      - Redémarrer TaskManager
    tags: [flask_app, service]

  # TASK 7: Activer et démarrer le service
  - name: Activer et démarrer le service TaskManager
    service:
      name:    taskmanager
      state:   started
      enabled: yes
    tags: [flask_app, service]

  # TASK 8: Vérifier que l'API répond
  - name: Attendre que l'API soit prête
    uri:
      url:             "http://localhost:{{ flask_port }}/health"
      status_code:     200
      return_content:  yes
    register: health_check
    until:   health_check.status == 200
    retries: 10
    delay:   3   # Attendre 3 secondes entre chaque tentative
    tags: [flask_app, verify]

  - name: Afficher le résultat du health check
    debug:
      msg: "API status: {{ (health_check.content | from_json).status }}"
    tags: [flask_app, verify]

  nano infrastructure/ansible/roles/flask_app/handlers/main.yml

  ---
  - name: Recharger systemd
    systemd: daemon_reload=yes

  - name: Redémarrer TaskManager
    service: name=taskmanager state=restarted

  - name: Recharger TaskManager (sans coupure)
    service: name=taskmanager state=reloaded

  nano infrastructure/ansible/roles/flask_app/templates/env.j2

  # Fichier .env — Généré par Ansible (NE PAS MODIFIER MANUELLEMENT)
  # Toute modification doit être faite dans group_vars/ ou vars/secrets.yml

  FLASK_ENV={{ flask_env }}
  FLASK_DEBUG={{ flask_debug }}
  SECRET_KEY={{ vault_secret_key }}

  DATABASE_URL=postgresql://{{ postgres_user }}:{{ vault_db_password }}@127.0.0.1:{{ postgres_port }}/{{ postgres_db }}
  REDIS_URL=redis://127.0.0.1:{{ redis_port }}/0

  APP_NAME={{ app_name }}
  APP_VERSION={{ app_version }}
  LOG_LEVEL={{ log_level }}

  # Variables optionnelles
  ALLOWED_HOSTS={{ domain }}
  CORS_ORIGINS=https://{{ domain }}

  nano infrastructure/ansible/roles/flask_app/templates/taskmanager.service.j2

  # Service systemd — Généré par Ansible
  [Unit]
  Description=TaskManager API Flask/Gunicorn
  After=network.target postgresql.service redis-server.service
  Requires=postgresql.service redis-server.service

  [Service]
  Type=exec
  User={{ app_user }}
  Group={{ app_group }}
  WorkingDirectory={{ app_dir }}
  EnvironmentFile={{ app_dir }}/.env
  ExecStart={{ venv_dir }}/bin/gunicorn \
      --workers {{ gunicorn_workers }} \
      --bind {{ gunicorn_bind }} \
      --timeout {{ gunicorn_timeout }} \
      --access-logfile /var/log/taskmanager/access.log \
      --error-logfile /var/log/taskmanager/error.log \
      --log-level {{ log_level | lower }} \
      run:app
  ExecReload=/bin/kill -HUP $MAINPID
  Restart=always
  RestartSec=5
  StandardOutput=journal
  StandardError=journal

  [Install]
  WantedBy=multi-user.target

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.10 — ROLE nginx: REVERSE PROXY
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/ansible/roles/nginx/tasks/main.yml

  ---
  - name: Installer Nginx
    apt:
      name:  nginx
      state: present
    tags: [nginx]

  - name: Supprimer le site default de Nginx
    file:
      path:  /etc/nginx/sites-enabled/default
      state: absent
    notify: Recharger Nginx
    tags: [nginx]

  - name: Copier la config Nginx de l'application (HTTP)
    template:
      src:   taskmanager.conf.j2
      dest:  /etc/nginx/sites-available/taskmanager
      mode:  "0644"
    notify: Tester et recharger Nginx
    tags: [nginx]

  - name: Activer le site Nginx
    file:
      src:   /etc/nginx/sites-available/taskmanager
      dest:  /etc/nginx/sites-enabled/taskmanager
      state: link
    notify: Tester et recharger Nginx
    tags: [nginx]

  - name: Démarrer Nginx et l'activer au boot
    service:
      name:    nginx
      state:   started
      enabled: yes
    tags: [nginx]

  nano infrastructure/ansible/roles/nginx/handlers/main.yml

  ---
  - name: Recharger Nginx
    service: name=nginx state=reloaded

  - name: Redémarrer Nginx
    service: name=nginx state=restarted

  - name: Tester et recharger Nginx
    block:
      - name: Tester la configuration Nginx
        command: nginx -t
        changed_when: false
      - name: Recharger Nginx
        service: name=nginx state=reloaded

  nano infrastructure/ansible/roles/nginx/templates/taskmanager.conf.j2

  # Nginx virtual host — Généré par Ansible

  # Upstream Flask/Gunicorn
  upstream flask_app {
      server 127.0.0.1:{{ flask_port }};
      keepalive 64;
  }

  {% if enable_ssl %}
  # Redirection HTTP -> HTTPS
  server {
      listen 80;
      server_name {{ nginx_server_name }};
      return 301 https://$host$request_uri;
  }

  server {
      listen 443 ssl http2;
      server_name {{ nginx_server_name }};

      ssl_certificate     {{ ssl_cert }};
      ssl_certificate_key {{ ssl_key }};
      ssl_protocols       TLSv1.2 TLSv1.3;
      ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
      ssl_prefer_server_ciphers on;
      ssl_session_cache   shared:SSL:10m;

      add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
      add_header X-Frame-Options DENY;
      add_header X-Content-Type-Options nosniff;

      location / {
          proxy_pass         http://flask_app;
          proxy_set_header   Host $host;
          proxy_set_header   X-Real-IP $remote_addr;
          proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header   X-Forwarded-Proto $scheme;
          proxy_read_timeout {{ gunicorn_timeout }}s;
          client_max_body_size {{ nginx_client_max_body }};
      }
  }
  {% else %}
  server {
      listen 80;
      server_name {{ nginx_server_name }};

      location / {
          proxy_pass       http://flask_app;
          proxy_set_header Host $host;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_read_timeout {{ gunicorn_timeout }}s;
      }

      location /health {
          proxy_pass       http://flask_app;
          access_log       off;   # Ne pas logger les health checks
      }
  }
  {% endif %}

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.11 — ROLE ssl: CERTIFICAT LET'S ENCRYPT (PRODUCTION UNIQUEMENT)
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/ansible/roles/ssl/tasks/main.yml

  ---
  - name: Installer Certbot
    apt:
      name:
        - certbot
        - python3-certbot-nginx
      state: present
    tags: [ssl]

  - name: Obtenir le certificat Let's Encrypt
    command: >
      certbot --nginx
      --non-interactive
      --agree-tos
      --email alice@equipe.com
      --domain {{ domain }}
      --redirect
    args:
      creates: "/etc/letsencrypt/live/{{ domain }}/fullchain.pem"
    when: enable_ssl | bool
    notify: Recharger Nginx
    tags: [ssl]

  - name: Configurer le renouvellement automatique (cron)
    cron:
      name: "Renouvellement Let's Encrypt"
      special_time: weekly
      job: "certbot renew --quiet --post-hook 'systemctl reload nginx'"
      user: root
    when: enable_ssl | bool
    tags: [ssl]

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 4.12 — ROLE monitoring: NODE EXPORTER POUR PROMETHEUS
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/ansible/roles/monitoring/tasks/main.yml

  ---
  - name: Créer l'utilisateur node_exporter (sans login)
    user:
      name:   node_exporter
      shell:  /usr/sbin/nologin
      system: yes
      create_home: no
    tags: [monitoring]

  - name: Télécharger Node Exporter
    get_url:
      url:  "https://github.com/prometheus/node_exporter/releases/download/v1.7.0/node_exporter-1.7.0.linux-amd64.tar.gz"
      dest: /tmp/node_exporter.tar.gz
      mode: "0644"
    tags: [monitoring]

  - name: Extraire Node Exporter
    unarchive:
      src:        /tmp/node_exporter.tar.gz
      dest:       /tmp/
      remote_src: yes   # L'archive est sur le serveur distant (pas sur le control node)
    tags: [monitoring]

  - name: Installer le binaire node_exporter
    copy:
      src:        /tmp/node_exporter-1.7.0.linux-amd64/node_exporter
      dest:       /usr/local/bin/node_exporter
      owner:      node_exporter
      group:      node_exporter
      mode:       "0755"
      remote_src: yes
    notify: Redémarrer node_exporter
    tags: [monitoring]

  - name: Créer le service systemd node_exporter
    copy:
      content: |
        [Unit]
        Description=Node Exporter (métriques système pour Prometheus)
        After=network.target

        [Service]
        User=node_exporter
        ExecStart=/usr/local/bin/node_exporter \
            --web.listen-address="127.0.0.1:9100" \
            --collector.systemd \
            --collector.processes
        Restart=always

        [Install]
        WantedBy=multi-user.target
      dest: /etc/systemd/system/node_exporter.service
    notify:
      - Recharger systemd
      - Redémarrer node_exporter
    tags: [monitoring]

  - name: Démarrer node_exporter
    service: name=node_exporter state=started enabled=yes
    tags: [monitoring]

================================================================================
PARTIE 5 — LES PLAYBOOKS ANSIBLE: ORCHESTRATION COMPLÈTE
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.1 — site.yml: PLAYBOOK PRINCIPAL (TOUT CONFIGURER)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI site.yml?
  ───────────────────
  Un seul fichier pour tout configurer: de l'OS nu jusqu'à l'API qui répond.
  Appellé lors de la première installation ou pour un re-provisioning complet.

  nano infrastructure/ansible/site.yml

  ---
  # ═══════════════════════════════════════════════════════════════════════════
  # site.yml — Playbook principal: configure un serveur de zéro
  # Usage:  ansible-playbook site.yml -i inventory/staging.ini
  # Vault:  ansible-playbook site.yml --vault-password-file ~/.vault_pass
  # ═══════════════════════════════════════════════════════════════════════════

  # PLAY 1: Configuration de base de TOUS les serveurs
  - name: Configuration système commune
    hosts: all
    become: yes
    vars_files:
      - vars/staging_secrets.yml   # Fichier chiffré Vault
    roles:
      - role: common
        tags: [common]
      - role: python
        tags: [python]

  # PLAY 2: Base de données (serveurs du groupe "db")
  - name: Configurer PostgreSQL et Redis
    hosts: db
    become: yes
    vars_files:
      - vars/staging_secrets.yml
    roles:
      - role: postgresql
        tags: [postgresql, database]
      - role: redis
        tags: [redis, cache]

  # PLAY 3: Application Flask (serveurs du groupe "web")
  - name: Déployer l'application Flask
    hosts: web
    become: yes
    vars_files:
      - vars/staging_secrets.yml
    roles:
      - role: flask_app
        tags: [flask_app, deploy]

  # PLAY 4: Reverse proxy Nginx
  - name: Configurer Nginx
    hosts: web
    become: yes
    roles:
      - role: nginx
        tags: [nginx]

  # PLAY 5: SSL/TLS (production uniquement)
  - name: Configurer SSL Let's Encrypt
    hosts: web
    become: yes
    roles:
      - role: ssl
        tags: [ssl]
        when: enable_ssl | bool

  # PLAY 6: Monitoring (optionnel)
  - name: Installer Node Exporter
    hosts: all
    become: yes
    roles:
      - role: monitoring
        tags: [monitoring]
        when: monitoring_enabled | bool

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.2 — deploy.yml: DÉPLOYER SEULEMENT LE CODE (SANS RECONFIGURER)
────────────────────────────────────────────────────────────────────────────────

  POURQUOI UN PLAYBOOK SÉPARÉ?
  ─────────────────────────────
  site.yml = tout reconfigurer (lent, ~15 min). Usage: premier install.
  deploy.yml = seulement mettre à jour le code (rapide, ~2 min). Usage: CI/CD.

  nano infrastructure/ansible/deploy.yml

  ---
  # deploy.yml — Mettre à jour seulement le code Flask
  # Usage: ansible-playbook deploy.yml -e "app_version=v1.2.0"
  # CI/CD: ansible-playbook deploy.yml -e "app_version={{ github.ref_name }}"

  - name: Déployer la nouvelle version
    hosts: web
    become: yes
    vars_files:
      - vars/staging_secrets.yml
    vars:
      app_version: "{{ app_version | default('main') }}"

    tasks:
      - name: Mettre à jour le code depuis Git
        become_user: "{{ app_user }}"
        git:
          repo:    "{{ app_repo }}"
          dest:    "{{ app_dir }}"
          version: "{{ app_version }}"
          force:   yes
        notify: Redémarrer TaskManager

      - name: Mettre à jour les dépendances Python
        become_user: "{{ app_user }}"
        pip:
          requirements: "{{ app_dir }}/requirements.txt"
          virtualenv:   "{{ venv_dir }}"
          state:        present
          extra_args:   "--upgrade"
        notify: Redémarrer TaskManager

      - name: Exécuter les migrations de BDD
        become_user: "{{ app_user }}"
        command: "{{ venv_dir }}/bin/flask db upgrade"
        args:
          chdir: "{{ app_dir }}"
        environment:
          FLASK_APP:     run.py
          FLASK_ENV:     "{{ flask_env }}"
          DATABASE_URL:  "postgresql://{{ postgres_user }}:{{ vault_db_password }}@127.0.0.1/{{ postgres_db }}"
        register: migration_result
        changed_when: "'Running upgrade' in migration_result.stdout"

      - name: Vérifier le health check après déploiement
        uri:
          url:         "http://localhost:{{ flask_port }}/health"
          status_code: 200
        retries: 10
        delay:   3
        register: health_response
        until:   health_response.status == 200

      - name: Afficher la version déployée
        debug:
          msg: "Déployé: {{ app_version }} — API status: {{ (health_response.content | from_json).status }}"

    handlers:
      - name: Redémarrer TaskManager
        service: name=taskmanager state=restarted

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.3 — rollback.yml: RETOUR À LA VERSION PRÉCÉDENTE
────────────────────────────────────────────────────────────────────────────────

  nano infrastructure/ansible/rollback.yml

  ---
  # rollback.yml — Revenir à la version précédente en cas de problème
  # Usage: ansible-playbook rollback.yml -e "rollback_version=v1.0.1"

  - name: Rollback de l'application
    hosts: web
    become: yes
    vars_files:
      - vars/staging_secrets.yml

    tasks:
      - name: Récupérer la version actuellement déployée
        become_user: "{{ app_user }}"
        command: git -C "{{ app_dir }}" describe --tags --always
        register: current_version
        changed_when: false

      - name: Afficher la version actuelle avant rollback
        debug:
          msg: "Version actuelle: {{ current_version.stdout }} -> rollback vers: {{ rollback_version }}"

      - name: Revenir à la version précédente
        become_user: "{{ app_user }}"
        git:
          repo:    "{{ app_repo }}"
          dest:    "{{ app_dir }}"
          version: "{{ rollback_version }}"
          force:   yes
        notify: Redémarrer TaskManager

      - name: Exécuter les migrations (downgrade si nécessaire)
        become_user: "{{ app_user }}"
        command: "{{ venv_dir }}/bin/flask db downgrade"
        args:
          chdir: "{{ app_dir }}"
        environment:
          FLASK_APP: run.py
          FLASK_ENV: "{{ flask_env }}"
          DATABASE_URL: "postgresql://{{ postgres_user }}:{{ vault_db_password }}@127.0.0.1/{{ postgres_db }}"
        when: rollback_run_downgrade | default(false) | bool

      - name: Vérifier le health check après rollback
        uri:
          url:         "http://localhost:{{ flask_port }}/health"
          status_code: 200
        retries: 10
        delay:   3

    handlers:
      - name: Redémarrer TaskManager
        service: name=taskmanager state=restarted

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 5.4 — ALICE EXÉCUTE LES PLAYBOOKS: TOUTES LES COMMANDES
────────────────────────────────────────────────────────────────────────────────

  ALICE DANS SON TERMINAL:
  ─────────────────────────
  cd infrastructure/ansible/

  ── VÉRIFIER L'INVENTAIRE ───────────────────────────────────────────────────

  # Lister les hôtes de l'inventaire:
  ansible-inventory -i inventory/staging.ini --list

  CE QUE VOUS VOYEZ:
  ───────────────────
  {
      "_meta": {
          "hostvars": {
              "taskmanager-staging": {
                  "ansible_host": "164.90.154.23",
                  "ansible_user": "ubuntu",
                  "app_port": "5000"
              }
          }
      },
      "staging": {"hosts": ["taskmanager-staging"]},
      "web": {"hosts": ["taskmanager-staging"]},
      "db":  {"hosts": ["taskmanager-staging"]}
  }

  # Tester la connexion SSH sur tous les hôtes:
  ansible all -i inventory/staging.ini -m ping

  CE QUE VOUS VOYEZ:
  ───────────────────
  taskmanager-staging | SUCCESS => {
      "changed": false,
      "ping": "pong"
  }

  SI "UNREACHABLE!":
  ───────────────────
  -> Vérifier que l'IP est correcte dans staging.ini.
  -> Vérifier que l'agent SSH est chargé: eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519_github
  -> Tester manuellement: ssh ubuntu@164.90.154.23

  ── COLLECTER LES FACTS (INFOS SUR LE SERVEUR) ─────────────────────────────

  ansible taskmanager-staging -i inventory/staging.ini -m gather_facts | head -60

  CE QUE VOUS VOYEZ:
  ───────────────────
  taskmanager-staging | SUCCESS => {
      "ansible_facts": {
          "ansible_architecture": "x86_64",
          "ansible_distribution": "Ubuntu",
          "ansible_distribution_release": "jammy",
          "ansible_distribution_version": "22.04",
          "ansible_hostname": "taskmanager-staging",
          "ansible_memtotal_mb": 3944,
          "ansible_processor_cores": 2,
          "ansible_python_version": "3.10.12",
          "ansible_default_ipv4": {
              "address": "164.90.154.23",
              ...
          }
      }
  }

  # Ces facts sont utilisables dans les playbooks: {{ ansible_distribution_release }}

  ── EXÉCUTER UNE COMMANDE AD-HOC ────────────────────────────────────────────

  # POURQUOI LES COMMANDES AD-HOC?
  # Parfois on a besoin d'exécuter UNE action sans écrire un playbook.
  # Exemples: vérifier l'espace disque, redémarrer un service, corriger un bug urgent.

  # Vérifier l'espace disque:
  ansible all -i inventory/staging.ini -m command -a "df -h /"

  CE QUE VOUS VOYEZ:
  ───────────────────
  taskmanager-staging | CHANGED | rc=0 >>
  Filesystem      Size  Used Avail Use% Mounted on
  /dev/vda1        78G  4.2G   70G   6% /

  # Redémarrer le service taskmanager:
  ansible web -i inventory/staging.ini -m service -a "name=taskmanager state=restarted" --become

  # Voir les derniers logs de l'application:
  ansible web -i inventory/staging.ini -m command -a "journalctl -u taskmanager --no-pager -n 50"

  # Copier un fichier vers le serveur:
  ansible web -i inventory/staging.ini -m copy \
    -a "src=./files/fix.py dest=/tmp/fix.py mode=0644"

  # Exécuter un script Python sur le serveur:
  ansible web -i inventory/staging.ini -m script -a "./scripts/fix_permissions.sh"

  # Vérifier le contenu d'un fichier de config:
  ansible web -i inventory/staging.ini -m command -a "cat /opt/taskmanager/.env" --become
  # ATTENTION: affiche les mots de passe! À utiliser avec précaution.

  ── LANCER LE PLAYBOOK PRINCIPAL ────────────────────────────────────────────

  POURQUOI?
  ─────────
  Configurer un serveur de zéro: OS -> Python -> PostgreSQL -> Redis -> Flask -> Nginx.
  QUAND: première installation, ou après un terraform destroy/apply.

  ansible-playbook site.yml \
    -i inventory/staging.ini \
    --vault-password-file ~/.vault_pass

  CE QUE VOUS VOYEZ (extrait):
  ─────────────────────────────
  PLAY [Configuration système commune] **********************************

  TASK [Gathering Facts] ************************************************
  ok: [taskmanager-staging]

  TASK [common : Mettre à jour le cache apt] ****************************
  ok: [taskmanager-staging]

  TASK [common : Installer les paquets système communs] *****************
  changed: [taskmanager-staging]

  TASK [common : Configurer le timezone] ********************************
  changed: [taskmanager-staging]

  TASK [python : Installer Python 3.11] *********************************
  changed: [taskmanager-staging]

  PLAY [Configurer PostgreSQL et Redis] *********************************

  TASK [postgresql : Installer PostgreSQL 15] ***************************
  changed: [taskmanager-staging]

  TASK [postgresql : Créer l'utilisateur PostgreSQL] ********************
  changed: [taskmanager-staging]

  TASK [postgresql : Créer la base de données] **************************
  changed: [taskmanager-staging]

  TASK [redis : Installer Redis 7] **************************************
  changed: [taskmanager-staging]

  PLAY [Déployer l'application Flask] ***********************************

  TASK [flask_app : Cloner/mettre à jour le code] ***********************
  changed: [taskmanager-staging]

  TASK [flask_app : Créer le virtualenv Python] *************************
  changed: [taskmanager-staging]

  TASK [flask_app : Installer les dépendances Python] *******************
  changed: [taskmanager-staging]

  TASK [flask_app : Créer le fichier .env] ******************************
  changed: [taskmanager-staging]

  TASK [flask_app : Exécuter les migrations Flask-Migrate] **************
  changed: [taskmanager-staging]

  TASK [flask_app : Vérifier le health check] ***************************
  ok: [taskmanager-staging]

  RUNNING HANDLERS **********************************************
  changed: [taskmanager-staging] (handler: Redémarrer TaskManager)
  changed: [taskmanager-staging] (handler: Recharger Nginx)

  PLAY RECAP ***************************************************
  taskmanager-staging : ok=32 changed=20 unreachable=0 failed=0 skipped=2

  # [OK] 32 tasks OK, 0 failed.

  # Vérification finale:
  curl http://164.90.154.23/health
  # -> {"status": "healthy", "database": "ok", "redis": "ok"}   [OK]

  EXÉCUTER AVEC DES OPTIONS AVANCÉES:
  ─────────────────────────────────────
  # Voir TOUTES les actions (mode verbose):
  ansible-playbook site.yml -v    # verbose
  ansible-playbook site.yml -vv   # plus verbose
  ansible-playbook site.yml -vvv  # très verbose (SSH, modules...)

  # Simuler sans modifier (dry run):
  ansible-playbook site.yml --check

  CE QUE VOUS VOYEZ EN --check:
  ──────────────────────────────
  TASK [common : Installer les paquets système communs] ***
  ok: [taskmanager-staging] (pas de changement en mode check)

  TASK [flask_app : Cloner/mettre à jour le code] ***
  changed: [taskmanager-staging]   # <- SERAIT changé si on appliquait vraiment

  # --check permet de prévoir l'impact avant d'exécuter.

  # Limiter à certains hôtes:
  ansible-playbook site.yml --limit taskmanager-staging
  ansible-playbook site.yml --limit web      # Seulement le groupe "web"

  # Limiter à certaines tasks (via tags):
  ansible-playbook site.yml --tags flask_app,nginx
  ansible-playbook site.yml --skip-tags monitoring

  # Commencer à partir d'une task spécifique:
  ansible-playbook site.yml --start-at-task "Créer le fichier .env"

  # Exécuter task par task avec confirmation:
  ansible-playbook site.yml --step

  # Passer des variables supplémentaires:
  ansible-playbook deploy.yml -e "app_version=v1.2.0"
  ansible-playbook deploy.yml -e '{"app_version":"v1.2.0","gunicorn_workers":4}'

  ── LINTER LES PLAYBOOKS ────────────────────────────────────────────────────

  ansible-lint site.yml

  CE QUE VOUS VOYEZ (problèmes détectés):
  ─────────────────────────────────────────
  WARNING  no-free-form: Use FQCN (Fully Qualified Collection Name) for modules.
  roles/postgresql/tasks/main.yml:5 Task/Handler: Installer PostgreSQL 15
  Found 1 issue(s) which can be fixed automatically.

  # Corriger: "apt:" -> "ansible.builtin.apt:"
  ansible-lint --fix site.yml   # Corriger automatiquement les problèmes simples
================================================================================
PARTIE 6 — INTÉGRATION CI/CD: TERRAFORM ET ANSIBLE DANS GITHUB ACTIONS
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 6.1 — WORKFLOW CI: VALIDER LES FICHIERS TERRAFORM ET ANSIBLE EN PR
────────────────────────────────────────────────────────────────────────────────

  POURQUOI VALIDER EN PR?
  ────────────────────────
  Bob ouvre une PR pour ajouter un serveur de monitoring.
  Il a une erreur dans main.tf. Sans validation auto: l'erreur passe en review,
  Alice passe du temps à revoir, Bob doit corriger.
  Avec validation auto: GitHub Actions détecte l'erreur en 2 minutes.

  ALICE CRÉE .github/workflows/infrastructure-ci.yml:
  ─────────────────────────────────────────────────────
  nano .github/workflows/infrastructure-ci.yml

  name: Infrastructure CI — Validation Terraform et Ansible

  on:
    pull_request:
      paths:
        - 'infrastructure/**'    # Déclencher seulement si infra modifiée
    push:
      branches: [main, develop]
      paths:
        - 'infrastructure/**'

  jobs:

    terraform-validate:
      name: Terraform Validate + Lint
      runs-on: ubuntu-latest
      strategy:
        matrix:
          environment: [staging, production]

      steps:
        - uses: actions/checkout@v4

        - uses: hashicorp/setup-terraform@v3
          with:
            terraform_version: 1.7.4

        - name: Terraform Init
          run: |
            cd infrastructure/terraform/environments/${{ matrix.environment }}
            terraform init -backend=false   # -backend=false = pas de config backend en CI
          env:
            TF_VAR_do_token: ${{ secrets.DO_TOKEN }}

        - name: Terraform Validate
          run: |
            cd infrastructure/terraform/environments/${{ matrix.environment }}
            terraform validate

        - name: Terraform Format Check
          run: |
            cd infrastructure/terraform
            terraform fmt -check -recursive
          # Si un fichier est mal formaté: terraform fmt -recursive (en local)

        - name: TFLint
          uses: terraform-linters/setup-tflint@v4
          with:
            tflint_version: latest
        - run: |
            cd infrastructure/terraform/environments/${{ matrix.environment }}
            tflint --init
            tflint

        - name: TFSec (scanner sécurité)
          uses: aquasecurity/tfsec-action@v1
          with:
            working_directory: infrastructure/terraform/environments/${{ matrix.environment }}
            soft_fail: true   # Ne pas bloquer la PR sur les warnings (seulement les erreurs)

    terraform-plan:
      name: Terraform Plan (staging)
      runs-on: ubuntu-latest
      needs: terraform-validate    # Valider avant de planner

      steps:
        - uses: actions/checkout@v4

        - uses: hashicorp/setup-terraform@v3
          with:
            terraform_version: 1.7.4
            cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}   # Terraform Cloud

        - name: Terraform Plan Staging
          id: plan
          run: |
            cd infrastructure/terraform/environments/staging
            terraform init
            terraform plan -no-color -out=plan.tfplan
          env:
            TF_VAR_do_token: ${{ secrets.DO_TOKEN }}
          continue-on-error: true   # Continuer même si plan contient des changements

        - name: Afficher le plan dans les commentaires PR
          uses: actions/github-script@v7
          if: github.event_name == 'pull_request'
          with:
            script: |
              const output = `#### Terraform Plan Staging [LISTE]
              \`\`\`\n${{ steps.plan.outputs.stdout }}\n\`\`\`

              *Déclenché par: ${{ github.actor }}*`;

              github.rest.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body: output
              })

    ansible-lint:
      name: Ansible Lint
      runs-on: ubuntu-latest

      steps:
        - uses: actions/checkout@v4

        - uses: actions/setup-python@v5
          with:
            python-version: '3.11'

        - run: pip install ansible ansible-lint

        - name: Ansible Lint
          run: |
            cd infrastructure/ansible
            ansible-lint site.yml deploy.yml rollback.yml

        - name: Ansible Syntax Check
          run: |
            cd infrastructure/ansible
            ansible-playbook site.yml --syntax-check -i inventory/staging.ini

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 6.2 — WORKFLOW CD: TERRAFORM APPLY + ANSIBLE DEPLOY SUR TAG
────────────────────────────────────────────────────────────────────────────────

  ALICE CRÉE .github/workflows/infrastructure-deploy.yml:
  ─────────────────────────────────────────────────────────
  nano .github/workflows/infrastructure-deploy.yml

  name: Infrastructure Deploy — Terraform Apply + Ansible Deploy

  on:
    push:
      tags: ['v*.*.*']   # Seulement sur les tags de version

  env:
    ENVIRONMENT: production

  jobs:

    terraform-apply:
      name: Terraform Apply Production
      runs-on: ubuntu-latest
      environment:
        name: production
        url: https://api.equipe.com/health
      outputs:
        server_ip: ${{ steps.outputs.outputs.server_ip }}

      steps:
        - uses: actions/checkout@v4

        - uses: hashicorp/setup-terraform@v3
          with:
            terraform_version: 1.7.4
            cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}

        - name: Terraform Init
          run: terraform init
          working-directory: infrastructure/terraform/environments/production
          env:
            TF_VAR_do_token: ${{ secrets.DO_TOKEN }}

        - name: Terraform Apply
          run: terraform apply -auto-approve
          working-directory: infrastructure/terraform/environments/production
          env:
            TF_VAR_do_token: ${{ secrets.DO_TOKEN }}

        - name: Récupérer l'IP du serveur
          id: outputs
          run: |
            IP=$(terraform output -raw server_ip)
            echo "server_ip=$IP" >> $GITHUB_OUTPUT
          working-directory: infrastructure/terraform/environments/production
          env:
            TF_VAR_do_token: ${{ secrets.DO_TOKEN }}

    ansible-deploy:
      name: Ansible Deploy Production
      runs-on: ubuntu-latest
      needs: terraform-apply

      steps:
        - uses: actions/checkout@v4

        - uses: actions/setup-python@v5
          with:
            python-version: '3.11'

        - run: pip install ansible

        - name: Configurer la clé SSH
          run: |
            mkdir -p ~/.ssh
            echo "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/deploy_key
            chmod 600 ~/.ssh/deploy_key
            ssh-keyscan -H ${{ needs.terraform-apply.outputs.server_ip }} >> ~/.ssh/known_hosts

        - name: Créer l'inventaire dynamique
          run: |
            cat > infrastructure/ansible/inventory/ci_production.ini << EOF
            [web]
            production ansible_host=${{ needs.terraform-apply.outputs.server_ip }}
            [db]
            production ansible_host=${{ needs.terraform-apply.outputs.server_ip }}
            [production:children]
            web
            db
            [all:vars]
            ansible_user=ubuntu
            ansible_ssh_private_key_file=~/.ssh/deploy_key
            EOF

        - name: Créer le fichier Vault password
          run: |
            echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ~/.vault_pass
            chmod 600 ~/.vault_pass

        - name: Ansible Deploy
          run: |
            cd infrastructure/ansible
            ansible-playbook deploy.yml \
              -i inventory/ci_production.ini \
              --vault-password-file ~/.vault_pass \
              -e "app_version=${{ github.ref_name }}"
          env:
            ANSIBLE_FORCE_COLOR: "1"
            ANSIBLE_HOST_KEY_CHECKING: "False"

        - name: Vérifier le déploiement
          run: |
            STATUS=$(curl -sf https://api.equipe.com/health | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['status'])")
            if [ "$STATUS" = "healthy" ]; then
              echo "[OK] Déploiement ${{ github.ref_name }} réussi!"
            else
              echo "[X] Health check échoué. Rollback automatique..."
              cd infrastructure/ansible
              ansible-playbook rollback.yml \
                -i inventory/ci_production.ini \
                --vault-password-file ~/.vault_pass \
                -e "rollback_version=$(git describe --tags --abbrev=0 HEAD~1)"
              exit 1
            fi

        - name: Notification Slack
          if: always()
          uses: 8398a7/action-slack@v3
          with:
            status: ${{ job.status }}
            text: "${{ job.status == 'success' && '[OK]' || '[X]' }} Deploy *${{ github.ref_name }}* prod"
          env:
            SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

================================================================================
PARTIE 7 — SCÉNARIOS COMPLETS DE L'ÉQUIPE
================================================================================

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 2 — ALICE: DÉPLOIEMENT COMPLET STAGING DU JOUR 0 AU "HEALTH: OK"
Durée: ~15 minutes totales | Résultat: API disponible sur api-staging.equipe.com
════════════════════════════════════════════════════════════════════════════════

  ALICE — 9H00: Créer l'infrastructure avec Terraform
  ─────────────────────────────────────────────────────
  cd infrastructure/terraform/environments/staging/
  cp terraform.tfvars.example terraform.tfvars
  # Remplir do_token dans terraform.tfvars
  terraform init
  terraform validate   # -> Success!
  terraform plan       # -> Plan: 9 to add, 0 to change, 0 to destroy.
  terraform apply      # -> Apply complete! server_ip = "164.90.154.23"

  ALICE — 9H02: Mettre à jour l'inventaire Ansible
  ──────────────────────────────────────────────────
  cd ../../ansible/
  IP=$(cd ../terraform/environments/staging && terraform output -raw server_ip)
  sed -i "s/ansible_host=.*/ansible_host=$IP/" inventory/staging.ini
  # Vérifier:
  cat inventory/staging.ini | grep ansible_host
  # -> taskmanager-staging ansible_host=164.90.154.23

  ALICE — 9H03: Tester la connectivité SSH
  ──────────────────────────────────────────
  ansible all -i inventory/staging.ini -m ping

  CE QUE VOUS VOYEZ:
  ───────────────────
  taskmanager-staging | SUCCESS => {"ping": "pong"}   [OK]

  SI "UNREACHABLE: Failed to connect to the host via ssh":
  ──────────────────────────────────────────────────────────
  -> Attendre 30s: le user_data (script bash initial) est encore en cours.
  -> Relancer: ansible all -i inventory/staging.ini -m ping

  ALICE — 9H05: Configurer le serveur avec Ansible
  ──────────────────────────────────────────────────
  ansible-playbook site.yml \
    -i inventory/staging.ini \
    --vault-password-file ~/.vault_pass

  [Alice observe les tasks défiler une par une: common -> python -> postgresql -> redis -> flask_app -> nginx]

  PLAY RECAP ************************************************************
  taskmanager-staging : ok=34 changed=22 unreachable=0 failed=0 skipped=2

  ALICE — 9H17: Vérification finale
  ───────────────────────────────────
  curl http://164.90.154.23/health

  CE QUE VOUS VOYEZ:
  ───────────────────
  {"status": "healthy", "database": "ok", "redis": "ok"}   [OK]

  curl http://164.90.154.23/tasks
  # -> {"tasks": [], "total": 0, "page": 1}   [OK]

  curl -X POST http://164.90.154.23/tasks \
    -H "Content-Type: application/json" \
    -d '{"title": "Premier test depuis Ansible", "priority": "high"}'
  # -> {"id": 1, "title": "Premier test depuis Ansible", "priority": "high", ...}   [OK]

  Alice poste dans Slack:
  "Staging déployé en 17 minutes, tout fonctionne.
  Bob, Claire: l'IP est 164.90.154.23 — curl /health pour vérifier."

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 3 — BOB: MODIFIER UNE VARIABLE ET REDÉPLOYER
Durée: 5 minutes | Objectif: augmenter gunicorn_workers de 2 à 3
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Après des tests de charge, Bob remarque que l'API est lente avec 2 workers.
  Il veut passer à 3 workers Gunicorn sur staging.

  POURQUOI PAS MODIFIER DIRECTEMENT SUR LE SERVEUR?
  ───────────────────────────────────────────────────
  Modifier directement via SSH = "configuration drift" (dérive de configuration).
  Le fichier de config sur le serveur diffère de ce qui est dans Git.
  La prochaine fois qu'Alice relance Ansible, la modification est écrasée.
  Toute modification doit passer par Git -> PR -> Ansible.

  BOB DANS SON TERMINAL:
  ───────────────────────
  git checkout develop
  git pull origin develop
  git checkout -b fix/gunicorn-workers-staging

  nano infrastructure/ansible/group_vars/staging.yml
  # Modifier:
  gunicorn_workers: 3   # était 2

  git add infrastructure/ansible/group_vars/staging.yml
  git commit -m "fix(ansible): increase gunicorn workers to 3 on staging

  Load tests showed 2 workers insufficient for concurrent requests.
  Increasing to 3 workers on staging for validation."

  git push -u origin fix/gunicorn-workers-staging
  # [Ouvre une PR -> Alice approuve -> Merge dans develop]

  # Appliquer le changement (seulement le role flask_app est nécessaire):
  cd infrastructure/ansible/
  ansible-playbook site.yml \
    -i inventory/staging.ini \
    --vault-password-file ~/.vault_pass \
    --tags flask_app

  CE QUE VOUS VOYEZ:
  ───────────────────
  PLAY [Déployer l'application Flask] ***************

  TASK [flask_app : Créer le service systemd TaskManager] ***
  changed: [taskmanager-staging]   # Le fichier .service a changé (3 workers)

  RUNNING HANDLERS **
  changed: [taskmanager-staging] (handler: Redémarrer TaskManager)

  PLAY RECAP
  taskmanager-staging : ok=9 changed=2 unreachable=0 failed=0

  # Vérifier:
  ansible web -i inventory/staging.ini \
    -m command -a "grep -i workers /etc/systemd/system/taskmanager.service"
  # -> --workers 3   [OK]

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 4 — CLAIRE: ÉCRIRE ET EXÉCUTER DES TESTS D'INFRASTRUCTURE AVEC Testinfra
Durée: 2 heures | Objectif: tester que le serveur est configuré correctement
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Claire remarque qu'on déploie sans jamais tester que le serveur est réellement
  configuré correctement. Elle propose d'ajouter des tests d'infrastructure
  avec Testinfra (bibliothèque Python qui teste des serveurs via SSH).

  POURQUOI TESTINFRA?
  ────────────────────
  Testinfra = pytest mais pour l'infrastructure.
  Tests comme: "Python 3.11 est installé?", "Le service taskmanager tourne?",
  "Redis écoute sur 127.0.0.1?", "Le port 5432 est fermé depuis l'extérieur?"

  CLAIRE — ÉTAPE 1: Installer Testinfra
  ──────────────────────────────────────
  pip install pytest-testinfra

  CLAIRE — ÉTAPE 2: Écrire les tests
  ────────────────────────────────────
  nano infrastructure/ansible/tests/test_server.py

  """
  Tests d'infrastructure Testinfra — TaskManager API
  Usage: pytest tests/test_server.py --hosts="ssh://ubuntu@164.90.154.23"
  """
  import pytest

  def test_python_installed(host):
      """Python 3.11 doit être installé."""
      python = host.run("python3.11 --version")
      assert python.rc == 0
      assert "3.11" in python.stdout

  def test_postgresql_running(host):
      """PostgreSQL doit être démarré et actif."""
      service = host.service("postgresql")
      assert service.is_running
      assert service.is_enabled

  def test_postgresql_port_local(host):
      """PostgreSQL doit écouter sur 127.0.0.1:5432."""
      socket = host.socket("tcp://127.0.0.1:5432")
      assert socket.is_listening

  def test_postgresql_port_not_public(host):
      """PostgreSQL NE DOIT PAS écouter sur l'interface publique."""
      # Si ce test échoue: ALERTE SÉCURITÉ — la DB est exposée sur Internet!
      socket = host.socket("tcp://0.0.0.0:5432")
      assert not socket.is_listening

  def test_redis_running(host):
      """Redis doit être démarré."""
      service = host.service("redis-server")
      assert service.is_running
      assert service.is_enabled

  def test_redis_port_local_only(host):
      """Redis écoute seulement sur 127.0.0.1."""
      assert host.socket("tcp://127.0.0.1:6379").is_listening
      assert not host.socket("tcp://0.0.0.0:6379").is_listening

  def test_flask_service_running(host):
      """Le service TaskManager (Gunicorn) doit tourner."""
      service = host.service("taskmanager")
      assert service.is_running
      assert service.is_enabled

  def test_flask_port_local(host):
      """Flask/Gunicorn écoute sur 127.0.0.1:5000."""
      socket = host.socket("tcp://127.0.0.1:5000")
      assert socket.is_listening

  def test_nginx_running(host):
      """Nginx doit tourner."""
      service = host.service("nginx")
      assert service.is_running
      assert service.is_enabled

  def test_nginx_port_80(host):
      """Nginx écoute sur le port 80."""
      socket = host.socket("tcp://0.0.0.0:80")
      assert socket.is_listening

  def test_api_health_check(host):
      """L'API doit répondre avec status: healthy."""
      result = host.run("curl -s http://localhost/health")
      assert result.rc == 0
      import json
      data = json.loads(result.stdout)
      assert data["status"] == "healthy"
      assert data["database"] == "ok"

  def test_deploy_user_exists(host):
      """L'utilisateur deploy doit exister."""
      user = host.user("deploy")
      assert user.exists
      assert user.shell == "/bin/bash"

  def test_app_directory(host):
      """Le répertoire de l'application existe avec les bons droits."""
      d = host.file("/opt/taskmanager")
      assert d.is_directory
      assert d.user == "deploy"
      assert d.mode == 0o755

  def test_env_file_permissions(host):
      """Le fichier .env existe avec des permissions restrictives."""
      f = host.file("/opt/taskmanager/.env")
      assert f.exists
      assert f.mode == 0o600   # Lisible seulement par le propriétaire
      assert f.user == "deploy"

  def test_env_file_no_debug_in_prod(host):
      """En production, FLASK_DEBUG ne doit pas être '1'."""
      env_content = host.file("/opt/taskmanager/.env").content_string
      # Ce test est conditionnel: seulement en production
      if "FLASK_ENV=production" in env_content:
          assert "FLASK_DEBUG=0" in env_content or "FLASK_DEBUG=" not in env_content

  def test_fail2ban_running(host):
      """fail2ban doit protéger contre les attaques SSH."""
      service = host.service("fail2ban")
      assert service.is_running

  def test_ufw_active(host):
      """Le pare-feu ufw doit être actif."""
      ufw = host.run("ufw status")
      assert "Status: active" in ufw.stdout

  CLAIRE — ÉTAPE 3: Exécuter les tests
  ──────────────────────────────────────
  cd infrastructure/ansible/
  pytest tests/test_server.py \
    --hosts="ssh://ubuntu@164.90.154.23" \
    --ssh-identity-file=~/.ssh/id_ed25519_github \
    -v

  CE QUE VOUS VOYEZ:
  ───────────────────
  tests/test_server.py::test_python_installed PASSED
  tests/test_server.py::test_postgresql_running PASSED
  tests/test_server.py::test_postgresql_port_local PASSED
  tests/test_server.py::test_postgresql_port_not_public PASSED
  tests/test_server.py::test_redis_running PASSED
  tests/test_server.py::test_redis_port_local_only PASSED
  tests/test_server.py::test_flask_service_running PASSED
  tests/test_server.py::test_flask_port_local PASSED
  tests/test_server.py::test_nginx_running PASSED
  tests/test_server.py::test_nginx_port_80 PASSED
  tests/test_server.py::test_api_health_check PASSED
  tests/test_server.py::test_deploy_user_exists PASSED
  tests/test_server.py::test_app_directory PASSED
  tests/test_server.py::test_env_file_permissions PASSED
  tests/test_server.py::test_fail2ban_running PASSED
  tests/test_server.py::test_ufw_active PASSED

  ======= 16 passed in 23.45s =======   [OK]

  # Claire ajoute ces tests au workflow CI/CD:
  # (dans infrastructure-deploy.yml, après ansible-deploy)
  - name: Tests d'infrastructure Testinfra
    run: |
      pip install pytest-testinfra
      pytest infrastructure/ansible/tests/test_server.py \
        --hosts="ssh://ubuntu@${{ needs.terraform-apply.outputs.server_ip }}" \
        --ssh-identity-file=~/.ssh/deploy_key \
        -v

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 5 — ALICE: HOTFIX INFRASTRUCTURE — SERVEUR QUI NE RÉPOND PLUS
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  16h30. Alice reçoit une alerte DigitalOcean: "CPU staging > 85% depuis 10 min."
  Le serveur est inaccessible. L'API ne répond plus.

  ALICE — DIAGNOSTIC:
  ────────────────────
  # Essayer SSH:
  ssh ubuntu@164.90.154.23
  # -> Connection timed out!   <- serveur complètement figé

  # Vérifier via Terraform:
  cd infrastructure/terraform/environments/staging/
  terraform refresh   # Synchroniser le state avec la réalité
  terraform state show digitalocean_droplet.api
  # -> status = "active" <- DigitalOcean dit que le serveur "tourne" mais SSH ne répond pas

  ALICE — OPTION 1: Forcer le redémarrage via DigitalOcean:
  ───────────────────────────────────────────────────────────
  # Via l'interface DigitalOcean: Droplets -> taskmanager-staging -> Power -> Power Cycle
  # Ou via leur CLI (doctl):
  doctl compute droplet-action power-cycle 987654321

  # Attendre 2 minutes, puis:
  ssh ubuntu@164.90.154.23
  # -> connexion OK!

  cd infrastructure/ansible/
  ansible-playbook site.yml -i inventory/staging.ini --vault-password-file ~/.vault_pass
  # Les services qui ne tournaient plus redémarrent (idempotence Ansible!)

  ALICE — OPTION 2: Recréer le serveur entièrement:
  ────────────────────────────────────────────────────
  # Si le serveur est irrécupérable:
  cd infrastructure/terraform/environments/staging/
  terraform destroy -auto-approve          # Supprimer le serveur cassé

  CE QUE VOUS VOYEZ:
  ───────────────────
  Destroy complete! Resources: 9 destroyed.

  terraform apply -auto-approve            # Recréer un serveur neuf

  CE QUE VOUS VOYEZ:
  ───────────────────
  Apply complete! Resources: 9 added.
  Outputs:
  server_ip = "164.90.154.99"             # Nouvelle IP (normale après recréation)

  # Mettre à jour l'inventaire avec la nouvelle IP:
  IP=$(terraform output -raw server_ip)
  cd ../../ansible/
  sed -i "s/ansible_host=.*/ansible_host=$IP/" inventory/staging.ini

  # Reconfigurer le serveur:
  ansible-playbook site.yml -i inventory/staging.ini --vault-password-file ~/.vault_pass

  # En 17 minutes: serveur recréé et API opérationnelle. [OK]
  # Avec les commandes manuelles d'avant: 45+ minutes et des erreurs possibles.

════════════════════════════════════════════════════════════════════════════════
SCÉNARIO 6 — L'ÉQUIPE: DÉPLOYER LA VERSION 1.1.0 EN PRODUCTION
Durée: ~25 minutes | Déclencheur: tag v1.1.0 poussé par Alice
════════════════════════════════════════════════════════════════════════════════

  CONTEXTE:
  ──────────
  Sprint v1.1.0 terminé. 3 features mergées dans main.
  Alice crée le tag v1.1.0 -> GitHub Actions prend le relais automatiquement.

  ALICE — Créer le tag de release:
  ─────────────────────────────────
  git checkout main && git pull origin main
  git tag -a v1.1.0 -m "Release v1.1.0 — filtrage, authentification JWT, monitoring"
  git push origin v1.1.0

  GitHub Actions se déclenche automatiquement:
  ─────────────────────────────────────────────
  [1/5] terraform-validate     -> [OK] Validé en 2 min
  [2/5] terraform-plan         -> [OK] "0 to add, 0 to change, 0 to destroy" (infra inchangée)
  [3/5] terraform-apply        -> [OK] Rien à faire (infra déjà existante)
  [4/5] ansible-deploy         -> [OK] Code v1.1.0 déployé en 4 min
  [5/5] testinfra tests        -> [OK] 16 tests passés

  Notification Slack automatique:
  ─────────────────────────────────
  #ops: "[OK] Deploy *v1.1.0* prod — https://api.equipe.com/health [OK]"

  Bob et Claire peuvent tester immédiatement:
  ────────────────────────────────────────────
  curl https://api.equipe.com/health
  # -> {"status": "healthy", "version": "v1.1.0", "database": "ok"}   [OK]

================================================================================
PARTIE 8 — FONCTIONNALITÉS AVANCÉES TERRAFORM ET ANSIBLE
================================================================================

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 8.1 — TERRAFORM: WORKSPACE POUR ENVIRONNEMENTS MULTIPLES
────────────────────────────────────────────────────────────────────────────────

  POURQUOI LES WORKSPACES?
  ─────────────────────────
  Un workspace = un state isolé pour la même configuration.
  Permet de gérer staging et prod dans le MÊME dossier terraform,
  avec des states complètement séparés.
  Alternative aux dossiers environments/staging et environments/production.

  # Créer et utiliser un workspace staging:
  terraform workspace new staging
  terraform workspace select staging
  terraform workspace list
  # -> default
  #   * staging
  #     production

  # Le state est isolé par workspace:
  # staging  -> state dans: terraform.tfstate.d/staging/
  # production -> state dans: terraform.tfstate.d/production/

  # Utiliser le nom du workspace dans les ressources:
  resource "digitalocean_droplet" "api" {
    name = "taskmanager-${terraform.workspace}"   # "taskmanager-staging" ou "taskmanager-production"
  }

  terraform apply   # Applique dans le workspace courant seulement

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 8.2 — TERRAFORM: EXPRESSIONS, FONCTIONS, BOUCLES
────────────────────────────────────────────────────────────────────────────────

  COMPTER DES RESSOURCES (count):
  ────────────────────────────────
  # Créer 3 serveurs identiques avec un index:
  resource "digitalocean_droplet" "workers" {
    count  = 3
    name   = "taskmanager-worker-${count.index + 1}"
    region = var.region
    size   = "s-1vcpu-2gb"
    image  = "ubuntu-22-04-x64"
  }

  output "worker_ips" {
    value = digitalocean_droplet.workers[*].ipv4_address
    # -> ["164.90.1.1", "164.90.1.2", "164.90.1.3"]
  }

  BOUCLES FOR_EACH (resources depuis une map):
  ─────────────────────────────────────────────
  # Créer des enregistrements DNS pour plusieurs sous-domaines:
  variable "subdomains" {
    default = {
      "api-staging" = "164.90.154.23"
      "api"         = "164.90.155.10"
    }
  }

  resource "digitalocean_record" "subdomains" {
    for_each = var.subdomains
    domain   = "equipe.com"
    type     = "A"
    name     = each.key     # "api-staging" ou "api"
    value    = each.value   # L'IP correspondante
    ttl      = 300
  }

  EXPRESSIONS CONDITIONNELLES:
  ──────────────────────────────
  # Activer les sauvegardes seulement en production:
  backups = var.environment == "production" ? true : false

  # Créer le volume seulement si la taille est > 0:
  count = var.data_volume_size_gb > 0 ? 1 : 0

  FONCTIONS TERRAFORM UTILES:
  ────────────────────────────
  # Dans terraform console:
  > upper("fra1")          -> "FRA1"
  > lower("FRA1")          -> "fra1"
  > length(["a","b","c"])  -> 3
  > join(", ", ["alice","bob","claire"])  -> "alice, bob, claire"
  > split(",", "a,b,c")   -> ["a","b","c"]
  > contains(["fra1","ams3"], "fra1")  -> true
  > format("taskmanager-%s-%s", var.environment, var.region)  -> "taskmanager-staging-fra1"
  > cidrhost("10.0.0.0/16", 10)  -> "10.0.0.10"
  > max(1, 5, 3)           -> 5
  > file("./scripts/init.sh")   -> Contenu du fichier sous forme de string

  LOCALS: VARIABLES CALCULÉES:
  ──────────────────────────────
  locals {
    # Calculer des valeurs dérivées une seule fois et les réutiliser.
    app_name    = "${var.project_name}-${var.environment}"
    is_prod     = var.environment == "production"
    common_tags = merge(var.default_tags, {environment = var.environment})
  }

  resource "digitalocean_droplet" "api" {
    name = local.app_name         # "taskmanager-staging"
    size = local.is_prod ? "s-4vcpu-8gb" : "s-2vcpu-4gb"
    tags = local.common_tags
  }

────────────────────────────────────────────────────────────────────────────────
ÉTAPE 8.3 — ANSIBLE: FONCTIONNALITÉS AVANCÉES
────────────────────────────────────────────────────────────────────────────────

  BOUCLES AVANCÉES:
  ──────────────────
  # Boucle avec un index:
  - name: Créer plusieurs dossiers de logs
    file:
      path:  "/var/log/taskmanager/{{ item }}"
      state: directory
      mode:  "0755"
    loop:
      - access
      - error
      - debug

  # Boucle sur une map (clé/valeur):
  - name: Configurer les variables d'environnement
    lineinfile:
      path: "{{ app_dir }}/.env"
      regexp: "^{{ item.key }}="
      line:   "{{ item.key }}={{ item.value }}"
    loop: "{{ env_vars | dict2items }}"
    vars:
      env_vars:
        FLASK_ENV: "{{ flask_env }}"
        LOG_LEVEL: "{{ log_level }}"

  CONDITIONNELLES:
  ─────────────────
  # Exécuter seulement en production:
  - name: Configurer le certificat SSL
    include_role:
      name: ssl
    when: enable_ssl | bool and ansible_distribution == "Ubuntu"

  # Exécuter seulement si un fichier n'existe pas:
  - name: Initialiser la base de données (première fois seulement)
    command: "{{ venv_dir }}/bin/flask db init"
    args:
      creates: "{{ app_dir }}/migrations/alembic.ini"

  BLOCS AVEC GESTION D'ERREURS:
  ───────────────────────────────
  - name: Déployer avec gestion d'erreur et rollback
    block:
      # Ces tasks sont dans le bloc "try"
      - name: Mettre à jour le code
        git: repo="{{ app_repo }}" dest="{{ app_dir }}" version="{{ app_version }}"
      - name: Tester le health check
        uri: url="http://localhost:{{ flask_port }}/health" status_code=200
    rescue:
      # Si une task du bloc échoue: exécuter ces tasks
      - name: Rollback en cas d'erreur
        git: repo="{{ app_repo }}" dest="{{ app_dir }}" version="{{ previous_version }}"
      - name: Redémarrer avec l'ancienne version
        service: name=taskmanager state=restarted
      - name: Signaler l'échec
        debug: msg="DÉPLOIEMENT ÉCHOUÉ. Rollback effectué vers {{ previous_version }}"
    always:
      # Toujours exécuté, que le bloc ait réussi ou échoué
      - name: Vérifier le service après tout
        service: name=taskmanager state=started

  DELEGATE_TO: EXÉCUTER UNE TASK SUR UNE AUTRE MACHINE:
  ────────────────────────────────────────────────────────
  # Cas: pendant le déploiement, retirer le serveur du load balancer,
  # déployer, puis le remettre. La task "load balancer" s'exécute
  # sur le serveur du LB, pas sur le serveur de l'app.
  - name: Retirer du load balancer
    command: "lb-cli remove {{ inventory_hostname }}"
    delegate_to: loadbalancer-server   # Exécuter sur CE serveur, pas l'hôte courant
    when: enable_loadbalancer | bool

  RUN_ONCE: EXÉCUTER UNE SEULE FOIS POUR TOUS LES HÔTES:
  ─────────────────────────────────────────────────────────
  # Cas: migration de BDD — exécuter UNE seule fois même si 3 serveurs:
  - name: Exécuter les migrations (une seule fois)
    command: "{{ venv_dir }}/bin/flask db upgrade"
    args: { chdir: "{{ app_dir }}" }
    run_once: true          # Exécuté sur le PREMIER hôte du groupe seulement
    delegate_to: "{{ groups['web'][0] }}"   # Sur le premier serveur web

  REGISTER + FACTS PERSONNALISÉS:
  ─────────────────────────────────
  # Capturer la sortie d'une commande et l'utiliser plus tard:
  - name: Récupérer la version de Flask installée
    command: "{{ venv_dir }}/bin/pip show flask"
    register: flask_info
    changed_when: false

  - name: Afficher la version Flask
    debug:
      msg: "Flask version: {{ flask_info.stdout | regex_search('Version: ([0-9.]+)', '\\1') | first }}"

  # Créer un fact personnalisé:
  - name: Définir la version de l'application comme fact
    set_fact:
      deployed_version: "{{ app_version }}"
      deploy_timestamp: "{{ ansible_date_time.iso8601 }}"

  ANSIBLE GALAXY: INSTALLER DES ROLES COMMUNAUTAIRES:
  ─────────────────────────────────────────────────────
  # POURQUOI?
  # Des centaines de roles maintenus par la communauté pour des logiciels courants.
  # Évite de réécrire ce que d'autres ont déjà testé en production.

  # Créer un fichier de requirements:
  nano infrastructure/ansible/requirements.yml

  ---
  roles:
    - name: geerlingguy.postgresql
      version: "3.5.0"
    - name: geerlingguy.redis
      version: "1.8.0"
    - name: geerlingguy.certbot
      version: "6.3.0"

  # Installer les roles:
  ansible-galaxy install -r requirements.yml -p roles/

  # Utiliser dans le playbook:
  roles:
    - role: geerlingguy.postgresql
      vars:
        postgresql_databases: [{name: taskmanager}]
        postgresql_users: [{name: taskmanager, password: "{{ vault_db_password }}"}]

================================================================================
PARTIE 9 — TABLEAUX DE BORD: RÔLES ET RESPONSABILITÉS
================================================================================

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  ALICE — Lead DevOps / Responsable Infrastructure                       │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  FAIT UNE SEULE FOIS (setup initial):                                   │
  │  [OK] Installe Terraform et Ansible sur toutes les machines                │
  │  [OK] Crée le token API DigitalOcean + enregistre les clés SSH             │
  │  [OK] Configure le backend Terraform Cloud (state partagé)                 │
  │  [OK] Crée la structure infrastructure/ dans le dépôt Git                  │
  │  [OK] Écrit main.tf, variables.tf, outputs.tf pour staging et prod         │
  │  [OK] Écrit tous les roles Ansible (common, python, flask, pg, redis, nginx)│
  │  [OK] Chiffre les secrets avec Ansible Vault                               │
  │  [OK] Configure les workflows GitHub Actions (CI + CD)                     │
  │  [OK] Ajoute les secrets GitHub (DO_TOKEN, ANSIBLE_VAULT_PASSWORD...)      │
  │                                                                          │
  │  FAIT À CHAQUE SPRINT / RELEASE:                                        │
  │  [OK] terraform apply pour les modifications d'infra (si nécessaire)       │
  │  [OK] Crée le tag de version -> déclenche le CD automatique                 │
  │  [OK] Surveille le déploiement automatique dans GitHub Actions             │
  │  [OK] Répond aux alertes DigitalOcean (CPU, RAM, disque)                   │
  │  [OK] Gère les rollbacks si un déploiement échoue                          │
  │                                                                          │
  │  COMMANDES LES PLUS UTILISÉES PAR ALICE:                                │
  │  terraform plan / terraform apply / terraform destroy                   │
  │  terraform state list / terraform output                                 │
  │  ansible-playbook site.yml --vault-password-file ~/.vault_pass          │
  │  ansible-vault edit vars/prod_secrets.yml                               │
  │  ansible all -m ping / ansible web -m command -a "..."                  │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  BOB — Développeur backend / Utilisateur de l'infra                     │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  FAIT UNE SEULE FOIS:                                                   │
  │  [OK] Installe Terraform et Ansible (pour pouvoir créer ses propres tests) │
  │  [OK] Copie terraform.tfvars.example -> terraform.tfvars et remplit         │
  │                                                                          │
  │  FAIT À CHAQUE FEATURE NÉCESSITANT UN SERVEUR DE TEST:                  │
  │  [OK] Copie environments/staging -> environments/bob-test-XXX               │
  │  [OK] terraform apply -> serveur de test en 90 secondes                     │
  │  [OK] ansible-playbook deploy.yml -> son code déployé en 2 minutes          │
  │  [OK] Tests manuels ou automatiques sur le vrai serveur                    │
  │  [OK] terraform destroy -> serveur supprimé, aucune facture                 │
  │                                                                          │
  │  FAIT QUAND IL MODIFIE UNE VARIABLE D'INFRA:                            │
  │  [OK] Modifie group_vars/ -> PR -> Ansible applique le changement            │
  │  [OK] JAMAIS modifier directement via SSH (configuration drift!)           │
  │                                                                          │
  │  COMMANDES LES PLUS UTILISÉES PAR BOB:                                  │
  │  terraform init / terraform apply / terraform destroy                   │
  │  ansible-playbook deploy.yml -e "app_version=feature/3"                 │
  │  ansible web -m command -a "journalctl -u taskmanager -n 50"            │
  └──────────────────────────────────────────────────────────────────────────┘

  ┌──────────────────────────────────────────────────────────────────────────┐
  │  CLAIRE — Développeuse / Responsable qualité et tests infra             │
  │  ────────────────────────────────────────────────────────────────────── │
  │                                                                          │
  │  FAIT UNE SEULE FOIS:                                                   │
  │  [OK] Installe Testinfra et Ansible dans WSL2                              │
  │  [OK] Écrit les tests Testinfra pour valider la configuration              │
  │                                                                          │
  │  FAIT APRÈS CHAQUE DÉPLOIEMENT:                                         │
  │  [OK] Exécute les tests Testinfra sur le serveur staging                   │
  │  [OK] Vérifie que tous les services tournent et sont correctement configurés│
  │  [OK] Signale via issue si un test échoue (ex: Redis exposé sur 0.0.0.0)   │
  │                                                                          │
  │  FAIT POUR CHAQUE MODIFICATION D'INFRA EN PR:                           │
  │  [OK] Relit les changements Terraform (terraform plan en commentaire PR)    │
  │  [OK] Vérifie qu'aucune règle de sécurité n'est affaiblie                  │
  │  [OK] Approuve la PR uniquement si les tests Testinfra passent             │
  │                                                                          │
  │  COMMANDES LES PLUS UTILISÉES PAR CLAIRE:                               │
  │  pytest tests/test_server.py --hosts="ssh://ubuntu@IP" -v               │
  │  ansible-playbook site.yml --check (dry run)                            │
  │  ansible-lint site.yml deploy.yml                                       │
  └──────────────────────────────────────────────────────────────────────────┘

================================================================================
PARTIE 10 — CHEATSHEET COMPLET TERRAFORM ET ANSIBLE
================================================================================

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TERRAFORM — TOUTES LES COMMANDES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  INITIALISATION:
  terraform init                    -> Télécharge providers et modules
  terraform init -upgrade           -> Forcer la mise à jour des providers
  terraform init -backend=false     -> Init sans configurer le backend (CI)

  VÉRIFICATION ET FORMATAGE:
  terraform validate                -> Valider la syntaxe HCL
  terraform fmt                     -> Formater les fichiers .tf
  terraform fmt -recursive          -> Formater récursivement (tous les sous-dossiers)
  terraform fmt -check              -> Vérifier le format sans modifier (CI)

  PLANIFICATION:
  terraform plan                    -> Aperçu des changements
  terraform plan -out=plan.tfplan   -> Sauvegarder le plan
  terraform plan -var "region=ams3" -> Surcharger une variable
  terraform plan -target=resource   -> Planner seulement une ressource

  APPLICATION:
  terraform apply                   -> Appliquer les changements (demande confirmation)
  terraform apply -auto-approve     -> Appliquer sans confirmation (CI/CD)
  terraform apply plan.tfplan       -> Appliquer un plan sauvegardé
  terraform apply -target=droplet.api -> Appliquer seulement une ressource

  DESTRUCTION:
  terraform destroy                 -> Supprimer toutes les ressources (confirmation)
  terraform destroy -auto-approve   -> Sans confirmation
  terraform destroy -target=volume  -> Supprimer seulement une ressource

  INSPECTION DE L'ÉTAT:
  terraform state list              -> Lister toutes les ressources dans le state
  terraform state show [resource]   -> Détails d'une ressource
  terraform output                  -> Afficher tous les outputs
  terraform output -raw server_ip   -> Un output sans guillemets (pour scripts)
  terraform refresh                 -> Synchroniser le state avec le cloud

  MANIPULATION DU STATE:
  terraform state mv src dst        -> Renommer une ressource dans le state
  terraform state rm resource       -> Supprimer du state sans détruire
  terraform import resource id      -> Importer une ressource existante
  terraform taint resource          -> Forcer la recréation d'une ressource
  terraform untaint resource        -> Annuler un taint

  OUTILS:
  terraform graph                   -> Graphe de dépendances (en DOT)
  terraform console                 -> REPL interactif pour tester des expressions
  terraform show                    -> Afficher le state ou un plan
  terraform workspace list          -> Lister les workspaces
  terraform workspace new staging   -> Créer un workspace
  terraform workspace select staging -> Changer de workspace
  terraform workspace delete test   -> Supprimer un workspace

  VARIABLES D'ENVIRONNEMENT:
  TF_VAR_do_token=xxx               -> Fournir une variable (préfixe TF_VAR_)
  TF_LOG=DEBUG                      -> Activer les logs de débogage
  TF_LOG_PATH=/tmp/tf.log           -> Sauvegarder les logs dans un fichier
  TF_CLI_ARGS_plan="-compact-warnings" -> Args par défaut pour une commande

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ANSIBLE — TOUTES LES COMMANDES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  INVENTAIRE:
  ansible-inventory --list          -> Afficher l'inventaire en JSON
  ansible-inventory --graph         -> Afficher l'inventaire en arbre
  ansible all -m ping               -> Tester la connectivité SSH
  ansible all -m gather_facts       -> Collecter les facts

  COMMANDES AD-HOC:
  ansible all -m command -a "uptime"          -> Exécuter une commande
  ansible web -m service -a "name=nginx state=restarted" --become  -> Redémarrer service
  ansible all -m apt -a "name=curl state=present" --become         -> Installer un paquet
  ansible web -m copy -a "src=./file dest=/tmp/file"               -> Copier un fichier
  ansible all -m shell -a "ps aux | grep flask"                    -> Commande shell

  PLAYBOOKS:
  ansible-playbook site.yml                     -> Lancer le playbook
  ansible-playbook site.yml -v                  -> Verbose (niveau 1)
  ansible-playbook site.yml -vvv                -> Très verbose (SSH + modules)
  ansible-playbook site.yml --check            -> Dry run (ne modifie rien)
  ansible-playbook site.yml --diff             -> Afficher les diffs de fichiers
  ansible-playbook site.yml --limit web        -> Limiter à un groupe
  ansible-playbook site.yml --tags flask_app   -> Seulement les tasks avec ce tag
  ansible-playbook site.yml --skip-tags monitoring  -> Exclure certaines tasks
  ansible-playbook site.yml --start-at-task "..."   -> Commencer à une task précise
  ansible-playbook site.yml --step             -> Confirmer chaque task
  ansible-playbook site.yml -e "app_version=v2"     -> Variables supplémentaires

  VAULT:
  ansible-vault create secrets.yml             -> Créer un fichier chiffré
  ansible-vault encrypt secrets.yml            -> Chiffrer un fichier existant
  ansible-vault decrypt secrets.yml            -> Déchiffrer
  ansible-vault view secrets.yml               -> Voir le contenu chiffré
  ansible-vault edit secrets.yml               -> Éditer le contenu chiffré
  ansible-vault rekey secrets.yml              -> Changer le mot de passe Vault
  ansible-playbook site.yml --ask-vault-pass   -> Demander le mdp Vault
  ansible-playbook site.yml --vault-password-file ~/.vault_pass  -> Fichier mdp

  GALAXY:
  ansible-galaxy install geerlingguy.nginx         -> Installer un role
  ansible-galaxy install -r requirements.yml       -> Installer tous les roles requis
  ansible-galaxy list                              -> Lister les roles installés
  ansible-galaxy init monrole                      -> Créer la structure d'un nouveau role
  ansible-galaxy search postgresql                 -> Chercher un role

  MODULES LES PLUS UTILISÉS:
  apt          -> Gérer les paquets Ubuntu/Debian
  yum/dnf      -> Gérer les paquets RHEL/CentOS
  pip          -> Installer des paquets Python
  service      -> Gérer les services systemd
  systemd      -> Contrôle avancé de systemd
  copy         -> Copier des fichiers (statiques)
  template     -> Copier des fichiers avec variables Jinja2
  file         -> Créer/supprimer fichiers, dossiers, liens symboliques
  git          -> Cloner/mettre à jour des dépôts Git
  user         -> Gérer les utilisateurs Unix
  group        -> Gérer les groupes Unix
  command      -> Exécuter une commande (sans shell)
  shell        -> Exécuter une commande (avec shell: pipes, redirections)
  script       -> Exécuter un script local sur le serveur distant
  uri          -> Faire des requêtes HTTP (health checks)
  get_url      -> Télécharger un fichier via HTTP
  unarchive    -> Extraire des archives (.tar.gz, .zip)
  mount        -> Monter des volumes/systèmes de fichiers
  lineinfile   -> Modifier une ligne dans un fichier
  blockinfile  -> Modifier un bloc dans un fichier
  cron         -> Gérer les tâches cron
  debug        -> Afficher un message ou variable
  set_fact     -> Définir un fact/variable
  include_vars -> Inclure des variables depuis un fichier
  include_role -> Inclure un role dynamiquement
  import_tasks -> Inclure des tasks statiquement
  assert       -> Vérifier une condition (échoue si fausse)
  fail         -> Forcer l'échec avec un message
  wait_for     -> Attendre qu'une condition soit remplie (port ouvert, fichier...)
  postgresql_db-> Gérer les bases de données PostgreSQL
  postgresql_user -> Gérer les utilisateurs PostgreSQL

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ERREURS FRÉQUENTES ET SOLUTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  TERRAFORM:

  ERREUR: "Error: No valid credential sources found"
  CAUSE:  TF_VAR_do_token non défini.
  FIX:    export TF_VAR_do_token="dop_v1_..."  puis relancer terraform.

  ERREUR: "Error locking state: state blob is already locked"
  CAUSE:  Un autre terraform apply est en cours (ou un apply précédent a planté).
  FIX:    terraform force-unlock [LOCK_ID]  (avec l'ID affiché dans l'erreur).
  ATTENTION: Ne pas forcer le unlock si quelqu'un travaille réellement!

  ERREUR: "Error: the plan file is stale"
  CAUSE:  Le state a changé entre le plan et l'apply.
  FIX:    Relancer terraform plan puis terraform apply.

  ERREUR: "-/+ replace" inattendu dans terraform plan
  CAUSE:  Vous avez modifié un attribut "forces replacement" (ex: la région).
  FIX:    Lire attentivement le plan. Si acceptable: terraform apply.
          Si non: annuler la modification dans .tf.

  ERREUR: "Error: Invalid function argument — var.region is sensitive"
  CAUSE:  Une variable sensitive utilisée dans un contexte qui l'interdit.
  FIX:    Retirer sensitive = true de cette variable, ou restructurer le code.

  ANSIBLE:

  ERREUR: "UNREACHABLE — Failed to connect to the host via ssh"
  CAUSE:  Mauvaise IP, SSH non démarré, clé SSH incorrecte.
  FIX:    Vérifier ansible_host dans l'inventaire.
          ssh -i ~/.ssh/id_ed25519_github ubuntu@IP  (test manuel)
          Vérifier que le serveur est bien démarré.

  ERREUR: "fatal: [host]: FAILED! — permission denied"
  CAUSE:  become=yes oublié, ou le user n'a pas les droits sudo.
  FIX:    Ajouter become: yes à la task ou dans ansible.cfg.

  ERREUR: "fatal: [host]: FAILED! — Error connecting: psycopg2.OperationalError"
  CAUSE:  PostgreSQL n'est pas démarré, ou pg_hba.conf mal configuré.
  FIX:    ansible web -m service -a "name=postgresql state=started" --become
          ansible web -m command -a "pg_lsclusters"

  ERREUR: "changed: [host]" sur une task censée être idempotente
  CAUSE:  La task ne vérifie pas l'état actuel (ex: command sans creates/when).
  FIX:    Ajouter changed_when: false si la task ne modifie rien.
          Ou ajouter creates: "/path/to/file" pour les tasks command.

  ERREUR: "ERROR! Decryption failed (no vault secrets were found)"
  CAUSE:  Le fichier Vault n'est pas fourni lors du playbook.
  FIX:    Ajouter --vault-password-file ~/.vault_pass à la commande ansible-playbook.

  ERREUR: "TASK [xxx] skipping — conditional check failed"
  CAUSE:  Une condition when: est fausse pour cet hôte.
  FIX:    Vérifier les variables: ansible host -m debug -a "var=enable_ssl"
          Normal si la task ne s'applique pas à cet environnement.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
LES 10 RÈGLES D'OR TERRAFORM ET ANSIBLE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1. TOUJOURS lire terraform plan avant terraform apply.
     -> "0 to destroy" ou modifier pour voir ce qui sera supprimé.

  2. JAMAIS mettre de secrets dans les fichiers .tf ou les playbooks.
     -> Secrets Terraform: variable sensitive + TF_VAR_xxx en environnement.
     -> Secrets Ansible: ansible-vault encrypt vars/secrets.yml.

  3. JAMAIS committer terraform.tfvars, terraform.tfstate, ou vars/secrets.yml non chiffrés.
     -> .gitignore pour tfvars et tfstate.
     -> ansible-vault encrypt avant git add pour les secrets.

  4. Le state Terraform doit être PARTAGÉ en équipe.
     -> Backend remote (Terraform Cloud ou S3+DynamoDB).
     -> Jamais de state local en équipe.

  5. TOUJOURS utiliser --force-with-lease / modifier le state prudemment.
     -> terraform state rm sans sauvegarde = données potentiellement perdues.

  6. Ansible est idempotent: ne pas avoir peur de relancer un playbook.
     -> "ok" = déjà dans l'état désiré, rien changé.
     -> "changed" = une modification a été nécessaire.

  7. Toute modification de configuration passe par Git -> PR -> Ansible.
     -> JAMAIS modifier directement via SSH (configuration drift).
     -> Sinon: la prochaine exécution Ansible écrasera votre modification.

  8. Tester avec --check avant d'appliquer sur la production.
     -> ansible-playbook site.yml --check (dry run)
     -> terraform plan -out=prod.tfplan (vérifier avant apply)

  9. Séparer les playbooks par fréquence d'usage:
     -> site.yml: configuration complète (rare, ~premier install)
     -> deploy.yml: code seulement (fréquent, à chaque release)
     -> rollback.yml: urgence

  10. Documenter les modules Terraform et les roles Ansible.
      -> terraform-docs: génère README.md automatiquement depuis les variables.tf
      -> README.md dans chaque role: usage, variables, exemples.

================================================================================
FIN DU GUIDE TERRAFORM & ANSIBLE
================================================================================

Ce guide couvre l'intégralité de l'utilisation de Terraform et Ansible
pour une équipe de 3 développeurs travaillant sur une application Flask.

Résumé de ce qui a été couvert:

TERRAFORM (toutes les fonctionnalités):
  [OK] Installation (Mac, Ubuntu, Windows WSL2)
  [OK] Provider DigitalOcean, token API, clés SSH
  [OK] Ressources: VPC, pare-feu, Droplets, volumes, DNS, alertes
  [OK] Variables avec validation, outputs, tfvars (template et réel)
  [OK] Data sources (lire ressources existantes)
  [OK] Commandes: init, validate, fmt, plan, apply, destroy
  [OK] State: list, show, mv, rm, import, taint, refresh, force-unlock
  [OK] Expressions: count, for_each, conditionnelles, fonctions, locals
  [OK] Workspaces pour environnements multiples
  [OK] Modules réutilisables avec variables et outputs
  [OK] Backend remote (Terraform Cloud) pour state partagé en équipe
  [OK] Intégration GitHub Actions CI (validate + lint + plan en commentaire PR)
  [OK] Intégration GitHub Actions CD (apply sur tag de version)

ANSIBLE (toutes les fonctionnalités):
  [OK] Installation (Mac, Ubuntu, Windows WSL2)
  [OK] ansible.cfg: configuration complète commentée
  [OK] Inventaires: statique .ini, dynamique Python (lit terraform output)
  [OK] Commandes ad-hoc: ping, command, service, copy
  [OK] Facts: collecte automatique, set_fact, cache
  [OK] Vault: encrypt, decrypt, view, edit, rekey, vault-id, --vault-password-file
  [OK] group_vars/all.yml, staging.yml, production.yml
  [OK] host_vars pour variables par serveur
  [OK] Roles complets: common, python, postgresql, redis, flask_app, nginx, ssl, monitoring
  [OK] Templates Jinja2: env.j2, taskmanager.service.j2, nginx.conf.j2, pg_hba.conf.j2
  [OK] Handlers: déclenchement conditionnel sur notify
  [OK] Playbooks: site.yml (tout), deploy.yml (code), rollback.yml, database.yml
  [OK] Options avancées: --check, --diff, --tags, --skip-tags, --start-at-task, --step
  [OK] Boucles: loop, with_items, for_each, dict2items
  [OK] Conditionnelles: when, ansible_distribution, variables booléennes
  [OK] Blocs: block/rescue/always (try/catch/finally)
  [OK] delegate_to, run_once, no_log, register, changed_when
  [OK] Galaxy: installer des roles communautaires (requirements.yml)
  [OK] ansible-lint et tests Testinfra
  [OK] Intégration GitHub Actions CI (lint + syntax-check)
  [OK] Intégration GitHub Actions CD (deploy + rollback automatique)

SCÉNARIOS RÉELS COUVERTS:
  [OK] Bob crée son serveur de test, le teste, le détruit (autonomie totale)
  [OK] Alice déploie de zéro en 17 minutes (Terraform + Ansible)
  [OK] Bob modifie une variable via PR -> Ansible applique
  [OK] Claire écrit et exécute 16 tests Testinfra sur le serveur
  [OK] Alice gère un serveur inaccessible (hotfix infra)
  [OK] Déploiement v1.1.0 entièrement automatique via tag Git

================================================================================