# [COURS] EXERCICES CORRIGÉS AWS - ULTRA DÉTAILLÉS

Je vais créer pour toi 10 exercices progressifs sur AWS avec le même niveau de détail pédagogique que les exercices Apache. Chaque exercice sera ultra-détaillé avec explications ligne par ligne.

## [LIVRE] STRUCTURE DES EXERCICES

**Progression :**
- [VERT] Exercices 1-2 : Débutant (EC2, S3)
- [JAUNE] Exercices 3-5 : Intermédiaire (RDS, Elastic Beanstalk, Lambda)
- [ROUGE] Exercices 6-8 : Avancé (ECS, Load Balancer, VPC)
- [ROUGE] Exercices 9-10 : Expert (CI/CD, Architecture complète)

Voici le début du document complet :

---

# [VERT] EXERCICE 1 : DÉPLOYER UN SITE WEB SUR EC2

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur junior dans une agence web. Un client te demande de déployer son site vitrine sur AWS. C'est ta première mission avec le cloud AWS.

### Cahier des charges

Le client souhaite :
- Un serveur web pour héberger son site HTML/CSS
- Accessible depuis Internet
- Coûts maîtrisés (tier gratuit AWS)
- Possibilité de se connecter en SSH pour la maintenance

### Contraintes techniques

- AWS Free Tier (t2.micro ou t3.micro)
- Ubuntu Server 22.04 LTS
- Serveur web : Apache ou Nginx
- Sécurité : Pare-feu configuré, accès SSH sécurisé
- Temps estimé : 2-3 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Créer un compte AWS et comprendre la console
- [OK] Lancer une instance EC2 (serveur virtuel)
- [OK] Configurer les Security Groups (pare-feu AWS)
- [OK] Se connecter en SSH à une instance EC2
- [OK] Installer et configurer un serveur web
- [OK] Associer une adresse IP élastique
- [OK] Comprendre les concepts de base d'EC2
- [OK] Gérer les coûts (tier gratuit)

---

## [DOCS] PRÉREQUIS

- Un ordinateur avec accès Internet
- Une carte bancaire (pour validation AWS, pas de débit si Free Tier)
- Connaissances Linux de base
- Notions de HTML/CSS

---

## [IDEE] CONCEPTS AWS À COMPRENDRE AVANT DE COMMENCER

### Qu'est-ce qu'EC2 ?

**EC2** = Elastic Compute Cloud

**Analogie simple :**
- Serveur physique traditionnel = Tu achètes un ordinateur, tu le gardes chez toi
- EC2 = Tu loues un ordinateur virtuel dans le datacenter d'AWS

**Caractéristiques :**
- Serveur virtuel dans le cloud
- Créé en quelques minutes
- Payé à l'heure (ou à la seconde)
- Scalable (tu peux changer la taille)
- Dans plusieurs régions du monde

---

### Les types d'instances EC2

AWS propose des dizaines de types d'instances. Voici les principales familles :

| Famille | Usage | Exemple | Tier gratuit ? |
|---------|-------|---------|----------------|
| **t2/t3** | Usage général, site web | t2.micro | [OK] Oui |
| **t4g** | ARM, économique | t4g.micro | [OK] Oui |
| **m5** | Équilibré (CPU/RAM) | m5.large | [X] Non |
| **c5** | CPU intensif | c5.xlarge | [X] Non |
| **r5** | RAM intensive | r5.large | [X] Non |
| **g4** | GPU (ML, rendu 3D) | g4dn.xlarge | [X] Non |

**Pour cet exercice : t2.micro ou t3.micro (Free Tier)**

---

### Anatomie d'un type d'instance

**Exemple : `t2.micro`**

```
t2.micro
│ │  │
│ │  └─ Taille : micro (1 vCPU, 1 Go RAM)
│ └──── Génération : 2
└────── Famille : t (burstable, usage général)
```

**Autres tailles dans la famille t2 :**
- `t2.nano` : 0.5 vCPU, 0.5 Go RAM
- `t2.micro` : 1 vCPU, 1 Go RAM (Free Tier)
- `t2.small` : 1 vCPU, 2 Go RAM
- `t2.medium` : 2 vCPU, 4 Go RAM
- `t2.large` : 2 vCPU, 8 Go RAM

---

### Les régions AWS

AWS a des datacenters partout dans le monde (régions).

**Exemples de régions :**
- `us-east-1` : Virginie du Nord (USA)
- `eu-west-1` : Irlande (Europe)
- `eu-west-3` : Paris (France)
- `ap-southeast-1` : Singapour (Asie)

**Pour cet exercice : choisir la région la plus proche de tes utilisateurs**
- En Europe : `eu-west-3` (Paris) ou `eu-west-1` (Irlande)
- En Afrique de l'Ouest : `eu-west-3` (Paris) est la plus proche

**[IDEE] Astuce :** Chaque région a des prix différents. Paris est légèrement plus cher qu'Irlande.

---

### Security Groups (pare-feu)

**Security Group** = Pare-feu virtuel pour ton instance EC2

**Règles :**
- **Inbound (entrantes)** : Qui peut se connecter à ton serveur ?
- **Outbound (sortantes)** : Vers où ton serveur peut se connecter ?

**Par défaut :**
- Inbound : Tout bloqué [X]
- Outbound : Tout autorisé [OK]

**Exemple de règles inbound nécessaires :**

| Type | Protocole | Port | Source | Usage |
|------|-----------|------|--------|-------|
| SSH | TCP | 22 | Mon IP | Connexion SSH |
| HTTP | TCP | 80 | 0.0.0.0/0 | Site web |
| HTTPS | TCP | 443 | 0.0.0.0/0 | Site web sécurisé |

**`0.0.0.0/0`** = Tout le monde (tout Internet)

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Créer un compte AWS

**1. Aller sur https://aws.amazon.com**

**2. Cliquer sur "Créer un compte AWS"**

**3. Remplir le formulaire :**

```
Adresse email : ton-email@example.com
Nom du compte : MonProjet (ou ton nom)
```

**4. Vérification email**

AWS envoie un code de vérification à ton email.

**5. Informations de contact**

```
Type de compte : Personnel
Nom complet : [Ton Nom]
Numéro de téléphone : [Ton numéro]
Pays : [Ton pays]
Adresse : [Ton adresse]
```

**6. Informations de paiement**

[ATTENTION] **IMPORTANT :** AWS demande une carte bancaire pour validation

**Pourquoi ?**
- Vérification d'identité
- Protection contre les bots
- En cas de dépassement du tier gratuit

**Rassure-toi :**
- Le tier gratuit est VRAIMENT gratuit pendant 12 mois
- AWS prélève 1 $ puis le rembourse (vérification)
- Tu recevras des alertes avant tout débit

**7. Vérification d'identité (téléphone)**

AWS appelle ton téléphone ou envoie un SMS avec un code PIN.

**8. Choix du plan de support**

```
[OK] Support de base (gratuit) - Suffisant pour débuter
[X] Support développeur (29 $/mois) - Pas nécessaire
[X] Support professionnel (100+ $/mois) - Pour entreprises
```

**9. Confirmation**

```
[BRAVO] Félicitations ! Ton compte AWS est créé !
```

**10. Se connecter à la console AWS**

https://console.aws.amazon.com

```
Identifiant : Ton email
Mot de passe : Ton mot de passe
```

---

### ÉTAPE 2 : Configurer les alertes de facturation

**[ATTENTION] TRÈS IMPORTANT : À faire IMMÉDIATEMENT !**

**Pourquoi ?**
- Éviter les mauvaises surprises
- Être alerté si tu dépasses le tier gratuit
- Protéger ton budget

**1. Aller dans la console AWS**

**2. Cliquer sur ton nom (en haut à droite) -> "Billing and Cost Management"**

**3. Dans le menu de gauche : "Billing preferences"**

**4. Activer :**

```
[x] Receive PDF Invoice By Email
[x] Receive Free Tier Usage Alerts
[x] Receive Billing Alerts
```

**5. Entrer ton email de facturation**

**6. Sauvegarder les préférences**

---

**Créer une alarme CloudWatch (budget) :**

**1. Aller dans "Budgets" (menu de gauche)**

**2. Créer un budget :**

```
Type : Cost budget
Nom : Mon-Budget-Mensuel
Montant : 10 $ (ou ce que tu veux)
Période : Mensuel
```

**3. Configurer les alertes :**

```
Seuil 1 : 80% du budget (8 $)
  -> Email d'alerte

Seuil 2 : 100% du budget (10 $)
  -> Email d'alerte

Seuil 3 : 150% du budget (15 $)
  -> Email d'alerte URGENT
```

**4. Créer le budget**

**[OK] Maintenant, tu seras alerté si tu dépasses ton budget !**

---

### ÉTAPE 3 : Choisir la région AWS

**1. Dans la console AWS, en haut à droite, tu vois la région actuelle**

Exemple : `Virginia du Nord` ou `US East (N. Virginia)`

**2. Cliquer sur la région pour afficher la liste**

**3. Choisir ta région :**

**Pour l'Europe :**
- `Europe (Paris) eu-west-3` * Recommandé (plus proche)
- `Europe (Ireland) eu-west-1` (légèrement moins cher)

**Pour l'Afrique :**
- `Europe (Paris) eu-west-3` (plus proche géographiquement)

**4. La région est maintenant sélectionnée**

**[ATTENTION] NOTE IMPORTANTE :**

Chaque région est TOTALEMENT INDÉPENDANTE.

Si tu crées une instance EC2 en `eu-west-3`, elle n'apparaîtra PAS dans `us-east-1`.

**[IDEE] Astuce :** Toujours vérifier la région avant de créer des ressources !

---

### ÉTAPE 4 : Créer une paire de clés SSH

**Avant de lancer l'instance, créons les clés SSH.**

**Qu'est-ce qu'une paire de clés SSH ?**

C'est comme un système de clé/serrure :
- **Clé publique** (sur le serveur) = La serrure
- **Clé privée** (sur ton PC) = La clé

**Sans la clé privée, impossible de se connecter au serveur !**

---

**1. Dans la console AWS, chercher "EC2" dans la barre de recherche**

**2. Cliquer sur "EC2"**

**3. Dans le menu de gauche, sous "Network & Security", cliquer sur "Key Pairs"**

**4. Cliquer sur "Create key pair"**

**5. Configuration :**

```
Nom : ma-cle-ec2

Type de clé :
  [x] RSA (recommandé)
  [ ] ED25519

Format de fichier :
  - Windows : [x] .ppk (pour PuTTY)
  - macOS/Linux : [x] .pem
```

**Explication des formats :**

**`.pem`** (Privacy Enhanced Mail)
- Format standard OpenSSH
- Utilisé sur macOS et Linux
- Peut être converti en `.ppk`

**`.ppk`** (PuTTY Private Key)
- Format PuTTY (logiciel SSH pour Windows)
- Utilisé sur Windows avec PuTTY

**6. Cliquer sur "Create key pair"**

**7. Le fichier se télécharge automatiquement**

Exemple : `ma-cle-ec2.pem` (ou `.ppk`)

**[ATTENTION] TRÈS IMPORTANT :**

**Sauvegarde cette clé en lieu sûr !**
- Tu ne pourras PAS la retélécharger
- Si tu la perds, tu ne pourras plus te connecter à ton instance
- Garde-la confidentielle (c'est ton mot de passe !)

---

**Sur macOS/Linux, sécuriser la clé :**

```bash
# Déplacer la clé dans un dossier sécurisé
mkdir -p ~/.ssh
mv ~/Downloads/ma-cle-ec2.pem ~/.ssh/

# Changer les permissions (obligatoire !)
chmod 400 ~/.ssh/ma-cle-ec2.pem
```

**Explication de `chmod 400` :**

```
4 = r-- (lecture seule)
0 = --- (aucun droit)
0 = --- (aucun droit)

Résultat : Seul TOI peux lire la clé
```

**Pourquoi ?**

SSH refuse de fonctionner si la clé privée a des permissions trop ouvertes (sécurité).

---

### ÉTAPE 5 : Lancer une instance EC2

**C'est le moment de créer notre serveur virtuel !**

**1. Dans la console EC2, cliquer sur "Instances" (menu de gauche)**

**2. Cliquer sur "Launch instances" (bouton orange)**

**3. Configuration de l'instance :**

---

#### **Étape 1/7 : Nom et tags**

```
Nom : mon-site-web
```

**Les tags** sont des métadonnées pour organiser tes ressources.

Exemple :
```
Key: Projet | Value: SiteVitrine
Key: Environnement | Value: Production
Key: Propriétaire | Value: TonNom
```

**Pour l'instant, juste le nom suffit.**

---

#### **Étape 2/7 : Choix de l'AMI (image système)**

**AMI** = Amazon Machine Image (image disque)

C'est l'OS (système d'exploitation) préinstallé sur le serveur.

**Options disponibles :**

| OS | AMI | Tier gratuit ? | Usage |
|----|-----|----------------|-------|
| **Ubuntu** | Ubuntu Server 22.04 LTS | [OK] Oui | * Recommandé (populaire, bien documenté) |
| Amazon Linux | Amazon Linux 2023 | [OK] Oui | Optimisé AWS, mais moins standard |
| Red Hat | RHEL 9 | [X] Non | Entreprise, payant |
| Windows | Windows Server 2022 | [X] Non | Coûteux |

**Sélectionner :**

```
[x] Ubuntu Server 22.04 LTS (HVM), SSD Volume Type
Architecture : 64-bit (x86)
```

**[OK] Vérifier que "Free tier eligible" est affiché !**

---

#### **Étape 3/7 : Type d'instance**

**Famille d'instance :**

Plusieurs choix selon ta région :

```
[x] t2.micro (1 vCPU, 1 GiB RAM) - Free tier eligible
[ ] t3.micro (2 vCPU, 1 GiB RAM) - Free tier eligible (si dispo)
[ ] t4g.micro (2 vCPU ARM, 1 GiB RAM) - Free tier (si dispo)
```

**Sélectionner : `t2.micro` ou `t3.micro`** (selon disponibilité)

**Différence t2 vs t3 :**

| Critère | t2.micro | t3.micro |
|---------|----------|----------|
| vCPU | 1 | 2 |
| RAM | 1 Go | 1 Go |
| Performance | Burst | Burst illimité |
| Réseau | Modéré | Jusqu'à 5 Gbps |
| Prix (hors Free Tier) | ~8 $/mois | ~7,5 $/mois |

**Les deux sont inclus dans le Free Tier !**

---

**Qu'est-ce que "Burst" ?**

**Instances t2/t3 = Instances "burstables" (à rafales)**

**Fonctionnement :**

Imagine une voiture avec un turbo :
- **Vitesse normale** : 10% CPU constant
- **Turbo (burst)** : 100% CPU pendant un court moment
- **Crédits CPU** : Tu accumules des crédits quand tu utilises peu de CPU
- **Consommation** : Tu consommes des crédits quand tu fais un burst

**Exemple concret :**

```
Ton site web reçoit peu de trafic :
-> CPU à 5% en moyenne
-> Tu accumules des crédits CPU

Soudain, pic de trafic (1000 visiteurs simultanés) :
-> CPU à 100% pendant 5 minutes
-> Tu consommes des crédits CPU
-> Puis retour à la normale
```

**C'est parfait pour :**
- Sites web avec trafic variable
- Applications qui tournent tranquillement 95% du temps
- Tâches de développement/test

**Pas adapté pour :**
- CPU constamment à 100%
- Applications de calcul intensif
- Encodage vidéo, rendu 3D, etc.

**Pour cet exercice : t2.micro est PARFAIT !**

---

#### **Étape 4/7 : Paire de clés (Key pair)**

**Sélectionner la clé créée précédemment :**

```
[x] ma-cle-ec2
```

**Si tu l'as oubliée :**

Tu peux en créer une nouvelle ici directement.

---

#### **Étape 5/7 : Paramètres réseau**

**Configuration réseau TRÈS IMPORTANTE !**

---

**VPC :**

```
VPC : (default) - Ne pas modifier
```

**VPC** = Virtual Private Cloud (réseau virtuel privé)

Par défaut, AWS crée un VPC pour toi. Pour l'instant, on l'utilise.

---

**Subnet (sous-réseau) :**

```
Subnet : No preference (AWS choisit)
```

Ou tu peux choisir une zone de disponibilité spécifique :
- `eu-west-3a`
- `eu-west-3b`
- `eu-west-3c`

**Zones de disponibilité (AZ)** = Datacenters physiquement séparés dans une même région.

Pour la haute disponibilité, on déploie sur plusieurs AZ.

Pour cet exercice : pas d'importance.

---

**Auto-assign public IP :**

```
[x] Enable
```

**OBLIGATOIRE** pour accéder à l'instance depuis Internet !

Une IP publique sera automatiquement attribuée.

---

**Firewall (Security Groups) :**

```
[x] Create security group

Nom du security group : mon-site-web-sg
Description : Autorise HTTP, HTTPS et SSH
```

**Règles inbound (entrantes) :**

**Règle 1 : SSH**

```
Type : SSH
Protocole : TCP
Port : 22
Source : My IP (ton IP actuelle)
```

**[ATTENTION] TRÈS IMPORTANT :**

**JAMAIS autoriser SSH depuis `0.0.0.0/0` (tout Internet) !**

C'est une faille de sécurité majeure.

**Pourquoi "My IP" ?**
- Seule TON IP peut se connecter en SSH
- Bloque les tentatives de brute-force
- Protection de base essentielle

**Si ton IP change (connexion mobile, etc.) :**

Tu devras modifier le Security Group plus tard (on verra comment).

---

**Règle 2 : HTTP**

```
Type : HTTP
Protocole : TCP
Port : 80
Source : Anywhere IPv4 (0.0.0.0/0)
```

**Ici, `0.0.0.0/0` est OK !**

Ton site web DOIT être accessible depuis tout Internet.

---

**Règle 3 : HTTPS**

```
Type : HTTPS
Protocole : TCP
Port : 443
Source : Anywhere IPv4 (0.0.0.0/0)
```

Même raison que HTTP.

---

**Résumé des règles :**

| Type | Port | Source | Raison |
|------|------|--------|--------|
| SSH | 22 | Mon IP | Connexion sécurisée |
| HTTP | 80 | 0.0.0.0/0 | Site web |
| HTTPS | 443 | 0.0.0.0/0 | Site web sécurisé |

**[OK] Configuration réseau terminée !**

---

#### **Étape 6/7 : Stockage (disque)**

**Configuration du disque :**

```
Volume 1 (Root) :
  Type : gp3 (SSD usage général)
  Taille : 8 GiB
  [x] Delete on termination (supprimer avec l'instance)
```

**Types de volumes EBS :**

| Type | Performance | Prix | Usage |
|------|-------------|------|-------|
| **gp3** | 3000 IOPS, 125 MB/s | 0,08 $/GB/mois | * Recommandé (usage général) |
| **gp2** | Jusqu'à 16000 IOPS | 0,10 $/GB/mois | Ancien standard |
| **io2** | Jusqu'à 64000 IOPS | 0,125 $/GB/mois + IOPS | Bases de données intensives |
| **st1** | HDD optimisé débit | 0,045 $/GB/mois | Big Data, logs |
| **sc1** | HDD bas coût | 0,015 $/GB/mois | Accès peu fréquent |

**IOPS** = Input/Output Operations Per Second (opérations disque/seconde)

**Pour cet exercice : gp3, 8 GiB est PARFAIT !**

**Free Tier inclut 30 GB de stockage EBS (gp2 ou gp3).**

---

**Taille du disque :**

8 GB suffit largement pour :
- OS Ubuntu (~2 GB)
- Serveur web Apache/Nginx (~100 MB)
- Ton site web (~1 GB)
- Logs (~500 MB)
- **Total : ~4 GB utilisés**

Si besoin de plus, tu peux :
1. Agrandir le volume plus tard
2. Attacher un volume supplémentaire

---

#### **Étape 7/7 : Paramètres avancés (optionnel)**

**Pour l'instant, on laisse les valeurs par défaut.**

**Résumé de ce qu'on PEUT configurer ici (pour info) :**

**IAM Instance Profile :**
- Permissions pour l'instance (accès S3, DynamoDB, etc.)
- On verra ça dans les exercices avancés

**Shutdown behavior :**
- Stop (arrêter) ou Terminate (supprimer) lors d'un `shutdown` depuis l'OS
- Par défaut : Stop

**Termination protection :**
- Empêche la suppression accidentelle
- Utile en production

**Monitoring :**
- Detailed monitoring (1 minute) : 2,10 $/instance/mois
- Basic monitoring (5 minutes) : Gratuit
- Par défaut : Basic (suffisant pour débuter)

**User data (script de démarrage) :**
- Script bash exécuté au premier boot
- Utile pour automatiser l'installation
- On le fera manuellement pour apprendre

**Pour cet exercice : Laisser tout par défaut.**

---

### **RÉSUMÉ DE LA CONFIGURATION**

Avant de lancer, vérifie :

```
[OK] Nom : mon-site-web
[OK] AMI : Ubuntu Server 22.04 LTS
[OK] Type : t2.micro (Free Tier)
[OK] Clé SSH : ma-cle-ec2
[OK] VPC : Default
[OK] IP publique : Enable
[OK] Security Group :
   - SSH (22) : Mon IP
   - HTTP (80) : 0.0.0.0/0
   - HTTPS (443) : 0.0.0.0/0
[OK] Disque : gp3, 8 GiB
```

---

### **LANCER L'INSTANCE !**

**Cliquer sur "Launch instance" (bouton orange)**

**AWS crée l'instance... [HOURGLASS_WITH_FLOWING_SAND]**

```
[OK] Successfully launched instance: i-0123456789abcdef0
```

**Cliquer sur le lien de l'instance pour voir les détails.**

---

### ÉTAPE 6 : Comprendre le tableau de bord EC2

**Tu es maintenant sur la page "Instances".**

**Colonnes importantes :**

| Colonne | Description | Exemple |
|---------|-------------|---------|
| **Instance ID** | Identifiant unique | i-0123456789abcdef0 |
| **Instance type** | Type d'instance | t2.micro |
| **Status check** | État de santé | 2/2 checks passed [OK] |
| **Alarm status** | Alarmes CloudWatch | Vert = OK |
| **Instance state** | État actuel | Running (en cours) |
| **Public IPv4 address** | IP publique | 35.180.123.45 |
| **Private IPv4 address** | IP privée (VPC) | 172.31.12.34 |

---

**États possibles de l'instance :**

| État | Signification | Facturation ? |
|------|---------------|---------------|
| **Pending** | Démarrage en cours | [X] Non |
| **Running** | En cours d'exécution | [OK] Oui |
| **Stopping** | Arrêt en cours | [OK] Oui (jusqu'à l'arrêt) |
| **Stopped** | Arrêtée | [X] Non (mais disque facturé) |
| **Shutting-down** | Suppression en cours | [X] Non |
| **Terminated** | Supprimée définitivement | [X] Non |

**[IDEE] Astuce coûts :**

Si tu n'utilises pas ton instance pendant plusieurs jours :
-> **Stop** (arrêter) pour ne pas payer l'instance
-> Tu continues de payer le disque (~0,60 $/mois pour 8 GB)

---

**Attendre que l'instance soit "Running" et que les "Status checks" passent à "2/2 checks passed".**

**Durée : 1-2 minutes.**

---

### ÉTAPE 7 : Se connecter en SSH à l'instance

**Récupérer l'IP publique de l'instance :**

Dans le tableau, colonne "Public IPv4 address" :

Exemple : `35.180.123.45`

---

#### **Connexion depuis macOS/Linux**

**Ouvrir un terminal.**

```bash
# Se connecter avec la clé SSH
ssh -i ~/.ssh/ma-cle-ec2.pem ubuntu@35.180.123.45
```

**Décomposition de la commande :**

**`ssh`**
- Commande pour se connecter en SSH

**`-i ~/.ssh/ma-cle-ec2.pem`**
- `-i` = Identity file (fichier d'identité, la clé privée)
- Chemin vers ta clé privée

**`ubuntu@35.180.123.45`**
- `ubuntu` = Nom d'utilisateur par défaut sur Ubuntu Server AMI
- `35.180.123.45` = IP publique de l'instance

**Autres noms d'utilisateur selon l'AMI :**
- Ubuntu : `ubuntu`
- Amazon Linux : `ec2-user`
- Red Hat : `ec2-user`
- Debian : `admin`

---

**Première connexion :**

```
The authenticity of host '35.180.123.45 (35.180.123.45)' can't be established.
ECDSA key fingerprint is SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
```

**Taper `yes` et appuyer sur Entrée.**

**Explication :**

C'est un mécanisme de sécurité SSH.

SSH te demande de confirmer que c'est bien le serveur attendu (protection contre man-in-the-middle).

---

**Résultat :**

```
Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 6.2.0-1009-aws x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

...

ubuntu@ip-172-31-12-34:~$
```

**[BRAVO] TU ES CONNECTÉ À TON SERVEUR EC2 ! [BRAVO]**

---

**Prompt décomposé :**

```
ubuntu@ip-172-31-12-34:~$
  │         │           │ │
  │         │           │ └─ Caractère du prompt ($=user, #=root)
  │         │           └─── Répertoire courant (~=home)
  │         └─────────────── Hostname (IP privée)
  └───────────────────────── Nom d'utilisateur
```

---

#### **Connexion depuis Windows**

**Option 1 : PowerShell (Windows 10+)**

Windows 10/11 inclut OpenSSH.

```powershell
ssh -i C:\Users\TonNom\.ssh\ma-cle-ec2.pem ubuntu@35.180.123.45
```

---

**Option 2 : PuTTY**

**Si tu as téléchargé la clé en format `.ppk` :**

1. Télécharger et installer PuTTY : https://www.putty.org/
2. Lancer PuTTY
3. Configuration :
   - **Host Name** : `ubuntu@35.180.123.45`
   - **Port** : `22`
   - **Connection type** : SSH
4. Dans le menu de gauche : Connection -> SSH -> Auth -> Credentials
5. **Private key file** : Parcourir et sélectionner `ma-cle-ec2.ppk`
6. Revenir à "Session" et sauvegarder la configuration (nom : "Mon EC2")
7. Cliquer sur "Open"

---

**Si tu as une clé `.pem` mais besoin de `.ppk` :**

Utiliser **PuTTYgen** pour convertir :

1. Lancer PuTTYgen
2. **Load** -> Sélectionner `ma-cle-ec2.pem` (changer le filtre pour voir tous les fichiers)
3. **Save private key** -> Sauvegarder en `.ppk`
4. Utiliser ce fichier `.ppk` dans PuTTY

---

### ÉTAPE 8 : Mettre à jour le système

**Première chose à faire sur un nouveau serveur : mettre à jour les paquets.**

```bash
# Mettre à jour la liste des paquets disponibles
sudo apt update
```

**Explication :**

**`sudo`** = Super User DO (exécuter en tant qu'administrateur)
**`apt`** = Advanced Package Tool (gestionnaire de paquets Debian/Ubuntu)
**`update`** = Met à jour la liste des paquets (pas les paquets eux-mêmes)

**Résultat :**

```
Hit:1 http://eu-west-3.ec2.archive.ubuntu.com/ubuntu jammy InRelease
Get:2 http://eu-west-3.ec2.archive.ubuntu.com/ubuntu jammy-updates InRelease [119 kB]
...
Fetched 15.2 MB in 2s (7,234 kB/s)
Reading package lists... Done
Building dependency tree... Done
```

---

```bash
# Installer les mises à jour
sudo apt upgrade -y
```

**`upgrade`** = Installe les nouvelles versions des paquets
**`-y`** = Répond automatiquement "yes" aux questions

**Durée : 2-5 minutes selon le nombre de mises à jour.**

**Résultat :**

```
Reading package lists... Done
Building dependency tree... Done
...
The following packages will be upgraded:
  base-files libc-bin libc6 ...
42 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
...
Processing triggers for man-db ...
Processing triggers for libc-bin ...
```

**[OK] Système à jour !**

---

### ÉTAPE 9 : Installer Apache

```bash
# Installer Apache
sudo apt install apache2 -y
```

**Durée : 30 secondes - 1 minute.**

**Résultat :**

```
Reading package lists... Done
...
The following NEW packages will be installed:
  apache2 apache2-bin apache2-data apache2-utils ...
...
Setting up apache2 (2.4.52-1ubuntu4) ...
Enabling module mpm_event.
Enabling module authz_core.
...
```

---

**Vérifier qu'Apache tourne :**

```bash
sudo systemctl status apache2
```

**Résultat :**

```
[BLACK_CIRCLE] apache2.service - The Apache HTTP Server
     Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2024-12-16 10:00:00 UTC; 30s ago
   Main PID: 1234 (apache2)
      Tasks: 55 (limit: 1131)
     Memory: 5.2M
        CPU: 123ms
```

**`Active: active (running)`** = Apache est en cours d'exécution [OK]

---

**Tester Apache depuis le serveur :**

```bash
curl http://localhost
```

**Résultat :**

```html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to Apache</title>
...
```

**[OK] Apache fonctionne en local !**

---

**Tester depuis ton navigateur :**

Ouvre ton navigateur et va sur :

```
http://35.180.123.45
```

**Remplace par TON IP publique !**

**[BRAVO] Tu devrais voir la page par défaut d'Apache ! [BRAVO]**

```
Apache2 Ubuntu Default Page
It works!
```

---

**Si ça ne fonctionne PAS :**

**Vérifications :**

**1. Apache tourne-t-il ?**

```bash
sudo systemctl status apache2
```

Si inactif :

```bash
sudo systemctl start apache2
```

---

**2. Le Security Group autorise-t-il HTTP (port 80) ?**

**Dans la console AWS :**

1. EC2 -> Instances
2. Sélectionner ton instance
3. Onglet "Security"
4. Cliquer sur le Security Group (lien bleu)
5. Onglet "Inbound rules"
6. Vérifier qu'il y a une règle :
   - Type : HTTP
   - Port : 80
   - Source : 0.0.0.0/0

**Si absente, l'ajouter :**

1. "Edit inbound rules"
2. "Add rule"
3. Type : HTTP
4. Source : Anywhere IPv4 (0.0.0.0/0)
5. "Save rules"

---

**3. Utilises-tu la bonne IP ?**

Vérifie que tu utilises l'**IP publique** (pas l'IP privée).

Dans la console AWS, colonne "Public IPv4 address".

---

### ÉTAPE 10 : Créer ton site web

**Supprimer la page par défaut :**

```bash
sudo rm /var/www/html/index.html
```

---

**Créer ta page d'accueil :**

```bash
sudo nano /var/www/html/index.html
```

**Contenu (exemple simple) :**

```html
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mon Site sur AWS EC2</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            color: white;
        }
        
        .container {
            text-align: center;
            background: rgba(255, 255, 255, 0.1);
            padding: 3rem;
            border-radius: 20px;
            backdrop-filter: blur(10px);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            max-width: 600px;
        }
        
        h1 {
            font-size: 3rem;
            margin-bottom: 1rem;
        }
        
        p {
            font-size: 1.2rem;
            line-height: 1.6;
            margin-bottom: 1rem;
        }
        
        .badge {
            display: inline-block;
            background: rgba(255, 255, 255, 0.2);
            padding: 0.5rem 1rem;
            border-radius: 20px;
            margin: 0.5rem;
            font-size: 0.9rem;
        }
        
        .emoji {
            font-size: 4rem;
            margin-bottom: 1rem;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="emoji">[CLOUD]</div>
        <h1>Bienvenue sur AWS ! </h1>
        <p>Ce site est hébergé sur <strong>Amazon EC2</strong></p>
        <p>Région : Paris (eu-west-3)</p>
        <div>
            <span class="badge">[ECRAN] Ubuntu 22.04</span>
            <span class="badge">[WEB] Apache</span>
            <span class="badge">[CLOUD] AWS EC2</span>
        </div>
        <p style="margin-top: 2rem; font-size: 0.9rem; opacity: 0.8;">
            Mon premier site déployé sur le cloud AWS !
        </p>
    </div>
</body>
</html>
```

**Sauvegarder : `Ctrl + O`, `Entrée`, `Ctrl + X`**

---

**Recharger la page dans ton navigateur :**

```
http://35.180.123.45
```

**[BRAVO] TON SITE PERSONNALISÉ EST EN LIGNE ! [BRAVO]**

---

### ÉTAPE 11 : Associer une adresse IP élastique (optionnel)

**Problème actuel :**

Si tu arrêtes puis redémarres l'instance, l'IP publique **CHANGE**.

**Solution : Elastic IP (IP élastique)**

---

**Qu'est-ce qu'une Elastic IP ?**

**Elastic IP** = Adresse IP publique statique (fixe)

**Avantages :**
- L'IP ne change jamais
- Peut être déplacée entre instances
- Utile pour un nom de domaine

**Inconvénient :**
- **Facturée si non associée à une instance en cours d'exécution**
- Gratuite si associée à une instance running
- 0,005 $/heure si non utilisée (~3,6 $/mois)

**[IDEE] Dans le Free Tier : 1 Elastic IP gratuite (si associée)**

---

**Allouer une Elastic IP :**

**1. Console EC2 -> Menu de gauche -> "Elastic IPs" (sous Network & Security)**

**2. Cliquer sur "Allocate Elastic IP address"**

**3. Configuration :**

```
Network Border Group : eu-west-3 (région actuelle)
Public IPv4 address pool : Amazon's pool
```

**4. "Allocate"**

**Résultat :**

```
[OK] Successfully allocated Elastic IP address
Allocated IPv4 address : 35.180.200.100
```

**[ATTENTION] NOTE : L'IP sera différente pour toi !**

---

**Associer l'Elastic IP à l'instance :**

**1. Sélectionner l'Elastic IP dans la liste**

**2. Actions -> Associate Elastic IP address**

**3. Configuration :**

```
Resource type : Instance
Instance : mon-site-web (sélectionner ton instance)
Private IP address : (laisser par défaut)
```

**4. "Associate"**

**Résultat :**

```
[OK] Successfully associated Elastic IP address
```

---

**Vérifier :**

**1. Retourner sur EC2 -> Instances**

**2. L'instance a maintenant l'Elastic IP dans la colonne "Public IPv4 address"**

**3. Tester dans le navigateur avec la nouvelle IP :**

```
http://35.180.200.100
```

**[OK] Ton site fonctionne avec la nouvelle IP !**

---

**Maintenant, même si tu arrêtes/redémarres l'instance, l'IP reste la même.**

---

### [OK] TESTS DE VALIDATION

**1. Instance en cours d'exécution**

- [ ] Instance state : Running
- [ ] Status checks : 2/2 checks passed
- [ ] IP publique assignée

---

**2. Connexion SSH**

- [ ] `ssh -i ma-cle-ec2.pem ubuntu@<IP>` fonctionne
- [ ] Pas de délai/timeout
- [ ] Prompt Ubuntu affiché

---

**3. Apache fonctionne**

```bash
sudo systemctl status apache2
# Résultat : active (running)
```

- [ ] Service actif
- [ ] Pas d'erreur dans les logs

---

**4. Site accessible depuis Internet**

- [ ] `http://<IP-publique>` charge le site
- [ ] Page HTML s'affiche correctement
- [ ] Pas d'erreur 403/404/500

---

**5. Security Group correct**

- [ ] SSH (22) : Mon IP uniquement
- [ ] HTTP (80) : 0.0.0.0/0
- [ ] HTTPS (443) : 0.0.0.0/0 (si configuré)

---

**6. Coûts sous contrôle**

**Vérifier le tier gratuit :**

Console AWS -> Billing -> Free Tier

- [ ] EC2 : X / 750 heures utilisées ce mois
- [ ] EBS : X / 30 GB utilisés ce mois
- [ ] Transfert : X / 15 GB sortant

**750 heures/mois = 1 instance t2.micro H24 pendant 31 jours**

**Si tu laisses tourner H24 : 100% gratuit pendant 12 mois !**

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Permission denied (publickey)

**Symptôme :**

```
Permission denied (publickey).
```

**Causes possibles :**

**1. Mauvaises permissions sur la clé privée**

```bash
chmod 400 ~/.ssh/ma-cle-ec2.pem
```

---

**2. Mauvais chemin vers la clé**

Vérifier :

```bash
ls -l ~/.ssh/ma-cle-ec2.pem
```

Si "No such file", la clé n'est pas au bon endroit.

---

**3. Mauvais nom d'utilisateur**

Pour Ubuntu AMI : `ubuntu`

```bash
ssh -i ~/.ssh/ma-cle-ec2.pem ubuntu@<IP>
```

---

#### Erreur 2 : Connection timed out

**Symptôme :**

```
ssh: connect to host 35.180.123.45 port 22: Connection timed out
```

**Causes possibles :**

**1. Security Group ne permet pas SSH depuis ton IP**

**Vérifier :**

Console AWS -> EC2 -> Security Groups -> Inbound rules

Doit avoir :
- Type : SSH
- Port : 22
- Source : **TON IP ACTUELLE**

**Si ton IP a changé :**

1. Trouver ton IP actuelle : https://whatismyipaddress.com/
2. Modifier la règle SSH dans le Security Group

---

**2. Instance arrêtée**

Vérifier que l'instance est "Running".

---

**3. Pare-feu local bloque SSH**

Désactiver temporairement le pare-feu de ton PC pour tester.

---

#### Erreur 3 : Site web inaccessible (ERR_CONNECTION_TIMED_OUT)

**Symptôme :**

Le navigateur ne charge pas `http://<IP>`.

**Causes possibles :**

**1. Apache ne tourne pas**

```bash
sudo systemctl status apache2
```

Si inactif :

```bash
sudo systemctl start apache2
```

---

**2. Security Group ne permet pas HTTP**

Vérifier la règle HTTP (port 80) dans le Security Group.

---

**3. Utilisation de l'IP privée au lieu de l'IP publique**

**IP privée (172.31.x.x)** -> Ne fonctionne PAS depuis Internet

**IP publique (35.x.x.x)** -> Fonctionne

---

#### Erreur 4 : Dépassement du Free Tier

**Symptôme :**

Email AWS : "Your AWS Free Tier usage is approaching the limit"

**Causes :**

**1. Plusieurs instances en cours d'exécution**

Free Tier = 750 heures/mois pour TOUTES les instances combinées.

2 instances t2.micro H24 = 1440 heures -> DÉPASSEMENT

**Solution :**

Arrêter les instances non utilisées.

---

**2. Utilisation d'une instance non Free Tier**

t2.micro [OK] Gratuit
t2.small [X] Payant (~17 $/mois)

**Solution :**

Arrêter et relancer avec t2.micro.

---

**3. Stockage EBS > 30 GB**

Free Tier = 30 GB

Si tu as 50 GB = 20 GB facturés (~1,60 $/mois)

**Solution :**

Réduire la taille du volume ou supprimer les volumes inutilisés.

---

**4. Elastic IP non associée**

Si tu as alloué une Elastic IP mais qu'elle n'est pas associée à une instance running :

-> Facturée 0,005 $/heure (~3,6 $/mois)

**Solution :**

Associer l'Elastic IP ou la libérer (Release).

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. EC2 = Serveurs virtuels dans le cloud**
- Créés en quelques minutes
- Scalables (changement de taille)
- Payés à l'heure

**2. Types d'instances**
- t2/t3 = Usage général, burstable
- Free Tier = t2.micro (750h/mois pendant 12 mois)

**3. AMI = Image système**
- Ubuntu, Amazon Linux, Windows, etc.
- Préinstallé, prêt à l'emploi

**4. Security Groups = Pare-feu**
- Inbound : Qui peut entrer
- Outbound : Où l'instance peut aller
- SSH (22) : Restreindre à ton IP
- HTTP/HTTPS (80/443) : Ouvrir à Internet

**5. Clés SSH**
- Paire clé publique/privée
- JAMAIS partager la clé privée
- Permissions 400 obligatoires

**6. IP publique vs Elastic IP**
- IP publique : Change à chaque redémarrage
- Elastic IP : Fixe, mais facturée si non utilisée

**7. Free Tier**
- 750 h/mois pendant 12 mois
- 30 GB stockage EBS
- Surveiller avec alertes

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Nom de domaine**

Associer un nom de domaine à ton IP :

1. Acheter un domaine (ex: OVH, Namecheap)
2. Créer un enregistrement DNS :
   - Type : A
   - Nom : @ (ou www)
   - Valeur : Ton Elastic IP

**Résultat : `http://mon-domaine.com` pointe vers ton EC2**

---

**2. HTTPS avec Let's Encrypt**

```bash
# Installer Certbot
sudo apt install certbot python3-certbot-apache -y

# Obtenir un certificat SSL
sudo certbot --apache -d mon-domaine.com
```

**Gratuit et renouvelé automatiquement !**

---

**3. Snapshot (sauvegarde)**

**Créer un snapshot du disque :**

Console AWS -> EC2 -> Volumes -> Sélectionner le volume -> Actions -> Create snapshot

**Utilité :**
- Sauvegarde complète de l'instance
- Restauration en cas de problème
- Création d'une nouvelle instance identique

---

**4. Automatiser avec User Data**

Au lieu d'installer Apache manuellement, automatiser au lancement :

**Dans "Advanced details" lors de la création :**

```bash
#!/bin/bash
apt update -y
apt upgrade -y
apt install apache2 -y
systemctl start apache2
systemctl enable apache2

# Créer la page d'accueil
cat > /var/www/html/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>Auto-déployé !</title></head>
<body><h1>Site déployé automatiquement !</h1></body>
</html>
EOF
```

**Au premier boot, tout est installé automatiquement !**

---

**5. Monitoring avec CloudWatch**

**Activer le monitoring détaillé :**

Console -> EC2 -> Instance -> Monitoring -> Manage detailed monitoring

**Métriques disponibles :**
- CPU Utilization
- Network In/Out
- Disk Read/Write
- Status Checks

**Créer des alarmes :**

Ex: Alerte si CPU > 80% pendant 5 minutes.

---

**6. Arrêter/Démarrer vs Terminer**

**Stop (arrêter) :**
- Instance arrêtée mais disque conservé
- Pas de frais d'instance
- Frais de stockage EBS (~0,60 $/mois pour 8 GB)
- Peut être redémarrée

**Terminate (supprimer) :**
- Instance ET disque supprimés
- Aucun frais
- IRRÉVERSIBLE (sauf si snapshot)

**Pour économiser :** Stop quand pas utilisé, Terminate si plus besoin.

---

## [COURS] CONCLUSION DE L'EXERCICE 1

**[BRAVO] Félicitations ! Tu as déployé ton premier site sur AWS EC2 ! [BRAVO]**

**Ce que tu as appris :**
- Créer un compte AWS
- Lancer une instance EC2
- Configurer les Security Groups
- Se connecter en SSH
- Installer et configurer Apache
- Déployer un site web
- Associer une Elastic IP
- Gérer les coûts (Free Tier)

**Compétences acquises :**
- [OK] AWS EC2 (niveau débutant)
- [OK] Gestion serveur Linux
- [OK] Sécurité réseau (Security Groups)
- [OK] SSH et clés publiques/privées
- [OK] Déploiement web basique

**Temps moyen de réalisation :** 2-3 heures

**Prochaine étape :** Exercice 2 - Héberger un site statique sur S3 + CloudFront ! [CLOUD]

---

Veux-tu que je continue avec les 9 autres exercices avec le même niveau de détail ? Je peux créer :

- **Exercice 2** : S3 + CloudFront (hébergement statique + CDN)
- **Exercice 3** : RDS + Flask (app web avec base de données)
- **Exercice 4** : Elastic Beanstalk (déploiement Flask automatisé)
- **Exercice 5** : Lambda + API Gateway (API serverless)
- Et les 5 exercices avancés/experts...

Chaque exercice sera aussi détaillé que celui-ci !


# [VERT] EXERCICE 2 : HÉBERGER UN SITE STATIQUE SUR S3 + CLOUDFRONT

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur frontend dans une startup. L'équipe veut héberger le site vitrine de l'entreprise (HTML/CSS/JS statique) de manière économique et performante, avec une distribution globale.

### Cahier des charges

Le client souhaite :
- Hébergement d'un site statique (pas de backend)
- Coûts très bas (quelques centimes par mois)
- Performance mondiale (CDN)
- HTTPS obligatoire
- Nom de domaine personnalisé (optionnel)

### Contraintes techniques

- AWS S3 pour l'hébergement
- CloudFront comme CDN
- Certificat SSL (AWS Certificate Manager)
- Bucket policy pour accès public
- Site entièrement statique (HTML/CSS/JS)
- Temps estimé : 2-3 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Créer et configurer un bucket S3
- [OK] Activer l'hébergement web statique sur S3
- [OK] Uploader des fichiers sur S3
- [OK] Configurer les permissions et bucket policies
- [OK] Créer une distribution CloudFront (CDN)
- [OK] Configurer HTTPS avec Certificate Manager
- [OK] Comprendre S3 vs EC2 pour l'hébergement
- [OK] Optimiser les coûts

---

## [DOCS] PRÉREQUIS

- Exercice 1 terminé (compréhension AWS Console)
- Connaissances HTML/CSS/JS
- Compte AWS actif

---

## [IDEE] CONCEPTS AWS À COMPRENDRE

### Qu'est-ce qu'Amazon S3 ?

**S3** = Simple Storage Service

**Analogie :**
- Dropbox/Google Drive = Stockage de fichiers personnel
- S3 = Stockage de fichiers à l'échelle d'Internet

**Caractéristiques :**
- Stockage d'objets (fichiers)
- Capacité illimitée
- 99,999999999% de durabilité (11 neuf)
- Accessible via HTTP/HTTPS
- Très économique

---

### Concepts clés de S3

#### **Bucket**

**Bucket** = Conteneur pour stocker des objets

**Analogie :** Un bucket = Un dossier racine

**Règles importantes :**
- Le nom du bucket doit être **UNIQUE MONDIALEMENT**
- Pas de majuscules
- Pas d'underscores
- Format DNS valide

**Exemples :**
- [OK] `mon-site-web-2024`
- [OK] `company-assets-prod`
- [X] `Mon_Site` (majuscules + underscore)
- [X] `test` (probablement déjà pris)

---

#### **Objets**

**Objet** = Fichier stocké dans S3

Chaque objet a :
- **Key (clé)** = Chemin du fichier
- **Value (valeur)** = Le contenu du fichier
- **Metadata** = Informations supplémentaires
- **Version ID** (si versioning activé)

**Exemple :**
```
Bucket : mon-site-web
Key : css/style.css
Value : [contenu du fichier CSS]
URL : https://mon-site-web.s3.amazonaws.com/css/style.css
```

---

#### **Classes de stockage S3**

| Classe | Usage | Durabilité | Disponibilité | Prix |
|--------|-------|------------|---------------|------|
| **S3 Standard** | Accès fréquent | 11 neuf | 99,99% | 0,023 $/GB/mois |
| **S3 IA** (Infrequent Access) | Accès rare | 11 neuf | 99,9% | 0,0125 $/GB/mois |
| **S3 Glacier** | Archive | 11 neuf | 99,99% | 0,004 $/GB/mois |
| **S3 Glacier Deep Archive** | Archive long terme | 11 neuf | 99,99% | 0,00099 $/GB/mois |

**Pour un site web : S3 Standard**

---

### Qu'est-ce qu'Amazon CloudFront ?

**CloudFront** = Content Delivery Network (CDN)

**Analogie simple :**

Sans CDN :
```
Utilisateur Paris -> Serveur USA (200 ms de latence)
Utilisateur Tokyo -> Serveur USA (300 ms de latence)
```

Avec CDN :
```
Utilisateur Paris -> Edge Paris (10 ms)
Utilisateur Tokyo -> Edge Tokyo (10 ms)
```

**Le CDN met en cache ton contenu dans des serveurs proches des utilisateurs.**

---

#### **Edge Locations**

**Edge Location** = Serveur de cache CloudFront

AWS a **400+ Edge Locations** dans le monde :
- Paris, Londres, Amsterdam (Europe)
- New York, Los Angeles, Toronto (Amérique)
- Tokyo, Singapour, Mumbai (Asie)
- Sydney (Océanie)

**Avantages :**
- [RAPIDE] Latence ultra-faible
- [MONDE] Performance mondiale
- [ARGENT] Réduit la charge sur l'origine (S3)
- [SECURITE] Protection DDoS basique

---

### S3 Static Website vs EC2

| Critère | S3 Static | EC2 |
|---------|-----------|-----|
| **Type de site** | Statique uniquement | Statique + Dynamique |
| **Coût** | ~0,50 $/mois | ~8 $/mois (t2.micro) |
| **Maintenance** | Zéro | Oui (OS, serveur) |
| **Scaling** | Automatique illimité | Manuel (ou Auto Scaling) |
| **SSL** | Gratuit via CloudFront | Let's Encrypt ou ACM |
| **Performance** | Excellente (+ CDN) | Bonne |

**Pour un site statique : S3 + CloudFront est TOUJOURS meilleur !**

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Créer le site web statique

**Avant d'uploader sur S3, créons un site web exemple.**

**Sur ton ordinateur, créer un dossier :**

```bash
mkdir mon-site-statique
cd mon-site-statique
```

---

**Créer `index.html` :**

```html
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mon Site sur S3</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header>
        <nav>
            <div class="logo">[CLOUD] Mon Site S3</div>
            <ul>
                <li><a href="index.html">Accueil</a></li>
                <li><a href="about.html">À propos</a></li>
                <li><a href="contact.html">Contact</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <section class="hero">
            <h1>Bienvenue sur mon site hébergé sur S3 !</h1>
            <p>Site statique ultra-rapide grâce à Amazon S3 et CloudFront CDN</p>
            <div class="badges">
                <span class="badge">[CLOUD] Amazon S3</span>
                <span class="badge">[RAPIDE] CloudFront CDN</span>
                <span class="badge">[VERROUILLE] HTTPS</span>
            </div>
        </section>

        <section class="features">
            <h2>Pourquoi S3 pour l'hébergement web ?</h2>
            <div class="feature-grid">
                <div class="feature-card">
                    <div class="icon">[ARGENT]</div>
                    <h3>Coût ultra-bas</h3>
                    <p>Quelques centimes par mois pour un site web complet</p>
                </div>
                <div class="feature-card">
                    <div class="icon">[RAPIDE]</div>
                    <h3>Performance</h3>
                    <p>CDN mondial avec 400+ points de présence</p>
                </div>
                <div class="feature-card">
                    <div class="icon">[HAUSSE]</div>
                    <h3>Scalabilité</h3>
                    <p>Gère automatiquement des millions de visiteurs</p>
                </div>
                <div class="feature-card">
                    <div class="icon">[SECURITE]</div>
                    <h3>Sécurité</h3>
                    <p>HTTPS gratuit, DDoS protection</p>
                </div>
            </div>
        </section>
    </main>

    <footer>
        <p>&copy; 2024 Mon Site S3 - Hébergé sur Amazon Web Services</p>
    </footer>

    <script src="js/script.js"></script>
</body>
</html>
```

---

**Créer `about.html` :**

```html
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>À propos - Mon Site sur S3</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header>
        <nav>
            <div class="logo">[CLOUD] Mon Site S3</div>
            <ul>
                <li><a href="index.html">Accueil</a></li>
                <li><a href="about.html">À propos</a></li>
                <li><a href="contact.html">Contact</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <section class="content">
            <h1>À propos de ce site</h1>
            <p>
                Ce site est un exemple d'hébergement web statique sur Amazon S3, 
                distribué mondialement via CloudFront CDN.
            </p>
            <h2>Technologies utilisées</h2>
            <ul>
                <li><strong>Amazon S3</strong> : Stockage et hébergement</li>
                <li><strong>CloudFront</strong> : CDN pour performance mondiale</li>
                <li><strong>Certificate Manager</strong> : Certificat SSL gratuit</li>
                <li><strong>Route 53</strong> : DNS (optionnel)</li>
            </ul>
            <h2>Avantages</h2>
            <p>
                L'hébergement sur S3 est idéal pour les sites statiques car il élimine 
                complètement la gestion de serveurs. Pas de mises à jour OS, pas de 
                surveillance de serveur, scaling automatique illimité.
            </p>
        </section>
    </main>

    <footer>
        <p>&copy; 2024 Mon Site S3 - Hébergé sur Amazon Web Services</p>
    </footer>
</body>
</html>
```

---

**Créer `contact.html` :**

```html
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contact - Mon Site sur S3</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header>
        <nav>
            <div class="logo">[CLOUD] Mon Site S3</div>
            <ul>
                <li><a href="index.html">Accueil</a></li>
                <li><a href="about.html">À propos</a></li>
                <li><a href="contact.html">Contact</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <section class="content">
            <h1>Contactez-nous</h1>
            <div class="contact-info">
                <div class="contact-card">
                    <h3>[EMAIL] Email</h3>
                    <p>contact@example.com</p>
                </div>
                <div class="contact-card">
                    <h3>[MOBILE] Téléphone</h3>
                    <p>+221 77 123 45 67</p>
                </div>
                <div class="contact-card">
                    <h3>[MONDE] Localisation</h3>
                    <p>Dakar, Sénégal</p>
                </div>
            </div>
            <p>
                <em>Note : Pour un formulaire de contact fonctionnel sur S3, 
                il faudrait utiliser AWS Lambda ou un service tiers comme Formspree.</em>
            </p>
        </section>
    </main>

    <footer>
        <p>&copy; 2024 Mon Site S3 - Hébergé sur Amazon Web Services</p>
    </footer>
</body>
</html>
```

---

**Créer le dossier CSS et `css/style.css` :**

```bash
mkdir css
```

```css
/* ═══════════════════════════════════════════════════════════════
   FEUILLE DE STYLE - SITE S3
   ═══════════════════════════════════════════════════════════════ */

/* Reset */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    color: #333;
    background: #f5f5f5;
}

/* Header et Navigation */
header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    position: sticky;
    top: 0;
    z-index: 1000;
}

nav {
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 1rem 2rem;
}

.logo {
    color: white;
    font-size: 1.5rem;
    font-weight: bold;
}

nav ul {
    list-style: none;
    display: flex;
    gap: 2rem;
}

nav ul li a {
    color: white;
    text-decoration: none;
    transition: all 0.3s ease;
    padding: 0.5rem 1rem;
    border-radius: 5px;
}

nav ul li a:hover {
    background: rgba(255,255,255,0.2);
}

/* Section Hero */
.hero {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    text-align: center;
    padding: 5rem 2rem;
}

.hero h1 {
    font-size: 3rem;
    margin-bottom: 1rem;
}

.hero p {
    font-size: 1.3rem;
    margin-bottom: 2rem;
}

.badges {
    display: flex;
    justify-content: center;
    gap: 1rem;
    flex-wrap: wrap;
}

.badge {
    background: rgba(255,255,255,0.2);
    padding: 0.5rem 1.5rem;
    border-radius: 20px;
    backdrop-filter: blur(10px);
}

/* Features Section */
.features {
    max-width: 1200px;
    margin: 4rem auto;
    padding: 0 2rem;
}

.features h2 {
    text-align: center;
    font-size: 2.5rem;
    margin-bottom: 3rem;
    color: #667eea;
}

.feature-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 2rem;
}

.feature-card {
    background: white;
    padding: 2rem;
    border-radius: 10px;
    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
    text-align: center;
    transition: transform 0.3s ease;
}

.feature-card:hover {
    transform: translateY(-10px);
}

.icon {
    font-size: 3rem;
    margin-bottom: 1rem;
}

.feature-card h3 {
    color: #667eea;
    margin-bottom: 1rem;
}

/* Content Section */
.content {
    max-width: 800px;
    margin: 3rem auto;
    padding: 3rem;
    background: white;
    border-radius: 10px;
    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}

.content h1 {
    color: #667eea;
    margin-bottom: 1.5rem;
}

.content h2 {
    color: #764ba2;
    margin-top: 2rem;
    margin-bottom: 1rem;
}

.content ul {
    margin-left: 2rem;
    margin-bottom: 1rem;
}

.content p {
    margin-bottom: 1rem;
    text-align: justify;
}

/* Contact Section */
.contact-info {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 2rem;
    margin: 2rem 0;
}

.contact-card {
    text-align: center;
    padding: 2rem;
    background: #f9f9f9;
    border-radius: 10px;
}

.contact-card h3 {
    color: #667eea;
    margin-bottom: 1rem;
}

/* Footer */
footer {
    background: #333;
    color: white;
    text-align: center;
    padding: 2rem;
    margin-top: 4rem;
}

/* Responsive */
@media (max-width: 768px) {
    nav {
        flex-direction: column;
        gap: 1rem;
    }
    
    nav ul {
        flex-direction: column;
        text-align: center;
        gap: 0.5rem;
    }
    
    .hero h1 {
        font-size: 2rem;
    }
    
    .content {
        padding: 1.5rem;
    }
}
```

---

**Créer le dossier JS et `js/script.js` :**

```bash
mkdir js
```

```javascript
// ═══════════════════════════════════════════════════════════════
// SCRIPT PRINCIPAL
// ═══════════════════════════════════════════════════════════════

document.addEventListener('DOMContentLoaded', function() {
    console.log('Site chargé depuis S3 + CloudFront !');
    
    // Ajouter la page active dans le menu
    const currentPage = window.location.pathname.split('/').pop() || 'index.html';
    const links = document.querySelectorAll('nav a');
    
    links.forEach(link => {
        if (link.getAttribute('href') === currentPage) {
            link.style.background = 'rgba(255,255,255,0.3)';
            link.style.fontWeight = 'bold';
        }
    });
    
    // Animation au scroll
    const observerOptions = {
        threshold: 0.2,
        rootMargin: '0px 0px -100px 0px'
    };
    
    const observer = new IntersectionObserver(function(entries) {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                entry.target.style.opacity = '1';
                entry.target.style.transform = 'translateY(0)';
            }
        });
    }, observerOptions);
    
    // Observer les feature cards
    document.querySelectorAll('.feature-card, .contact-card').forEach(card => {
        card.style.opacity = '0';
        card.style.transform = 'translateY(50px)';
        card.style.transition = 'all 0.6s ease';
        observer.observe(card);
    });
});
```

---

**Structure finale :**

```
mon-site-statique/
├── index.html
├── about.html
├── contact.html
├── css/
│   └── style.css
└── js/
    └── script.js
```

**[OK] Site web prêt à être uploadé !**

---

### ÉTAPE 2 : Créer un bucket S3

**1. Console AWS -> Chercher "S3" dans la barre de recherche**

**2. Cliquer sur "Create bucket"**

---

**Configuration du bucket :**

#### **General configuration**

```
Bucket name : mon-site-web-s3-2024

[ATTENTION] IMPORTANT : Le nom doit être UNIQUE mondialement !
Si "mon-site-web-s3-2024" est pris, essayer :
- mon-site-web-s3-<ton-nom>
- <ton-nom>-portfolio-2024
- etc.

AWS Region : Europe (Paris) eu-west-3
```

**Pourquoi choisir Paris ?**
- Plus proche des utilisateurs européens/africains
- Conformité RGPD
- Latence réduite

---

#### **Object Ownership**

```
[x] ACLs disabled (recommended)
```

**Explication :**

**ACLs** = Access Control Lists (ancien système de permissions)

**Recommandation AWS : Utiliser les Bucket Policies (plus modernes)**

---

#### **Block Public Access settings**

**[ATTENTION] TRÈS IMPORTANT !**

Par défaut, AWS bloque TOUT accès public (sécurité).

Pour un site web, on DOIT autoriser l'accès public.

```
[ ] Block all public access

[ATTENTION] DÉCOCHER cette case !

Un avertissement s'affiche :
"Turning off block all public access might result in this bucket 
and the objects within becoming public"

[x] I acknowledge that the current settings might result in this 
bucket and the objects within becoming public
```

**C'est voulu ! On veut que le site soit public.**

---

#### **Bucket Versioning**

```
[ ] Enable (optionnel)
```

**Versioning** = Garde l'historique des modifications

**Avantages :**
- Récupération en cas d'erreur
- Protection contre suppressions accidentelles

**Inconvénient :**
- Coût de stockage multiplié

**Pour ce tuto : Désactivé**

---

#### **Tags**

**Optionnel** - Pour organiser/facturation :

```
Key: Projet | Value: SiteWeb
Key: Environnement | Value: Production
```

---

#### **Default encryption**

```
[x] Enable

Encryption type:
  [x] Server-side encryption with Amazon S3 managed keys (SSE-S3)
```

**Chiffrement automatique de tous les fichiers.**

**SSE-S3** = Gratuit, géré par AWS

---

**Cliquer sur "Create bucket"**

**Résultat :**

```
[OK] Successfully created bucket "mon-site-web-s3-2024"
```

---

### ÉTAPE 3 : Uploader les fichiers sur S3

**1. Cliquer sur le nom du bucket pour l'ouvrir**

**2. Cliquer sur "Upload"**

**3. Glisser-déposer TOUS les fichiers et dossiers :**

```
index.html
about.html
contact.html
css/ (dossier entier)
js/ (dossier entier)
```

**Ou cliquer sur "Add files" / "Add folder"**

---

**4. Configuration de l'upload (laisser par défaut) :**

**Permissions :**
```
[x] Grant public-read access (on va le faire autrement)
```

**Laisser décoché pour l'instant.**

---

**5. Properties :**

```
Storage class : S3 Standard
```

---

**6. Cliquer sur "Upload"**

**Durée : Quelques secondes**

**Résultat :**

```
[OK] Upload succeeded
5 objects uploaded
```

---

**7. Cliquer sur "Close"**

**Tu vois maintenant tes fichiers dans le bucket :**

```
[FICHIER] about.html
[FICHIER] contact.html
[DOSSIER] css/
[FICHIER] index.html
[DOSSIER] js/
```

---

### ÉTAPE 4 : Activer l'hébergement web statique

**1. Dans le bucket, onglet "Properties"**

**2. Descendre jusqu'à "Static website hosting"**

**3. Cliquer sur "Edit"**

**4. Configuration :**

```
[x] Enable

Hosting type:
  [x] Host a static website

Index document : index.html
Error document : index.html (ou créer une page 404.html)
```

**Explication :**

**Index document :**
- Fichier affiché quand on accède à la racine
- `http://bucket.s3-website.com/` -> `index.html`
- `http://bucket.s3-website.com/css/` -> Erreur (pas d'index dans css/)

**Error document :**
- Page affichée en cas d'erreur 404
- Pour l'instant : `index.html` (redirige vers l'accueil)

---

**5. Cliquer sur "Save changes"**

**6. Retourner dans l'onglet "Properties" et descendre jusqu'à "Static website hosting"**

**Tu vois maintenant l'URL du site :**

```
Bucket website endpoint:
http://mon-site-web-s3-2024.s3-website.eu-west-3.amazonaws.com
```

**[ATTENTION] NOTE : C'est l'URL S3 directe (HTTP uniquement, pas HTTPS)**

**On va utiliser CloudFront pour avoir HTTPS.**

---

**7. Tester l'URL en HTTP (ne fonctionnera pas encore) :**

```
http://mon-site-web-s3-2024.s3-website.eu-west-3.amazonaws.com
```

**Résultat : 403 Forbidden**

**Normal ! On n'a pas encore configuré les permissions publiques.**

---

### ÉTAPE 5 : Configurer les permissions publiques (Bucket Policy)

**On va créer une Bucket Policy pour autoriser la lecture publique.**

**1. Onglet "Permissions"**

**2. Section "Bucket policy" -> Cliquer sur "Edit"**

**3. Copier-coller cette policy :**

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::mon-site-web-s3-2024/*"
        }
    ]
}
```

**[ATTENTION] REMPLACE `mon-site-web-s3-2024` par le nom de TON bucket !**

---

**Explication DÉTAILLÉE de la policy :**

```json
{
    "Version": "2012-10-17",
```

**Version du langage de policy.**

Toujours `"2012-10-17"` (dernière version).

---

```json
    "Statement": [
```

**Statement** = Liste de règles

On peut avoir plusieurs statements dans une policy.

---

```json
        {
            "Sid": "PublicReadGetObject",
```

**Sid** = Statement ID (identifiant de la règle)

Optionnel, mais utile pour identifier la règle.

---

```json
            "Effect": "Allow",
```

**Effect** = Effet de la règle

Valeurs possibles :
- `"Allow"` = Autoriser
- `"Deny"` = Refuser

---

```json
            "Principal": "*",
```

**Principal** = À qui s'applique cette règle

`"*"` = Tout le monde (accès public)

Autres exemples :
- `"AWS": "arn:aws:iam::123456789012:user/Bob"` = Utilisateur Bob
- `"AWS": "*"` = Tous les utilisateurs AWS
- `"Service": "cloudfront.amazonaws.com"` = CloudFront

---

```json
            "Action": "s3:GetObject",
```

**Action** = Quelle action est autorisée

`"s3:GetObject"` = Lire (télécharger) les objets

Autres actions S3 :
- `s3:PutObject` = Écrire (uploader)
- `s3:DeleteObject` = Supprimer
- `s3:ListBucket` = Lister les fichiers
- `s3:*` = Toutes les actions (dangereux !)

---

```json
            "Resource": "arn:aws:s3:::mon-site-web-s3-2024/*"
        }
```

**Resource** = Sur quelles ressources s'applique l'action

**ARN** = Amazon Resource Name (identifiant unique AWS)

Format : `arn:aws:s3:::nom-du-bucket/*`

`/*` = Tous les objets dans le bucket

Sans le `/*`, ça s'appliquerait au bucket lui-même (pas aux fichiers).

---

**Résumé de la policy :**

```
Autoriser (Allow)
à tout le monde (Principal: *)
de lire (Action: s3:GetObject)
tous les fichiers (Resource: bucket/*)
```

---

**4. Cliquer sur "Save changes"**

**AWS affiche un avertissement :**

```
[ATTENTION] This bucket has public access
```

**C'est voulu ! Cliquer sur "Save" pour confirmer.**

---

**5. Retourner à l'onglet "Permissions"**

**En haut, tu vois maintenant :**

```
[ATTENTION] Publicly accessible
```

**[OK] Parfait ! Le bucket est maintenant public.**

---

### ÉTAPE 6 : Tester le site en HTTP

**Retourner dans l'onglet "Properties" -> "Static website hosting"**

**Copier l'URL du site :**

```
http://mon-site-web-s3-2024.s3-website.eu-west-3.amazonaws.com
```

**Ouvrir dans le navigateur.**

**[BRAVO] TON SITE S3 EST EN LIGNE ! [BRAVO]**

---

**Tester la navigation :**

- [ ] Page d'accueil s'affiche
- [ ] CSS chargé (couleurs, mise en page)
- [ ] JavaScript fonctionne (console log, animations)
- [ ] Liens "À propos" et "Contact" fonctionnent
- [ ] Images/assets chargent

**[OK] Site fonctionnel en HTTP !**

---

**Limitations actuelles :**

- [X] Pas de HTTPS (pas sécurisé)
- [X] URL longue et moche
- [X] Pas de CDN (latence depuis l'autre bout du monde)

**-> Solution : CloudFront !**

---

### ÉTAPE 7 : Créer une distribution CloudFront

**CloudFront va :**
- Ajouter HTTPS
- Distribuer le site mondialement (CDN)
- Donner une URL propre (`xxx.cloudfront.net`)

---

**1. Console AWS -> Chercher "CloudFront"**

**2. Cliquer sur "Create distribution"**

---

#### **Origin settings**

**Origin domain :**

**[ATTENTION] NE PAS sélectionner le bucket dans la liste déroulante !**

**À la place, copier l'URL S3 Website Endpoint :**

```
mon-site-web-s3-2024.s3-website.eu-west-3.amazonaws.com
```

**(Sans le `http://` au début)**

**Pourquoi ?**

Si tu sélectionnes le bucket dans la liste :
- CloudFront utilise l'API REST S3
- Ne fonctionne pas avec le static website hosting

Si tu colles l'URL website :
- CloudFront utilise l'endpoint website HTTP
- [OK] Fonctionne correctement

---

**Origin path :**

```
(laisser vide)
```

---

**Name :**

```
S3-mon-site-web (généré automatiquement)
```

---

**Protocol :**

```
[x] HTTP only
```

**CloudFront -> S3 en HTTP**
**Client -> CloudFront en HTTPS**

---

#### **Default cache behavior**

**Viewer protocol policy :**

```
[x] Redirect HTTP to HTTPS
```

**Forcer HTTPS pour les visiteurs.**

---

**Allowed HTTP methods :**

```
[x] GET, HEAD
```

**Site statique : seulement lecture.**

---

**Cache policy :**

```
CachingOptimized (recommended)
```

**Optimise le cache pour les fichiers statiques.**

---

#### **Function associations**

```
(laisser vide pour l'instant)
```

---

#### **Settings**

**Price class :**

```
[x] Use all edge locations (best performance)
```

**Autres options :**
- Use only North America and Europe : Moins cher, moins de perf en Asie
- Use only North America, Europe, Asia, Middle East, and Africa : Exclut Océanie/Amérique du Sud

**Pour ce tuto : All edge locations**

---

**Alternate domain names (CNAME) :**

```
(laisser vide pour l'instant)
```

**On reviendra là-dessus pour un nom de domaine personnalisé.**

---

**Custom SSL certificate :**

```
[x] Default CloudFront certificate (*.cloudfront.net)
```

**Certificat SSL gratuit pour l'URL CloudFront.**

---

**Supported HTTP versions :**

```
[x] HTTP/2, HTTP/3
```

**Protocoles modernes et performants.**

---

**Default root object :**

```
index.html
```

**Fichier affiché quand on accède à la racine.**

---

**Standard logging :**

```
[ ] Off (pour l'instant)
```

**Active les logs CloudFront (payant).**

---

**IPv6 :**

```
[x] Enabled
```

---

**3. Cliquer sur "Create distribution"**

**AWS crée la distribution... [HOURGLASS_WITH_FLOWING_SAND]**

**Durée : 5-15 minutes**

**Tu verras :**

```
Status : Deploying
```

**Attendre que ça passe à :**

```
Status : Enabled
Last modified : quelques minutes ago
```

---

**4. Récupérer l'URL CloudFront**

**Dans la liste des distributions, colonne "Domain name" :**

```
d1234567890abc.cloudfront.net
```

**C'est ton URL CloudFront !**

---

**5. Tester l'URL CloudFront**

**Ouvrir dans le navigateur :**

```
https://d1234567890abc.cloudfront.net
```

**[BRAVO] TON SITE EST MAINTENANT EN HTTPS VIA CLOUDFRONT ! [BRAVO]**

---

**Vérifications :**

- [ ] HTTPS fonctionne (cadenas dans la barre d'adresse)
- [ ] Site s'affiche correctement
- [ ] CSS et JS chargent
- [ ] Navigation fonctionne

**[OK] Site distribué mondialement avec CDN !**

---

### ÉTAPE 8 : Tester la performance mondiale

**Test de latence depuis différentes régions :**

**Outil : https://www.webpagetest.org/**

1. Entrer ton URL CloudFront
2. Test depuis plusieurs locations :
   - Paris
   - New York
   - Tokyo
   - Sydney
3. Comparer les temps de chargement

**Résultat attendu :**

Toutes les régions : ~100-300 ms (très rapide !)

**Sans CDN (S3 direct) :**

Paris -> 50 ms
New York -> 200 ms
Tokyo -> 300 ms
Sydney -> 400 ms

**Avec CDN (CloudFront) :**

Paris -> 50 ms
New York -> 50 ms
Tokyo -> 50 ms
Sydney -> 50 ms

**Le CDN met le contenu proche des utilisateurs partout dans le monde ! [MONDE]**

---

### ÉTAPE 9 : Mettre à jour le site (workflow)

**Quand tu veux modifier ton site :**

**1. Modifier les fichiers localement**

Exemple : changer le titre dans `index.html`.

---

**2. Uploader sur S3**

Console S3 -> Bucket -> Upload -> Sélectionner les fichiers modifiés

---

**3. Invalider le cache CloudFront**

**Problème :** CloudFront garde les fichiers en cache (jusqu'à 24h).

**Solution :** Créer une invalidation.

**Console CloudFront -> Sélectionner la distribution -> Onglet "Invalidations"**

**Cliquer sur "Create invalidation"**

```
Object paths :
/*

(Tous les fichiers)

Ou spécifique :
/index.html
/css/*
```

**Cliquer sur "Create invalidation"**

**Durée : 1-5 minutes**

**Le cache CloudFront est vidé, les nouveaux fichiers sont servis.**

---

**[IDEE] Astuce pour éviter les invalidations (coûteuses) :**

**Versioning des fichiers :**

```
style.css -> style.v2.css
script.js -> script.v3.js
```

**CloudFront voit un nouveau fichier -> pas de cache.**

---

### ÉTAPE 10 : Ajouter un nom de domaine personnalisé (optionnel)

**Pour avoir `https://www.mon-site.com` au lieu de `d123.cloudfront.net`**

---

#### **Prérequis**

1. Acheter un nom de domaine (OVH, Namecheap, etc.)
2. Transférer la gestion DNS vers Route 53 (recommandé mais pas obligatoire)

---

#### **A. Demander un certificat SSL**

**1. Console AWS -> Certificate Manager**

**[ATTENTION] IMPORTANT : Région US East (N. Virginia) us-east-1 !**

CloudFront ne fonctionne qu'avec des certificats dans `us-east-1`.

---

**2. Request a certificate**

```
Certificate type : Request a public certificate

Domain names :
  www.mon-site.com
  mon-site.com

Validation method :
  [x] DNS validation (recommended)

Key algorithm : RSA 2048
```

---

**3. Request**

**AWS génère des enregistrements DNS à ajouter.**

```
CNAME Name : _abc123...
CNAME Value : _xyz456...
```

---

**4. Ajouter ces CNAME dans ton DNS**

Si tu utilises OVH :

1. Panneau OVH -> Domaines -> mon-site.com -> Zone DNS
2. Ajouter un enregistrement CNAME
3. Copier Name et Value depuis AWS

---

**5. Attendre la validation (5-30 minutes)**

**Statut passe de "Pending validation" à "Issued"**

**[OK] Certificat SSL délivré !**

---

#### **B. Configurer CloudFront**

**1. Console CloudFront -> Ta distribution -> General -> Edit**

**Alternate domain names (CNAMEs) :**

```
www.mon-site.com
mon-site.com
```

---

**Custom SSL certificate :**

```
Sélectionner le certificat créé
```

---

**2. Save changes**

---

#### **C. Configurer le DNS**

**Ajouter des enregistrements DNS :**

**Dans Route 53 (ou ton fournisseur DNS) :**

```
Type : CNAME
Name : www
Value : d1234567890abc.cloudfront.net

Type : CNAME (ou ALIAS si Route 53)
Name : @ (racine)
Value : d1234567890abc.cloudfront.net
```

---

**Attendre la propagation DNS (5 minutes - 48 heures)**

---

**Tester :**

```
https://www.mon-site.com
https://mon-site.com
```

**[BRAVO] TON SITE EST MAINTENANT SUR TON PROPRE DOMAINE ! [BRAVO]**

---

### [OK] TESTS DE VALIDATION

**1. Site accessible via S3 Website Endpoint**

```
http://mon-site-web-s3-2024.s3-website.eu-west-3.amazonaws.com
```

- [ ] Site s'affiche
- [ ] Navigation fonctionne

---

**2. Site accessible via CloudFront (HTTPS)**

```
https://d1234567890abc.cloudfront.net
```

- [ ] HTTPS fonctionne (cadenas vert)
- [ ] Site s'affiche
- [ ] Performance rapide

---

**3. Bucket Policy correcte**

- [ ] Accès public autorisé
- [ ] Policy `s3:GetObject` présente

---

**4. CloudFront configuré**

- [ ] Origine = S3 Website Endpoint
- [ ] Redirect HTTP to HTTPS activé
- [ ] Default root object = index.html

---

**5. Performance mondiale**

Tester avec WebPageTest depuis plusieurs régions :

- [ ] Temps de chargement < 500 ms partout

---

**6. Coûts sous contrôle**

**Estimation mensuelle :**

```
S3 Storage (1 GB) : 0,023 $
S3 Requests (10 000) : 0,004 $
CloudFront Data Transfer (10 GB) : 0,85 $
CloudFront Requests (100 000) : 0,01 $

Total : ~0,90 $/mois
```

**Tier gratuit AWS :**
- S3 : 5 GB gratuits pendant 12 mois
- CloudFront : 50 GB sortant gratuits pendant 12 mois
- CloudFront : 2 millions de requêtes gratuites

**-> GRATUIT pendant 12 mois pour un site peu visité !**

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : 403 Forbidden sur S3

**Symptôme :**

```
<Error>
  <Code>AccessDenied</Code>
  <Message>Access Denied</Message>
</Error>
```

**Causes :**

**1. Bucket Policy manquante ou incorrecte**

Vérifier la policy dans l'onglet "Permissions".

**2. Block Public Access activé**

Onglet "Permissions" -> "Block public access" -> Tout décoché ?

**3. Faute de frappe dans l'ARN**

```json
"Resource": "arn:aws:s3:::mon-bucket-EXACT/*"
```

Le nom doit correspondre EXACTEMENT.

---

#### Erreur 2 : Erreur 404 sur CloudFront

**Symptôme :**

Page d'erreur CloudFront au lieu du site.

**Causes :**

**1. Origine CloudFront incorrecte**

Doit être : `bucket.s3-website.region.amazonaws.com`
Pas : `bucket.s3.amazonaws.com` (API REST)

**Solution :**

Éditer la distribution -> Origine -> Utiliser le Website Endpoint.

---

**2. Default root object manquant**

Distribution settings -> Default root object = `index.html`

---

**3. Cache CloudFront**

Créer une invalidation : `/*`

---

#### Erreur 3 : CSS/JS ne chargent pas

**Symptôme :**

Site s'affiche mais sans style (blanc avec texte brut).

**Causes :**

**1. Chemins relatifs incorrects**

Dans `index.html` :

```html
[OK] <link rel="stylesheet" href="css/style.css">
[X] <link rel="stylesheet" href="/css/style.css">
```

Le `/` au début cherche à la racine du domaine.

---

**2. Fichiers CSS/JS non uploadés**

Vérifier dans S3 que le dossier `css/` existe avec `style.css` dedans.

---

**3. Content-Type incorrect**

S3 doit servir `style.css` avec `Content-Type: text/css`.

Par défaut, AWS détecte automatiquement, mais vérifier :

S3 -> Sélectionner `style.css` -> Actions -> Edit metadata

```
Content-Type : text/css
```

---

#### Erreur 4 : CloudFront ne met pas à jour

**Symptôme :**

Tu as uploadé un nouveau fichier sur S3, mais CloudFront montre toujours l'ancien.

**Cause : Cache CloudFront**

**Solution : Invalidation**

CloudFront -> Distribution -> Invalidations -> Create invalidation

```
Object paths : /*
```

---

#### Erreur 5 : Certificat SSL invalide

**Symptôme :**

```
NET::ERR_CERT_COMMON_NAME_INVALID
```

**Causes :**

**1. Certificat dans la mauvaise région**

Le certificat DOIT être dans `us-east-1` pour CloudFront.

---

**2. CNAME pas dans le certificat**

Si tu veux utiliser `www.mon-site.com`, le certificat doit l'inclure.

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. S3 Static Website**
- Hébergement ultra-économique
- Pas de gestion de serveur
- Scaling automatique illimité

**2. Bucket Policy**
- Nécessaire pour accès public
- Format JSON
- `s3:GetObject` pour lecture

**3. CloudFront CDN**
- 400+ Edge Locations
- HTTPS gratuit
- Cache configurable
- Invalidations pour forcer mise à jour

**4. Workflow de mise à jour**
1. Modifier fichiers localement
2. Upload sur S3
3. Invalidation CloudFront

**5. Coûts**
- S3 : ~0,023 $/GB/mois
- CloudFront : 0,085 $/GB sortant
- Certificat SSL : Gratuit
- Total site basique : < 1 $/mois

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. AWS CLI pour upload automatisé**

```bash
# Installer AWS CLI
pip install awscli

# Configurer
aws configure

# Sync local -> S3
aws s3 sync ./mon-site-statique s3://mon-bucket/ --delete

# Invalidation CloudFront
aws cloudfront create-invalidation --distribution-id E1234 --paths "/*"
```

---

**2. CI/CD avec GitHub Actions**

```yaml
# .github/workflows/deploy.yml
name: Deploy to S3

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Configure AWS
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-west-3
      - name: Deploy to S3
        run: aws s3 sync . s3://mon-bucket/ --delete
      - name: Invalidate CloudFront
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CF_ID }} --paths "/*"
```

**Résultat : Chaque push sur GitHub déploie automatiquement !**

---

**3. Logs CloudFront**

**Activer les logs :**

Distribution -> Edit -> Standard logging -> Enable

**Les logs seront stockés dans un bucket S3.**

**Analyse avec AWS Athena :**

Requêtes SQL sur les logs pour :
- Pages les plus visitées
- Pays des visiteurs
- User-agents (navigateurs)
- Erreurs 404

---

**4. Lambda@Edge**

**Exécuter du code à la bordure (edge) pour :**
- Réécriture d'URL
- Authentification
- A/B testing
- Personnalisation

---

**5. S3 + CloudFront + Route 53 + ACM = Stack parfait**

```
Route 53 (DNS)
    v
CloudFront (CDN + HTTPS)
    v
S3 (Storage)
```

**Avantages :**
- HTTPS automatique
- Performance mondiale
- Coût ultra-bas
- Zero maintenance

---

## [COURS] CONCLUSION DE L'EXERCICE 2

**[BRAVO] Félicitations ! Tu as déployé un site statique sur S3 + CloudFront ! [BRAVO]**

**Ce que tu as appris :**
- Créer et configurer un bucket S3
- Activer l'hébergement web statique
- Configurer les permissions publiques (Bucket Policy)
- Créer une distribution CloudFront
- Configurer HTTPS avec certificat SSL
- Invalider le cache CloudFront
- Optimiser les coûts

**Compétences acquises :**
- [OK] Amazon S3 (niveau intermédiaire)
- [OK] CloudFront CDN
- [OK] Bucket Policies (JSON)
- [OK] Certificate Manager
- [OK] Performance web mondiale

**Comparaison avec EC2 :**

| Critère | S3 + CloudFront | EC2 |
|---------|-----------------|-----|
| Coût | ~0,90 $/mois | ~8 $/mois |
| Maintenance | Zéro | Oui |
| Performance | ***** | **** |
| Scaling | Automatique | Manuel |

**Pour sites statiques : S3 + CloudFront est LE choix !**

**Temps moyen de réalisation :** 2-3 heures

**Prochaine étape :** Exercice 3 - RDS + Flask (application web dynamique avec base de données) ! [POSTGRES]

---

Veux-tu que je continue avec l'exercice 3 (RDS + Flask) avec le même niveau de détail ? [RAPIDE]


# [JAUNE] EXERCICE 3 : APPLICATION FLASK + RDS (BASE DE DONNÉES)

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur full-stack dans une startup. L'équipe veut créer une application de gestion de tâches (TODO list) accessible en ligne. Le site doit permettre de créer, lire, mettre à jour et supprimer des tâches, avec stockage persistant dans une base de données.

### Cahier des charges

Le client souhaite :
- Application web CRUD complète (Create, Read, Update, Delete)
- Base de données relationnelle managée
- Sécurité des données (chiffrement, backups)
- Scalabilité (possibilité de monter en charge)
- Interface web responsive

### Contraintes techniques

- Framework : Flask (Python)
- Base de données : RDS PostgreSQL
- Déploiement : EC2
- ORM : SQLAlchemy
- Architecture : Séparation app/database
- Temps estimé : 3-4 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Créer une instance RDS (base de données managée)
- [OK] Configurer les Security Groups pour RDS
- [OK] Développer une application Flask avec SQLAlchemy
- [OK] Connecter Flask à RDS via PostgreSQL
- [OK] Gérer les migrations de base de données
- [OK] Déployer Flask sur EC2
- [OK] Sécuriser les credentials (variables d'environnement)
- [OK] Comprendre RDS vs base locale

---

## [DOCS] PRÉREQUIS

- Exercice 1 terminé (EC2)
- Connaissances Python de base
- Notions SQL
- Compréhension des bases de données relationnelles

---

## [IDEE] CONCEPTS AWS À COMPRENDRE

### Qu'est-ce qu'Amazon RDS ?

**RDS** = Relational Database Service

**Analogie :**
- Base de données traditionnelle = Tu installes MySQL sur un serveur, tu le configures, tu le maintiens
- RDS = AWS gère tout ça pour toi (installation, backups, patches)

**Moteurs de bases de données supportés :**
- MySQL
- PostgreSQL
- MariaDB
- Oracle
- Microsoft SQL Server
- Amazon Aurora (compatible MySQL/PostgreSQL)

---

### Avantages de RDS vs base locale

| Critère | RDS | Base locale (EC2) |
|---------|-----|-------------------|
| **Installation** | Automatique | Manuelle |
| **Backups** | Automatiques | À configurer |
| **Haute dispo** | Multi-AZ facile | Complex |
| **Patches** | Automatiques | Manuels |
| **Scaling** | Clic ou API | Arrêt + resize |
| **Monitoring** | Intégré | À installer |
| **Coût** | Plus cher | Moins cher |

**RDS coûte plus cher mais économise ÉNORMÉMENT de temps.**

---

### Anatomie d'une instance RDS

**Instance RDS** = Serveur de base de données managé

**Composants :**

```
Instance RDS
├── Moteur (PostgreSQL, MySQL, etc.)
├── Classe d'instance (db.t3.micro, db.m5.large, etc.)
├── Stockage (gp3, io1, magnetic)
├── VPC et Security Groups
├── Parameter Group (configuration)
└── Option Group (plugins)
```

---

### Classes d'instances RDS

Similaires aux instances EC2 :

| Classe | Usage | Exemple | Free Tier ? |
|--------|-------|---------|-------------|
| **db.t3/t4g** | Usage général | db.t3.micro | [OK] Oui |
| **db.m5** | Équilibré | db.m5.large | [X] Non |
| **db.r5** | RAM intensive | db.r5.xlarge | [X] Non |
| **db.x1e** | Mémoire extrême | db.x1e.32xlarge | [X] Non |

**Pour ce tuto : db.t3.micro (Free Tier)**

---

### Multi-AZ vs Read Replicas

#### **Multi-AZ (Haute Disponibilité)**

```
Zone A (Primary)     Zone B (Standby)
    [RDS] --------sync-------> [RDS]
                                  ^
                            (Failover automatique)
```

**Si la primary tombe, bascule automatique vers standby.**

**Coût : ~2x le prix**

---

#### **Read Replicas (Lecture seule)**

```
    Primary (Write)
        v
    Réplication
        v
┌───────┼───────┐
Read1  Read2  Read3
```

**Répartir les lectures sur plusieurs instances.**

**Cas d'usage :** Applications avec beaucoup plus de lectures que d'écritures.

---

### PostgreSQL vs MySQL

| Critère | PostgreSQL | MySQL |
|---------|------------|-------|
| **Conformité SQL** | Très stricte | Flexible |
| **Types de données** | Très riches (JSON, Array) | Standard |
| **Performance** | Complexe élevée | Lecture simple |
| **Open Source** | 100% | Partiellement |
| **Popularité** | #4 | #2 |

**Pour ce tuto : PostgreSQL** (plus moderne, types avancés)

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Créer une instance RDS PostgreSQL

**1. Console AWS -> Chercher "RDS"**

**2. Dans le menu de gauche : "Databases"**

**3. Cliquer sur "Create database"**

---

#### **Choose a database creation method**

```
[x] Standard create
[ ] Easy create
```

**Standard = Plus d'options, plus de contrôle**

---

#### **Engine options**

**Engine type :**

```
[x] PostgreSQL
```

**Version :**

```
PostgreSQL 15.5-R2 (ou la dernière version)
```

**[IDEE] Pourquoi PostgreSQL 15+ ?**
- Améliorations performance
- Nouveaux types de données
- Meilleur support JSON

---

#### **Templates**

```
[x] Free tier
```

**Cela pré-sélectionne les options Free Tier (db.t3.micro, 20 GB gp3, Single-AZ).**

---

#### **Settings**

**DB instance identifier :**

```
flask-todo-db
```

**Credentials settings :**

```
Master username : postgres (par défaut)

[x] Auto generate a password
```

**Ou définir manuellement :**

```
Master password : MonMotDePasseRDS2024!
Confirm password : MonMotDePasseRDS2024!
```

**[ATTENTION] IMPORTANT : Note bien ce mot de passe !**

---

#### **Instance configuration**

**DB instance class :**

```
[x] Burstable classes (includes t classes)
db.t3.micro (2 vCPUs, 1 GiB RAM) - Free tier eligible
```

---

#### **Storage**

```
Storage type : General Purpose SSD (gp3)
Allocated storage : 20 GiB (minimum Free Tier)

[ ] Enable storage autoscaling
```

**Explication :**

**Storage autoscaling** = Augmente automatiquement l'espace si plein

**Désactivé pour contrôler les coûts.**

---

#### **Connectivity**

**Compute resource :**

```
[x] Don't connect to an EC2 compute resource
```

**On connectera manuellement plus tard.**

---

**Virtual private cloud (VPC) :**

```
Default VPC
```

---

**DB subnet group :**

```
default
```

---

**Public access :**

```
[ ] No (recommandé)
```

**[ATTENTION] TRÈS IMPORTANT pour la sécurité !**

**Explication :**

**Public access = Yes** :
- Instance accessible depuis Internet
- IP publique attribuée
- [X] Risque de sécurité MAJEUR

**Public access = No** :
- Accessible seulement depuis le VPC
- Pas d'IP publique
- [OK] Sécurisé
- Notre EC2 (dans le même VPC) pourra se connecter

---

**VPC security group :**

```
[x] Create new

New VPC security group name : rds-flask-sg
```

**On configurera les règles après.**

---

**Availability Zone :**

```
No preference
```

---

#### **Database authentication**

```
[x] Password authentication
```

**Autres options (avancées) :**
- IAM database authentication
- Kerberos authentication

---

#### **Monitoring**

```
[x] Enable Enhanced monitoring (optionnel)

Granularity : 60 seconds
```

**Désactiver pour économiser (hors Free Tier).**

---

#### **Additional configuration**

**Initial database name :**

```
todo_db
```

**[ATTENTION] IMPORTANT : Créer la base de données initiale**

Si tu laisses vide, tu devras créer la base manuellement après.

---

**DB parameter group :**

```
default.postgres15
```

**Parameter group** = Configuration PostgreSQL (max_connections, shared_buffers, etc.)

---

**Backup :**

```
[x] Enable automated backups

Backup retention period : 7 days
Backup window : No preference
```

**Free Tier inclut 20 GB de backups.**

---

**Encryption :**

```
[x] Enable encryption

AWS KMS key : (default) aws/rds
```

**Chiffrement au repos gratuit.**

---

**Log exports :**

```
[ ] PostgreSQL log (optionnel, hors Free Tier)
```

---

**Maintenance :**

```
[x] Enable auto minor version upgrade

Maintenance window : No preference
```

**PostgreSQL 15.5 -> 15.6 automatiquement.**

---

**Deletion protection :**

```
[ ] Enable deletion protection
```

**En production : TOUJOURS activer !**

Pour ce tuto : Désactivé (on pourra supprimer facilement).

---

**4. Estimer les coûts (en bas de page) :**

```
Estimated monthly costs:
Free tier: $0.00 for 750 hours
```

**[OK] Gratuit dans le Free Tier !**

---

**5. Cliquer sur "Create database"**

**AWS crée l'instance... [HOURGLASS_WITH_FLOWING_SAND]**

**Durée : 5-10 minutes**

```
Status : Creating
```

**Attendre que ça passe à :**

```
Status : Available
```

---

**6. Récupérer le mot de passe auto-généré (si tu as choisi cette option)**

**Pendant que l'instance est en "Creating", cliquer sur "View credential details"**

**Copier et sauvegarder :**

```
Master username : postgres
Master password : aBcD1234eFgH5678...
```

**[ATTENTION] TU NE POURRAS PLUS LE VOIR APRÈS !**

Si tu l'oublies, tu devras le réinitialiser (Modify database -> New password).

---

### ÉTAPE 2 : Configurer le Security Group RDS

**Notre EC2 doit pouvoir se connecter à RDS.**

**1. Console EC2 -> Security Groups (menu de gauche)**

**2. Chercher le Security Group RDS (créé automatiquement) :**

```
rds-flask-sg
```

**3. Sélectionner -> Onglet "Inbound rules" -> Edit inbound rules**

---

**Par défaut, il n'y a AUCUNE règle inbound.**

**Ajouter une règle :**

```
Type : PostgreSQL
Protocol : TCP
Port range : 5432
Source : Custom

[ATTENTION] Ici, on a 2 options :
```

---

#### **Option 1 : Autoriser le Security Group de l'EC2 (RECOMMANDÉ)**

```
Source : sg-xxxxx (Security Group de ton EC2)
```

**Comment trouver le SG de ton EC2 ?**

Console EC2 -> Instances -> Ton instance -> Onglet Security -> Security groups

Exemple : `sg-0abc123def456789`

**Copier cet ID.**

**Dans la règle RDS :**

```
Source : sg-0abc123def456789
Description : Allow from EC2
```

**Avantage :**

N'importe quelle instance avec ce SG peut se connecter.

Si l'IP de l'EC2 change, ça fonctionne toujours.

---

#### **Option 2 : Autoriser l'IP de l'EC2 (moins flexible)**

```
Source : 172.31.12.34/32 (IP privée de l'EC2)
```

**Problème :**

Si tu redémarres l'EC2, l'IP privée peut changer.

---

**Choisir l'option 1 (Security Group).**

**Cliquer sur "Save rules"**

---

**Résumé de la configuration :**

```
RDS Security Group (rds-flask-sg) :
Inbound :
  PostgreSQL (5432) depuis EC2 Security Group [OK]

EC2 ne peut PAS accéder à RDS [X]
RDS attend les connexions sur le port 5432 [OK]
EC2 (avec le bon SG) peut se connecter [OK]
```

---

### ÉTAPE 3 : Récupérer l'endpoint RDS

**L'endpoint = URL de connexion à la base de données**

**Console RDS -> Databases -> flask-todo-db**

**Dans l'onglet "Connectivity & security" :**

```
Endpoint : flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com
Port : 5432
```

**Copier cet endpoint !**

**Format de connexion :**

```
postgresql://postgres:MOT_DE_PASSE@ENDPOINT:5432/todo_db
```

**Exemple concret :**

```
postgresql://postgres:MonMotDePasseRDS2024!@flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com:5432/todo_db
```

---

### ÉTAPE 4 : Créer l'instance EC2 pour Flask

**On va créer une nouvelle EC2 (ou réutiliser celle de l'exercice 1).**

**Si tu réutilises l'EC2 de l'exercice 1 :**

Sauter directement à l'ÉTAPE 5.

---

**Si tu crées une nouvelle EC2 :**

**Configuration similaire à l'exercice 1 :**

```
Nom : flask-app-server
AMI : Ubuntu Server 22.04 LTS
Type : t2.micro
Key pair : (ta clé SSH existante)
VPC : Default
Subnet : No preference
Auto-assign public IP : Enable

Security Group : flask-sg
Règles inbound :
  - SSH (22) : Mon IP
  - HTTP (80) : 0.0.0.0/0
  - Custom TCP (5000) : 0.0.0.0/0 (pour Flask en dev)

Disque : gp3, 8 GiB
```

**Lancer l'instance.**

**Attendre qu'elle soit "Running".**

---

**[ATTENTION] IMPORTANT : Vérifier que le Security Group de l'EC2 est autorisé dans RDS !**

Si tu as créé une nouvelle EC2 avec un nouveau SG :

Retourner dans le Security Group RDS -> Ajouter le nouveau SG EC2.

---

### ÉTAPE 5 : Se connecter à l'EC2 et installer Python

**Se connecter en SSH :**

```bash
ssh -i ~/.ssh/ma-cle-ec2.pem ubuntu@<IP-EC2>
```

---

**Mettre à jour le système :**

```bash
sudo apt update && sudo apt upgrade -y
```

---

**Installer Python et pip :**

```bash
# Python 3 est déjà installé sur Ubuntu 22.04
python3 --version
# Python 3.10.12

# Installer pip
sudo apt install python3-pip python3-venv -y

# Vérifier
pip3 --version
```

---

**Installer PostgreSQL client (psql) :**

```bash
sudo apt install postgresql-client -y
```

**On va l'utiliser pour tester la connexion à RDS.**

---

### ÉTAPE 6 : Tester la connexion à RDS

**Avant de développer l'app, vérifions qu'on peut se connecter à RDS.**

```bash
psql -h flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com \
     -U postgres \
     -d todo_db
```

**Décomposition de la commande :**

**`psql`** = Client PostgreSQL

**`-h HOSTNAME`** = Host (endpoint RDS)

**`-U USERNAME`** = Utilisateur (`postgres`)

**`-d DATABASE`** = Nom de la base (`todo_db`)

---

**Il demande le mot de passe :**

```
Password for user postgres:
```

**Entrer le mot de passe RDS.**

---

**Résultat (si succès) :**

```
psql (14.10 (Ubuntu 14.10-0ubuntu0.22.04.1), server 15.5)
WARNING: psql major version 14, server major version 15.
         Some psql features might not work.
Type "help" for help.

todo_db=>
```

**[OK] CONNEXION RÉUSSIE À RDS ! [BRAVO]**

---

**Tester quelques commandes SQL :**

```sql
-- Lister les tables (vide pour l'instant)
\dt

-- Créer une table de test
CREATE TABLE test (id SERIAL PRIMARY KEY, name VARCHAR(50));

-- Insérer des données
INSERT INTO test (name) VALUES ('Hello RDS!');

-- Sélectionner
SELECT * FROM test;
```

**Résultat :**

```
 id |    name    
----+------------
  1 | Hello RDS!
(1 row)
```

---

**Supprimer la table de test :**

```sql
DROP TABLE test;
```

---

**Quitter psql :**

```sql
\q
```

**Ou `Ctrl + D`**

---

**Si la connexion échoue :**

**Erreur courante :**

```
psql: error: connection to server at "flask-todo-db..." failed: 
Connection timed out
```

**Vérifications :**

**1. Security Group RDS autorise-t-il l'EC2 ?**

RDS -> Security group -> Inbound -> Port 5432 depuis EC2 SG ?

---

**2. Endpoint correct ?**

Vérifier dans RDS -> Connectivity & security -> Endpoint

---

**3. VPC et subnets identiques ?**

EC2 et RDS doivent être dans le même VPC (ou VPC peering configuré).

Par défaut, ils sont dans le Default VPC -> OK.

---

### ÉTAPE 7 : Développer l'application Flask

**Créer un dossier pour l'application :**

```bash
mkdir ~/flask-todo-app
cd ~/flask-todo-app
```

---

**Créer un environnement virtuel Python :**

```bash
python3 -m venv venv
```

**Explication :**

**Environnement virtuel** = Isolation des dépendances Python

Sans environnement virtuel :
```
pip install flask -> Installe globalement
Risque de conflits entre projets
```

Avec environnement virtuel :
```
pip install flask -> Installe dans venv/
Chaque projet a ses propres versions
```

---

**Activer l'environnement virtuel :**

```bash
source venv/bin/activate
```

**Le prompt change :**

```
(venv) ubuntu@ip-172-31-12-34:~/flask-todo-app$
```

**`(venv)` indique que l'environnement est activé.**

---

**Installer les dépendances :**

```bash
pip install Flask Flask-SQLAlchemy psycopg2-binary python-dotenv
```

**Explication des packages :**

**Flask** = Framework web minimaliste

**Flask-SQLAlchemy** = ORM (Object-Relational Mapping)
- Abstraction de la base de données
- Manipuler des objets Python au lieu de SQL

**psycopg2-binary** = Driver PostgreSQL pour Python
- Permet la connexion à PostgreSQL

**python-dotenv** = Gestion des variables d'environnement
- Stocker les credentials en sécurité

---

**Créer un fichier `.env` pour les secrets :**

```bash
nano .env
```

**Contenu :**

```bash
# Configuration de la base de données RDS
DATABASE_URL=postgresql://postgres:MonMotDePasseRDS2024!@flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com:5432/todo_db

# Clé secrète Flask (pour les sessions)
SECRET_KEY=ma-cle-secrete-super-longue-et-aleatoire-123456789

# Environnement
FLASK_ENV=development
```

**[ATTENTION] REMPLACER :**
- Le mot de passe RDS
- L'endpoint RDS
- Générer une vraie clé secrète

**Pour générer une clé secrète aléatoire :**

```bash
python3 -c "import secrets; print(secrets.token_hex(32))"
```

**Résultat :**

```
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6...
```

**Copier dans `SECRET_KEY`.**

---

**[ATTENTION] SÉCURITÉ : Ne JAMAIS committer `.env` dans Git !**

```bash
echo ".env" >> .gitignore
echo "venv/" >> .gitignore
```

---

**Créer `app.py` (application principale) :**

```bash
nano app.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# APPLICATION FLASK TODO LIST + RDS
# ═══════════════════════════════════════════════════════════════

from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import os
from dotenv import load_dotenv

# Charger les variables d'environnement depuis .env
load_dotenv()

"""
load_dotenv() lit le fichier .env et charge les variables
dans os.environ

Exemple :
.env contient : DATABASE_URL=postgresql://...
Après load_dotenv() : os.environ['DATABASE_URL'] est disponible
"""

# ───────────────────────────────────────────────────────────────
# CONFIGURATION DE L'APPLICATION
# ───────────────────────────────────────────────────────────────

app = Flask(__name__)

# Configuration de la base de données
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')

"""
os.environ.get('DATABASE_URL') récupère la valeur depuis .env

Pourquoi ne pas mettre directement :
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://...'

Parce que :
1. Sécurité : Le mot de passe n'est pas dans le code
2. Flexibilité : Différentes bases en dev/prod
3. Best practice : Configuration externe
"""

# Désactiver le tracking des modifications (performance)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

"""
SQLALCHEMY_TRACK_MODIFICATIONS :
Si True : SQLAlchemy track chaque modification d'objet
-> Consomme de la mémoire
-> Utile seulement pour des cas avancés

Recommandation : False (sauf besoin spécifique)
"""

# Clé secrète pour les sessions et CSRF
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')

"""
SECRET_KEY :
Utilisée pour :
- Signer les cookies de session (empêche la falsification)
- Protection CSRF (Cross-Site Request Forgery)
- flash() messages

[ATTENTION] DOIT être secret et aléatoire !
Si quelqu'un connaît ta SECRET_KEY :
-> Il peut forger des sessions
-> Il peut usurper l'identité des utilisateurs
"""

# Initialiser SQLAlchemy
db = SQLAlchemy(app)

# ───────────────────────────────────────────────────────────────
# MODÈLES DE BASE DE DONNÉES
# ───────────────────────────────────────────────────────────────

class Task(db.Model):
    """
    Modèle de tâche
    
    SQLAlchemy crée automatiquement la table 'task' avec :
    - id (clé primaire auto-incrémentée)
    - title (texte, obligatoire)
    - description (texte, optionnel)
    - completed (booléen, défaut False)
    - created_at (datetime, défaut maintenant)
    """
    
    __tablename__ = 'task'
    
    """
    __tablename__ : Nom de la table en base
    
    Par défaut, SQLAlchemy utilise le nom de la classe en minuscule
    Task -> table 'task'
    
    On peut forcer un nom différent :
    __tablename__ = 'my_tasks'
    """
    
    id = db.Column(db.Integer, primary_key=True)
    
    """
    db.Column() définit une colonne
    
    db.Integer : Type entier (INT en SQL)
    primary_key=True : Clé primaire (identifiant unique)
    
    Équivalent SQL :
    CREATE TABLE task (
        id INTEGER PRIMARY KEY
    );
    
    PostgreSQL ajoute automatiquement AUTO_INCREMENT (SERIAL)
    """
    
    title = db.Column(db.String(200), nullable=False)
    
    """
    db.String(200) : VARCHAR(200) en SQL
    nullable=False : NOT NULL (obligatoire)
    
    Équivalent SQL :
    title VARCHAR(200) NOT NULL
    """
    
    description = db.Column(db.Text, nullable=True)
    
    """
    db.Text : Type TEXT en SQL (texte illimité)
    nullable=True : Peut être NULL (optionnel)
    
    db.Text vs db.String :
    - String : Longueur limitée, indexable
    - Text : Illimité, moins performant pour index
    """
    
    completed = db.Column(db.Boolean, default=False, nullable=False)
    
    """
    db.Boolean : Type BOOLEAN en SQL
    default=False : Valeur par défaut
    
    Lors de la création :
    task = Task(title='...')
    task.completed sera automatiquement False
    """
    
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    
    """
    db.DateTime : Type TIMESTAMP en SQL
    default=datetime.utcnow : Fonction appelée lors de la création
    
    [ATTENTION] IMPORTANT : datetime.utcnow SANS parenthèses !
    
    Correct : default=datetime.utcnow
    Incorrect : default=datetime.utcnow()
    
    Pourquoi ?
    datetime.utcnow = Référence à la fonction
    -> Appelée lors de CHAQUE création
    
    datetime.utcnow() = Résultat de la fonction MAINTENANT
    -> Même timestamp pour toutes les tâches
    """
    
    def __repr__(self):
        """
        Représentation string de l'objet
        Utile pour le debug
        """
        return f'<Task {self.id}: {self.title}>'

# ───────────────────────────────────────────────────────────────
# ROUTES (ENDPOINTS)
# ───────────────────────────────────────────────────────────────

@app.route('/')
def index():
    """
    Page d'accueil : Liste de toutes les tâches
    
    Route : GET /
    """
    
    # Récupérer toutes les tâches depuis la base
    tasks = Task.query.all()
    
    """
    Task.query : Builder de requête SQLAlchemy
    .all() : Retourne TOUTES les lignes
    
    Équivalent SQL :
    SELECT * FROM task;
    
    Autres méthodes :
    .first() : Première ligne ou None
    .get(id) : Ligne avec cet ID ou None
    .filter_by(completed=True).all() : Lignes avec WHERE
    .order_by(Task.created_at.desc()).all() : Tri
    """
    
    return render_template('index.html', tasks=tasks)
    
    """
    render_template() :
    1. Charge le template templates/index.html
    2. Remplace les variables {{ tasks }} par la valeur
    3. Retourne le HTML généré
    """

@app.route('/add', methods=['GET', 'POST'])
def add_task():
    """
    Ajouter une tâche
    
    GET /add : Affiche le formulaire
    POST /add : Traite le formulaire
    """
    
    if request.method == 'POST':
        """
        request.method : Méthode HTTP (GET, POST, etc.)
        
        Formulaire HTML :
        <form method="POST" action="/add">
        -> request.method == 'POST'
        """
        
        # Récupérer les données du formulaire
        title = request.form.get('title')
        description = request.form.get('description')
        
        """
        request.form : Dictionnaire des données POST
        
        HTML :
        <input name="title" value="Acheter du pain">
        
        Python :
        request.form.get('title') -> "Acheter du pain"
        
        .get() vs [] :
        request.form.get('title') -> None si absent
        request.form['title'] -> Erreur si absent
        
        Recommandation : .get() (plus sûr)
        """
        
        # Validation
        if not title:
            flash('Le titre est obligatoire !', 'error')
            return redirect(url_for('add_task'))
        
        """
        flash() : Stocke un message dans la session
        Affiché dans le template avec get_flashed_messages()
        
        'error' : Catégorie (pour le style CSS)
        Autres : 'success', 'warning', 'info'
        """
        
        # Créer une nouvelle tâche
        new_task = Task(
            title=title,
            description=description
        )
        
        """
        Task(...) crée un objet Python
        
        L'objet existe en mémoire
        PAS encore dans la base de données
        """
        
        # Ajouter à la session SQLAlchemy
        db.session.add(new_task)
        
        """
        db.session : Session SQLAlchemy (transaction)
        
        .add(obj) : Marque l'objet pour insertion
        L'objet est en "pending" (en attente)
        Pas encore dans la base
        """
        
        # Enregistrer dans la base de données
        db.session.commit()
        
        """
        .commit() : Exécute la transaction
        
        Envoie la requête SQL :
        INSERT INTO task (title, description, completed, created_at)
        VALUES ('...', '...', FALSE, NOW());
        
        Si une erreur survient AVANT commit :
        -> Rien n'est enregistré (transaction annulée)
        
        Après commit :
        -> new_task.id est automatiquement rempli avec l'ID généré
        """
        
        flash('Tâche ajoutée avec succès !', 'success')
        return redirect(url_for('index'))
        
        """
        redirect() : Redirection HTTP 302
        url_for('index') : Génère l'URL de la route 'index'
        
        Pourquoi url_for() au lieu de redirect('/') ?
        
        url_for('index') -> Calcule l'URL depuis le nom de route
        Si tu changes @app.route('/') en @app.route('/home')
        -> url_for('index') fonctionne toujours
        
        redirect('/') -> URL hardcodée
        -> Casse si tu changes la route
        """
    
    return render_template('add_task.html')

@app.route('/edit/<int:task_id>', methods=['GET', 'POST'])
def edit_task(task_id):
    """
    Modifier une tâche
    
    <int:task_id> : Paramètre d'URL (entier)
    /edit/5 -> task_id = 5
    """
    
    # Récupérer la tâche
    task = Task.query.get_or_404(task_id)
    
    """
    .get_or_404(id) :
    - Si la tâche existe : Retourne l'objet
    - Si elle n'existe pas : Erreur 404 automatique
    
    Équivalent SQL :
    SELECT * FROM task WHERE id = 5;
    
    Alternative :
    task = Task.query.get(task_id)
    if not task:
        abort(404)
    """
    
    if request.method == 'POST':
        # Mettre à jour les champs
        task.title = request.form.get('title')
        task.description = request.form.get('description')
        task.completed = 'completed' in request.form
        
        """
        'completed' in request.form :
        
        Checkbox HTML :
        <input type="checkbox" name="completed">
        
        Si cochée : request.form contient 'completed'
        Si non cochée : request.form NE CONTIENT PAS 'completed'
        
        Donc :
        'completed' in request.form -> True si cochée
        """
        
        # Enregistrer
        db.session.commit()
        
        """
        Pas besoin de .add() car l'objet est déjà dans la session
        
        SQLAlchemy track les modifications automatiquement :
        task.title = '...' -> Marqué comme "dirty" (modifié)
        db.session.commit() -> UPDATE envoyé
        
        Équivalent SQL :
        UPDATE task SET title='...', description='...', completed=...
        WHERE id=5;
        """
        
        flash('Tâche modifiée avec succès !', 'success')
        return redirect(url_for('index'))
    
    return render_template('edit_task.html', task=task)

@app.route('/delete/<int:task_id>')
def delete_task(task_id):
    """
    Supprimer une tâche
    
    Route : GET /delete/5
    """
    
    task = Task.query.get_or_404(task_id)
    
    # Supprimer
    db.session.delete(task)
    db.session.commit()
    
    """
    .delete(obj) : Marque pour suppression
    .commit() : Exécute la suppression
    
    Équivalent SQL :
    DELETE FROM task WHERE id=5;
    """
    
    flash('Tâche supprimée avec succès !', 'success')
    return redirect(url_for('index'))

@app.route('/toggle/<int:task_id>')
def toggle_task(task_id):
    """
    Basculer le statut complété/non complété
    
    Route : GET /toggle/5
    """
    
    task = Task.query.get_or_404(task_id)
    task.completed = not task.completed
    db.session.commit()
    
    flash('Statut modifié !', 'success')
    return redirect(url_for('index'))

# ───────────────────────────────────────────────────────────────
# POINT D'ENTRÉE
# ───────────────────────────────────────────────────────────────

if __name__ == '__main__':
    """
    Ce code s'exécute SEULEMENT si le fichier est lancé directement :
    python app.py
    
    Pas si importé :
    from app import app (n'exécute pas le code ci-dessous)
    """
    
    # Créer les tables dans la base de données
    with app.app_context():
        db.create_all()
    
    """
    db.create_all() :
    Crée toutes les tables définies dans les modèles
    
    Équivalent SQL :
    CREATE TABLE IF NOT EXISTS task (
        id SERIAL PRIMARY KEY,
        title VARCHAR(200) NOT NULL,
        description TEXT,
        completed BOOLEAN DEFAULT FALSE NOT NULL,
        created_at TIMESTAMP DEFAULT NOW() NOT NULL
    );
    
    [ATTENTION] create_all() ne MODIFIE PAS les tables existantes
    Si tu changes un modèle, utiliser des migrations (Flask-Migrate)
    
    app.app_context() :
    Nécessaire pour accéder à db en dehors d'une requête
    """
    
    # Lancer le serveur Flask
    app.run(debug=True, host='0.0.0.0', port=5000)
    
    """
    debug=True :
    - Recharge automatiquement quand le code change
    - Affiche les erreurs détaillées dans le navigateur
    - [ATTENTION] NE JAMAIS utiliser en production (faille de sécurité)
    
    host='0.0.0.0' :
    - Écoute sur toutes les interfaces réseau
    - Accessible depuis l'extérieur (pas seulement localhost)
    - Nécessaire pour EC2 (sinon seulement accessible depuis le serveur)
    
    host='127.0.0.1' : Seulement en local
    host='0.0.0.0' : Depuis n'importe où
    
    port=5000 :
    - Port d'écoute
    - http://IP:5000/
    """

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde : `Ctrl + O`, `Entrée`, `Ctrl + X`**

---

**Créer le dossier templates :**

```bash
mkdir templates
```

---

**Créer `templates/base.html` (template de base) :**

```bash
nano templates/base.html
```

**Contenu :**

```html
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}TODO List{% endblock %}</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 2rem;
        }
        
        .container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            border-radius: 20px;
            padding: 2rem;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
        }
        
        h1 {
            color: #667eea;
            margin-bottom: 2rem;
            text-align: center;
        }
        
        .messages {
            margin-bottom: 1rem;
        }
        
        .alert {
            padding: 1rem;
            border-radius: 5px;
            margin-bottom: 1rem;
        }
        
        .alert-success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        
        .alert-error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        
        .btn {
            display: inline-block;
            padding: 0.5rem 1rem;
            border-radius: 5px;
            text-decoration: none;
            transition: all 0.3s ease;
            border: none;
            cursor: pointer;
            font-size: 1rem;
        }
        
        .btn-primary {
            background: #667eea;
            color: white;
        }
        
        .btn-primary:hover {
            background: #5568d3;
        }
        
        .btn-success {
            background: #28a745;
            color: white;
        }
        
        .btn-danger {
            background: #dc3545;
            color: white;
        }
        
        .btn-secondary {
            background: #6c757d;
            color: white;
        }
        
        .btn-sm {
            padding: 0.25rem 0.5rem;
            font-size: 0.875rem;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>[NOTE] TODO List - Flask + RDS</h1>
        
        <!-- Messages flash -->
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                <div class="messages">
                    {% for category, message in messages %}
                        <div class="alert alert-{{ category }}">
                            {{ message }}
                        </div>
                    {% endfor %}
                </div>
            {% endif %}
        {% endwith %}
        
        <!-- Contenu des pages enfants -->
        {% block content %}{% endblock %}
    </div>
</body>
</html>
```

---

**Créer `templates/index.html` :**

```bash
nano templates/index.html
```

**Contenu :**

```html
{% extends "base.html" %}

{% block content %}
    <div style="margin-bottom: 2rem; text-align: center;">
        <a href="{{ url_for('add_task') }}" class="btn btn-primary">
            + Ajouter une tâche
        </a>
    </div>
    
    {% if tasks %}
        <div class="task-list">
            {% for task in tasks %}
                <div class="task-item" style="background: {% if task.completed %}#d4edda{% else %}#f8f9fa{% endif %}; padding: 1rem; margin-bottom: 1rem; border-radius: 10px; border-left: 4px solid {% if task.completed %}#28a745{% else %}#667eea{% endif %};">
                    <div style="display: flex; justify-content: space-between; align-items: center;">
                        <div style="flex: 1;">
                            <h3 style="{% if task.completed %}text-decoration: line-through; color: #6c757d;{% endif %}">
                                {{ task.title }}
                            </h3>
                            {% if task.description %}
                                <p style="color: #666; margin-top: 0.5rem;">{{ task.description }}</p>
                            {% endif %}
                            <small style="color: #999;">
                                Créée le {{ task.created_at.strftime('%d/%m/%Y à %H:%M') }}
                            </small>
                        </div>
                        <div style="display: flex; gap: 0.5rem;">
                            <a href="{{ url_for('toggle_task', task_id=task.id) }}" 
                               class="btn btn-sm {% if task.completed %}btn-secondary{% else %}btn-success{% endif %}"
                               title="{% if task.completed %}Marquer comme non terminée{% else %}Marquer comme terminée{% endif %}">
                                {% if task.completed %}<-{% else %}[OK]{% endif %}
                            </a>
                            <a href="{{ url_for('edit_task', task_id=task.id) }}" 
                               class="btn btn-sm btn-primary"
                               title="Modifier">
                                [EDIT]
                            </a>
                            <a href="{{ url_for('delete_task', task_id=task.id) }}" 
                               class="btn btn-sm btn-danger"
                               title="Supprimer"
                               onclick="return confirm('Supprimer cette tâche ?');">
                                [SUPPRIMER]
                            </a>
                        </div>
                    </div>
                </div>
            {% endfor %}
        </div>
    {% else %}
        <div style="text-align: center; padding: 3rem; color: #999;">
            <p style="font-size: 3rem;">[EMAIL]</p>
            <p>Aucune tâche pour le moment. Ajoutez-en une !</p>
        </div>
    {% endif %}
{% endblock %}
```

---

**Créer `templates/add_task.html` :**

```bash
nano templates/add_task.html
```

**Contenu :**

```html
{% extends "base.html" %}

{% block title %}Ajouter une tâche{% endblock %}

{% block content %}
    <h2 style="color: #667eea; margin-bottom: 1.5rem;">+ Ajouter une tâche</h2>
    
    <form method="POST" action="{{ url_for('add_task') }}">
        <div style="margin-bottom: 1.5rem;">
            <label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
                Titre *
            </label>
            <input type="text" 
                   id="title" 
                   name="title" 
                   required
                   style="width: 100%; padding: 0.8rem; border: 2px solid #ddd; border-radius: 5px; font-size: 1rem;">
        </div>
        
        <div style="margin-bottom: 1.5rem;">
            <label for="description" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
                Description
            </label>
            <textarea id="description" 
                      name="description" 
                      rows="4"
                      style="width: 100%; padding: 0.8rem; border: 2px solid #ddd; border-radius: 5px; font-size: 1rem; font-family: inherit;"></textarea>
        </div>
        
        <div style="display: flex; gap: 1rem;">
            <button type="submit" class="btn btn-primary">
                [SAUVEGARDE] Enregistrer
            </button>
            <a href="{{ url_for('index') }}" class="btn btn-secondary">
                [X] Annuler
            </a>
        </div>
    </form>
{% endblock %}
```

---

**Créer `templates/edit_task.html` :**

```bash
nano templates/edit_task.html
```

**Contenu :**

```html
{% extends "base.html" %}

{% block title %}Modifier la tâche{% endblock %}

{% block content %}
    <h2 style="color: #667eea; margin-bottom: 1.5rem;">[EDIT] Modifier la tâche</h2>
    
    <form method="POST" action="{{ url_for('edit_task', task_id=task.id) }}">
        <div style="margin-bottom: 1.5rem;">
            <label for="title" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
                Titre *
            </label>
            <input type="text" 
                   id="title" 
                   name="title" 
                   value="{{ task.title }}"
                   required
                   style="width: 100%; padding: 0.8rem; border: 2px solid #ddd; border-radius: 5px; font-size: 1rem;">
        </div>
        
        <div style="margin-bottom: 1.5rem;">
            <label for="description" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
                Description
            </label>
            <textarea id="description" 
                      name="description" 
                      rows="4"
                      style="width: 100%; padding: 0.8rem; border: 2px solid #ddd; border-radius: 5px; font-size: 1rem; font-family: inherit;">{{ task.description }}</textarea>
        </div>
        
        <div style="margin-bottom: 1.5rem;">
            <label style="display: flex; align-items: center; cursor: pointer;">
                <input type="checkbox" 
                       name="completed" 
                       {% if task.completed %}checked{% endif %}
                       style="width: 20px; height: 20px; margin-right: 0.5rem;">
                <span>Tâche terminée</span>
            </label>
        </div>
        
        <div style="display: flex; gap: 1rem;">
            <button type="submit" class="btn btn-primary">
                [SAUVEGARDE] Enregistrer
            </button>
            <a href="{{ url_for('index') }}" class="btn btn-secondary">
                [X] Annuler
            </a>
        </div>
    </form>
{% endblock %}
```

---

**Structure finale :**

```
flask-todo-app/
├── venv/
├── .env
├── app.py
└── templates/
    ├── base.html
    ├── index.html
    ├── add_task.html
    └── edit_task.html
```

---

### ÉTAPE 8 : Lancer l'application Flask

**Dans le dossier `flask-todo-app` avec l'environnement virtuel activé :**

```bash
python app.py
```

**Résultat :**

```
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://172.31.12.34:5000
Press CTRL+C to quit
```

**[OK] Flask est lancé !**

---

**Accéder au site depuis ton navigateur :**

```
http://<IP-PUBLIQUE-EC2>:5000
```

**Exemple :**

```
http://35.180.123.45:5000
```

**[BRAVO] TON APPLICATION FLASK + RDS EST EN LIGNE ! [BRAVO]**

---

**Tester l'application :**

- [ ] Page d'accueil s'affiche (vide au début)
- [ ] Cliquer "Ajouter une tâche"
- [ ] Remplir le formulaire et enregistrer
- [ ] La tâche apparaît dans la liste
- [ ] Marquer comme terminée ([OK]) -> Fond vert, texte barré
- [ ] Modifier la tâche ([EDIT])
- [ ] Supprimer la tâche ([SUPPRIMER])

**[OK] CRUD complet fonctionnel !**

---

**Vérifier dans la base RDS :**

**Dans un autre terminal SSH (ou onglet tmux) :**

```bash
psql -h flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com \
     -U postgres \
     -d todo_db
```

```sql
-- Voir les tables créées
\dt

-- Résultat :
--           List of relations
--  Schema | Name | Type  |  Owner   
-- --------+------+-------+----------
--  public | task | table | postgres

-- Voir les données
SELECT * FROM task;

-- Résultat :
--  id |       title        |    description     | completed |        created_at
-- ----+--------------------+--------------------+-----------+---------------------------
--   1 | Acheter du pain    | Boulangerie du ... | f         | 2024-12-16 15:30:00.123
--   2 | Finir exercice AWS | Exercice 3 RDS     | t         | 2024-12-16 15:35:00.456

\q
```

**[OK] Les données sont bien dans RDS PostgreSQL ! [BRAVO]**

---

Veux-tu que je continue avec la suite de l'exercice 3 (déploiement en production avec Gunicorn, Nginx, etc.) et les exercices 4-10 ? [RAPIDE]

# [JAUNE] EXERCICE 3 : APPLICATION FLASK + RDS (SUITE)

## ÉTAPE 9 : Déployer en production avec Gunicorn

**Actuellement, on utilise le serveur de développement Flask.**

**Problème :**
- [X] Pas adapté à la production
- [X] Mono-thread (une requête à la fois)
- [X] Pas sécurisé
- [X] Pas performant

**Solution : Gunicorn (WSGI Server)**

---

### Qu'est-ce que Gunicorn ?

**Gunicorn** = Green Unicorn = Serveur WSGI pour Python

**WSGI** = Web Server Gateway Interface (interface standard Python)

**Architecture :**

```
Client
  v
Nginx (reverse proxy)
  v
Gunicorn (serveur WSGI)
  v
Flask (application)
  v
RDS PostgreSQL
```

**Rôles :**
- **Nginx** : Gère le trafic HTTP, SSL, fichiers statiques
- **Gunicorn** : Exécute le code Python Flask
- **Flask** : Logique métier de l'application

---

### Installer Gunicorn

**Dans l'environnement virtuel :**

```bash
cd ~/flask-todo-app
source venv/bin/activate
pip install gunicorn
```

---

### Tester Gunicorn

**Arrêter Flask (Ctrl+C si lancé).**

**Lancer avec Gunicorn :**

```bash
gunicorn --bind 0.0.0.0:5000 app:app
```

**Décomposition de la commande :**

**`gunicorn`** = La commande

**`--bind 0.0.0.0:5000`** = Écouter sur toutes les interfaces, port 5000

**`app:app`** = `fichier:variable`
- Premier `app` = Nom du fichier (app.py)
- Deuxième `app` = Nom de la variable Flask (app = Flask(__name__))

---

**Résultat :**

```
[2024-12-16 15:45:00 +0000] [12345] [INFO] Starting gunicorn 21.2.0
[2024-12-16 15:45:00 +0000] [12345] [INFO] Listening at: http://0.0.0.0:5000 (12345)
[2024-12-16 15:45:00 +0000] [12345] [INFO] Using worker: sync
[2024-12-16 15:45:00 +0000] [12346] [INFO] Booting worker with pid: 12346
```

---

**Tester dans le navigateur :**

```
http://<IP-EC2>:5000
```

**[OK] L'application fonctionne avec Gunicorn !**

---

### Configuration optimale de Gunicorn

**Arrêter Gunicorn (Ctrl+C).**

**Créer un fichier de configuration :**

```bash
nano gunicorn_config.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# CONFIGURATION GUNICORN
# ═══════════════════════════════════════════════════════════════

import multiprocessing

# Nombre de workers (processus)
workers = multiprocessing.cpu_count() * 2 + 1

"""
Formule recommandée : (2 × CPU) + 1

Pourquoi ?
- Plus de workers = Plus de requêtes simultanées
- Mais trop de workers = Overhead mémoire

Instance t2.micro (1 vCPU) :
workers = (2 × 1) + 1 = 3 workers

Instance t3.medium (2 vCPU) :
workers = (2 × 2) + 1 = 5 workers

multiprocessing.cpu_count() retourne le nombre de vCPU
"""

# Type de worker
worker_class = 'sync'

"""
Types de workers :

sync (défaut) :
- Worker synchrone
- Bloquant (attend la fin de chaque requête)
- Simple, fiable
- [OK] Recommandé pour la plupart des cas

gthread :
- Worker avec threads
- Multi-threading
- Bon pour I/O-bound (DB, API)

gevent/eventlet :
- Worker asynchrone
- Concurrent, non-bloquant
- Excellent pour WebSocket, streaming

Recommandation : sync (sauf besoins spécifiques)
"""

# Timeout (secondes)
timeout = 30

"""
Temps maximum d'exécution d'une requête

Si une requête prend > 30s :
-> Gunicorn kill le worker
-> Démarre un nouveau worker

Évite les workers bloqués indéfiniment
"""

# Bind (adresse:port)
bind = '0.0.0.0:8000'

"""
0.0.0.0 : Toutes les interfaces
8000 : Port d'écoute

On va utiliser Nginx sur le port 80
Nginx -> reverse proxy -> Gunicorn:8000
"""

# Daemon mode
daemon = False

"""
daemon = True : Gunicorn tourne en arrière-plan
daemon = False : Gunicorn au premier plan (utile pour systemd)

On va utiliser systemd pour gérer le démon
-> daemon = False
"""

# Logs
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
loglevel = 'info'

"""
Niveaux de log :
- debug : Très verbeux
- info : Normal
- warning : Seulement warnings/erreurs
- error : Seulement erreurs
- critical : Seulement critiques

Recommandation : info
"""

# PID file
pidfile = '/var/run/gunicorn.pid'

"""
Fichier contenant le PID (Process ID) du processus maître

Utile pour :
- Vérifier si Gunicorn tourne
- Envoyer des signaux (reload, stop)

Exemple :
kill -HUP $(cat /var/run/gunicorn.pid)  # Recharge
"""

# Preload app
preload_app = True

"""
preload_app = True :
- Charge l'application AVANT de fork les workers
- Économise la RAM (code partagé)
- Démarrage plus rapide

preload_app = False :
- Chaque worker charge l'app indépendamment
- Plus de RAM
- Utile si l'app n'est pas "fork-safe"

Recommandation : True (sauf problèmes)
"""
```

**Sauvegarde.**

---

**Créer le dossier des logs :**

```bash
sudo mkdir -p /var/log/gunicorn
sudo chown ubuntu:ubuntu /var/log/gunicorn
```

---

**Tester avec la configuration :**

```bash
gunicorn -c gunicorn_config.py app:app
```

**Résultat :**

```
[2024-12-16 15:50:00 +0000] [12350] [INFO] Starting gunicorn 21.2.0
[2024-12-16 15:50:00 +0000] [12350] [INFO] Listening at: http://0.0.0.0:8000 (12350)
[2024-12-16 15:50:00 +0000] [12350] [INFO] Using worker: sync
[2024-12-16 15:50:00 +0000] [12351] [INFO] Booting worker with pid: 12351
[2024-12-16 15:50:00 +0000] [12352] [INFO] Booting worker with pid: 12352
[2024-12-16 15:50:00 +0000] [12353] [INFO] Booting worker with pid: 12353
```

**3 workers lancés (1 CPU × 2 + 1) !**

---

**Tester :**

```
http://<IP-EC2>:8000
```

**[OK] Fonctionne !**

---

### ÉTAPE 10 : Créer un service systemd

**On veut que Gunicorn démarre automatiquement au boot.**

**systemd** = Gestionnaire de services Linux

---

**Créer le fichier service :**

```bash
sudo nano /etc/systemd/system/flask-todo.service
```

**Contenu :**

```ini
# ═══════════════════════════════════════════════════════════════
# SERVICE SYSTEMD - FLASK TODO APP
# ═══════════════════════════════════════════════════════════════

[Unit]
Description=Flask TODO Application with Gunicorn
After=network.target

# Description : Nom lisible du service
# After : Démarre APRÈS le réseau (sinon pas de connexion RDS)

[Service]
Type=notify

# Type de service :
# simple : Processus principal du service
# forking : Processus se met en arrière-plan
# notify : Processus notifie systemd quand prêt (Gunicorn supporte)

User=ubuntu
Group=ubuntu

# Utilisateur/Groupe pour exécuter le service
# Ne PAS utiliser root (sécurité)

WorkingDirectory=/home/ubuntu/flask-todo-app

# Répertoire de travail
# Équivalent à "cd /home/ubuntu/flask-todo-app" avant de lancer

Environment="PATH=/home/ubuntu/flask-todo-app/venv/bin"

# Variables d'environnement
# PATH : Chemin vers l'environnement virtuel
# Permet d'utiliser le Python et les packages du venv

ExecStart=/home/ubuntu/flask-todo-app/venv/bin/gunicorn \
          -c gunicorn_config.py \
          app:app

# Commande pour démarrer le service
# Chemin ABSOLU obligatoire
# \ pour continuer sur la ligne suivante

ExecReload=/bin/kill -s HUP $MAINPID

# Commande pour recharger (sans arrêter)
# HUP = Signal "Hang Up"
# Gunicorn recharge les workers sans couper les connexions

Restart=always

# Politique de redémarrage :
# no : Ne jamais redémarrer
# on-failure : Redémarre si crash
# always : TOUJOURS redémarrer (même si arrêt normal)

RestartSec=3

# Attendre 3 secondes avant de redémarrer
# Évite les redémarrages en boucle rapide

StandardOutput=append:/var/log/gunicorn/systemd.log
StandardError=append:/var/log/gunicorn/systemd.log

# Logs de systemd (en plus des logs Gunicorn)
# append : Ajoute à la fin du fichier (vs truncate)

[Install]
WantedBy=multi-user.target

# WantedBy : Quand activer ce service
# multi-user.target = Mode multi-utilisateur (démarrage normal)
# Équivalent à "runlevel 3" (sans interface graphique)
```

**Sauvegarde.**

---

**Recharger systemd (pour qu'il voit le nouveau service) :**

```bash
sudo systemctl daemon-reload
```

---

**Activer le service (démarrage automatique) :**

```bash
sudo systemctl enable flask-todo.service
```

**Résultat :**

```
Created symlink /etc/systemd/system/multi-user.target.wants/flask-todo.service 
-> /etc/systemd/system/flask-todo.service.
```

---

**Démarrer le service :**

```bash
sudo systemctl start flask-todo.service
```

---

**Vérifier le statut :**

```bash
sudo systemctl status flask-todo.service
```

**Résultat :**

```
[BLACK_CIRCLE] flask-todo.service - Flask TODO Application with Gunicorn
     Loaded: loaded (/etc/systemd/system/flask-todo.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2024-12-16 16:00:00 UTC; 10s ago
   Main PID: 12400 (gunicorn)
      Tasks: 4 (limit: 1131)
     Memory: 75.2M
        CPU: 1.234s
     CGroup: /system.slice/flask-todo.service
             ├─12400 /home/ubuntu/flask-todo-app/venv/bin/python3 /home/ubuntu/flask-todo-app/venv/bin/gunicorn -c gunicorn_config.py app:app
             ├─12401 /home/ubuntu/flask-todo-app/venv/bin/python3 /home/ubuntu/flask-todo-app/venv/bin/gunicorn -c gunicorn_config.py app:app
             ├─12402 /home/ubuntu/flask-todo-app/venv/bin/python3 /home/ubuntu/flask-todo-app/venv/bin/gunicorn -c gunicorn_config.py app:app
             └─12403 /home/ubuntu/flask-todo-app/venv/bin/python3 /home/ubuntu/flask-todo-app/venv/bin/gunicorn -c gunicorn_config.py app:app

Dec 16 16:00:00 ip-172-31-12-34 systemd[1]: Started Flask TODO Application with Gunicorn.
Dec 16 16:00:00 ip-172-31-12-34 gunicorn[12400]: [2024-12-16 16:00:00 +0000] [12400] [INFO] Starting gunicorn 21.2.0
```

**`Active: active (running)`** = [OK] Service actif !

---

**Commandes systemd utiles :**

```bash
# Démarrer
sudo systemctl start flask-todo

# Arrêter
sudo systemctl stop flask-todo

# Redémarrer
sudo systemctl restart flask-todo

# Recharger (sans couper les connexions)
sudo systemctl reload flask-todo

# Voir les logs en temps réel
sudo journalctl -u flask-todo -f

# Voir les logs récents
sudo journalctl -u flask-todo --since "10 minutes ago"
```

---

### ÉTAPE 11 : Installer et configurer Nginx

**Nginx** = Serveur web haute performance

**Rôle :**
- Servir les fichiers statiques (CSS, JS, images)
- Gérer SSL/TLS
- Reverse proxy vers Gunicorn
- Load balancing (si plusieurs Gunicorn)
- Cache

---

**Installer Nginx :**

```bash
sudo apt install nginx -y
```

---

**Vérifier qu'il tourne :**

```bash
sudo systemctl status nginx
```

---

**Créer la configuration pour notre app :**

```bash
sudo nano /etc/nginx/sites-available/flask-todo
```

**Contenu :**

```nginx
# ═══════════════════════════════════════════════════════════════
# NGINX CONFIGURATION - FLASK TODO APP
# ═══════════════════════════════════════════════════════════════

server {
    # Port d'écoute
    listen 80;
    listen [::]:80;  # IPv6
    
    # Nom de domaine
    server_name _;  # Accepte tous les noms de domaine
    
    # En production, remplacer par :
    # server_name todo.mon-domaine.com;
    
    # ───────────────────────────────────────────────────────────
    # LOGS
    # ───────────────────────────────────────────────────────────
    
    access_log /var/log/nginx/flask-todo-access.log;
    error_log /var/log/nginx/flask-todo-error.log;
    
    # ───────────────────────────────────────────────────────────
    # LOCATION / (APPLICATION)
    # ───────────────────────────────────────────────────────────
    
    location / {
        # Proxy vers Gunicorn
        proxy_pass http://127.0.0.1:8000;
        
        """
        proxy_pass : Transfère la requête vers cette URL
        
        Client -> Nginx:80 -> proxy_pass -> Gunicorn:8000
        
        127.0.0.1 = localhost (Gunicorn sur la même machine)
        
        Si Gunicorn était sur une autre machine :
        proxy_pass http://10.0.1.50:8000;
        """
        
        # Headers pour identifier le client original
        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;
        
        """
        Sans ces headers, Flask verrait toutes les requêtes comme venant de 127.0.0.1 (Nginx)
        
        $host : Nom de domaine demandé (todo.example.com)
        $remote_addr : IP du client (35.180.123.45)
        $proxy_add_x_forwarded_for : Liste des proxies traversés
        $scheme : Protocole (http ou https)
        
        Flask peut alors :
        request.headers['X-Real-IP']  # IP du vrai client
        request.headers['X-Forwarded-Proto']  # http ou https
        """
        
        # Timeouts
        proxy_connect_timeout 60;
        proxy_send_timeout 60;
        proxy_read_timeout 60;
        
        """
        proxy_connect_timeout : Temps pour se connecter à Gunicorn
        proxy_send_timeout : Temps pour envoyer la requête
        proxy_read_timeout : Temps pour recevoir la réponse
        
        60 secondes = Généreux pour une app web
        
        Si une requête prend > 60s :
        -> Nginx retourne 504 Gateway Timeout
        """
        
        # Buffering
        proxy_buffering on;
        proxy_buffer_size 4k;
        proxy_buffers 8 4k;
        
        """
        Nginx met en buffer les réponses de Gunicorn
        
        Avantage :
        Gunicorn peut fermer la connexion rapidement
        Nginx gère l'envoi lent au client
        
        Libère un worker Gunicorn plus vite
        """
    }
    
    # ───────────────────────────────────────────────────────────
    # FICHIERS STATIQUES (OPTIONNEL)
    # ───────────────────────────────────────────────────────────
    
    # Si tu avais des fichiers statiques (CSS, JS, images)
    # location /static/ {
    #     alias /home/ubuntu/flask-todo-app/static/;
    #     expires 30d;
    #     add_header Cache-Control "public, immutable";
    # }
    
    """
    Nginx sert les fichiers statiques directement
    Plus rapide que de passer par Flask
    
    expires 30d : Cache 30 jours
    immutable : Le fichier ne changera jamais (optimisation)
    """
    
    # ───────────────────────────────────────────────────────────
    # GZIP COMPRESSION
    # ───────────────────────────────────────────────────────────
    
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
    """
    gzip : Active la compression
    gzip_min_length 1024 : Compresse si > 1 KB
    gzip_types : Types MIME à compresser
    
    Réduit la taille des réponses (~70%)
    """
    
    # ───────────────────────────────────────────────────────────
    # SÉCURITÉ
    # ───────────────────────────────────────────────────────────
    
    # Cacher la version de Nginx
    server_tokens off;
    
    # Limiter la taille du body
    client_max_body_size 10M;
    
    """
    client_max_body_size : Taille max des uploads
    10M = 10 mégaoctets
    
    Bloque les uploads > 10 MB
    Protège contre les attaques de saturation
    """
}
```

**Sauvegarde.**

---

**Activer le site :**

```bash
sudo ln -s /etc/nginx/sites-available/flask-todo /etc/nginx/sites-enabled/
```

**Explication :**

```
/etc/nginx/sites-available/ : Configurations disponibles
/etc/nginx/sites-enabled/ : Configurations actives

Lien symbolique : sites-enabled/flask-todo -> sites-available/flask-todo

Pourquoi ?
- Garder les configs dans sites-available
- Activer/désactiver en créant/supprimant le lien
```

---

**Désactiver le site par défaut de Nginx :**

```bash
sudo rm /etc/nginx/sites-enabled/default
```

---

**Tester la configuration Nginx :**

```bash
sudo nginx -t
```

**Résultat :**

```
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```

**[OK] Configuration OK !**

---

**Recharger Nginx :**

```bash
sudo systemctl reload nginx
```

---

**Tester le site :**

```
http://<IP-EC2>
```

**(Port 80, pas 8000 ni 5000)**

**[BRAVO] L'APPLICATION EST ACCESSIBLE VIA NGINX ! [BRAVO]**

---

**Architecture finale :**

```
Client (navigateur)
  v
Internet
  v
AWS EC2 (Port 80)
  v
Nginx (reverse proxy)
  v
Gunicorn (port 8000, 3 workers)
  v
Flask (app Python)
  v
RDS PostgreSQL (port 5432)
```

---

### ÉTAPE 12 : Ajouter HTTPS avec Let's Encrypt (optionnel)

**Pour avoir HTTPS, il faut un nom de domaine.**

**Si tu as un nom de domaine :**

---

**Installer Certbot :**

```bash
sudo apt install certbot python3-certbot-nginx -y
```

---

**Obtenir un certificat SSL :**

```bash
sudo certbot --nginx -d todo.mon-domaine.com
```

**Certbot va :**
1. Vérifier que tu contrôles le domaine
2. Obtenir un certificat SSL de Let's Encrypt
3. Configurer Nginx automatiquement
4. Rediriger HTTP -> HTTPS

---

**Renouvellement automatique :**

```bash
sudo systemctl status certbot.timer
```

**Let's Encrypt : Certificats valides 90 jours, renouvelés automatiquement.**

---

### [OK] TESTS DE VALIDATION

**1. RDS accessible depuis EC2**

```bash
psql -h flask-todo-db....rds.amazonaws.com -U postgres -d todo_db -c "SELECT 1;"
```

**Résultat : `?column? | 1`**

---

**2. Service systemd actif**

```bash
sudo systemctl is-active flask-todo
```

**Résultat : `active`**

---

**3. Gunicorn écoute sur 8000**

```bash
curl http://127.0.0.1:8000
```

**Résultat : HTML de l'app**

---

**4. Nginx écoute sur 80**

```bash
curl http://127.0.0.1
```

**Résultat : HTML de l'app**

---

**5. Site accessible depuis Internet**

```
http://<IP-EC2>
```

- [ ] Page d'accueil charge
- [ ] Peut ajouter une tâche
- [ ] Tâche apparaît dans la liste
- [ ] Peut modifier/supprimer

---

**6. Données persistantes**

```bash
# Redémarrer le serveur
sudo reboot
```

**Après redémarrage :**

- [ ] Le service flask-todo démarre automatiquement
- [ ] Les tâches sont toujours présentes (stockées dans RDS)

---

**7. Performance**

```bash
# Installer Apache Bench
sudo apt install apache2-utils -y

# Tester la performance
ab -n 1000 -c 10 http://127.0.0.1/
```

**Résultat :**

```
Requests per second: 150-300 (selon instance)
Time per request: 30-60 ms
```

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Connection refused (RDS)

**Symptôme :**

```
psycopg2.OperationalError: could not connect to server: Connection refused
```

**Causes :**

**1. Security Group RDS ne permet pas l'EC2**

Vérifier : RDS SG -> Inbound -> Port 5432 depuis EC2 SG

---

**2. Endpoint incorrect dans .env**

Vérifier : RDS Console -> Endpoint

---

**3. VPC différent**

EC2 et RDS doivent être dans le même VPC (ou peering configuré).

---

#### Erreur 2 : 502 Bad Gateway (Nginx)

**Symptôme :**

```
502 Bad Gateway
nginx/1.18.0
```

**Causes :**

**1. Gunicorn ne tourne pas**

```bash
sudo systemctl status flask-todo
```

Si inactif :

```bash
sudo systemctl start flask-todo
```

---

**2. Gunicorn n'écoute pas sur 8000**

```bash
sudo netstat -tulpn | grep 8000
```

Si vide, vérifier la config Gunicorn.

---

**3. Erreur dans app.py**

```bash
sudo journalctl -u flask-todo -n 50
```

Regarder les erreurs Python.

---

#### Erreur 3 : ModuleNotFoundError

**Symptôme :**

```
ModuleNotFoundError: No module named 'flask'
```

**Cause : Mauvais environnement virtuel**

**Vérifier dans le service systemd :**

```
ExecStart=/home/ubuntu/flask-todo-app/venv/bin/gunicorn ...
```

**Le chemin doit pointer vers le venv.**

---

#### Erreur 4 : Permission denied (logs)

**Symptôme :**

```
PermissionError: [Errno 13] Permission denied: '/var/log/gunicorn/access.log'
```

**Solution :**

```bash
sudo chown -R ubuntu:ubuntu /var/log/gunicorn
```

---

#### Erreur 5 : DATABASE_URL non trouvée

**Symptôme :**

```
KeyError: 'DATABASE_URL'
```

**Cause : .env non chargé**

**Vérifier que `load_dotenv()` est appelé dans app.py.**

**Vérifier que .env existe :**

```bash
cat ~/flask-todo-app/.env
```

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. RDS vs base locale**
- RDS = Managé, backups auto, Multi-AZ
- Plus cher mais gain de temps énorme

**2. Security Groups**
- EC2 doit pouvoir se connecter à RDS (port 5432)
- RDS ne doit PAS être public (Public access: No)

**3. SQLAlchemy ORM**
- Abstraction de la base de données
- Pas de SQL brut
- Migrations avec Flask-Migrate (production)

**4. Gunicorn vs Flask dev server**
- Flask dev : Développement uniquement
- Gunicorn : Production, multi-workers

**5. Architecture production**
- Nginx : Reverse proxy, SSL, statiques
- Gunicorn : WSGI server
- systemd : Gestion du service
- RDS : Base de données

**6. Variables d'environnement**
- Jamais de credentials dans le code
- Utiliser .env + python-dotenv
- .env dans .gitignore

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Migrations de base de données**

**Installer Flask-Migrate :**

```bash
pip install Flask-Migrate
```

**Dans app.py :**

```python
from flask_migrate import Migrate

migrate = Migrate(app, db)
```

**Créer une migration :**

```bash
flask db init
flask db migrate -m "Initial migration"
flask db upgrade
```

**Avantage : Versionner les changements de schéma.**

---

**2. Variables d'environnement dans systemd**

**Au lieu de .env, utiliser `EnvironmentFile` :**

```ini
[Service]
EnvironmentFile=/home/ubuntu/flask-todo-app/.env
```

---

**3. Redis pour les sessions**

**Au lieu des sessions en mémoire :**

```python
from flask_session import Session
import redis

app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url('redis://localhost:6379')
Session(app)
```

**Avantage : Sessions partagées entre workers.**

---

**4. Load Balancer AWS**

**Ajouter un Application Load Balancer devant l'EC2 :**

```
Client -> ALB -> EC2-1 (Nginx -> Gunicorn)
           v
           └──-> EC2-2 (Nginx -> Gunicorn)
```

**Haute disponibilité + scaling automatique.**

---

**5. RDS Read Replica**

**Pour les lectures intensives :**

```python
# Connexion principale (write)
SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']

# Connexion read-only
SQLALCHEMY_BINDS = {
    'read': os.environ['READ_REPLICA_URL']
}

# Dans les requêtes
tasks = Task.query.with_bind('read').all()
```

---

**6. Monitoring avec CloudWatch**

**Installer CloudWatch Agent :**

```bash
wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
sudo dpkg -i amazon-cloudwatch-agent.deb
```

**Métriques collectées :**
- CPU, RAM, Disk
- Logs applicatifs
- Métriques custom

---

**7. CI/CD avec GitHub Actions**

```yaml
name: Deploy to EC2

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd ~/flask-todo-app
            git pull
            source venv/bin/activate
            pip install -r requirements.txt
            sudo systemctl restart flask-todo
```

**Push sur GitHub -> Déploiement automatique !**

---

## [COURS] CONCLUSION DE L'EXERCICE 3

**[BRAVO] Félicitations ! Tu as créé une application web complète avec Flask + RDS ! [BRAVO]**

**Ce que tu as appris :**
- Créer une instance RDS PostgreSQL
- Configurer les Security Groups pour RDS
- Développer une app Flask avec SQLAlchemy
- Connecter Flask à RDS
- Déployer en production avec Gunicorn
- Configurer Nginx comme reverse proxy
- Créer un service systemd
- Gérer les variables d'environnement
- Architecture production complète

**Compétences acquises :**
- [OK] RDS (base de données managée)
- [OK] Flask + SQLAlchemy (niveau intermédiaire)
- [OK] Gunicorn (WSGI server)
- [OK] Nginx (reverse proxy)
- [OK] systemd (gestion de services)
- [OK] Architecture web en production

**Coûts estimés :**

```
EC2 t2.micro : Gratuit (Free Tier)
RDS db.t3.micro : Gratuit (Free Tier, 750h/mois)
Stockage RDS 20 GB : Gratuit (Free Tier)
Transfert données : Gratuit (15 GB sortant/mois)

Total : 0 $/mois (dans le Free Tier)
```

**Temps moyen de réalisation :** 3-4 heures

---

**Prochaine étape :** Exercice 4 - Elastic Beanstalk (déploiement Flask automatisé) ! [RAPIDE]

Veux-tu que je continue avec les exercices 4 à 10 ? Je vais créer :

- **Exercice 4** : Elastic Beanstalk (PaaS pour Flask)
- **Exercice 5** : Lambda + API Gateway (Serverless)
- **Exercice 6** : ECS + ECR (Containers Docker)
- **Exercice 7** : Application Load Balancer + Auto Scaling
- **Exercice 8** : VPC personnalisé + sous-réseaux
- **Exercice 9** : CI/CD avec CodePipeline
- **Exercice 10** : Architecture complète (tous les services)

Chaque exercice aura le même niveau de détail ! [DOCS]

# [JAUNE] EXERCICE 4 : ELASTIC BEANSTALK - DÉPLOIEMENT FLASK AUTOMATISÉ

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur dans une startup en croissance rapide. L'équipe veut déployer rapidement plusieurs applications web sans gérer l'infrastructure (serveurs, load balancers, auto-scaling). Le CTO veut une solution Platform-as-a-Service (PaaS).

### Cahier des charges

Le client souhaite :
- Déploiement simplifié (zip + upload)
- Infrastructure managée automatiquement
- Auto-scaling intégré
- Monitoring et logs centralisés
- Rollback facile en cas de problème
- Zero downtime deployment

### Contraintes techniques

- Framework : Flask (Python)
- PaaS : AWS Elastic Beanstalk
- Base de données : RDS (existante ou nouvelle)
- Application : TODO list (même que l'exercice 3)
- Temps estimé : 2-3 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre Elastic Beanstalk (PaaS)
- [OK] Différencier IaaS vs PaaS
- [OK] Préparer une app Flask pour Beanstalk
- [OK] Créer un environnement Beanstalk
- [OK] Déployer via la console AWS
- [OK] Configurer les variables d'environnement
- [OK] Connecter à RDS depuis Beanstalk
- [OK] Monitorer et déboguer l'application
- [OK] Mettre à jour l'application (rolling updates)

---

## [DOCS] PRÉREQUIS

- Exercices 1 et 3 terminés
- Application Flask fonctionnelle
- Compte AWS actif
- Connaissances Python/Flask

---

## [IDEE] CONCEPTS AWS À COMPRENDRE

### Qu'est-ce qu'Elastic Beanstalk ?

**Elastic Beanstalk** = Platform as a Service (PaaS) d'AWS

**Analogie :**

```
Heroku, Google App Engine = PaaS célèbres
Elastic Beanstalk = PaaS d'AWS
```

**Tu donnes ton code -> AWS gère tout le reste.**

---

### IaaS vs PaaS vs SaaS

| Niveau | Responsabilité | Exemples AWS | Tu gères |
|--------|----------------|--------------|----------|
| **IaaS** | Infrastructure | EC2, RDS | OS, runtime, app, data |
| **PaaS** | Plateforme | Elastic Beanstalk | App, data |
| **SaaS** | Application | Gmail, Salesforce | Data seulement |

---

**Exemple concret avec notre TODO app :**

#### **IaaS (EC2 - Exercice 3)**

**Tu gères :**
- [OK] Créer l'instance EC2
- [OK] Installer Ubuntu
- [OK] Installer Python, pip, virtualenv
- [OK] Installer Nginx
- [OK] Configurer Gunicorn
- [OK] Créer le service systemd
- [OK] Configurer le pare-feu
- [OK] Surveiller les logs
- [OK] Faire les mises à jour de sécurité

**Temps : ~3-4 heures**

---

#### **PaaS (Elastic Beanstalk - Exercice 4)**

**Tu gères :**
- [OK] Zipper ton code
- [OK] Upload sur Beanstalk

**AWS gère automatiquement :**
- [BOT] Création des EC2
- [BOT] Installation Python/Nginx/Gunicorn
- [BOT] Configuration du load balancer
- [BOT] Auto-scaling
- [BOT] Monitoring
- [BOT] Logs centralisés
- [BOT] Mises à jour de sécurité

**Temps : ~30 minutes**

---

### Architecture d'Elastic Beanstalk

**Ce que Beanstalk crée pour toi :**

```
                    Internet
                        v
              [Elastic Load Balancer]
                    <-     ->
          [EC2-1]        [EC2-2]
          (Nginx)        (Nginx)
             v              v
          (Gunicorn)    (Gunicorn)
             v              v
          (Flask)        (Flask)
                    v
              [RDS PostgreSQL]
```

**Composants créés automatiquement :**

1. **EC2 Instances** : Serveurs pour ton application
2. **Elastic Load Balancer** : Répartit le trafic
3. **Auto Scaling Group** : Ajoute/retire des instances automatiquement
4. **Security Groups** : Pare-feu configuré automatiquement
5. **CloudWatch** : Monitoring et logs
6. **S3 Bucket** : Stockage des versions de l'app

**Tout ça en un clic ! [BRAVO]**

---

### Concepts clés Elastic Beanstalk

#### **Application**

**Application** = Conteneur logique pour tes environnements

```
Application "todo-app"
├── Environment "todo-dev" (développement)
├── Environment "todo-staging" (pré-production)
└── Environment "todo-prod" (production)
```

**Une application peut avoir plusieurs environnements.**

---

#### **Environment (Environnement)**

**Environment** = Instance déployée de ton application

**Types d'environnements :**

**1. Web Server Environment**
- Application web classique (HTTP/HTTPS)
- Load balancer + EC2
- Pour Flask, Django, Node.js, etc.

**2. Worker Environment**
- Traitement de tâches en arrière-plan
- Consomme des messages depuis SQS
- Pas de load balancer

**Pour notre TODO app : Web Server Environment**

---

#### **Platform**

**Platform** = Stack technique (langage + version)

**Plateformes disponibles :**
- Python 3.11 running on 64bit Amazon Linux 2023
- Python 3.9 running on 64bit Amazon Linux 2
- Node.js 18
- PHP 8.2
- Java 17 (Corretto)
- .NET Core 6
- Ruby 3.2
- Go 1.x
- Docker (multi-container)

**Pour nous : Python 3.11 sur Amazon Linux 2023**

---

#### **Application Version**

**Version** = Package de ton code à un instant T

```
Version 1 : todo-app-v1.zip
Version 2 : todo-app-v2.zip (avec nouvelles features)
Version 3 : todo-app-v3.zip (avec bug fix)
```

**Beanstalk garde l'historique -> rollback facile.**

---

### Configuration Beanstalk

**Beanstalk utilise des fichiers de configuration :**

**`.ebextensions/`** = Dossier de configuration

```
.ebextensions/
├── 01-packages.config      # Installer des packages système
├── 02-python.config        # Config Python
├── 03-flask.config         # Config Flask/WSGI
└── 04-environment.config   # Variables d'environnement
```

**Format : YAML**

---

### Deployment Policies (Politiques de déploiement)

**Comment Beanstalk met à jour ton app :**

#### **1. All at once (Tout d'un coup)**

```
[V1] [V1] [V1] [V1]
       v
[V2] [V2] [V2] [V2]

Downtime : Oui (~1 min)
Rollback : Manuel
```

**Le plus rapide, mais avec downtime.**

---

#### **2. Rolling (Progressif)**

```
[V1] [V1] [V1] [V1]
[V2] [V1] [V1] [V1]  (Batch 1)
[V2] [V2] [V1] [V1]  (Batch 2)
[V2] [V2] [V2] [V1]  (Batch 3)
[V2] [V2] [V2] [V2]  (Terminé)

Downtime : Non
Capacité réduite : Oui (temporaire)
```

**Pas de downtime, mais capacité réduite pendant le déploiement.**

---

#### **3. Rolling with additional batch**

```
[V1] [V1] [V1] [V1]
[V1] [V1] [V1] [V1] [V2] [V2]  (Nouvelles instances)
[V2] [V2] [V1] [V1] [V2] [V2]  (Batch 1)
[V2] [V2] [V2] [V2] [V2] [V2]  (Batch 2)
[V2] [V2] [V2] [V2]             (Supprime les extra)

Downtime : Non
Capacité réduite : Non
```

**Idéal pour la production.**

---

#### **4. Immutable**

```
[V1] [V1] [V1] [V1]  (Ancien ASG)

[V2] [V2] [V2] [V2]  (Nouvel ASG créé)

Test OK -> Switch -> Supprime ancien ASG

Downtime : Non
Rollback : Instantané (switch back)
```

**Le plus sûr, mais plus lent et coûteux.**

---

#### **5. Blue/Green**

```
Environment Blue (V1)  -> Actif
Environment Green (V2) -> Créé en parallèle

Test OK -> Swap URLs -> Green devient actif

Downtime : 0 secondes (DNS swap)
```

**Zéro downtime, rollback instantané.**

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Préparer l'application Flask pour Beanstalk

**On va réutiliser l'app de l'exercice 3, mais adaptée pour Beanstalk.**

---

**Sur ton ordinateur local (pas sur EC2), créer un nouveau dossier :**

```bash
mkdir flask-beanstalk-app
cd flask-beanstalk-app
```

---

**Créer `application.py` :**

**[ATTENTION] IMPORTANT : Le fichier DOIT s'appeler `application.py` (pas `app.py`) !**

**Beanstalk cherche `application.py` par défaut.**

```bash
nano application.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# APPLICATION FLASK POUR ELASTIC BEANSTALK
# ═══════════════════════════════════════════════════════════════

from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import os

"""
[ATTENTION] DIFFÉRENCES AVEC L'EXERCICE 3 :

1. Fichier s'appelle application.py (pas app.py)
2. Pas de load_dotenv() (variables d'env configurées dans Beanstalk)
3. application au lieu de app (convention Beanstalk)
"""

# ───────────────────────────────────────────────────────────────
# CONFIGURATION
# ───────────────────────────────────────────────────────────────

application = Flask(__name__)

# Configuration de la base de données
# Les variables viennent de l'environnement Beanstalk
application.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get(
    'DATABASE_URL',
    'sqlite:///todo.db'  # Fallback pour développement local
)

"""
En local : Utilise SQLite (todo.db)
Sur Beanstalk : Utilise RDS (DATABASE_URL configuré dans l'env)

os.environ.get('KEY', 'default') :
- Retourne la valeur de KEY si elle existe
- Sinon retourne 'default'
"""

application.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
application.config['SECRET_KEY'] = os.environ.get(
    'SECRET_KEY',
    'dev-secret-key-change-in-production'
)

# Initialiser SQLAlchemy
db = SQLAlchemy(application)

# ───────────────────────────────────────────────────────────────
# MODÈLES
# ───────────────────────────────────────────────────────────────

class Task(db.Model):
    __tablename__ = 'task'
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    description = db.Column(db.Text, nullable=True)
    completed = db.Column(db.Boolean, default=False, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    
    def __repr__(self):
        return f'<Task {self.id}: {self.title}>'

# ───────────────────────────────────────────────────────────────
# ROUTES
# ───────────────────────────────────────────────────────────────

@application.route('/')
def index():
    tasks = Task.query.order_by(Task.created_at.desc()).all()
    return render_template('index.html', tasks=tasks)

@application.route('/add', methods=['GET', 'POST'])
def add_task():
    if request.method == 'POST':
        title = request.form.get('title')
        description = request.form.get('description')
        
        if not title:
            flash('Le titre est obligatoire !', 'error')
            return redirect(url_for('add_task'))
        
        new_task = Task(title=title, description=description)
        db.session.add(new_task)
        db.session.commit()
        
        flash('Tâche ajoutée avec succès !', 'success')
        return redirect(url_for('index'))
    
    return render_template('add_task.html')

@application.route('/edit/<int:task_id>', methods=['GET', 'POST'])
def edit_task(task_id):
    task = Task.query.get_or_404(task_id)
    
    if request.method == 'POST':
        task.title = request.form.get('title')
        task.description = request.form.get('description')
        task.completed = 'completed' in request.form
        
        db.session.commit()
        
        flash('Tâche modifiée avec succès !', 'success')
        return redirect(url_for('index'))
    
    return render_template('edit_task.html', task=task)

@application.route('/delete/<int:task_id>')
def delete_task(task_id):
    task = Task.query.get_or_404(task_id)
    db.session.delete(task)
    db.session.commit()
    
    flash('Tâche supprimée avec succès !', 'success')
    return redirect(url_for('index'))

@application.route('/toggle/<int:task_id>')
def toggle_task(task_id):
    task = Task.query.get_or_404(task_id)
    task.completed = not task.completed
    db.session.commit()
    
    flash('Statut modifié !', 'success')
    return redirect(url_for('index'))

@application.route('/health')
def health():
    """
    Endpoint de santé pour le load balancer
    
    Le load balancer vérifie périodiquement si l'app est en bonne santé
    Si /health ne répond pas -> instance marquée comme unhealthy
    """
    return {'status': 'healthy', 'message': 'Application is running'}, 200

# ───────────────────────────────────────────────────────────────
# INITIALISATION DE LA BASE
# ───────────────────────────────────────────────────────────────

with application.app_context():
    db.create_all()

"""
[ATTENTION] IMPORTANT : db.create_all() est appelé au démarrage

En production, utiliser des migrations (Flask-Migrate)
Ici pour simplifier l'exercice
"""

# ───────────────────────────────────────────────────────────────
# POINT D'ENTRÉE (développement local uniquement)
# ───────────────────────────────────────────────────────────────

if __name__ == '__main__':
    # Ceci s'exécute seulement en local : python application.py
    # Sur Beanstalk, Gunicorn est utilisé automatiquement
    application.run(debug=True, host='0.0.0.0', port=5000)
```

**Sauvegarde.**

---

**Créer `requirements.txt` :**

```bash
nano requirements.txt
```

**Contenu :**

```
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
psycopg2-binary==2.9.9
```

**[ATTENTION] TRÈS IMPORTANT : Pas de versions fantaisistes !**

**Beanstalk installe automatiquement les packages de `requirements.txt`.**

---

**Créer le dossier templates et copier les templates de l'exercice 3 :**

```bash
mkdir templates
```

**Copier les fichiers :**
- `templates/base.html`
- `templates/index.html`
- `templates/add_task.html`
- `templates/edit_task.html`

**(Même contenu que l'exercice 3)**

---

**Structure actuelle :**

```
flask-beanstalk-app/
├── application.py
├── requirements.txt
└── templates/
    ├── base.html
    ├── index.html
    ├── add_task.html
    └── edit_task.html
```

---

### ÉTAPE 2 : Configurer Elastic Beanstalk

**Créer le dossier `.ebextensions` :**

```bash
mkdir .ebextensions
```

**Ce dossier contient les configurations Beanstalk.**

---

**Créer `.ebextensions/01-flask.config` :**

```bash
nano .ebextensions/01-flask.config
```

**Contenu :**

```yaml
# ═══════════════════════════════════════════════════════════════
# CONFIGURATION FLASK POUR ELASTIC BEANSTALK
# ═══════════════════════════════════════════════════════════════

option_settings:
  # Configuration WSGI
  aws:elasticbeanstalk:container:python:
    WSGIPath: application:application
    
    """
    WSGIPath : Chemin vers l'application WSGI
    
    Format : fichier:variable
    application:application = fichier application.py, variable application
    
    Équivalent à :
    gunicorn application:application
    """
  
  # Configuration du proxy (Nginx)
  aws:elasticbeanstalk:environment:proxy:
    ProxyServer: nginx
    
    """
    ProxyServer : Serveur proxy frontal
    
    Options :
    - nginx (recommandé)
    - apache (alternative)
    - none (pas de proxy, Gunicorn direct)
    """
  
  # Variables d'environnement (temporaires, à remplacer dans la console)
  aws:elasticbeanstalk:application:environment:
    FLASK_ENV: production
    PYTHONUNBUFFERED: "1"
    
    """
    PYTHONUNBUFFERED : Désactive le buffering des logs Python
    Permet de voir les logs en temps réel dans CloudWatch
    
    "1" = True (en string)
    """

# ───────────────────────────────────────────────────────────────
# COMMANDES À EXÉCUTER
# ───────────────────────────────────────────────────────────────

commands:
  01_upgrade_pip:
    command: /var/app/venv/*/bin/pip install --upgrade pip
    
    """
    Mise à jour de pip avant d'installer les dépendances
    /var/app/venv/*/bin/pip : Chemin vers pip dans le venv Beanstalk
    """

# ───────────────────────────────────────────────────────────────
# PACKAGES SYSTÈME (optionnel)
# ───────────────────────────────────────────────────────────────

packages:
  yum:
    postgresql-devel: []
    
    """
    postgresql-devel : Headers PostgreSQL pour psycopg2
    
    yum : Package manager Amazon Linux
    [] : Installe la dernière version
    
    Si tu utilises MySQL :
    mysql-devel: []
    """
```

**Sauvegarde.**

---

**Créer `.ebextensions/02-nginx.config` (optionnel) :**

```bash
nano .ebextensions/02-nginx.config
```

**Contenu :**

```yaml
# ═══════════════════════════════════════════════════════════════
# CONFIGURATION NGINX PERSONNALISÉE
# ═══════════════════════════════════════════════════════════════

files:
  "/etc/nginx/conf.d/proxy.conf":
    mode: "000644"
    owner: root
    group: root
    content: |
      # Timeouts personnalisés
      proxy_connect_timeout 60;
      proxy_send_timeout 60;
      proxy_read_timeout 60;
      
      # Taille max du body (uploads)
      client_max_body_size 10M;
      
      # Compression Gzip
      gzip on;
      gzip_vary on;
      gzip_min_length 1024;
      gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

"""
files : Crée ou modifie des fichiers sur l'instance

/etc/nginx/conf.d/proxy.conf : Configuration Nginx supplémentaire
Nginx inclut automatiquement les fichiers dans conf.d/

mode: "000644" : Permissions (rw-r--r--)
owner/group: root : Propriétaire

content: | : Contenu du fichier (multiligne avec |)
"""

container_commands:
  01_reload_nginx:
    command: "service nginx reload"
    
    """
    container_commands : Commandes exécutées APRÈS déploiement
    
    Différence avec commands :
    - commands : Avant déploiement
    - container_commands : Après déploiement
    
    Recharge Nginx pour appliquer la config
    """
```

**Sauvegarde.**

---

**Créer `.ebignore` (ignore des fichiers) :**

```bash
nano .ebignore
```

**Contenu :**

```
# Ne pas uploader ces fichiers/dossiers
venv/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.db
*.sqlite
*.sqlite3
.env
.git/
.gitignore
.DS_Store
```

**Similar à `.gitignore`, mais pour Beanstalk.**

---

**Structure finale :**

```
flask-beanstalk-app/
├── application.py
├── requirements.txt
├── .ebignore
├── .ebextensions/
│   ├── 01-flask.config
│   └── 02-nginx.config
└── templates/
    ├── base.html
    ├── index.html
    ├── add_task.html
    └── edit_task.html
```

---

### ÉTAPE 3 : Créer une application Elastic Beanstalk

**Console AWS -> Chercher "Elastic Beanstalk"**

**Cliquer sur "Create application"**

---

#### **Configure environment**

**Application name :**

```
flask-todo-beanstalk
```

---

**Environment name :**

```
FlaskTodo-env

(Généré automatiquement, peut être personnalisé)
```

**L'URL sera : `flasktodo-env.us-east-1.elasticbeanstalk.com`**

---

**Domain (optionnel) :**

```
(laisser vide ou personnaliser)
```

**Exemple : `my-todo-app`**

**URL devient : `my-todo-app.us-east-1.elasticbeanstalk.com`**

---

**Environment description :**

```
Environment de production pour TODO list Flask
```

---

#### **Platform**

**Platform :**

```
[x] Python
```

---

**Platform branch :**

```
Python 3.11 running on 64bit Amazon Linux 2023
```

**Utilise la dernière version stable.**

---

**Platform version :**

```
(Recommandé - latest)
```

---

#### **Application code**

```
[x] Upload your code
```

---

**Version label :**

```
v1.0.0
```

**Identifiant de cette version.**

---

**Source code origin :**

```
[x] Local file
```

**Cliquer sur "Choose file"**

---

**Zipper l'application :**

**Dans ton dossier local `flask-beanstalk-app/` :**

```bash
# macOS/Linux
zip -r ../flask-todo-app.zip . -x "*.git*" "*__pycache__*" "*.pyc"

# Windows (PowerShell)
Compress-Archive -Path * -DestinationPath ..\flask-todo-app.zip
```

**[ATTENTION] IMPORTANT : Zipper le CONTENU du dossier, pas le dossier lui-même !**

**Structure correcte dans le zip :**

```
flask-todo-app.zip
├── application.py
├── requirements.txt
├── .ebextensions/
└── templates/
```

**[X] INCORRECT :**

```
flask-todo-app.zip
└── flask-beanstalk-app/
    ├── application.py
    └── ...
```

---

**Uploader le fichier `flask-todo-app.zip`**

---

#### **Presets**

```
[x] Single instance (free tier eligible)
```

**Explication des presets :**

**Single instance :**
- 1 seule instance EC2
- Pas de load balancer
- [OK] Free Tier
- Développement/test

**High availability :**
- Plusieurs instances (min 2)
- Load balancer
- Auto-scaling
- [X] Payant
- Production

**Custom configuration :**
- Configuration manuelle complète

**Pour ce tuto : Single instance**

---

**Cliquer sur "Next"**

---

#### **Configure service access**

**Service role :**

```
[x] Create and use new service role

Role name : aws-elasticbeanstalk-service-role (auto-généré)
```

**Service role** = Rôle IAM pour que Beanstalk puisse créer des ressources (EC2, Load Balancer, etc.)

---

**EC2 key pair :**

```
Sélectionner ta clé SSH (ma-cle-ec2)
```

**Pour pouvoir se connecter en SSH aux instances si besoin.**

---

**EC2 instance profile :**

```
[x] Create and use new instance profile

Profile name : aws-elasticbeanstalk-ec2-role (auto-généré)
```

**Instance profile** = Rôle IAM pour les instances EC2 (accès CloudWatch, S3, etc.)

---

**Cliquer sur "Next"**

---

#### **Set up networking, database, and tags**

**VPC :**

```
Default VPC
```

---

**Public IP address :**

```
[x] Activated
```

**Nécessaire pour que l'app soit accessible depuis Internet.**

---

**Instance subnets :**

```
[x] Cocher au moins 1 subnet (ou plusieurs pour HA)
```

---

**Database (optionnel) :**

```
[ ] Enable database

(On va utiliser la RDS de l'exercice 3, ou en créer une après)
```

**Si tu coches "Enable database" ici :**
- Beanstalk crée une RDS
- RDS liée à l'environnement
- [ATTENTION] Si tu supprimes l'environnement -> RDS supprimée aussi !

**Recommandation : Créer RDS séparément (comme dans l'exercice 3)**

---

**Tags (optionnel) :**

```
Key: Projet | Value: TODO-Beanstalk
Key: Environnement | Value: Production
```

---

**Cliquer sur "Next"**

---

#### **Configure instance traffic and scaling**

**(Page de configuration avancée)**

**Pour "Single instance", peu d'options.**

---

**Root volume (disk) :**

```
Type : General Purpose (SSD)
Size : 10 GB
```

---

**EC2 security groups :**

```
[x] Sélectionner ou créer un security group
```

**Si tu veux autoriser SSH depuis ton IP :**

Créer/sélectionner un SG avec :
- SSH (22) : Mon IP
- HTTP (80) : 0.0.0.0/0

---

**Cliquer sur "Next"**

---

#### **Configure updates, monitoring, and logging**

**Health reporting :**

```
System : Basic (Free Tier)
```

**Basic vs Enhanced :**
- Basic : Checks HTTP (gratuit)
- Enhanced : Métriques détaillées (payant)

---

**Managed updates :**

```
[x] Activated (recommandé)

Maintenance window : No preference
```

**Beanstalk applique automatiquement les patchs de sécurité.**

---

**Platform updates :**

```
[x] Activated (recommandé)

Minor and patch updates
```

---

**Rolling updates :**

```
Deployment policy : All at once (pour Single instance)
```

**(Peu importe en Single instance, mais important en HA)**

---

**CloudWatch logs :**

```
[x] Stream logs to CloudWatch Logs

Log retention : 7 days
```

**Logs centralisés dans CloudWatch.**

---

**Cliquer sur "Next"**

---

#### **Review**

**Vérifier la configuration.**

**Estimation des coûts :**

```
Single instance (t3.micro) : Free Tier eligible
Total : $0/mois (dans le Free Tier)
```

---

**Cliquer sur "Submit"**

---

**Beanstalk crée l'environnement... [HOURGLASS_WITH_FLOWING_SAND]**

**Durée : 5-10 minutes**

**Tu verras :**

```
Environment health : Grey (Creating)
```

**Progression des étapes :**

```
1. Creating environment...
2. Launching EC2 instance...
3. Deploying application version...
4. Running post-deployment commands...
5. Health check...
```

---

**Quand terminé :**

```
Environment health : Green (OK)
URL : http://flasktodo-env.eu-west-3.elasticbeanstalk.com
```

**[BRAVO] TON APPLICATION EST DÉPLOYÉE SUR BEANSTALK ! [BRAVO]**

---

### ÉTAPE 4 : Tester l'application

**Cliquer sur l'URL de l'environnement :**

```
http://flasktodo-env.eu-west-3.elasticbeanstalk.com
```

---

**Si tout fonctionne :**

- [ ] Page d'accueil charge
- [ ] Interface TODO s'affiche

**Mais... les tâches ne persistent pas (SQLite en mémoire).**

**Il faut connecter à RDS !**

---

### ÉTAPE 5 : Connecter à RDS PostgreSQL

**Option 1 : Réutiliser la RDS de l'exercice 3**

**Option 2 : Créer une nouvelle RDS**

**On va faire l'option 1 (réutiliser).**

---

**Récupérer l'endpoint RDS de l'exercice 3 :**

```
flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com
```

---

**Configurer les variables d'environnement dans Beanstalk :**

**Console Beanstalk -> Environment -> Configuration**

**Dans la section "Updates, monitoring, and logging" -> Edit**

**Ou :**

**Menu gauche -> Configuration -> Environment properties -> Edit**

---

**Ajouter ces variables :**

```
DATABASE_URL = postgresql://postgres:MOT_DE_PASSE@flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com:5432/todo_db

SECRET_KEY = ta-cle-secrete-aleatoire-longue-123456
```

**[ATTENTION] REMPLACER :**
- `MOT_DE_PASSE` : Mot de passe RDS
- `endpoint RDS` : Ton endpoint
- `SECRET_KEY` : Clé générée aléatoirement

---

**Cliquer sur "Apply"**

**Beanstalk redémarre l'environnement avec les nouvelles variables.**

---

**Attendre le redémarrage (2-3 min).**

---

**[ATTENTION] IMPORTANT : Autoriser Beanstalk dans le Security Group RDS**

**Console EC2 -> Security Groups**

**Trouver le Security Group de l'instance Beanstalk :**

Menu Beanstalk -> Environment -> Configuration -> Instances -> EC2 security groups

Exemple : `sg-0beanstalk123456`

---

**Modifier le Security Group RDS :**

RDS -> Databases -> flask-todo-db -> VPC security groups -> rds-flask-sg

**Inbound rules -> Edit -> Add rule :**

```
Type : PostgreSQL
Port : 5432
Source : sg-0beanstalk123456 (SG de Beanstalk)
Description : Allow from Beanstalk
```

**Save rules**

---

**Tester à nouveau l'application :**

```
http://flasktodo-env.eu-west-3.elasticbeanstalk.com
```

- [ ] Ajouter une tâche
- [ ] La tâche apparaît
- [ ] Recharger la page -> La tâche est toujours là (persistée dans RDS)

**[OK] APPLICATION CONNECTÉE À RDS ! [BRAVO]**

---

### ÉTAPE 6 : Monitorer l'application

**Console Beanstalk -> Environment -> Monitoring**

**Métriques disponibles (Basic) :**

```
Environment health
Instance health
Requests (total, par minute)
Latency (P50, P90, P99)
HTTP status codes (2xx, 3xx, 4xx, 5xx)
CPU utilization
Network in/out
```

---

**Voir les logs :**

**Environment -> Logs -> Request logs -> Last 100 lines**

**Ou télécharger les logs complets :**

**Request logs -> Full logs -> Download**

---

**Logs CloudWatch (si activé) :**

**Console CloudWatch -> Log groups**

```
/aws/elasticbeanstalk/flask-todo-beanstalk/var/log/web.stdout.log
/aws/elasticbeanstalk/flask-todo-beanstalk/var/log/nginx/access.log
/aws/elasticbeanstalk/flask-todo-beanstalk/var/log/nginx/error.log
```

---

### ÉTAPE 7 : Mettre à jour l'application

**Scénario : Tu veux ajouter une nouvelle feature.**

---

**Modifier `application.py` localement :**

**Ajouter une route `/about` :**

```python
@application.route('/about')
def about():
    return render_template('about.html')
```

**Créer `templates/about.html` :**

```html
{% extends "base.html" %}

{% block content %}
    <h2>À propos de cette application</h2>
    <p>TODO List déployée sur AWS Elastic Beanstalk</p>
    <p>Version 2.0</p>
    <a href="{{ url_for('index') }}" class="btn btn-primary">Retour</a>
{% endblock %}
```

---

**Re-zipper l'application :**

```bash
zip -r ../flask-todo-app-v2.zip . -x "*.git*" "*__pycache__*" "*.pyc"
```

---

**Déployer la nouvelle version :**

**Console Beanstalk -> Environment -> Upload and deploy**

**Version label :**

```
v2.0.0
```

**Choose file : `flask-todo-app-v2.zip`**

**Deployment policy :**

```
All at once (Single instance)
```

---

**Cliquer sur "Deploy"**

**Beanstalk :**
1. Upload le zip sur S3
2. Arrête l'ancienne version
3. Déploie la nouvelle
4. Redémarre

**Durée : 2-3 minutes**

---

**Tester la nouvelle route :**

```
http://flasktodo-env.eu-west-3.elasticbeanstalk.com/about
```

**[OK] Nouvelle feature déployée !**

---

### ÉTAPE 8 : Rollback (revenir à une version précédente)

**Si la version 2 a un bug, rollback vers v1.**

**Console Beanstalk -> Application versions**

**Sélectionner `v1.0.0` -> Actions -> Deploy**

**Sélectionner l'environnement -> Deploy**

**Beanstalk redéploie la version 1.**

**Durée : 2-3 minutes**

**[OK] Rollback instantané !**

---

### ÉTAPE 9 : Auto-scaling (passer en High Availability)

**Pour gérer des pics de trafic.**

---

**Console Beanstalk -> Configuration -> Capacity -> Edit**

**Environment type :**

```
[x] Load balanced

Min instances : 2
Max instances : 4
```

---

**Scaling triggers :**

```
Metric : CPUUtilization
Statistic : Average
Unit : Percent

Scale up if > 70%
Scale down if < 30%
```

---

**Load balancer :**

```
Type : Application Load Balancer

Health check path : /health
```

**(Rappel : On a créé la route `/health` dans application.py)**

---

**Cliquer sur "Apply"**

**Beanstalk :**
1. Crée un Application Load Balancer
2. Lance 2 instances EC2
3. Configure l'Auto Scaling Group

**Durée : 10-15 minutes**

---

**Architecture après :**

```
         Internet
            v
   [Application Load Balancer]
        <-           ->
    [EC2-1]       [EC2-2]
    (Flask)       (Flask)
        v             v
    [RDS PostgreSQL]
```

**Si charge CPU > 70% -> Beanstalk lance EC2-3**

**Si charge CPU < 30% -> Beanstalk termine EC2-3**

---

**Test de charge (optionnel) :**

```bash
# Installer Apache Bench
sudo apt install apache2-utils -y

# Envoyer 10000 requêtes avec 100 connexions simultanées
ab -n 10000 -c 100 http://flasktodo-env.eu-west-3.elasticbeanstalk.com/
```

**Surveiller dans Beanstalk -> Monitoring**

**Si CPU > 70% pendant 5 min -> Nouvelle instance lancée**

---

### [OK] TESTS DE VALIDATION

**1. Application accessible via URL Beanstalk**

```
http://flasktodo-env.eu-west-3.elasticbeanstalk.com
```

- [ ] Page charge
- [ ] CRUD fonctionne

---

**2. Connexion à RDS**

- [ ] Ajouter une tâche
- [ ] Recharger la page -> Tâche toujours là

---

**3. Environment health : Green**

Console Beanstalk -> Environment -> Health

- [ ] Environment health : Ok (Green)
- [ ] Tous les checks passent

---

**4. Logs accessibles**

- [ ] Environment -> Logs -> Peut télécharger
- [ ] CloudWatch Logs -> Logs présents

---

**5. Mise à jour réussie**

- [ ] Déployer une v2
- [ ] Nouvelle version active
- [ ] Rollback vers v1 fonctionne

---

**6. Variables d'environnement configurées**

Console -> Configuration -> Environment properties

- [ ] DATABASE_URL présente
- [ ] SECRET_KEY présente

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Environment health : Severe (Red)

**Symptôme :**

```
Environment health : Severe
Health check failed
```

**Causes :**

**1. Application ne démarre pas**

Voir les logs :

```bash
Environment -> Logs -> Request logs -> Last 100 lines
```

Chercher les erreurs Python.

---

**2. Port incorrect**

Beanstalk attend l'app sur le port configuré par la variable `PORT`.

Gunicorn utilise automatiquement cette variable.

---

**3. Health check path introuvable**

Si tu as configuré un health check personnalisé (`/health`), vérifier que la route existe.

---

#### Erreur 2 : Application works locally but not on Beanstalk

**Causes courantes :**

**1. Fichier s'appelle `app.py` au lieu de `application.py`**

Renommer : `mv app.py application.py`

---

**2. Dépendances manquantes dans `requirements.txt`**

Toutes les dépendances doivent être dans `requirements.txt`.

Tester en local :

```bash
python -m venv test-venv
source test-venv/bin/activate
pip install -r requirements.txt
python application.py
```

---

**3. Variables d'environnement manquantes**

Vérifier : Configuration -> Environment properties

---

#### Erreur 3 : Can't connect to RDS

**Symptôme :**

```
psycopg2.OperationalError: could not connect to server
```

**Causes :**

**1. Security Group RDS ne permet pas Beanstalk**

Ajouter le SG de Beanstalk dans les inbound rules de RDS.

---

**2. DATABASE_URL incorrecte**

Format exact :

```
postgresql://USER:PASSWORD@ENDPOINT:5432/DATABASE
```

Vérifier :
- Mot de passe (caractères spéciaux encodés ?)
- Endpoint correct
- Port 5432
- Nom de la base

---

#### Erreur 4 : Deployment failed

**Symptôme :**

```
Deployment failed: Command failed on instance
```

**Voir les logs détaillés :**

```
Environment -> Logs -> Request logs -> Full logs
```

Chercher dans `/var/log/eb-engine.log`

---

**Causes courantes :**

**1. Erreur de syntaxe dans `.ebextensions/*.config`**

YAML est sensible à l'indentation.

Tester avec un validateur YAML en ligne.

---

**2. Commande qui échoue**

Dans les `.config`, si une commande échoue -> déploiement annulé.

Exemple :

```yaml
commands:
  01_install_package:
    command: yum install -y non-existent-package
```

Vérifier que les commandes fonctionnent.

---

#### Erreur 5 : 502 Bad Gateway intermittent

**Cause : Worker timeout**

Si une requête prend > 30s -> Nginx retourne 502.

**Solution : Augmenter les timeouts**

Dans `.ebextensions/nginx.config` :

```yaml
files:
  "/etc/nginx/conf.d/timeout.conf":
    content: |
      proxy_read_timeout 300;
```

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. Elastic Beanstalk = PaaS**
- Tu gères : Code
- AWS gère : Tout le reste

**2. Architecture Beanstalk**
- EC2 + (Load Balancer) + Auto Scaling + CloudWatch
- Créé automatiquement

**3. Deployment**
- Upload zip -> Beanstalk déploie
- Versions conservées -> Rollback facile

**4. Configuration**
- `.ebextensions/*.config` : Configuration avancée
- Environment properties : Variables d'environnement

**5. Monitoring**
- CloudWatch intégré
- Logs centralisés
- Health checks automatiques

**6. Scaling**
- Single instance : 1 EC2, pas de LB (Free Tier)
- Load balanced : Multi-EC2, ALB, Auto Scaling

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. CI/CD avec Beanstalk**

**GitHub Actions -> Déploiement auto :**

```yaml
name: Deploy to Beanstalk

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Generate deployment package
        run: zip -r deploy.zip . -x '*.git*'
      
      - name: Deploy to EB
        uses: einaregilsson/beanstalk-deploy@v21
        with:
          aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          application_name: flask-todo-beanstalk
          environment_name: FlaskTodo-env
          version_label: ${{ github.sha }}
          region: eu-west-3
          deployment_package: deploy.zip
```

---

**2. Blue/Green Deployment**

**Console Beanstalk -> Environment -> Clone environment**

```
Blue (Production actuelle)
Green (Nouvelle version)

Test Green -> Swap URLs -> Green devient production
```

**Zéro downtime.**

---

**3. Utiliser Docker avec Beanstalk**

**`Dockerfile` :**

```dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .

EXPOSE 5000
CMD ["gunicorn", "-b", "0.0.0.0:5000", "application:application"]
```

**Platform : Docker au lieu de Python**

---

**4. Managed database dans Beanstalk**

**[ATTENTION] Attention : Si tu crées RDS dans Beanstalk :**
- Lié à l'environnement
- Suppression de l'env -> RDS supprimée

**Pour production : RDS séparée (comme on a fait)**

---

**5. Custom domain avec Route 53**

**Route 53 -> Hosted zone -> Create record :**

```
Name : todo.mon-domaine.com
Type : CNAME
Value : flasktodo-env.eu-west-3.elasticbeanstalk.com
```

**Puis configurer le SSL dans Beanstalk (Load Balancer settings).**

---

**6. Worker environment pour tâches asynchrones**

**Créer un environnement Worker :**

```
Type : Worker
Queue : SQS
```

**Application consomme des messages SQS et traite les tâches.**

**Exemple : Envoyer des emails, générer des PDFs**

---

**7. Alerte automatique**

**CloudWatch -> Alarms -> Create alarm :**

```
Metric : Environment health
Condition : < 15 (Degraded)
Action : SNS -> Email
```

**Email automatique si l'app devient unhealthy.**

---

## [COURS] CONCLUSION DE L'EXERCICE 4

**[BRAVO] Félicitations ! Tu as déployé Flask sur Elastic Beanstalk ! [BRAVO]**

**Ce que tu as appris :**
- Comprendre PaaS vs IaaS
- Préparer une app Flask pour Beanstalk
- Créer un environnement Beanstalk
- Déployer via upload de zip
- Configurer variables d'environnement
- Connecter à RDS externe
- Monitorer avec CloudWatch
- Mettre à jour et rollback
- Passer en High Availability

**Compétences acquises :**
- [OK] Elastic Beanstalk (PaaS)
- [OK] Déploiement automatisé
- [OK] Configuration `.ebextensions`
- [OK] Auto-scaling
- [OK] Blue/Green deployment
- [OK] CI/CD ready

**Comparaison des 3 méthodes :**

| Critère | EC2 manuel | Beanstalk | Lambda |
|---------|------------|-----------|--------|
| Contrôle | ***** | *** | * |
| Simplicité | * | ***** | **** |
| Coût | Moyen | Moyen | Très bas |
| Maintenance | Élevée | Faible | Zéro |
| Scaling | Manuel | Auto | Infini |

**Pour Flask web app : Beanstalk est idéal ! [RAPIDE]**

**Temps moyen de réalisation :** 2-3 heures

---

**Prochaine étape :** Exercice 5 - Lambda + API Gateway (Serverless) ! [RAPIDE]

Veux-tu que je continue avec l'exercice 5 et les suivants (6-10) ? [OBJECTIF]

# [JAUNE] EXERCICE 5 : LAMBDA + API GATEWAY - ARCHITECTURE SERVERLESS

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es développeur dans une startup. L'équipe veut créer une API REST pour une application mobile TODO list. Les contraintes : coûts ultra-bas (payer seulement ce qu'on utilise), scaling automatique illimité, zero maintenance de serveurs.

### Cahier des charges

Le client souhaite :
- API REST complète (GET, POST, PUT, DELETE)
- Coût proche de zéro pour faible trafic
- Scaling automatique (1 à 1 million de requêtes)
- Pas de serveurs à gérer
- Haute disponibilité native
- Temps de réponse < 500ms

### Contraintes techniques

- Architecture : Serverless (AWS Lambda)
- API : API Gateway REST
- Base de données : RDS PostgreSQL (depuis exercice 3)
- Langage : Python 3.11
- Framework : Pas de Flask (Lambda pur)
- Temps estimé : 3-4 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre l'architecture serverless
- [OK] Créer des fonctions Lambda
- [OK] Configurer API Gateway
- [OK] Connecter Lambda à RDS
- [OK] Gérer les cold starts
- [OK] Utiliser Lambda Layers pour les dépendances
- [OK] Sécuriser avec IAM et VPC
- [OK] Monitorer avec CloudWatch
- [OK] Optimiser les coûts et performances

---

## [DOCS] PRÉREQUIS

- Exercice 3 terminé (RDS PostgreSQL)
- Connaissances Python
- Notions d'API REST
- Compréhension HTTP

---

## [IDEE] CONCEPTS AWS À COMPRENDRE

### Qu'est-ce que le Serverless ?

**Serverless** ≠ Sans serveur

**Serverless** = Tu n'as PAS À GÉRER les serveurs

**Analogie :**

```
Serveur traditionnel (EC2) :
Tu achètes une voiture
-> Tu payes l'essence (même quand tu ne roules pas)
-> Tu fais l'entretien
-> Tu trouves un parking

Serverless (Lambda) :
Tu appelles un Uber
-> Tu payes uniquement quand tu roules
-> Pas d'entretien
-> Pas de parking
```

---

### AWS Lambda - Expliqué en détail

**Lambda** = Fonction as a Service (FaaS)

**Principe :**

```
1. Tu écris une fonction Python
2. Tu l'uploades sur AWS
3. AWS l'exécute quand elle est appelée
4. Tu payes SEULEMENT pour le temps d'exécution
```

---

**Exemple concret :**

```python
def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': 'Hello from Lambda!'
    }
```

**C'est tout ! AWS gère le reste.**

---

### Anatomie d'une fonction Lambda

**Composants :**

```
Fonction Lambda
├── Runtime (Python 3.11, Node.js, Java, etc.)
├── Handler (point d'entrée)
├── Memory (128 MB - 10 GB)
├── Timeout (max 15 minutes)
├── Environment variables
├── IAM Role (permissions)
└── Layers (dépendances partagées)
```

---

#### **Runtime**

**Runtime** = Environnement d'exécution

**Runtimes disponibles :**
- Python 3.12, 3.11, 3.10, 3.9
- Node.js 20, 18, 16
- Java 17, 11, 8
- .NET 8, 6
- Ruby 3.2
- Go 1.x
- Custom runtime (n'importe quel langage)

**Pour nous : Python 3.11**

---

#### **Handler**

**Handler** = Fonction appelée par Lambda

**Format :** `nom_fichier.nom_fonction`

**Exemple :**

```
Fichier : app.py
Fonction : lambda_handler

Handler : app.lambda_handler
```

---

#### **Memory (RAM)**

**Configuration : 128 MB - 10 240 MB (10 GB)**

**Important :**

```
Plus de RAM = Plus de CPU aussi !

128 MB = 0.08 vCPU
1024 MB (1 GB) = 0.58 vCPU
3008 MB (~3 GB) = 1 vCPU complet
10240 MB (10 GB) = ~6 vCPU
```

**Stratégie :**
- Fonction simple : 128-512 MB
- Traitement moyen : 512-1024 MB
- Lourd (ML, images) : 1024-3008 MB

**Prix proportionnel à la RAM × temps d'exécution**

---

#### **Timeout**

**Temps maximum d'exécution : 1 seconde - 15 minutes**

**Défaut : 3 secondes**

**Si timeout dépassé -> Lambda arrête la fonction**

**Pour API REST : 3-30 secondes suffisent**

---

#### **Event**

**Event** = Données d'entrée de la fonction

**Exemple (API Gateway) :**

```json
{
  "httpMethod": "POST",
  "path": "/tasks",
  "body": "{\"title\": \"Acheter du pain\"}",
  "headers": {
    "Content-Type": "application/json"
  },
  "queryStringParameters": {
    "filter": "completed"
  }
}
```

---

#### **Context**

**Context** = Métadonnées sur l'invocation

```python
context.request_id          # ID unique de l'invocation
context.function_name       # Nom de la fonction
context.memory_limit_in_mb  # RAM allouée
context.log_group_name      # CloudWatch Log Group
```

---

### Cold Start vs Warm Start

**Problème majeur de Lambda : Cold Start**

#### **Cold Start (Démarrage à froid)**

```
1re invocation (ou après inactivité) :

[Créer conteneur] -> [Charger code] -> [Initialiser runtime] -> [Exécuter fonction]
     ~500ms            ~200ms            ~300ms                ~100ms
                                                          
Total : ~1100ms (1,1 seconde)
```

---

#### **Warm Start (Démarrage à chaud)**

```
2e invocation (conteneur déjà créé) :

[Réutiliser conteneur] -> [Exécuter fonction]
                              ~100ms

Total : ~100ms
```

**Différence : 10x plus rapide !**

---

**Durée de vie du conteneur : ~15 minutes d'inactivité**

```
Requête 1 (t=0s)     -> Cold start  (1100ms)
Requête 2 (t=2s)     -> Warm start  (100ms)
Requête 3 (t=10s)    -> Warm start  (100ms)
Requête 4 (t=20min)  -> Cold start  (1100ms) - conteneur recyclé
```

---

**Optimisations Cold Start :**

**1. Provisioned Concurrency**
- Garde X conteneurs toujours chauds
- [X] Coûteux (payer même si non utilisé)

**2. Initialiser hors du handler**
```python
# [X] MAUVAIS : Initialisation dans le handler
def lambda_handler(event, context):
    db = connect_to_database()  # Cold start chaque fois
    
# [OK] BON : Initialisation hors du handler
db = connect_to_database()  # Une seule fois (warm start réutilise)

def lambda_handler(event, context):
    # db déjà connecté
```

**3. Langage rapide**
- Python : ~200ms
- Node.js : ~150ms
- Go : ~100ms (le plus rapide)
- Java : ~500ms (le plus lent)

---

### API Gateway

**API Gateway** = Service pour créer des API REST/HTTP

**Rôle :**

```
Client (Mobile/Web)
    v
[API Gateway] (Endpoint public)
    v
[Lambda] (Logique métier)
    v
[RDS] (Base de données)
```

---

**Types d'API Gateway :**

#### **1. REST API**
- Complète (caching, throttling, etc.)
- Prix : $3.50 / million de requêtes
- Use case : Production, features avancées

#### **2. HTTP API**
- Simplifié, plus rapide
- Prix : $1.00 / million de requêtes
- Use case : Microservices simples

#### **3. WebSocket API**
- Connexions bidirectionnelles
- Use case : Chat, real-time

**Pour ce tuto : REST API (plus de features)**

---

### Concepts API Gateway

#### **Resources (Ressources)**

**Resource** = Chemin dans l'URL

```
/tasks          -> Resource "tasks"
/tasks/{id}     -> Resource "{id}" (sous-ressource)
```

---

#### **Methods (Méthodes)**

**Method** = Verbe HTTP

```
GET /tasks       -> Lister les tâches
POST /tasks      -> Créer une tâche
GET /tasks/{id}  -> Récupérer une tâche
PUT /tasks/{id}  -> Modifier une tâche
DELETE /tasks/{id} -> Supprimer une tâche
```

---

#### **Integration (Intégration)**

**Integration** = Backend appelé

**Types :**
- **Lambda** : Appelle une fonction Lambda
- **HTTP** : Proxy vers une URL externe
- **AWS Service** : Appelle un service AWS direct (DynamoDB, S3)
- **Mock** : Retourne une réponse statique

**Pour nous : Lambda**

---

#### **Stage (Environnement)**

**Stage** = Environnement de déploiement

```
API : todo-api

Stages :
- dev     -> https://abc123.execute-api.eu-west-3.amazonaws.com/dev
- staging -> https://abc123.execute-api.eu-west-3.amazonaws.com/staging
- prod    -> https://abc123.execute-api.eu-west-3.amazonaws.com/prod
```

**Chaque stage peut pointer vers des Lambdas différentes.**

---

#### **Deployment (Déploiement)**

**Deployment** = Snapshot de l'API à un instant T

```
Deployment 1 : Version initiale
Deployment 2 : Ajout endpoint /users
Deployment 3 : Correction bug

Chaque deployment -> associé à un stage
```

---

### Lambda + RDS : VPC et Cold Start

**Problème : Lambda doit accéder à RDS dans un VPC**

**Solution : Mettre Lambda dans le même VPC que RDS**

**Mais... Cold Start augmenté ! [ATTENTION]**

```
Lambda publique (hors VPC) :
Cold start : ~300ms

Lambda dans VPC :
Cold start : ~300ms + ~10-15s (ENI creation)
           = ~10-15 secondes !
```

**ENI** = Elastic Network Interface (carte réseau virtuelle)

---

**Optimisation depuis 2019 :**

AWS a amélioré le système -> Cold start VPC : ~1-2s maintenant

**Mais toujours plus lent que Lambda publique.**

---

**Alternatives pour éviter VPC Lambda :**

**1. RDS Data API (Aurora Serverless)**
- API HTTP pour interroger la DB
- Pas besoin de VPC
- [ATTENTION] Seulement Aurora (pas RDS standard)

**2. RDS Proxy**
- Pool de connexions managé
- Réduit les connexions DB
- Compatible avec Lambda dans VPC

**Pour ce tuto : Lambda dans VPC (standard)**

---

### Coûts AWS Lambda

**Modèle de prix :**

```
Prix = (Nombre de requêtes × $0.20 / million)
     + (Durée × RAM × $0.0000166667 / GB-seconde)
```

---

**Exemple concret :**

```
Application :
- 1 million de requêtes/mois
- 512 MB de RAM
- Temps d'exécution moyen : 200ms

Calcul :
Requêtes : 1M × $0.20/M = $0.20

Durée : 1M × 0.2s × 0.5GB × $0.0000166667
      = 1M × 0.1 GB-s × $0.0000166667
      = $1.67

Total : $0.20 + $1.67 = $1.87/mois
```

**Pour 1 million de requêtes : ~2 $/mois !**

---

**Comparaison EC2 vs Lambda :**

| Méthode | Coût mensuel | Trafic |
|---------|--------------|--------|
| **EC2 t2.micro** | $8-10 | Fixe |
| **Lambda** | $0.20 | 100K req/mois |
| **Lambda** | $2 | 1M req/mois |
| **Lambda** | $20 | 10M req/mois |

**Lambda = Idéal pour trafic variable ou faible**

---

**Free Tier Lambda :**

```
1 million de requêtes gratuites/mois (à vie)
400 000 GB-secondes gratuits/mois (à vie)
```

**Exemple gratuit :**

```
1M requêtes × 128 MB × 100ms
= 1M × 0.128 GB × 0.1s
= 12 800 GB-s
< 400 000 GB-s

-> GRATUIT ! [BRAVO]
```

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Préparer la structure du projet

**Sur ton ordinateur local :**

```bash
mkdir lambda-todo-api
cd lambda-todo-api
```

---

**Structure du projet :**

```
lambda-todo-api/
├── functions/
│   ├── create_task/
│   │   └── app.py
│   ├── list_tasks/
│   │   └── app.py
│   ├── get_task/
│   │   └── app.py
│   ├── update_task/
│   │   └── app.py
│   └── delete_task/
│       └── app.py
├── layers/
│   └── database/
│       └── python/
│           └── db.py
└── requirements.txt
```

**Approche : 1 Lambda par endpoint**

**Pourquoi ?**
- Déploiement indépendant
- Scaling indépendant
- Debugging plus facile
- Cold start réduit (code plus léger)

---

**Alternative (1 Lambda pour tout) :**

Possible mais :
- Cold start plus long (plus de code)
- Déploiement all-or-nothing
- Moins "serverless"

---

### ÉTAPE 2 : Créer la couche (Layer) pour la base de données

**Layer** = Code partagé entre plusieurs Lambdas

**Avantages :**
- Éviter la duplication
- Réduire la taille des Lambdas
- Mise à jour centralisée

---

**Créer l'arborescence :**

```bash
mkdir -p layers/database/python
```

**[ATTENTION] IMPORTANT : Respecter la structure `python/` !**

Lambda cherche les packages dans `python/` ou `nodejs/` selon le runtime.

---

**Créer `layers/database/python/db.py` :**

```bash
nano layers/database/python/db.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# DATABASE LAYER - CONNEXION POSTGRESQL
# ═══════════════════════════════════════════════════════════════

import psycopg2
import os
from psycopg2.extras import RealDictCursor

"""
psycopg2 : Driver PostgreSQL pour Python
RealDictCursor : Retourne les résultats en dictionnaires (pas en tuples)

Exemple sans RealDictCursor :
result = cursor.fetchone()
# -> (1, 'Acheter du pain', False)  # Tuple

Exemple avec RealDictCursor :
result = cursor.fetchone()
# -> {'id': 1, 'title': 'Acheter du pain', 'completed': False}  # Dict
"""

# ───────────────────────────────────────────────────────────────
# CONNEXION (HORS DU HANDLER POUR RÉUTILISATION)
# ───────────────────────────────────────────────────────────────

# Variables d'environnement (configurées dans Lambda)
DB_HOST = os.environ.get('DB_HOST')
DB_NAME = os.environ.get('DB_NAME')
DB_USER = os.environ.get('DB_USER')
DB_PASSWORD = os.environ.get('DB_PASSWORD')
DB_PORT = os.environ.get('DB_PORT', '5432')

"""
[ATTENTION] IMPORTANT : Connexion HORS de la fonction

Si la connexion est DANS la fonction :
-> Nouvelle connexion à chaque invocation (cold + warm)
-> Saturation des connexions DB

Si la connexion est HORS de la fonction :
-> Connexion créée au cold start
-> Réutilisée sur warm starts
-> Beaucoup plus efficace !
"""

# Variable globale pour la connexion
connection = None

def get_connection():
    """
    Retourne une connexion PostgreSQL (réutilise si possible)
    """
    global connection
    
    # Si pas de connexion OU connexion fermée
    if connection is None or connection.closed:
        print("Creating new database connection...")
        
        connection = psycopg2.connect(
            host=DB_HOST,
            database=DB_NAME,
            user=DB_USER,
            password=DB_PASSWORD,
            port=DB_PORT,
            # Options importantes pour Lambda
            connect_timeout=10,  # Timeout connexion
            keepalives=1,        # Keep-alive TCP
            keepalives_idle=30,  # Envoie keep-alive après 30s inactivité
            keepalives_interval=10,  # Intervalle entre keep-alives
            keepalives_count=5   # Nombre de tentatives avant déconnexion
        )
        
        """
        connect_timeout : Temps max pour établir la connexion
        
        keepalives_* : Évite que la connexion soit coupée
        pendant l'inactivité (entre 2 invocations Lambda)
        
        Sans keep-alive :
        Lambda invoquée (t=0s) -> Connexion ouverte
        Lambda idle (t=5min) -> Connexion coupée par RDS
        Lambda invoquée (t=5min+1s) -> Erreur "connection closed"
        
        Avec keep-alive :
        Connexion maintenue active -> Pas d'erreur
        """
        
    else:
        print("Reusing existing database connection")
    
    return connection

def execute_query(query, params=None, fetch_one=False, fetch_all=False):
    """
    Exécute une requête SQL
    
    Args:
        query (str): Requête SQL
        params (tuple): Paramètres de la requête (évite SQL injection)
        fetch_one (bool): Retourne 1 ligne
        fetch_all (bool): Retourne toutes les lignes
    
    Returns:
        dict ou list ou None
    """
    conn = get_connection()
    cursor = conn.cursor(cursor_factory=RealDictCursor)
    
    """
    cursor_factory=RealDictCursor :
    Les résultats sont des dictionnaires
    
    Facilite la conversion en JSON pour l'API
    """
    
    try:
        # Exécuter la requête
        cursor.execute(query, params or ())
        
        """
        params : Utilise le paramétrage SQL (sécurisé)
        
        [X] MAUVAIS (SQL injection) :
        query = f"SELECT * FROM tasks WHERE id = {user_input}"
        
        [OK] BON :
        query = "SELECT * FROM tasks WHERE id = %s"
        cursor.execute(query, (user_input,))
        
        psycopg2 échappe automatiquement les valeurs
        """
        
        # Récupérer les résultats si nécessaire
        if fetch_one:
            result = cursor.fetchone()
        elif fetch_all:
            result = cursor.fetchall()
        else:
            result = None
        
        # Commit (pour INSERT/UPDATE/DELETE)
        conn.commit()
        
        """
        commit() : Valide la transaction
        
        Sans commit :
        INSERT -> Données pas enregistrées
        
        PostgreSQL est en mode "autocommit=False" par défaut
        """
        
        return result
    
    except Exception as e:
        # Rollback en cas d'erreur
        conn.rollback()
        
        """
        rollback() : Annule la transaction en cours
        
        Exemple :
        INSERT tâche 1 -> OK
        INSERT tâche 2 -> Erreur
        rollback() -> Tâche 1 aussi annulée (atomicité)
        """
        
        print(f"Database error: {str(e)}")
        raise
    
    finally:
        cursor.close()

def init_database():
    """
    Crée la table tasks si elle n'existe pas
    
    Appelée au démarrage de chaque Lambda
    """
    query = """
    CREATE TABLE IF NOT EXISTS tasks (
        id SERIAL PRIMARY KEY,
        title VARCHAR(200) NOT NULL,
        description TEXT,
        completed BOOLEAN DEFAULT FALSE NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
    )
    """
    
    execute_query(query)
    print("Database initialized")

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde.**

---

### ÉTAPE 3 : Créer les fonctions Lambda

#### **Fonction 1 : Créer une tâche (POST /tasks)**

```bash
mkdir -p functions/create_task
nano functions/create_task/app.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# LAMBDA FUNCTION : CREATE TASK (POST /tasks)
# ═══════════════════════════════════════════════════════════════

import json
import sys

# Ajouter le layer au path Python
sys.path.append('/opt/python')

from db import execute_query, init_database

"""
/opt/python : Chemin où Lambda monte les layers

Avec le layer installé :
/opt/python/db.py est accessible

sys.path.append() permet d'importer db
"""

# Initialiser la DB au démarrage (hors handler)
init_database()

"""
[ATTENTION] IMPORTANT : init_database() HORS du handler

Exécuté une seule fois au cold start
Warm starts ne le réexécutent pas
"""

def lambda_handler(event, context):
    """
    Handler principal de la Lambda
    
    Args:
        event (dict): Données de la requête API Gateway
        context (object): Contexte Lambda (request_id, etc.)
    
    Returns:
        dict: Réponse HTTP
    """
    
    print(f"Event: {json.dumps(event)}")
    
    """
    Logging : Les print() vont dans CloudWatch Logs
    
    Utile pour le debugging
    """
    
    try:
        # Parser le body JSON
        body = json.loads(event.get('body', '{}'))
        
        """
        event['body'] : String JSON
        
        Exemple :
        event['body'] = '{"title": "Acheter du pain"}'
        
        json.loads() : Convertit string -> dict
        body = {'title': 'Acheter du pain'}
        """
        
        # Validation
        title = body.get('title')
        if not title:
            return {
                'statusCode': 400,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'  # CORS
                },
                'body': json.dumps({
                    'error': 'Title is required'
                })
            }
        
        description = body.get('description', '')
        
        # Insérer dans la base
        query = """
        INSERT INTO tasks (title, description)
        VALUES (%s, %s)
        RETURNING id, title, description, completed, created_at
        """
        
        """
        RETURNING : PostgreSQL retourne les valeurs insérées
        
        Équivalent à :
        INSERT ...
        SELECT * FROM tasks WHERE id = LAST_INSERT_ID()
        
        Mais en une seule requête (plus efficace)
        """
        
        result = execute_query(
            query,
            (title, description),
            fetch_one=True
        )
        
        """
        (title, description) : Tuple de paramètres
        
        Correspond aux %s dans la requête
        
        %s : Placeholder PostgreSQL (pas Python string format !)
        """
        
        # Formater la réponse
        task = {
            'id': result['id'],
            'title': result['title'],
            'description': result['description'],
            'completed': result['completed'],
            'created_at': result['created_at'].isoformat()
        }
        
        """
        .isoformat() : Convertit datetime -> string ISO 8601
        
        datetime(2024, 12, 16, 15, 30) -> "2024-12-16T15:30:00"
        
        JSON ne peut pas sérialiser datetime directement
        """
        
        return {
            'statusCode': 201,  # Created
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps(task)
        }
        
    except Exception as e:
        print(f"Error: {str(e)}")
        
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'error': 'Internal server error',
                'message': str(e)
            })
        }

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde.**

---

#### **Fonction 2 : Lister les tâches (GET /tasks)**

```bash
mkdir -p functions/list_tasks
nano functions/list_tasks/app.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# LAMBDA FUNCTION : LIST TASKS (GET /tasks)
# ═══════════════════════════════════════════════════════════════

import json
import sys

sys.path.append('/opt/python')
from db import execute_query, init_database

init_database()

def lambda_handler(event, context):
    """
    Liste toutes les tâches
    
    Supporte les query parameters :
    - completed=true : Filtrer par statut
    - limit=10 : Limiter le nombre de résultats
    """
    
    try:
        # Récupérer les query parameters
        query_params = event.get('queryStringParameters') or {}
        
        """
        event['queryStringParameters'] :
        
        Requête : GET /tasks?completed=true&limit=10
        
        query_params = {
            'completed': 'true',
            'limit': '10'
        }
        
        [ATTENTION] Valeurs toujours en STRING !
        """
        
        completed_filter = query_params.get('completed')
        limit = int(query_params.get('limit', 100))
        
        # Construire la requête SQL
        query = "SELECT * FROM tasks"
        params = []
        
        if completed_filter is not None:
            # Convertir string "true"/"false" -> boolean
            completed_bool = completed_filter.lower() == 'true'
            query += " WHERE completed = %s"
            params.append(completed_bool)
        
        query += " ORDER BY created_at DESC LIMIT %s"
        params.append(limit)
        
        """
        Construction dynamique de la requête
        
        Sans filtre :
        SELECT * FROM tasks ORDER BY created_at DESC LIMIT 100
        
        Avec filtre :
        SELECT * FROM tasks WHERE completed = true ORDER BY created_at DESC LIMIT 100
        """
        
        # Exécuter
        results = execute_query(query, tuple(params), fetch_all=True)
        
        # Formater les résultats
        tasks = []
        for row in results:
            tasks.append({
                'id': row['id'],
                'title': row['title'],
                'description': row['description'],
                'completed': row['completed'],
                'created_at': row['created_at'].isoformat()
            })
        
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'tasks': tasks,
                'count': len(tasks)
            })
        }
        
    except Exception as e:
        print(f"Error: {str(e)}")
        
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'error': 'Internal server error',
                'message': str(e)
            })
        }

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde.**

---

#### **Fonction 3 : Récupérer une tâche (GET /tasks/{id})**

```bash
mkdir -p functions/get_task
nano functions/get_task/app.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# LAMBDA FUNCTION : GET TASK (GET /tasks/{id})
# ═══════════════════════════════════════════════════════════════

import json
import sys

sys.path.append('/opt/python')
from db import execute_query, init_database

init_database()

def lambda_handler(event, context):
    """
    Récupère une tâche par son ID
    """
    
    try:
        # Récupérer l'ID depuis les path parameters
        path_params = event.get('pathParameters') or {}
        task_id = path_params.get('id')
        
        """
        event['pathParameters'] :
        
        Route : GET /tasks/{id}
        Requête : GET /tasks/5
        
        path_params = {'id': '5'}
        
        [ATTENTION] Toujours en STRING !
        """
        
        if not task_id:
            return {
                'statusCode': 400,
                'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'error': 'Task ID is required'})
            }
        
        # Convertir en int
        try:
            task_id = int(task_id)
        except ValueError:
            return {
                'statusCode': 400,
                'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'error': 'Invalid task ID'})
            }
        
        # Récupérer la tâche
        query = "SELECT * FROM tasks WHERE id = %s"
        result = execute_query(query, (task_id,), fetch_one=True)
        
        if not result:
            return {
                'statusCode': 404,
                'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'error': 'Task not found'})
            }
        
        # Formater
        task = {
            'id': result['id'],
            'title': result['title'],
            'description': result['description'],
            'completed': result['completed'],
            'created_at': result['created_at'].isoformat()
        }
        
        return {
            'statusCode': 200,
            'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
            'body': json.dumps(task)
        }
        
    except Exception as e:
        print(f"Error: {str(e)}")
        
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
            'body': json.dumps({'error': 'Internal server error', 'message': str(e)})
        }

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde.**

---

#### **Fonction 4 : Mettre à jour une tâche (PUT /tasks/{id})**

```bash
mkdir -p functions/update_task
nano functions/update_task/app.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# LAMBDA FUNCTION : UPDATE TASK (PUT /tasks/{id})
# ═══════════════════════════════════════════════════════════════

import json
import sys

sys.path.append('/opt/python')
from db import execute_query, init_database

init_database()

def lambda_handler(event, context):
    """
    Met à jour une tâche
    """
    
    try:
        # ID
        path_params = event.get('pathParameters') or {}
        task_id = path_params.get('id')
        
        if not task_id:
            return {
                'statusCode': 400,
                'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'error': 'Task ID is required'})
            }
        
        task_id = int(task_id)
        
        # Body
        body = json.loads(event.get('body', '{}'))
        
        # Construire la requête UPDATE dynamiquement
        fields_to_update = []
        params = []
        
        if 'title' in body:
            fields_to_update.append("title = %s")
            params.append(body['title'])
        
        if 'description' in body:
            fields_to_update.append("description = %s")
            params.append(body['description'])
        
        if 'completed' in body:
            fields_to_update.append("completed = %s")
            params.append(body['completed'])
        
        if not fields_to_update:
            return {
                'statusCode': 400,
                'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'error': 'No fields to update'})
            }
        
        # Ajouter l'ID à la fin des params
        params.append(task_id)
        
        # Construire la requête
        query = f"""
        UPDATE tasks
        SET {', '.join(fields_to_update)}
        WHERE id = %s
        RETURNING id, title, description, completed, created_at
        """
        
        """
        Construction dynamique :
        
        Si body = {'title': 'Nouveau titre', 'completed': True}
        
        query = '''
        UPDATE tasks
        SET title = %s, completed = %s
        WHERE id = %s
        RETURNING ...
        '''
        
        params = ['Nouveau titre', True, 5]
        """
        
        result = execute_query(query, tuple(params), fetch_one=True)
        
        if not result:
            return {
                'statusCode': 404,
                'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'error': 'Task not found'})
            }
        
        task = {
            'id': result['id'],
            'title': result['title'],
            'description': result['description'],
            'completed': result['completed'],
            'created_at': result['created_at'].isoformat()
        }
        
        return {
            'statusCode': 200,
            'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
            'body': json.dumps(task)
        }
        
    except Exception as e:
        print(f"Error: {str(e)}")
        
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
            'body': json.dumps({'error': 'Internal server error', 'message': str(e)})
        }

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde.**

---

#### **Fonction 5 : Supprimer une tâche (DELETE /tasks/{id})**

```bash
mkdir -p functions/delete_task
nano functions/delete_task/app.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# LAMBDA FUNCTION : DELETE TASK (DELETE /tasks/{id})
# ═══════════════════════════════════════════════════════════════

import json
import sys

sys.path.append('/opt/python')
from db import execute_query, init_database

init_database()

def lambda_handler(event, context):
    """
    Supprime une tâche
    """
    
    try:
        # ID
        path_params = event.get('pathParameters') or {}
        task_id = path_params.get('id')
        
        if not task_id:
            return {
                'statusCode': 400,
                'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'error': 'Task ID is required'})
            }
        
        task_id = int(task_id)
        
        # Vérifier que la tâche existe
        check_query = "SELECT id FROM tasks WHERE id = %s"
        existing = execute_query(check_query, (task_id,), fetch_one=True)
        
        if not existing:
            return {
                'statusCode': 404,
                'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
                'body': json.dumps({'error': 'Task not found'})
            }
        
        # Supprimer
        delete_query = "DELETE FROM tasks WHERE id = %s"
        execute_query(delete_query, (task_id,))
        
        return {
            'statusCode': 204,  # No Content
            'headers': {'Access-Control-Allow-Origin': '*'},
            'body': ''
        }
        
        """
        204 No Content : Succès sans body
        
        Standard REST pour DELETE réussi
        """
        
    except Exception as e:
        print(f"Error: {str(e)}")
        
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'},
            'body': json.dumps({'error': 'Internal server error', 'message': str(e)})
        }

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde.**

---

**Structure complète :**

```
lambda-todo-api/
├── functions/
│   ├── create_task/
│   │   └── app.py
│   ├── list_tasks/
│   │   └── app.py
│   ├── get_task/
│   │   └── app.py
│   ├── update_task/
│   │   └── app.py
│   └── delete_task/
│       └── app.py
└── layers/
    └── database/
        └── python/
            └── db.py
```

---

### ÉTAPE 4 : Créer le Lambda Layer (psycopg2)

**psycopg2 doit être compilé pour Amazon Linux.**

**On ne peut pas juste faire `pip install psycopg2` en local (macOS/Windows).**

---

**Solution 1 : Utiliser un layer pré-compilé (le plus simple)**

**Aller sur :**

```
https://github.com/jetbridge/psycopg2-lambda-layer
```

**Télécharger le zip pour Python 3.11 et la région eu-west-3.**

**Ou utiliser l'ARN public :**

```
arn:aws:lambda:eu-west-3:898466741470:layer:psycopg2-py311:1
```

**(Attention : Vérifier que le layer existe pour ta région)**

---

**Solution 2 : Compiler soi-même avec Docker (avancé)**

```bash
docker run -v "$PWD":/var/task "public.ecr.aws/sam/build-python3.11" /bin/sh -c "pip install -r requirements.txt -t python/lib/python3.11/site-packages/; exit"

zip -r psycopg2-layer.zip python
```

---

**Pour ce tuto : On va utiliser la solution 1 (ARN public).**

---

### ÉTAPE 5 : Créer le layer pour la logique DB

**Zipper le code de la base de données :**

```bash
cd layers/database
zip -r ../../database-layer.zip python/
cd ../..
```

**Résultat : `database-layer.zip` contenant `python/db.py`**

---

Je vais continuer avec les étapes de déploiement sur AWS dans le prochain message. Veux-tu que je continue ? [RAPIDE]

# [JAUNE] EXERCICE 5 : LAMBDA + API GATEWAY (SUITE)

### ÉTAPE 6 : Créer les Lambda Layers sur AWS

**Console AWS -> Chercher "Lambda"**

**Menu gauche -> Layers -> Create layer**

---

#### **Layer 1 : psycopg2 (driver PostgreSQL)**

**Layer name :**

```
psycopg2-python311
```

---

**Description :**

```
PostgreSQL driver for Python 3.11
```

---

**Upload method :**

```
[x] Provide a layer version ARN
```

**ARN :**

```
arn:aws:lambda:eu-west-3:898466741470:layer:psycopg2-py311:1
```

**[ATTENTION] IMPORTANT : Remplacer `eu-west-3` par ta région !**

**Liste des ARN publics par région :**

```
us-east-1: arn:aws:lambda:us-east-1:898466741470:layer:psycopg2-py311:1
eu-west-1: arn:aws:lambda:eu-west-1:898466741470:layer:psycopg2-py311:1
eu-west-3: arn:aws:lambda:eu-west-3:898466741470:layer:psycopg2-py311:1
ap-southeast-1: arn:aws:lambda:ap-southeast-1:898466741470:layer:psycopg2-py311:1
```

---

**Compatible runtimes :**

```
[x] Python 3.11
```

---

**[ATTENTION] Si l'ARN ne fonctionne pas dans ta région :**

**Alternative : Uploader le zip**

**Télécharger depuis GitHub :**

```
https://github.com/jetbridge/psycopg2-lambda-layer/releases
```

**Choisir : `psycopg2-py311-layer.zip`**

**Upload method : Upload a .zip file**

**Cliquer sur "Create"**

---

#### **Layer 2 : database (logique DB)**

**Create layer**

**Layer name :**

```
database-logic
```

---

**Description :**

```
Database connection and query utilities
```

---

**Upload method :**

```
[x] Upload a .zip file
```

**Cliquer sur "Upload" -> Sélectionner `database-layer.zip`**

---

**Compatible runtimes :**

```
[x] Python 3.11
```

---

**Cliquer sur "Create"**

**[OK] Layers créés !**

---

### ÉTAPE 7 : Créer un VPC Endpoint (optionnel mais recommandé)

**Pourquoi ?**

Lambda dans VPC doit accéder à Internet pour :
- Logs CloudWatch
- Autres services AWS

**Options :**

**1. NAT Gateway** (payant : ~$32/mois)

**2. VPC Endpoints** (gratuit pour CloudWatch Logs)

**On va créer un VPC Endpoint.**

---

**Console VPC -> Endpoints -> Create endpoint**

**Service category :**

```
[x] AWS services
```

---

**Services :**

Chercher et sélectionner :

```
com.amazonaws.eu-west-3.logs (CloudWatch Logs)
```

---

**VPC :**

```
Default VPC
```

---

**Subnets :**

```
[x] Sélectionner tous les subnets
```

---

**Security groups :**

**Créer un nouveau SG ou utiliser le default.**

**Inbound rules nécessaires :**

```
Type : HTTPS (443)
Source : VPC CIDR (172.31.0.0/16)
```

---

**Policy :**

```
Full access
```

---

**Cliquer sur "Create endpoint"**

**Durée : 2-3 minutes**

**[OK] VPC Endpoint créé !**

---

### ÉTAPE 8 : Créer les fonctions Lambda

**On va créer les 5 fonctions Lambda.**

---

#### **Fonction 1 : create_task**

**Console Lambda -> Create function**

---

**Function name :**

```
create-task
```

---

**Runtime :**

```
Python 3.11
```

---

**Architecture :**

```
[x] x86_64
```

**x86_64 vs arm64 :**
- x86_64 : Processeurs Intel/AMD (standard)
- arm64 : Processeurs ARM (Graviton2, 20% moins cher, 20% plus rapide)

**Pour ce tuto : x86_64 (plus compatible)**

---

**Permissions :**

```
[x] Create a new role with basic Lambda permissions
```

**AWS crée automatiquement un rôle IAM avec :**
- CloudWatch Logs (écriture)

---

**Cliquer sur "Create function"**

---

**Configuration de la fonction :**

**1. Code source**

**Code -> Upload from -> .zip file**

**Zipper le code :**

```bash
cd functions/create_task
zip -r ../../create-task.zip app.py
cd ../..
```

**Upload `create-task.zip`**

---

**2. Runtime settings**

**Handler :**

```
app.lambda_handler
```

**Déjà configuré par défaut.**

---

**3. Configuration**

**General configuration -> Edit**

**Memory :**

```
512 MB
```

**Timeout :**

```
30 seconds
```

**Ephemeral storage :**

```
512 MB (défaut)
```

---

**4. Environment variables**

**Configuration -> Environment variables -> Edit**

**Ajouter :**

```
DB_HOST = flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com
DB_NAME = todo_db
DB_USER = postgres
DB_PASSWORD = ton-mot-de-passe-rds
DB_PORT = 5432
```

**[ATTENTION] REMPLACER avec tes vraies valeurs RDS !**

---

**5. Layers**

**Code -> Layers -> Add a layer**

**Ajouter les 2 layers :**

**Layer 1 :**
```
Source : Custom layers
Layer : psycopg2-python311
Version : 1
```

**Layer 2 :**
```
Source : Custom layers
Layer : database-logic
Version : 1
```

**Cliquer sur "Add"**

---

**6. VPC Configuration**

**Configuration -> VPC -> Edit**

**VPC :**

```
Default VPC
```

---

**Subnets :**

```
[x] Sélectionner au moins 2 subnets (dans différentes AZ)
```

**Pourquoi 2 subnets ?**
- Haute disponibilité
- Si une AZ tombe, Lambda peut utiliser l'autre

---

**Security groups :**

**Créer ou sélectionner un SG avec :**

**Outbound rules :**
```
Type : PostgreSQL (5432)
Destination : Security Group de RDS
```

**Ou simplement :**
```
Type : All traffic
Destination : 0.0.0.0/0
```

---

**[ATTENTION] IMPORTANT : Autoriser ce SG Lambda dans RDS !**

**Console RDS -> flask-todo-db -> VPC security groups -> Edit inbound rules**

**Ajouter :**

```
Type : PostgreSQL (5432)
Source : Security Group Lambda
Description : Allow from Lambda
```

---

**Cliquer sur "Save" pour la config VPC**

---

**7. Permissions IAM (ajouter VPC permissions)**

**Configuration -> Permissions -> Execution role**

**Cliquer sur le rôle (ouvre IAM)**

**Add permissions -> Attach policies**

**Chercher et attacher :**

```
AWSLambdaVPCAccessExecutionRole
```

**Cette policy permet à Lambda de :**
- Créer des ENI (Elastic Network Interfaces)
- Accéder au VPC

**Sans cette policy : Lambda ne peut pas se connecter au VPC -> Erreur**

---

**[OK] Fonction create-task configurée !**

---

#### **Répéter pour les 4 autres fonctions**

**Fonction 2 : list-tasks**

```
Function name : list-tasks
Code : functions/list_tasks/app.py
Handler : app.lambda_handler
Memory : 512 MB
Timeout : 30s
Layers : psycopg2-python311 + database-logic
Environment variables : (mêmes que create-task)
VPC : Default VPC + 2 subnets + SG
```

---

**Fonction 3 : get-task**

```
Function name : get-task
Code : functions/get_task/app.py
Handler : app.lambda_handler
Memory : 256 MB (moins de RAM, fonction simple)
Timeout : 10s
Layers : psycopg2-python311 + database-logic
Environment variables : (mêmes)
VPC : (même config)
```

---

**Fonction 4 : update-task**

```
Function name : update-task
Code : functions/update_task/app.py
Handler : app.lambda_handler
Memory : 512 MB
Timeout : 30s
Layers : psycopg2-python311 + database-logic
Environment variables : (mêmes)
VPC : (même config)
```

---

**Fonction 5 : delete-task**

```
Function name : delete-task
Code : functions/delete_task/app.py
Handler : app.lambda_handler
Memory : 256 MB
Timeout : 10s
Layers : psycopg2-python311 + database-logic
Environment variables : (mêmes)
VPC : (même config)
```

---

**[ATTENTION] Astuce pour aller plus vite :**

**Créer la première fonction complètement.**

**Puis : Actions -> Export function -> Download deployment package**

**Pour les autres :**
- Remplacer `app.py`
- Re-zipper
- Upload

**Les layers, env vars, VPC, etc. : Copier-coller les configs.**

---

### ÉTAPE 9 : Tester les fonctions Lambda (avant API Gateway)

**On va tester chaque fonction indépendamment.**

**Console Lambda -> Fonction create-task -> Test**

---

**Create new test event**

**Event name :**

```
test-create-task
```

---

**Event JSON :**

```json
{
  "httpMethod": "POST",
  "path": "/tasks",
  "body": "{\"title\": \"Test Lambda\", \"description\": \"Tâche de test\"}",
  "headers": {
    "Content-Type": "application/json"
  },
  "queryStringParameters": null,
  "pathParameters": null
}
```

**Cliquer sur "Save"**

---

**Cliquer sur "Test"**

**Résultat attendu :**

```json
{
  "statusCode": 201,
  "headers": {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*"
  },
  "body": "{\"id\": 1, \"title\": \"Test Lambda\", \"description\": \"Tâche de test\", \"completed\": false, \"created_at\": \"2024-12-16T16:30:00\"}"
}
```

**[OK] Fonction fonctionne !**

---

**Si erreur :**

**Regarder les logs CloudWatch :**

**Monitor -> Logs -> View logs in CloudWatch**

**Erreurs courantes :**

**1. Timeout**
```
Task timed out after 3.00 seconds
```

**Solution : Augmenter le timeout**

---

**2. Connexion refusée**
```
psycopg2.OperationalError: could not connect to server
```

**Causes :**
- Security Group Lambda ne permet pas sortie vers RDS
- Security Group RDS ne permet pas entrée depuis Lambda
- Mauvais endpoint dans env vars

---

**3. Module not found**
```
ModuleNotFoundError: No module named 'psycopg2'
```

**Cause : Layer psycopg2 pas attaché ou incorrect**

**Solution : Vérifier les layers**

---

**4. Import error (db)**
```
ModuleNotFoundError: No module named 'db'
```

**Cause : Layer database-logic pas attaché**

**Solution : Vérifier les layers**

---

**Tester list-tasks :**

**Event JSON :**

```json
{
  "httpMethod": "GET",
  "path": "/tasks",
  "body": null,
  "headers": {},
  "queryStringParameters": null,
  "pathParameters": null
}
```

**Résultat attendu :**

```json
{
  "statusCode": 200,
  "body": "{\"tasks\": [{\"id\": 1, \"title\": \"Test Lambda\", ...}], \"count\": 1}"
}
```

---

**Tester get-task :**

**Event JSON :**

```json
{
  "httpMethod": "GET",
  "path": "/tasks/1",
  "pathParameters": {
    "id": "1"
  }
}
```

---

**Tester update-task :**

**Event JSON :**

```json
{
  "httpMethod": "PUT",
  "path": "/tasks/1",
  "pathParameters": {
    "id": "1"
  },
  "body": "{\"completed\": true}"
}
```

---

**Tester delete-task :**

**Event JSON :**

```json
{
  "httpMethod": "DELETE",
  "path": "/tasks/1",
  "pathParameters": {
    "id": "1"
  }
}
```

**Résultat : `statusCode: 204`**

---

**[OK] Toutes les fonctions Lambda fonctionnent !**

**Maintenant, on va les exposer via API Gateway.**

---

### ÉTAPE 10 : Créer l'API Gateway

**Console AWS -> API Gateway -> Create API**

---

**Choose an API type :**

```
[x] REST API
```

**REST API vs HTTP API :**

| Feature | REST API | HTTP API |
|---------|----------|----------|
| Prix | $3.50/M | $1.00/M |
| Caching | [OK] | [X] |
| API Keys | [OK] | [X] |
| Request validation | [OK] | [X] |
| WAF | [OK] | [X] |
| VPC Link | [OK] | [OK] |

**Pour production : REST API (plus de features)**

---

**Cliquer sur "Build" sous REST API**

---

**Create new API :**

```
[x] New API
```

---

**API name :**

```
todo-api
```

---

**Description :**

```
API REST pour application TODO list
```

---

**Endpoint Type :**

```
[x] Regional
```

**Types d'endpoints :**

**Regional :**
- API dans une région AWS
- Latence locale minimale
- [OK] Recommandé

**Edge Optimized :**
- API devant CloudFront
- Distribution mondiale
- Plus cher

**Private :**
- Accessible seulement depuis VPC
- Pas d'Internet

---

**Cliquer sur "Create API"**

---

### ÉTAPE 11 : Créer les resources et methods

**API créée avec une ressource racine `/`**

---

#### **Créer la ressource /tasks**

**Actions -> Create Resource**

**Resource Name :**

```
tasks
```

**Resource Path :**

```
/tasks (généré automatiquement)
```

---

**Enable API Gateway CORS :**

```
[x] Oui
```

**CORS** = Cross-Origin Resource Sharing

**Permet aux apps web (frontend) d'appeler l'API depuis un autre domaine.**

**Exemple :**

```
Frontend : https://mon-app.com
API : https://abc123.execute-api.eu-west-3.amazonaws.com

Sans CORS -> Navigateur bloque
Avec CORS -> Autorisé
```

---

**Cliquer sur "Create Resource"**

---

#### **Ajouter la méthode GET (lister les tâches)**

**Sélectionner `/tasks` -> Actions -> Create Method**

**Méthode : GET**

**Cliquer sur le [OK]**

---

**Configuration de l'intégration :**

**Integration type :**

```
[x] Lambda Function
```

---

**Use Lambda Proxy integration :**

```
[x] Coché
```

**Explication :**

**Proxy integration** = API Gateway transmet tout à Lambda (headers, body, etc.)

**Sans proxy :**
- API Gateway transforme la requête
- Lambda reçoit seulement le body
- Plus de configuration nécessaire

**Avec proxy :**
- Lambda reçoit TOUT (event complet)
- Plus flexible
- [OK] Recommandé

---

**Lambda Function :**

```
list-tasks
```

**Région : (ta région)**

---

**Use Default Timeout :**

```
[x] Coché
```

---

**Cliquer sur "Save"**

**AWS demande la permission d'invoquer Lambda :**

```
Add Permission to Lambda Function

You are about to give API Gateway permission to invoke your Lambda function:
arn:aws:lambda:eu-west-3:123456789012:function:list-tasks

OK | Cancel
```

**Cliquer sur "OK"**

**[ATTENTION] Cette permission est CRITIQUE !**

Sans elle :
```
API Gateway -> Lambda
[X] 403 Forbidden (pas la permission)
```

Avec elle :
```
API Gateway -> Lambda
[OK] 200 OK
```

---

#### **Ajouter la méthode POST (créer une tâche)**

**Sélectionner `/tasks` -> Actions -> Create Method**

**Méthode : POST**

**Configuration identique :**

```
Integration type : Lambda Function
Lambda Proxy integration : [OK]
Lambda Function : create-task
```

**Save -> OK**

---

#### **Créer la ressource /tasks/{id}**

**Sélectionner `/tasks` -> Actions -> Create Resource**

**Resource Name :**

```
{id}
```

**[ATTENTION] IMPORTANT : Bien mettre les accolades !**

**Resource Path :**

```
/tasks/{id}
```

---

**Enable API Gateway CORS :**

```
[x] Oui
```

---

**Create Resource**

---

#### **Ajouter GET /tasks/{id}**

**Sélectionner `/tasks/{id}` -> Actions -> Create Method**

**Méthode : GET**

**Configuration :**

```
Lambda Function : get-task
Lambda Proxy integration : [OK]
```

**Save -> OK**

---

#### **Ajouter PUT /tasks/{id}**

**Méthode : PUT**

**Configuration :**

```
Lambda Function : update-task
Lambda Proxy integration : [OK]
```

**Save -> OK**

---

#### **Ajouter DELETE /tasks/{id}**

**Méthode : DELETE**

**Configuration :**

```
Lambda Function : delete-task
Lambda Proxy integration : [OK]
```

**Save -> OK**

---

**Structure finale de l'API :**

```
/
└── /tasks
    ├── GET (list-tasks)
    ├── POST (create-task)
    └── /{id}
        ├── GET (get-task)
        ├── PUT (update-task)
        └── DELETE (delete-task)
```

---

### ÉTAPE 12 : Déployer l'API

**L'API n'est pas encore accessible. Il faut la déployer sur un stage.**

**Actions -> Deploy API**

---

**Deployment stage :**

```
[x] [New Stage]
```

---

**Stage name :**

```
prod
```

---

**Stage description :**

```
Production stage
```

---

**Deployment description :**

```
Initial deployment
```

---

**Cliquer sur "Deploy"**

---

**L'API est maintenant déployée ! [BRAVO]**

**Invoke URL :**

```
https://abc123xyz.execute-api.eu-west-3.amazonaws.com/prod
```

**C'est l'URL de base de ton API.**

**Endpoints :**

```
GET    /prod/tasks           -> Lister
POST   /prod/tasks           -> Créer
GET    /prod/tasks/{id}      -> Récupérer
PUT    /prod/tasks/{id}      -> Modifier
DELETE /prod/tasks/{id}      -> Supprimer
```

---

### ÉTAPE 13 : Tester l'API avec curl

**Copier ton Invoke URL :**

```bash
export API_URL="https://abc123xyz.execute-api.eu-west-3.amazonaws.com/prod"
```

---

**Test 1 : Créer une tâche**

```bash
curl -X POST $API_URL/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Acheter du pain", "description": "Boulangerie du coin"}'
```

**Résultat attendu :**

```json
{
  "id": 1,
  "title": "Acheter du pain",
  "description": "Boulangerie du coin",
  "completed": false,
  "created_at": "2024-12-16T17:00:00"
}
```

---

**Test 2 : Lister les tâches**

```bash
curl $API_URL/tasks
```

**Résultat :**

```json
{
  "tasks": [
    {
      "id": 1,
      "title": "Acheter du pain",
      "description": "Boulangerie du coin",
      "completed": false,
      "created_at": "2024-12-16T17:00:00"
    }
  ],
  "count": 1
}
```

---

**Test 3 : Récupérer une tâche**

```bash
curl $API_URL/tasks/1
```

---

**Test 4 : Mettre à jour**

```bash
curl -X PUT $API_URL/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'
```

---

**Test 5 : Supprimer**

```bash
curl -X DELETE $API_URL/tasks/1
```

**Résultat : (vide, status 204)**

---

**Test 6 : Vérifier la suppression**

```bash
curl $API_URL/tasks
```

**Résultat :**

```json
{
  "tasks": [],
  "count": 0
}
```

---

**[OK] L'API FONCTIONNE PARFAITEMENT ! [BRAVO]**

---

### ÉTAPE 14 : Monitorer avec CloudWatch

**Console CloudWatch -> Log groups**

**Tu verras un log group par Lambda :**

```
/aws/lambda/create-task
/aws/lambda/list-tasks
/aws/lambda/get-task
/aws/lambda/update-task
/aws/lambda/delete-task
```

---

**Cliquer sur `/aws/lambda/list-tasks`**

**Tu vois les logs de chaque invocation :**

```
START RequestId: abc123... Version: $LATEST
Event: {"httpMethod": "GET", "path": "/tasks", ...}
Reusing existing database connection
END RequestId: abc123...
REPORT RequestId: abc123... Duration: 150.23 ms Billed Duration: 151 ms Memory Size: 512 MB Max Memory Used: 85 MB Init Duration: 1200.45 ms
```

---

**Analyse du REPORT :**

```
Duration: 150.23 ms         -> Temps d'exécution réel
Billed Duration: 151 ms     -> Temps facturé (arrondi à la ms supérieure)
Memory Size: 512 MB         -> RAM allouée
Max Memory Used: 85 MB      -> RAM réellement utilisée
Init Duration: 1200.45 ms   -> Cold start time (si présent)
```

---

**Optimisation :**

**Si Max Memory Used = 85 MB et Memory Size = 512 MB :**

-> Tu payes pour 512 MB mais utilises seulement 85 MB

-> Réduire à 256 MB économiserait de l'argent

---

**Console Lambda -> Fonction -> Monitoring**

**Métriques disponibles :**

```
Invocations (nombre d'appels)
Duration (temps d'exécution)
Error count (nombre d'erreurs)
Throttles (requêtes limitées)
Concurrent executions (exécutions simultanées)
```

---

**API Gateway métriques :**

**Console API Gateway -> Stages -> prod -> Logs/Tracing**

**Activer CloudWatch Logs (optionnel) :**

```
[x] Enable CloudWatch Logs
Log level : INFO
Log full requests/responses data : [OK]
```

**[ATTENTION] Coût supplémentaire (logs stockés dans CloudWatch)**

---

### ÉTAPE 15 : Optimiser les performances

#### **1. Provisioned Concurrency (réduire cold starts)**

**Console Lambda -> Fonction -> Configuration -> Provisioned concurrency**

**Add configuration**

```
Version : $LATEST
Provisioned concurrency : 1
```

**Garde 1 conteneur toujours chaud.**

**Coût :**

```
1 instance × 512 MB × 24h × 30 jours
= ~$10/mois

Mais :
- Zéro cold start
- Temps de réponse constant
```

**Trade-off : Coût vs Performance**

---

#### **2. Reserved Concurrency (limiter le nombre d'instances)**

**Configuration -> Concurrency -> Reserved concurrency**

```
Reserved concurrency : 10
```

**Limite à 10 exécutions simultanées maximum.**

**Pourquoi limiter ?**

**Protection contre :**
- Coûts explosifs (attaque DDoS)
- Saturation de la base de données

**Exemple :**

Sans limite :
```
1000 requêtes/s -> 1000 Lambdas -> 1000 connexions DB -> RDS saturé
```

Avec limite :
```
1000 requêtes/s -> 10 Lambdas max -> 10 connexions DB -> RDS OK
Requêtes supplémentaires -> Throttled (429)
```

---

#### **3. RDS Proxy (connection pooling)**

**Problème : Lambda ouvre/ferme des connexions DB constamment**

**Chaque Lambda :**
```
Cold start -> Ouvre connexion DB
Warm start -> Réutilise connexion
Idle > 15min -> Connexion fermée
```

**Avec beaucoup de Lambdas :**
```
100 Lambdas simultanées = 100 connexions DB
RDS max_connections = 150 (db.t3.micro)
-> Risque de saturation
```

---

**Solution : RDS Proxy**

```
Lambda -> RDS Proxy -> RDS

RDS Proxy :
- Pool de connexions
- Réutilisation efficace
- Gère les connexions automatiquement
```

**Console RDS -> Proxies -> Create proxy**

**Configuration :**

```
Proxy identifier : todo-rds-proxy
Engine : PostgreSQL
Target group : flask-todo-db
Authentication : IAM ou Secrets Manager
VPC : Default VPC
Subnets : (sélectionner tous)
```

**Coût : ~$15/mois (0.015 $/heure)**

---

**Modifier Lambda pour utiliser le proxy :**

**Environment variables :**

```
DB_HOST = todo-rds-proxy.proxy-abc123.eu-west-3.rds.amazonaws.com
(au lieu de l'endpoint RDS direct)
```

**Avantages :**
- Connexions plus efficaces
- Moins de cold start DB
- Scaling automatique

---

#### **4. Caching API Gateway**

**Console API Gateway -> Stages -> prod -> Settings**

**Cache Settings :**

```
[x] Enable API cache

Cache capacity : 0.5 GB
Cache time-to-live (TTL) : 300 seconds (5 minutes)
```

**Coût : ~$15/mois (0.02 $/heure)**

---

**Activer le cache par méthode :**

**Resources -> GET /tasks -> Method Request**

**Settings :**

```
[x] Enable caching
Cache key parameters : (aucun pour GET /tasks)
```

**Pour GET /tasks?completed=true :**

```
Cache key parameters : completed
```

**Résultat :**

```
1re requête : GET /tasks -> Lambda -> 150ms
2e requête : GET /tasks -> Cache -> 10ms (15x plus rapide !)
```

---

### [OK] TESTS DE VALIDATION

**1. API accessible publiquement**

```bash
curl https://ton-api-url.amazonaws.com/prod/tasks
```

- [ ] Retourne un JSON valide
- [ ] Status 200

---

**2. CRUD complet fonctionne**

- [ ] POST /tasks -> Créer (201)
- [ ] GET /tasks -> Lister (200)
- [ ] GET /tasks/{id} -> Récupérer (200)
- [ ] PUT /tasks/{id} -> Modifier (200)
- [ ] DELETE /tasks/{id} -> Supprimer (204)

---

**3. Validation des erreurs**

```bash
# Tâche inexistante
curl https://ton-api-url/prod/tasks/999
# -> 404

# Body invalide
curl -X POST https://ton-api-url/prod/tasks \
  -H "Content-Type: application/json" \
  -d '{}'
# -> 400 (title required)
```

---

**4. Logs CloudWatch**

Console CloudWatch -> Log groups

- [ ] Logs présents pour chaque Lambda
- [ ] Pas d'erreurs visibles

---

**5. Cold start acceptable**

**Première invocation :**
```
Init Duration: 1000-2000 ms (avec VPC)
Duration: 100-300 ms
```

**Warm start :**
```
Duration: 100-200 ms
```

---

**6. Connexions DB réutilisées**

**Dans les logs, vérifier :**

```
Cold start : "Creating new database connection..."
Warm start : "Reusing existing database connection"
```

---

**7. Coûts sous contrôle**

**Console Billing -> Cost Explorer**

**Estimation pour 100K requêtes/mois :**

```
Lambda invocations : 100K × $0.20/M = $0.02
Lambda duration : 100K × 0.2s × 0.5GB × $0.0000166667 = $0.17
API Gateway : 100K × $3.50/M = $0.35

Total : ~$0.54/mois
```

**Free Tier (1 an) :**
```
Lambda : 1M requêtes gratuites
API Gateway : 1M requêtes gratuites (12 mois)

-> 100K/mois = GRATUIT pendant 1 an !
```

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : 502 Bad Gateway

**Symptôme :**

```json
{"message": "Internal server error"}
```

**Causes :**

**1. Lambda timeout**

Lambda met > 30s -> API Gateway timeout

**Solution :**
- Augmenter timeout Lambda
- Optimiser le code
- Vérifier connexion DB

---

**2. Lambda n'a pas la permission VPC**

```
Error: You do not have permission to create/manage ENI
```

**Solution :**

Rôle IAM Lambda -> Attacher `AWSLambdaVPCAccessExecutionRole`

---

**3. Format de réponse incorrect**

Lambda doit retourner :

```python
{
  'statusCode': 200,
  'body': '...',  # STRING (pas dict !)
  'headers': {...}
}
```

**[X] ERREUR :**

```python
return {'statusCode': 200, 'body': {'tasks': []}}  # body = dict
```

**[OK] CORRECT :**

```python
return {'statusCode': 200, 'body': json.dumps({'tasks': []})}  # body = string
```

---

#### Erreur 2 : Lambda ne peut pas se connecter à RDS

**Symptôme :**

```
psycopg2.OperationalError: timeout expired
```

**Causes :**

**1. Lambda pas dans le VPC**

Configuration -> VPC -> (aucun)

**Solution : Configurer VPC**

---

**2. Security Group RDS bloque Lambda**

**Solution :**

RDS SG Inbound -> Ajouter :
```
PostgreSQL (5432) depuis Lambda SG
```

---

**3. Lambda SG ne permet pas sortie vers RDS**

**Solution :**

Lambda SG Outbound -> Ajouter :
```
All traffic vers 0.0.0.0/0
(ou PostgreSQL vers RDS SG)
```

---

**4. Endpoint RDS incorrect**

Vérifier `DB_HOST` dans env vars.

---

#### Erreur 3 : CORS errors

**Symptôme (navigateur) :**

```
Access to fetch at '...' from origin '...' has been blocked by CORS policy
```

**Solution :**

**1. Activer CORS sur les ressources**

Resources -> /tasks -> Actions -> Enable CORS

---

**2. Headers CORS dans Lambda**

```python
'headers': {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type'
}
```

---

**3. Méthode OPTIONS manquante**

API Gateway crée automatiquement OPTIONS si CORS activé.

Vérifier : /tasks -> OPTIONS existe ?

---

#### Erreur 4 : Trop de connexions DB

**Symptôme :**

```
psycopg2.OperationalError: FATAL: remaining connection slots are reserved
```

**Cause : RDS max_connections dépassé**

**db.t3.micro : max 150 connexions**

**Solution 1 : Réduire Reserved Concurrency Lambda**

```
Reserved concurrency : 10
-> Max 10 connexions simultanées
```

---

**Solution 2 : Utiliser RDS Proxy**

Pool de connexions réutilisées.

---

**Solution 3 : Augmenter instance RDS**

```
db.t3.small : max 200 connexions
```

---

#### Erreur 5 : Cold start trop long (> 10s)

**Cause : Lambda dans VPC + ENI creation**

**Solutions :**

**1. Provisioned Concurrency**

Garde des conteneurs chauds.

---

**2. Réduire les dépendances**

Plus de code = Cold start plus long.

---

**3. Utiliser arm64**

Architecture ARM = 20% plus rapide.

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. Serverless = Pay per use**
- Pas de serveurs à gérer
- Coûts proportionnels à l'usage
- Scaling automatique illimité

**2. Architecture Lambda**
- 1 fonction = 1 responsabilité
- Layers pour code partagé
- VPC pour accès RDS

**3. Cold start**
- Première invocation lente (~1-2s avec VPC)
- Warm start rapide (~100ms)
- Optimiser avec Provisioned Concurrency

**4. API Gateway**
- Point d'entrée HTTP
- Intégration Lambda Proxy
- CORS pour frontend

**5. Monitoring**
- CloudWatch Logs pour debugging
- Métriques pour optimisation
- Alertes pour incidents

**6. Coûts**
- Lambda : $0.20/M requêtes + durée × RAM
- API Gateway : $3.50/M requêtes
- 100K req/mois = ~$0.50

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Sécurité avec API Keys**

**API Gateway -> API Keys -> Create**

```
API Key : abc123def456...
```

**Usage Plans -> Create**

```
Throttle : 1000 requêtes/s
Quota : 100K/mois
```

**Associer à l'API Key**

**Requêtes :**

```bash
curl https://api-url/prod/tasks \
  -H "x-api-key: abc123def456..."
```

---

**2. Authentification avec Cognito**

**Créer User Pool Cognito**

**API Gateway -> Authorizers -> Create**

```
Type : Cognito
User Pool : (ton pool)
```

**Méthodes -> Method Request -> Authorization : Cognito**

**Résultat : JWT token requis**

---

**3. Custom Domain**

**API Gateway -> Custom domain names -> Create**

```
Domain name : api.mon-domaine.com
Certificate : (ACM certificate)
```

**Base path mapping :**

```
Path : /
Stage : prod
```

**Route 53 -> Create record :**

```
Name : api.mon-domaine.com
Type : A - Alias
Target : API Gateway domain
```

---

**4. DynamoDB au lieu de RDS**

**Serverless complet :**

```
API Gateway -> Lambda -> DynamoDB
```

**Avantages :**
- Pas de VPC (cold start plus rapide)
- Scaling illimité
- Moins cher (pour faible trafic)

**Inconvénients :**
- NoSQL (moins flexible que SQL)
- Requêtes complexes difficiles

---

**5. Step Functions pour workflows**

**Orchestrer plusieurs Lambdas :**

```
[Lambda 1: Créer tâche]
        v
[Lambda 2: Envoyer notification]
        v
[Lambda 3: Logger dans analytics]
```

**Gestion d'erreurs, retries, parallélisme.**

---

**6. EventBridge pour événements**

**Architecture event-driven :**

```
Lambda -> EventBridge -> Multiple Lambdas

Exemple :
Tâche créée -> Event -> Email + Slack + Analytics
```

---

**7. Lambda@Edge**

**Exécuter Lambda au niveau CloudFront (edge) :**

```
Client -> CloudFront -> Lambda@Edge -> Origin

Use cases :
- A/B testing
- Auth
- URL rewriting
```

---

## [COURS] CONCLUSION DE L'EXERCICE 5

**[BRAVO] Félicitations ! Tu as créé une API REST serverless avec Lambda + API Gateway ! [BRAVO]**

**Ce que tu as appris :**
- Architecture serverless (FaaS)
- Créer et configurer Lambda
- Lambda Layers pour dépendances
- Connecter Lambda à RDS (VPC)
- Créer une API REST avec API Gateway
- Gérer cold starts
- Monitorer avec CloudWatch
- Optimiser coûts et performances

**Compétences acquises :**
- [OK] AWS Lambda (avancé)
- [OK] API Gateway REST
- [OK] VPC networking
- [OK] IAM policies
- [OK] CloudWatch monitoring
- [OK] Architecture serverless

**Comparaison des 4 méthodes de déploiement :**

| Critère | EC2 | Beanstalk | Lambda | Fargate |
|---------|-----|-----------|--------|---------|
| **Contrôle** | ***** | *** | ** | *** |
| **Simplicité** | * | **** | ***** | *** |
| **Coût (low traffic)** | $8-10 | $8-10 | $0.50 | $5 |
| **Scaling** | Manuel | Auto | Infini | Auto |
| **Cold start** | N/A | N/A | 1-2s | 10-30s |
| **Maintenance** | Élevée | Faible | Zéro | Faible |

**Lambda = Idéal pour APIs avec trafic variable ! [RAPIDE]**

**Coûts estimés :**

```
100K requêtes/mois :
EC2 : $8-10/mois
Beanstalk : $8-10/mois
Lambda : $0.50/mois

1M requêtes/mois :
EC2 : $8-10/mois (surchargé)
Beanstalk : $8-20/mois (auto-scaling)
Lambda : $2/mois

10M requêtes/mois :
Lambda : $20/mois
Beanstalk : $50-100/mois
```

**Temps moyen de réalisation :** 3-4 heures

---

**Prochaine étape :** Exercice 6 - ECS + ECR (Containers Docker) ! [DOCKER]

Veux-tu que je continue avec les exercices 6 à 10 ? Je vais créer des tutoriels sur :

- **Exercice 6** : ECS Fargate + ECR (Containerisation Docker)
- **Exercice 7** : Application Load Balancer + Auto Scaling Group
- **Exercice 8** : VPC personnalisé avec sous-réseaux publics/privés
- **Exercice 9** : CI/CD avec CodePipeline + CodeBuild + CodeDeploy
- **Exercice 10** : Architecture complète (tous les services intégrés)

Chaque exercice aura le même niveau de détail ultra-pédagogique ! [DOCS]

# [ROUGE] EXERCICE 6 : ECS FARGATE + ECR - CONTAINERS DOCKER

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es DevOps dans une entreprise qui veut moderniser son infrastructure. L'équipe souhaite containeriser l'application Flask TODO pour bénéficier de la portabilité Docker et du déploiement simplifié. Le CTO veut éviter de gérer des serveurs (EC2) tout en gardant le contrôle sur l'environnement d'exécution.

### Cahier des charges

Le client souhaite :
- Application containerisée avec Docker
- Déploiement serverless (sans gérer EC2)
- Registre d'images privé et sécurisé
- Load balancer avec health checks
- Auto-scaling basé sur CPU/RAM
- Rolling updates zero-downtime
- Logs centralisés

### Contraintes techniques

- Containerisation : Docker
- Orchestration : ECS (Elastic Container Service)
- Compute : Fargate (serverless containers)
- Registry : ECR (Elastic Container Registry)
- Base de données : RDS PostgreSQL (exercice 3)
- Load Balancer : Application Load Balancer
- Temps estimé : 4-5 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre Docker et la containerisation
- [OK] Créer un Dockerfile optimisé pour Flask
- [OK] Construire et tester des images Docker
- [OK] Utiliser ECR (registre Docker AWS)
- [OK] Comprendre ECS, Fargate, tasks et services
- [OK] Déployer des containers sur Fargate
- [OK] Configurer un ALB avec target groups
- [OK] Implémenter auto-scaling
- [OK] Monitorer avec CloudWatch Container Insights

---

## [DOCS] PRÉREQUIS

- Exercice 3 terminé (RDS PostgreSQL)
- Docker installé en local
- Compte AWS actif
- Connaissances Linux de base
- AWS CLI installé (optionnel)

---

## [IDEE] CONCEPTS AWS À COMPRENDRE

### Qu'est-ce que Docker ?

**Docker** = Plateforme de containerisation

**Analogie :**

```
Machine Virtuelle (VM) :
[ENTREPRISE] Immeuble complet avec :
- Fondations (hypervisor)
- Appartements (VMs)
- Chaque appart a sa cuisine, salle de bain, etc.

Container Docker :
[CONSTRUCTION] Immeuble avec appartements partagés :
- Fondations communes (kernel Linux)
- Appartements légers (containers)
- Cuisine/salle de bain partagées (OS)
```

**Avantages :**
- [RAPIDE] Plus léger (pas d'OS complet)
- [RAPIDE] Démarrage instantané (secondes vs minutes)
- [PACKAGE] Portabilité ("fonctionne partout")
- [ARGENT] Moins cher (plus de containers par serveur)

---

### Anatomie d'un Container Docker

**Image** = Template (classe en POO)

**Container** = Instance en cours d'exécution (objet en POO)

```
Dockerfile -> Image -> Container

Dockerfile :
FROM python:3.11
COPY app.py /app/
CMD ["python", "/app/app.py"]

Build :
docker build -t my-app .
-> Crée l'image "my-app"

Run :
docker run my-app
-> Crée un container depuis l'image
```

---

### VM vs Container

| Aspect | VM | Container |
|--------|-----|-----------|
| **OS** | OS complet | Partage l'OS hôte |
| **Taille** | Go (GB) | Mo (MB) |
| **Boot** | Minutes | Secondes |
| **Isolation** | Forte (hypervisor) | Moyenne (namespaces) |
| **Performance** | Overhead | Quasi-native |
| **Portabilité** | Moyenne | Excellente |

**Exemple concret :**

```
Application Flask + PostgreSQL

Avec VMs :
VM1 (Ubuntu 22.04, 20 GB) : Flask
VM2 (Ubuntu 22.04, 20 GB) : PostgreSQL
Total : 40 GB

Avec Containers :
Container Flask : 150 MB
Container PostgreSQL : 200 MB
Total : 350 MB (100x plus léger !)
```

---

### Qu'est-ce qu'Amazon ECS ?

**ECS** = Elastic Container Service

**ECS** = Orchestrateur de containers AWS

**Orchestrateur** = Gère le cycle de vie des containers :
- Démarrage
- Arrêt
- Scaling
- Load balancing
- Santé (health checks)
- Mise à jour

**Analogie :**

```
Docker = Moteur de voiture
ECS = Chef d'orchestre qui dirige une flotte de voitures

Chef d'orchestre décide :
- Combien de voitures lancer
- Où les placer
- Quand les remplacer
- Comment répartir le trafic
```

---

### ECS vs Kubernetes

| Critère | ECS | Kubernetes (EKS) |
|---------|-----|------------------|
| **Complexité** | Simple | Complexe |
| **Courbe d'apprentissage** | Douce | Raide |
| **Portabilité** | AWS uniquement | Multi-cloud |
| **Coût** | ECS gratuit (payer compute) | EKS $0.10/h (~$73/mois) |
| **Écosystème** | AWS natif | Open source |
| **Use case** | Apps AWS natives | Apps multi-cloud |

**ECS = Plus simple, AWS-centric**

**Kubernetes = Plus puissant, portable**

---

### ECS : EC2 vs Fargate

**2 modes de lancement ECS :**

#### **ECS sur EC2**

```
Tu gères :
- Instances EC2
- OS
- Mises à jour
- Scaling des instances

ECS gère :
- Placement des containers
- Orchestration
```

**Avantages :**
- Contrôle total
- Moins cher (si utilisation constante)

**Inconvénients :**
- Gérer les serveurs
- Configuration complexe

---

#### **ECS sur Fargate (Serverless)**

```
Tu gères :
- Containers (image Docker)

AWS gère :
- Infrastructure
- Serveurs
- OS
- Scaling
- Patches
```

**Avantages :**
- Zero management
- Pay-per-use (par seconde)
- Scaling automatique

**Inconvénients :**
- Moins de contrôle
- Plus cher (pour utilisation constante)

**Pour ce tuto : Fargate (serverless) !**

---

### Concepts ECS

#### **Cluster**

**Cluster** = Groupe logique de ressources ECS

```
Cluster "production"
├── Service "web-app" (3 tasks)
├── Service "api" (5 tasks)
└── Service "worker" (2 tasks)
```

**Cluster = Conteneur organisationnel**

---

#### **Task Definition**

**Task Definition** = Blueprint du container

**Contient :**
- Image Docker à utiliser
- CPU et RAM alloués
- Variables d'environnement
- Ports exposés
- Commande de démarrage
- Volumes
- Logs

**Analogie :**

```
Task Definition = Recette de cuisine
- Ingrédients (image Docker)
- Portions (CPU/RAM)
- Instructions (commande)

Task = Plat préparé selon la recette
```

---

#### **Task**

**Task** = Instance d'exécution d'une Task Definition

**1 Task = 1 ou plusieurs containers**

```
Task Definition "web-app"
├── Container "flask-app"
└── Container "nginx" (optionnel)

Quand lancé -> Task avec 2 containers
```

---

#### **Service**

**Service** = Maintient un nombre désiré de tasks

```
Service "web-app"
├── Desired count : 3
├── Running count : 3
└── Health checks : Actifs

Si 1 task meurt -> Service en relance automatiquement
```

**Service = Garantit la disponibilité**

---

### Architecture ECS + Fargate

```
                    Internet
                        v
            [Application Load Balancer]
                    <-     v     ->
            [Task-1] [Task-2] [Task-3]
            (Fargate) (Fargate) (Fargate)
                        v
                  [RDS PostgreSQL]
```

**Composants :**

1. **ALB** : Répartit le trafic
2. **Tasks** : Containers Flask
3. **Fargate** : Exécute les containers (serverless)
4. **RDS** : Base de données

---

### Amazon ECR (Elastic Container Registry)

**ECR** = Docker Hub privé sur AWS

**Docker Hub** = Registre public

```
docker pull nginx
-> Télécharge depuis Docker Hub public
```

**ECR** = Registre privé AWS

```
docker pull 123456789012.dkr.ecr.eu-west-3.amazonaws.com/my-app:latest
-> Télécharge depuis ton ECR privé
```

---

**Avantages ECR :**

- [VERROUILLE] Privé et sécurisé (IAM)
- [MONDE] Intégré AWS (pas de sortie réseau)
- [RECHERCHE] Scan de vulnérabilités automatique
- [ARGENT] 500 MB gratuits/mois (Free Tier)
- [RAPIDE] Transfert rapide (même région)

---

### Coûts ECS + Fargate

**ECS** = Gratuit (orchestrateur)

**Fargate** = Payer seulement le compute

**Modèle de prix :**

```
Prix = (vCPU × $0.04048 / vCPU / heure)
     + (RAM × $0.004445 / GB / heure)
```

**Exemple :**

```
Task :
- 0.25 vCPU
- 0.5 GB RAM
- 24/7 pendant 1 mois

CPU : 0.25 × $0.04048 × 24 × 30 = $7.29
RAM : 0.5 × $0.004445 × 24 × 30 = $1.60

Total : ~$9/mois par task
```

**Pour 3 tasks (haute dispo) : ~$27/mois**

---

**Comparaison avec EC2 :**

| Méthode | Coût/mois | Management | Scaling |
|---------|-----------|------------|---------|
| **EC2 t2.micro** | $8 | Manuel | Manuel |
| **Fargate 0.25 vCPU** | $9 | Zero | Auto |
| **Fargate 0.5 vCPU** | $18 | Zero | Auto |

**Fargate = Légèrement plus cher mais zero management**

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Préparer l'application Flask pour Docker

**Sur ton ordinateur local :**

```bash
mkdir flask-docker-app
cd flask-docker-app
```

---

**Créer `application.py` :**

```bash
nano application.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# APPLICATION FLASK POUR ECS FARGATE
# ═══════════════════════════════════════════════════════════════

from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import os
import sys

application = Flask(__name__)

# ───────────────────────────────────────────────────────────────
# CONFIGURATION
# ───────────────────────────────────────────────────────────────

# Base de données
application.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get(
    'DATABASE_URL',
    'sqlite:///todo.db'
)

"""
En production (ECS) : DATABASE_URL sera définie dans Task Definition
En local : Utilise SQLite
"""

application.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
application.config['SECRET_KEY'] = os.environ.get(
    'SECRET_KEY',
    'dev-secret-key-change-in-production'
)

db = SQLAlchemy(application)

# ───────────────────────────────────────────────────────────────
# MODÈLES
# ───────────────────────────────────────────────────────────────

class Task(db.Model):
    __tablename__ = 'task'
    
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    description = db.Column(db.Text, nullable=True)
    completed = db.Column(db.Boolean, default=False, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
    
    def to_dict(self):
        """Convertit en dictionnaire pour JSON"""
        return {
            'id': self.id,
            'title': self.title,
            'description': self.description,
            'completed': self.completed,
            'created_at': self.created_at.isoformat()
        }

# ───────────────────────────────────────────────────────────────
# ROUTES WEB
# ───────────────────────────────────────────────────────────────

@application.route('/')
def index():
    tasks = Task.query.order_by(Task.created_at.desc()).all()
    return render_template('index.html', tasks=tasks)

@application.route('/add', methods=['GET', 'POST'])
def add_task():
    if request.method == 'POST':
        title = request.form.get('title')
        description = request.form.get('description')
        
        if not title:
            flash('Le titre est obligatoire !', 'error')
            return redirect(url_for('add_task'))
        
        new_task = Task(title=title, description=description)
        db.session.add(new_task)
        db.session.commit()
        
        flash('Tâche ajoutée avec succès !', 'success')
        return redirect(url_for('index'))
    
    return render_template('add_task.html')

@application.route('/edit/<int:task_id>', methods=['GET', 'POST'])
def edit_task(task_id):
    task = Task.query.get_or_404(task_id)
    
    if request.method == 'POST':
        task.title = request.form.get('title')
        task.description = request.form.get('description')
        task.completed = 'completed' in request.form
        
        db.session.commit()
        
        flash('Tâche modifiée avec succès !', 'success')
        return redirect(url_for('index'))
    
    return render_template('edit_task.html', task=task)

@application.route('/delete/<int:task_id>')
def delete_task(task_id):
    task = Task.query.get_or_404(task_id)
    db.session.delete(task)
    db.session.commit()
    
    flash('Tâche supprimée avec succès !', 'success')
    return redirect(url_for('index'))

@application.route('/toggle/<int:task_id>')
def toggle_task(task_id):
    task = Task.query.get_or_404(task_id)
    task.completed = not task.completed
    db.session.commit()
    
    flash('Statut modifié !', 'success')
    return redirect(url_for('index'))

# ───────────────────────────────────────────────────────────────
# API REST (pour tests)
# ───────────────────────────────────────────────────────────────

@application.route('/api/tasks', methods=['GET'])
def api_list_tasks():
    """API : Lister les tâches"""
    tasks = Task.query.all()
    return jsonify({
        'tasks': [task.to_dict() for task in tasks],
        'count': len(tasks)
    })

@application.route('/api/tasks', methods=['POST'])
def api_create_task():
    """API : Créer une tâche"""
    data = request.get_json()
    
    if not data or 'title' not in data:
        return jsonify({'error': 'Title is required'}), 400
    
    new_task = Task(
        title=data['title'],
        description=data.get('description', '')
    )
    db.session.add(new_task)
    db.session.commit()
    
    return jsonify(new_task.to_dict()), 201

# ───────────────────────────────────────────────────────────────
# HEALTH CHECK
# ───────────────────────────────────────────────────────────────

@application.route('/health')
def health():
    """
    Endpoint de santé pour ALB
    
    ALB vérifie périodiquement cet endpoint
    Si retourne 200 -> Task healthy
    Si erreur -> Task unhealthy (remplacée)
    """
    try:
        # Vérifier la connexion DB
        db.session.execute('SELECT 1')
        
        return jsonify({
            'status': 'healthy',
            'database': 'connected'
        }), 200
    
    except Exception as e:
        return jsonify({
            'status': 'unhealthy',
            'error': str(e)
        }), 500

"""
Pourquoi un health check ?

Sans health check :
Task crash -> Continue de recevoir du trafic -> Erreurs 500

Avec health check :
Task crash -> ALB détecte -> Arrête d'envoyer du trafic -> Remplace la task
"""

# ───────────────────────────────────────────────────────────────
# INITIALISATION
# ───────────────────────────────────────────────────────────────

with application.app_context():
    db.create_all()
    print("Database initialized", file=sys.stderr)

"""
[ATTENTION] IMPORTANT : db.create_all() au démarrage

En production, utiliser des migrations (Alembic/Flask-Migrate)
Ici pour simplifier l'exercice
"""

# ───────────────────────────────────────────────────────────────
# POINT D'ENTRÉE
# ───────────────────────────────────────────────────────────────

if __name__ == '__main__':
    # Développement local
    application.run(debug=True, host='0.0.0.0', port=5000)
else:
    # Production (Gunicorn)
    print("Starting in production mode with Gunicorn", file=sys.stderr)

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde.**

---

**Créer `requirements.txt` :**

```bash
nano requirements.txt
```

**Contenu :**

```
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
psycopg2-binary==2.9.9
gunicorn==21.2.0
```

**`gunicorn` = Serveur WSGI pour production**

---

**Créer le dossier templates et les templates :**

```bash
mkdir templates
```

**Copier les templates de l'exercice 3 :**
- `templates/base.html`
- `templates/index.html`
- `templates/add_task.html`
- `templates/edit_task.html`

**(Même contenu que l'exercice 3)**

---

**Structure actuelle :**

```
flask-docker-app/
├── application.py
├── requirements.txt
└── templates/
    ├── base.html
    ├── index.html
    ├── add_task.html
    └── edit_task.html
```

---

### ÉTAPE 2 : Créer le Dockerfile

**Dockerfile** = Recette pour construire l'image Docker

```bash
nano Dockerfile
```

**Contenu :**

```dockerfile
# ═══════════════════════════════════════════════════════════════
# DOCKERFILE - FLASK TODO APP
# ═══════════════════════════════════════════════════════════════

# ───────────────────────────────────────────────────────────────
# STAGE 1 : BASE IMAGE
# ───────────────────────────────────────────────────────────────

FROM python:3.11-slim as base

"""
FROM : Image de base

python:3.11-slim :
- Python 3.11 pré-installé
- Variant "slim" = Minimaliste (~150 MB vs ~900 MB)
- Debian-based

Alternatives :
- python:3.11-alpine : Encore plus léger (~50 MB) mais parfois incompatible
- python:3.11 : Complet mais lourd (~900 MB)

Recommandation : slim (bon compromis)

as base : Nomme ce stage "base" (pour multi-stage builds)
"""

# Définir le répertoire de travail
WORKDIR /app

"""
WORKDIR : Change le répertoire courant dans le container

Équivalent à : cd /app

Tous les COPY, RUN, CMD suivants s'exécutent dans /app
"""

# Variables d'environnement Python
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

"""
ENV : Définit des variables d'environnement

PYTHONUNBUFFERED=1 :
- Désactive le buffering des sorties Python
- Les logs apparaissent immédiatement (pas de retard)
- CRITIQUE pour CloudWatch Logs

PYTHONDONTWRITEBYTECODE=1 :
- Ne crée pas de fichiers .pyc (bytecode compilé)
- Réduit la taille de l'image
- Pas nécessaire en container (éphémère)

PIP_NO_CACHE_DIR=1 :
- pip ne garde pas de cache
- Réduit la taille de l'image

PIP_DISABLE_PIP_VERSION_CHECK=1 :
- Ne vérifie pas si pip est à jour
- Accélère les builds
"""

# Installer les dépendances système
RUN apt-get update && apt-get install -y --no-install-recommends \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

"""
RUN : Exécute une commande pendant le build

apt-get update :
- Met à jour la liste des packages

apt-get install -y --no-install-recommends :
- -y : Répond "yes" automatiquement
- --no-install-recommends : N'installe pas les packages suggérés (réduit taille)

postgresql-client :
- Client PostgreSQL (psql)
- Nécessaire pour psycopg2
- Contient libpq (bibliothèque PostgreSQL)

&& rm -rf /var/lib/apt/lists/* :
- Nettoie le cache apt
- Réduit la taille de l'image (~20-30 MB économisés)
- Bonne pratique Docker

[ATTENTION] IMPORTANT : Tout en une seule commande RUN
Chaque RUN crée une nouvelle couche (layer)
Plus de layers = Image plus grosse
"""

# ───────────────────────────────────────────────────────────────
# STAGE 2 : DEPENDENCIES
# ───────────────────────────────────────────────────────────────

FROM base as dependencies

# Copier uniquement requirements.txt
COPY requirements.txt .

"""
COPY source destination

COPY requirements.txt . :
- Copie requirements.txt depuis l'hôte
- Vers le répertoire courant du container (/app)

Pourquoi copier seulement requirements.txt ici ?

Docker utilise le cache de build :
Si requirements.txt ne change pas -> Réutilise le cache
Si on copiait tout le code -> Invalidation du cache à chaque modif

Ordre optimal :
1. COPY requirements.txt
2. RUN pip install (cache si requirements.txt inchangé)
3. COPY le reste du code

Exemple :
Modif de application.py -> Étapes 1-2 en cache -> Rapide !
Modif de requirements.txt -> Rebuild depuis étape 2
"""

# Installer les dépendances Python
RUN pip install --no-cache-dir -r requirements.txt

"""
pip install :
- Installe les packages Python

--no-cache-dir :
- Ne garde pas de cache pip
- Réduit la taille

-r requirements.txt :
- Lit la liste depuis le fichier
"""

# ───────────────────────────────────────────────────────────────
# STAGE 3 : RUNTIME
# ───────────────────────────────────────────────────────────────

FROM base as runtime

# Copier les dépendances installées
COPY --from=dependencies /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=dependencies /usr/local/bin /usr/local/bin

"""
COPY --from=stage :
- Copie depuis un stage précédent (multi-stage build)

Multi-stage build :
- Stage 1 : Base + outils de build
- Stage 2 : Installation dépendances
- Stage 3 : Runtime final (seulement l'essentiel)

Avantage : Image finale plus légère

On copie seulement :
- Les packages Python installés (/usr/local/lib/python3.11/site-packages)
- Les binaires (/usr/local/bin) comme gunicorn

On NE copie PAS :
- Le cache pip
- Les fichiers temporaires
- Les outils de build
"""

# Copier le code de l'application
COPY application.py .
COPY templates/ templates/

"""
Copier tout le code de l'app

. (destination) = /app (WORKDIR défini plus haut)

Structure dans le container :
/app/
├── application.py
└── templates/
"""

# Créer un utilisateur non-root
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app

"""
Sécurité : Ne PAS exécuter en root

useradd :
- Crée un utilisateur

-m : Crée le home directory
-u 1000 : UID 1000 (standard pour premier utilisateur)

chown -R appuser:appuser /app :
- Change le propriétaire de /app
- appuser peut lire/écrire dans /app

Pourquoi ?
Si le container est compromis et tourne en root :
-> Attaquant a les pleins pouvoirs dans le container
-> Peut potentiellement échapper vers l'hôte

En non-root :
-> Permissions limitées
-> Attaque plus difficile
"""

USER appuser

"""
USER : Change l'utilisateur pour les commandes suivantes

Toutes les commandes après cette ligne s'exécutent en tant que appuser

CMD et ENTRYPOINT s'exécuteront aussi avec cet utilisateur
"""

# Exposer le port
EXPOSE 5000

"""
EXPOSE : Documente quel port l'app utilise

[ATTENTION] EXPOSE ne publie PAS réellement le port !
C'est juste de la documentation

Pour publier : docker run -p 5000:5000 ...
"""

# Variables d'environnement par défaut
ENV FLASK_APP=application.py \
    FLASK_ENV=production

"""
Variables pour Flask

En production, Flask sait qu'il ne doit pas utiliser debug mode
"""

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
    CMD python -c "import requests; requests.get('http://localhost:5000/health', timeout=2)" || exit 1

"""
HEALTHCHECK : Commande pour vérifier la santé du container

--interval=30s : Vérifier toutes les 30 secondes
--timeout=3s : Si > 3s, considérer comme failed
--start-period=40s : Attendre 40s avant de commencer (temps de démarrage)
--retries=3 : 3 échecs consécutifs -> container unhealthy

CMD : Commande à exécuter
Ici : Appeler /health en Python

|| exit 1 : Si erreur, exit code 1 (failure)

[ATTENTION] Problème : requests n'est pas dans requirements.txt !

Alternative (sans requests) :
HEALTHCHECK CMD curl -f http://localhost:5000/health || exit 1

Mais curl n'est pas installé dans python:slim

Solution pragmatique pour ECS :
Ne pas mettre de HEALTHCHECK dans Dockerfile
-> ECS a son propre système de health checks (ALB Target Group)
"""

# Commande de démarrage
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "--threads", "2", "--timeout", "60", "--access-logfile", "-", "--error-logfile", "-", "application:application"]

"""
CMD : Commande exécutée au démarrage du container

Format : ["executable", "param1", "param2", ...]

gunicorn : Serveur WSGI production
--bind 0.0.0.0:5000 : Écoute sur toutes les interfaces, port 5000
--workers 4 : 4 processus worker (formule : 2 × CPU + 1)
--threads 2 : 2 threads par worker (total : 4 × 2 = 8 threads)
--timeout 60 : Timeout de 60s pour les requêtes
--access-logfile - : Logs d'accès vers stdout
--error-logfile - : Logs d'erreur vers stderr
application:application : fichier:variable Flask

Pourquoi - pour les logfiles ?
- = stdout/stderr
-> Les logs vont dans les sorties standard
-> Docker/ECS les collecte automatiquement
-> Envoyés vers CloudWatch Logs

[ATTENTION] Alternative pour Fargate :
Fargate alloue 0.25-4 vCPU
Avec 0.25 vCPU, 4 workers = trop !

Formule adaptative :
--workers = $(( 2 * $(nproc) + 1 ))

Ou laisser Gunicorn décider :
--workers = 2 (pour Fargate 0.25 vCPU)
"""

# ═══════════════════════════════════════════════════════════════
```

**Sauvegarde.**

---

**Créer `.dockerignore` :**

```bash
nano .dockerignore
```

**Contenu :**

```
# Ne pas copier ces fichiers dans l'image Docker
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.db
*.sqlite
*.sqlite3
.env
.git/
.gitignore
.vscode/
.idea/
*.log
venv/
env/
*.md
Dockerfile
.dockerignore
```

**Similar à `.gitignore`, mais pour Docker.**

**Réduit la taille de l'image et accélère le build.**

---

### ÉTAPE 3 : Construire l'image Docker localement

**Tester le build en local avant de déployer sur AWS.**

```bash
docker build -t flask-todo-app .
```

**Décomposition de la commande :**

**`docker build`** = Construire une image

**`-t flask-todo-app`** = Tag (nom) de l'image

**`.`** = Context (répertoire courant)

---

**Résultat :**

```
[+] Building 45.2s (15/15) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 1.23kB
 => [internal] load .dockerignore
 => => transferring context: 120B
 => [internal] load metadata for docker.io/library/python:3.11-slim
 => [base 1/5] FROM docker.io/library/python:3.11-slim
 => [internal] load build context
 => => transferring context: 15.23kB
 => [base 2/5] WORKDIR /app
 => [base 3/5] ENV PYTHONUNBUFFERED=1 ...
 => [base 4/5] RUN apt-get update && apt-get install -y --no-install-recommends ...
 => [dependencies 1/2] COPY requirements.txt .
 => [dependencies 2/2] RUN pip install --no-cache-dir -r requirements.txt
 => [runtime 1/5] COPY --from=dependencies /usr/local/lib/python3.11/site-packages ...
 => [runtime 2/5] COPY application.py .
 => [runtime 3/5] COPY templates/ templates/
 => [runtime 4/5] RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
 => exporting to image
 => => exporting layers
 => => writing image sha256:abc123...
 => => naming to docker.io/library/flask-todo-app

[OK] Image construite avec succès !
```

---

**Vérifier l'image :**

```bash
docker images
```

**Résultat :**

```
REPOSITORY        TAG       IMAGE ID       CREATED          SIZE
flask-todo-app    latest    abc123def456   30 seconds ago   220MB
```

**220 MB = Taille raisonnable pour une image Flask**

---

### ÉTAPE 4 : Tester l'image Docker localement

**Lancer le container :**

```bash
docker run -d -p 5000:5000 --name flask-test flask-todo-app
```

**Décomposition :**

**`docker run`** = Créer et démarrer un container

**`-d`** = Detached mode (arrière-plan)

**`-p 5000:5000`** = Publier le port

```
Format : -p HOST_PORT:CONTAINER_PORT

5000:5000 = Port 5000 de l'hôte -> Port 5000 du container

Accès : http://localhost:5000
```

**`--name flask-test`** = Nom du container

**`flask-todo-app`** = Image à utiliser

---

**Vérifier que le container tourne :**

```bash
docker ps
```

**Résultat :**

```
CONTAINER ID   IMAGE            COMMAND                  STATUS         PORTS
abc123def456   flask-todo-app   "gunicorn --bind 0.0…"   Up 10 seconds  0.0.0.0:5000->5000/tcp
```

---

**Voir les logs :**

```bash
docker logs flask-test
```

**Résultat :**

```
Database initialized
[2024-12-16 18:00:00 +0000] [1] [INFO] Starting gunicorn 21.2.0
[2024-12-16 18:00:00 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2024-12-16 18:00:00 +0000] [7] [INFO] Booting worker with pid: 7
[2024-12-16 18:00:00 +0000] [8] [INFO] Booting worker with pid: 8
```

---

**Tester l'application :**

```bash
curl http://localhost:5000/health
```

**Résultat :**

```json
{
  "status": "healthy",
  "database": "connected"
}
```

**[OK] Container fonctionne !**

---

**Tester dans le navigateur :**

```
http://localhost:5000
```

**Page d'accueil s'affiche.**

**[ATTENTION] Note : Avec SQLite (DB locale dans le container)**

**Pour RDS, on configurera ça dans ECS.**

---

**Arrêter et supprimer le container :**

```bash
docker stop flask-test
docker rm flask-test
```

---

### ÉTAPE 5 : Créer un repository ECR

**Console AWS -> Chercher "ECR"**

**Elastic Container Registry -> Repositories -> Create repository**

---

**Visibility settings :**

```
[x] Private
```

**Private vs Public :**
- Private : Nécessite authentification AWS
- Public : Accessible publiquement (comme Docker Hub)

---

**Repository name :**

```
flask-todo-app
```

**[ATTENTION] IMPORTANT : Nom en minuscules, pas d'espaces**

---

**Tag immutability :**

```
[ ] Disabled
```

**Immutability :**

```
Disabled : Peut remplacer un tag existant
  docker push my-app:v1.0
  docker push my-app:v1.0 (remplace l'ancien)

Enabled : Tag immuable (ne peut pas être remplacé)
  docker push my-app:v1.0
  docker push my-app:v1.0 -> Erreur !
```

**Pour ce tuto : Disabled (plus flexible)**

---

**Scan on push :**

```
[x] Enabled
```

**Scan de vulnérabilités automatique.**

**ECR scanne l'image après chaque push.**

**Gratuit pour 1 scan/image/mois.**

---

**Encryption settings :**

```
[x] AES-256
```

**Chiffrement des images au repos.**

---

**Cliquer sur "Create repository"**

---

**Repository créé ! [BRAVO]**

**URI du repository :**

```
123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app
```

**Format : `AWS_ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/REPO_NAME`**

---

### ÉTAPE 6 : Pousser l'image vers ECR

**On va pousser l'image Docker vers ECR.**

---

#### **1. Installer AWS CLI (si pas déjà fait)**

**macOS :**

```bash
brew install awscli
```

**Linux :**

```bash
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
```

**Windows :**

Télécharger depuis : https://aws.amazon.com/cli/

---

**Vérifier :**

```bash
aws --version
```

**Résultat : `aws-cli/2.x.x ...`**

---

#### **2. Configurer AWS CLI**

```bash
aws configure
```

**Prompts :**

```
AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name: eu-west-3
Default output format: json
```

**Où trouver les clés ?**

**Console AWS -> Mon compte (coin supérieur droit) -> Security credentials**

**Access keys -> Create access key**

**[ATTENTION] IMPORTANT : Sauvegarder la clé secrète, elle ne sera plus visible !**

---

#### **3. Se connecter à ECR**

```bash
aws ecr get-login-password --region eu-west-3 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.eu-west-3.amazonaws.com
```

**[ATTENTION] REMPLACER :**
- `eu-west-3` : Ta région
- `123456789012` : Ton AWS Account ID

---

**Décomposition de la commande :**

**`aws ecr get-login-password`** = Obtenir le mot de passe ECR

**`|`** = Pipe (passe la sortie à la commande suivante)

**`docker login --username AWS --password-stdin`** = Se connecter à Docker

**`--username AWS`** = Username toujours "AWS" pour ECR

**`--password-stdin`** = Lire le mot de passe depuis stdin (le pipe)

---

**Résultat :**

```
Login Succeeded
```

**[OK] Authentifié auprès d'ECR !**

---

#### **4. Taguer l'image pour ECR**

```bash
docker tag flask-todo-app:latest 123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app:latest
```

**Format :**

```
docker tag LOCAL_IMAGE:TAG ECR_URI:TAG
```

**Explication :**

Docker utilise des tags pour identifier les images.

Pour pousser vers ECR, l'image doit avoir le tag ECR complet.

---

**Vérifier :**

```bash
docker images
```

**Résultat :**

```
REPOSITORY                                                    TAG      IMAGE ID       SIZE
flask-todo-app                                                 latest   abc123def456   220MB
123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app   latest   abc123def456   220MB
```

**Même IMAGE ID = Même image, 2 tags différents**

---

#### **5. Pousser l'image vers ECR**

```bash
docker push 123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app:latest
```

**Durée : 1-3 minutes (selon connexion)**

**Résultat :**

```
The push refers to repository [123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app]
abc123: Pushed
def456: Pushed
ghi789: Pushed
latest: digest: sha256:abc123... size: 1234
```

**[OK] Image poussée vers ECR ! [BRAVO]**

---

**Vérifier dans la console ECR :**

**ECR -> Repositories -> flask-todo-app -> Images**

**Tu vois :**

```
Image tag : latest
Pushed at : il y a quelques minutes
Size : 220 MB
Scan status : COMPLETE (ou IN_PROGRESS)
```

---

**Cliquer sur l'image -> Vulnerabilities**

**ECR affiche les vulnérabilités trouvées :**

```
Critical : 0
High : 2
Medium : 5
Low : 10
```

**Tu peux voir les détails et les CVE.**

---

Je vais continuer avec la création du cluster ECS et le déploiement sur Fargate dans le prochain message. Veux-tu que je continue ? [RAPIDE]


# [ROUGE] EXERCICE 6 : ECS FARGATE + ECR (SUITE)

### ÉTAPE 7 : Créer un cluster ECS

**Console AWS -> ECS -> Clusters -> Create cluster**

---

**Cluster configuration :**

**Cluster name :**

```
flask-todo-cluster
```

---

**Namespace (optionnel) :**

```
(laisser vide)
```

**Namespace = Service Discovery (DNS interne pour les containers)**

**Utile pour microservices qui communiquent entre eux.**

---

**Infrastructure :**

```
[x] AWS Fargate (serverless)
```

**Options :**
- **AWS Fargate** : Serverless (pas de serveurs à gérer)
- **Amazon EC2** : Tu gères les instances
- **External** : Serveurs on-premise

**Pour ce tuto : Fargate**

---

**Monitoring :**

```
[x] Use Container Insights
```

**Container Insights** = Métriques détaillées dans CloudWatch

**Métriques disponibles :**
- CPU par container
- RAM par container
- Réseau par container
- Stockage

**Coût :** ~$0.30/container/mois

**Utile pour debug et optimisation.**

---

**Tags (optionnel) :**

```
Key: Projet | Value: TODO-ECS
Key: Environnement | Value: Production
```

---

**Cliquer sur "Create"**

**Durée : 10-20 secondes**

**[OK] Cluster créé !**

---

**Le cluster est vide pour l'instant.**

```
flask-todo-cluster
├── Services : 0
├── Tasks : 0
└── Capacity providers : Fargate, Fargate Spot
```

**On va créer une Task Definition puis un Service.**

---

### ÉTAPE 8 : Créer une Task Definition

**Task Definition** = Blueprint du container

**ECS -> Task Definitions -> Create new task definition**

---

**Task definition family :**

```
flask-todo-task
```

**Family** = Groupe de versions

```
Versions :
flask-todo-task:1
flask-todo-task:2
flask-todo-task:3
```

**À chaque modification -> nouvelle version.**

---

#### **Infrastructure requirements**

**Launch type :**

```
[x] AWS Fargate
```

---

**Operating system/Architecture :**

```
Linux/X86_64
```

**Architectures disponibles :**
- Linux/X86_64 : Standard (Intel/AMD)
- Linux/ARM64 : AWS Graviton (20% moins cher, 20% plus rapide)

**Pour ce tuto : X86_64 (notre image Docker est x86)**

---

**Task size :**

**CPU :**

```
0.5 vCPU
```

**Memory :**

```
1 GB
```

**Combinaisons valides Fargate :**

| CPU | RAM disponible |
|-----|----------------|
| 0.25 vCPU | 0.5 GB, 1 GB, 2 GB |
| 0.5 vCPU | 1 GB, 2 GB, 3 GB, 4 GB |
| 1 vCPU | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB |
| 2 vCPU | 4 GB - 16 GB (incréments de 1 GB) |
| 4 vCPU | 8 GB - 30 GB (incréments de 1 GB) |

**Choix pour notre app :**
- 0.5 vCPU + 1 GB = ~$18/mois
- Suffisant pour charge modérée

---

**Task role :**

```
(laisser vide pour l'instant)
```

**Task Role** = Permissions IAM pour l'application

**Exemple :**
- Accéder à S3
- Envoyer des emails (SES)
- Publier sur SNS

**Notre app n'a pas besoin (juste RDS via VPC).**

---

**Task execution role :**

```
[x] Create new role (auto-généré)
```

**Task Execution Role** = Permissions IAM pour ECS

**Nécessaire pour :**
- Puller l'image depuis ECR
- Écrire les logs vers CloudWatch
- Récupérer les secrets (Secrets Manager)

**ECS crée automatiquement : `ecsTaskExecutionRole`**

---

#### **Container - 1**

**Container details :**

**Name :**

```
flask-app
```

---

**Image URI :**

```
123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app:latest
```

**[ATTENTION] REMPLACER avec ton URI ECR !**

**Copier depuis : ECR -> flask-todo-app -> Images -> Copy URI**

---

**Essential container :**

```
[x] Yes
```

**Essential** = Si ce container crash, toute la task est arrêtée

**Scenarios :**

```
Essential = Yes :
Container crash -> Task arrêtée -> Service relance une nouvelle task

Essential = No :
Container crash -> Task continue (avec les autres containers)
```

**Pour une app web : Essential = Yes**

---

**Port mappings :**

**Container port :**

```
5000
```

**Protocol :**

```
TCP
```

**App name :**

```
(laisser vide)
```

---

**Explication :**

```
Container écoute sur port 5000 (Flask/Gunicorn)

Fargate mappe automatiquement :
Port éphémère aléatoire (32768-65535) -> Port 5000 du container

Load Balancer -> Port éphémère -> Port 5000 container
```

**Pas besoin de spécifier le port externe (Fargate le gère).**

---

#### **Environment variables**

**Ajouter les variables d'environnement :**

**Cliquer sur "Add environment variable"**

---

**Variable 1 :**

```
Key : DATABASE_URL
Value : postgresql://postgres:MOT_DE_PASSE@flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com:5432/todo_db
```

**[ATTENTION] REMPLACER :**
- `MOT_DE_PASSE` : Mot de passe RDS
- `flask-todo-db.c...` : Endpoint RDS

---

**Variable 2 :**

```
Key : SECRET_KEY
Value : ta-cle-secrete-super-longue-aleatoire-123456789
```

---

**Variable 3 :**

```
Key : FLASK_ENV
Value : production
```

---

**[ATTENTION] SÉCURITÉ : Mots de passe en clair !**

**Problème :**

Variables d'environnement visibles dans :
- Console ECS
- Describe task API
- CloudWatch Logs (si loggées)

**Solution (avancée) : AWS Secrets Manager**

```
Key : DATABASE_URL
ValueFrom : arn:aws:secretsmanager:eu-west-3:123456789012:secret:rds-password-abc123
Type : Secret
```

**Secrets Manager :**
- Stockage sécurisé
- Rotation automatique
- Chiffrement
- Auditable

**Coût : $0.40/secret/mois + $0.05 / 10K requêtes**

**Pour ce tuto : Variables simples (mais attention en production !)**

---

#### **HealthCheck (optionnel)**

**Command :**

```
CMD-SHELL, curl -f http://localhost:5000/health || exit 1
```

**[ATTENTION] Problème : curl pas installé dans notre image**

**Alternative : Laisser vide**

**ECS utilisera le health check du Load Balancer (plus fiable).**

---

#### **Logging**

**Log configuration :**

```
[x] Use log collection
```

**Log driver :**

```
awslogs
```

**awslogs** = CloudWatch Logs

---

**Log group :**

```
/ecs/flask-todo-task (auto-généré)
```

---

**Stream prefix :**

```
ecs
```

**Format des logs :**

```
/ecs/flask-todo-task/ecs/CONTAINER_NAME/TASK_ID
```

---

#### **Storage**

**Ephemeral storage :**

```
21 GiB (défaut)
```

**Ephemeral storage** = Disque temporaire

**Caractéristiques :**
- Éphémère (perdu quand task arrêtée)
- Max 200 GiB
- Inclus : 20 GiB gratuits
- Au-delà : $0.0001112/GiB/heure (~$0.08/GiB/mois)

**Pour notre app : 21 GiB suffisant**

---

**Volumes (optionnel) :**

```
(aucun)
```

**Volumes = Stockage persistant**

**Types :**
- **EFS** : Stockage partagé entre tasks
- **Docker volumes** : Éphémère

**Notre app n'a pas besoin (DB dans RDS).**

---

#### **Tags**

```
Key: Environnement | Value: Production
```

---

**Cliquer sur "Create"**

**[OK] Task Definition créée ! [BRAVO]**

---

**Révision créée :**

```
flask-todo-task:1
```

**Chaque modification crée une nouvelle révision.**

---

### ÉTAPE 9 : Créer un Application Load Balancer

**Avant de créer le Service ECS, on a besoin d'un Load Balancer.**

**Console EC2 -> Load Balancers -> Create load balancer**

---

**Load balancer types :**

```
[x] Application Load Balancer
```

**Types de LB :**

**Application Load Balancer (ALB) :**
- HTTP/HTTPS (Layer 7)
- Routing avancé (path, host, headers)
- WebSocket support
- [OK] Recommandé pour web apps

**Network Load Balancer (NLB) :**
- TCP/UDP (Layer 4)
- Ultra performance (millions req/s)
- IP statiques
- Use case : Gaming, IoT

**Gateway Load Balancer :**
- Appliances virtuelles (firewalls, IDS)
- Transparent

---

**Cliquer sur "Create" sous Application Load Balancer**

---

#### **Basic configuration**

**Load balancer name :**

```
flask-todo-alb
```

---

**Scheme :**

```
[x] Internet-facing
```

**Internet-facing vs Internal :**

```
Internet-facing :
- IP publique
- Accessible depuis Internet
- Use case : Sites web publics

Internal :
- Pas d'IP publique
- Accessible seulement depuis VPC
- Use case : Microservices internes
```

---

**IP address type :**

```
[x] IPv4
```

---

#### **Network mapping**

**VPC :**

```
Default VPC
```

---

**Mappings :**

```
[x] Sélectionner AU MOINS 2 Availability Zones
```

**Exemple :**

```
[x] eu-west-3a (Subnet public)
[x] eu-west-3b (Subnet public)
[x] eu-west-3c (Subnet public)
```

**Pourquoi 2+ AZs ?**

**Haute disponibilité :**

```
AZ-a tombe -> ALB route vers AZ-b (pas de downtime)
```

**Minimum : 2 AZs**

**Recommandé : 3 AZs**

---

#### **Security groups**

**Créer un nouveau Security Group :**

**Ouvrir un nouvel onglet -> EC2 -> Security Groups -> Create security group**

---

**Security group name :**

```
flask-todo-alb-sg
```

---

**Description :**

```
Security group for Flask TODO ALB
```

---

**VPC :**

```
Default VPC
```

---

**Inbound rules :**

**Add rule 1 :**

```
Type : HTTP
Protocol : TCP
Port : 80
Source : 0.0.0.0/0
Description : Allow HTTP from Internet
```

**Add rule 2 (optionnel si HTTPS) :**

```
Type : HTTPS
Protocol : TCP
Port : 443
Source : 0.0.0.0/0
Description : Allow HTTPS from Internet
```

---

**Outbound rules :**

```
(laisser par défaut : All traffic vers 0.0.0.0/0)
```

---

**Create security group**

---

**Retourner dans l'onglet ALB**

**Sélectionner le SG créé :**

```
[x] flask-todo-alb-sg
```

**[ATTENTION] Décocher le "default" SG si présent**

---

#### **Listeners and routing**

**Listener** = Port d'écoute du Load Balancer

**Default listener :**

```
Protocol : HTTP
Port : 80
```

---

**Default action :**

```
Forward to : (on va créer un Target Group)
```

**Cliquer sur "Create target group"**

---

**S'ouvre dans un nouvel onglet.**

---

### ÉTAPE 10 : Créer un Target Group

**Target Group** = Groupe de cibles (containers) pour le Load Balancer

---

**Choose a target type :**

```
[x] IP addresses
```

**Types de cibles :**

**Instances :**
- Cibles = Instances EC2
- Use case : ECS sur EC2

**IP addresses :**
- Cibles = Adresses IP
- Use case : **Fargate** (pas d'instances)

**Lambda :**
- Cibles = Fonctions Lambda
- Use case : Serverless

**ALB :**
- Cibles = Autres ALBs

**Pour Fargate : IP addresses**

---

**Target group name :**

```
flask-todo-tg
```

---

**Protocol :**

```
HTTP
```

---

**Port :**

```
80
```

**[ATTENTION] Note : Le port ici n'a pas d'importance pour Fargate**

**Fargate utilise des ports dynamiques.**

**On va override dans le Service ECS.**

---

**IP address type :**

```
IPv4
```

---

**VPC :**

```
Default VPC
```

---

#### **Health checks**

**Health check protocol :**

```
HTTP
```

---

**Health check path :**

```
/health
```

**[ATTENTION] IMPORTANT : Correspond à la route dans Flask !**

```python
@application.route('/health')
def health():
    return {'status': 'healthy'}, 200
```

---

**Advanced health check settings :**

**Healthy threshold :**

```
2
```

**2 checks réussis consécutifs -> Cible healthy**

---

**Unhealthy threshold :**

```
2
```

**2 checks ratés consécutifs -> Cible unhealthy**

---

**Timeout :**

```
5 seconds
```

**Si /health ne répond pas en 5s -> Check raté**

---

**Interval :**

```
30 seconds
```

**Vérifier toutes les 30 secondes.**

---

**Success codes :**

```
200
```

**Codes HTTP considérés comme succès.**

**On peut mettre : `200,201,204` si besoin.**

---

**Cliquer sur "Next"**

---

#### **Register targets**

**Ne rien sélectionner ici.**

**Les targets (containers) seront ajoutées automatiquement par ECS.**

---

**Cliquer sur "Create target group"**

**[OK] Target Group créé !**

---

**Retourner dans l'onglet ALB**

**Rafraîchir la liste des Target Groups :**

**Cliquer sur l'icône [SYNC] à côté de "Forward to"**

---

**Sélectionner :**

```
flask-todo-tg
```

---

#### **Tags**

```
Key: Projet | Value: TODO-ECS
```

---

**Cliquer sur "Create load balancer"**

**Durée : 2-3 minutes**

---

**Load Balancer créé ! [BRAVO]**

**DNS name :**

```
flask-todo-alb-123456789.eu-west-3.elb.amazonaws.com
```

**C'est l'URL d'accès à ton application.**

---

**État :**

```
State : Provisioning -> Active (attendre 2-3 min)
```

---

### ÉTAPE 11 : Créer le Service ECS

**Maintenant qu'on a :**
- [OK] Cluster ECS
- [OK] Task Definition
- [OK] Application Load Balancer
- [OK] Target Group

**On peut créer le Service.**

---

**Console ECS -> Clusters -> flask-todo-cluster -> Services -> Create**

---

#### **Environment**

**Compute options :**

```
[x] Launch type
```

**Launch type :**

```
FARGATE
```

---

**Platform version :**

```
LATEST
```

**Fargate platform versions :**
- 1.4.0 (stable)
- 1.3.0 (ancienne)
- LATEST (recommandé, pointe vers la dernière stable)

---

#### **Deployment configuration**

**Application type :**

```
[x] Service
```

**Service vs Task :**

**Service :**
- Long-running
- Maintient N tasks
- Load balanced
- Auto-scaling
- Use case : Web apps, APIs

**Task :**
- One-off job
- Exécution unique
- Pas de load balancer
- Use case : Batch jobs, migrations

---

**Task definition :**

```
Family : flask-todo-task
Revision : 1 (latest)
```

---

**Service name :**

```
flask-todo-service
```

---

**Service type :**

```
[x] Replica
```

**Replica vs Daemon :**

**Replica :**
- Nombre fixe de tasks (ex: 3)
- Réparties dans le cluster

**Daemon :**
- 1 task par nœud
- Use case : Agents de monitoring

**Pour Fargate : Toujours Replica**

---

**Desired tasks :**

```
2
```

**Nombre de tasks à maintenir.**

**2 tasks = Haute disponibilité**

```
Task-1 dans AZ-a
Task-2 dans AZ-b

AZ-a tombe -> Task-2 continue de servir
```

**Minimum recommandé : 2**

---

#### **Deployment options**

**Min running tasks :**

```
100 %
```

**Max running tasks :**

```
200 %
```

**Explication du rolling update :**

```
Desired : 2 tasks

Min 100% = Garder au moins 2 tasks pendant le déploiement
Max 200% = Autoriser jusqu'à 4 tasks temporairement

Déploiement :
1. Lancer 2 nouvelles tasks (total : 4, 200%)
2. Attendre qu'elles soient healthy
3. Arrêter les 2 anciennes
4. Retour à 2 tasks

Résultat : Zero downtime !
```

---

**Deployment type :**

```
[x] Rolling update
```

**Types :**

**Rolling update :**
- Remplace progressivement
- Zero downtime

**Blue/Green (via CodeDeploy) :**
- Créer un nouvel environnement
- Switch de trafic
- Rollback instantané

---

#### **Networking**

**VPC :**

```
Default VPC
```

---

**Subnets :**

```
[x] Sélectionner les MÊMES subnets que l'ALB
```

**Exemple :**

```
[x] eu-west-3a
[x] eu-west-3b
[x] eu-west-3c
```

---

**Security group :**

**Créer un nouveau Security Group pour les tasks.**

**Nouvel onglet -> EC2 -> Security Groups -> Create security group**

---

**Security group name :**

```
flask-todo-ecs-tasks-sg
```

---

**Description :**

```
Security group for ECS tasks
```

---

**VPC :**

```
Default VPC
```

---

**Inbound rules :**

**Add rule :**

```
Type : Custom TCP
Protocol : TCP
Port range : 5000
Source : Custom

[ATTENTION] IMPORTANT : Sélectionner le Security Group de l'ALB !

Source : flask-todo-alb-sg
Description : Allow traffic from ALB
```

**Explication :**

```
ALB (port 80) -> Tasks (port 5000)

Security Group Tasks autorise :
- Port 5000 SEULEMENT depuis ALB SG
- Pas d'accès direct depuis Internet

Sécurité renforcée !
```

---

**Outbound rules :**

**Add rule :**

```
Type : PostgreSQL
Protocol : TCP
Port : 5432
Destination : Security Group RDS
Description : Allow connection to RDS
```

**[ATTENTION] Sélectionner le SG de RDS (rds-flask-sg)**

---

**Add rule 2 (pour les logs CloudWatch) :**

```
Type : HTTPS
Protocol : TCP
Port : 443
Destination : 0.0.0.0/0
Description : Allow CloudWatch Logs
```

---

**Create security group**

---

**[ATTENTION] IMPORTANT : Autoriser le SG ECS dans RDS !**

**Console RDS -> flask-todo-db -> VPC security groups -> rds-flask-sg -> Edit inbound rules**

**Add rule :**

```
Type : PostgreSQL
Port : 5432
Source : flask-todo-ecs-tasks-sg
Description : Allow from ECS tasks
```

**Save rules**

---

**Retourner dans l'onglet ECS Service**

**Sélectionner le SG :**

```
[x] flask-todo-ecs-tasks-sg
```

---

**Public IP :**

```
[ ] Disabled
```

**Fargate peut avoir une IP publique, mais pas nécessaire.**

**Pourquoi ?**

```
Architecture :
Internet -> ALB (IP publique) -> Tasks (IP privée)

Tasks n'ont pas besoin d'IP publique
```

**Exception : Si tasks doivent accéder à Internet directement**

**Pour nous : Disabled**

---

#### **Load balancing**

**Load balancer type :**

```
[x] Application Load Balancer
```

---

**Use an existing load balancer :**

```
[x] Yes
```

**Load balancer :**

```
flask-todo-alb
```

---

**Listener :**

```
Use an existing listener
80 : HTTP
```

---

**Target group :**

```
Use an existing target group
flask-todo-tg
```

---

**Health check grace period :**

```
60 seconds
```

**Grace period** = Temps d'attente avant de commencer les health checks

**Pourquoi ?**

```
Task démarre (0s) -> Container boot (~10s) -> App initialise (~20s) -> Prête (~30s)

Si ALB check à t=5s -> App pas prête -> Unhealthy -> Task tuée

Avec grace period 60s :
ALB attend 60s avant de checker -> App est prête
```

**Recommandé : 30-60s**

---

#### **Service auto scaling**

**Use service auto scaling :**

```
[x] Yes
```

---

**Min tasks :**

```
2
```

---

**Max tasks :**

```
4
```

**Auto-scaling va varier entre 2 et 4 tasks.**

---

**Scaling policy type :**

```
[x] Target tracking
```

**Types :**

**Target tracking :**
- Maintient une métrique cible (ex: CPU = 70%)
- Simple
- [OK] Recommandé

**Step scaling :**
- Règles de scaling personnalisées
- Plus complexe

---

**Policy name :**

```
flask-todo-cpu-scaling
```

---

**ECS service metric :**

```
ECSServiceAverageCPUUtilization
```

**Métriques disponibles :**
- CPU
- RAM
- ALB Request Count per Target

---

**Target value :**

```
70
```

**Maintenir le CPU moyen à 70%.**

**Comportement :**

```
CPU > 70% pendant 3 min -> Scale up (ajouter une task)
CPU < 49% pendant 15 min -> Scale down (retirer une task)

Pourquoi 49% et pas 70% ?
Éviter le flapping (monter/descendre en boucle)
```

---

**Scale-out cooldown period :**

```
300 seconds (5 min)
```

**Cooldown** = Temps d'attente entre 2 scale-out

**Évite de lancer trop de tasks d'un coup.**

---

**Scale-in cooldown period :**

```
300 seconds
```

---

**Tags :**

```
Key: Environnement | Value: Production
```

---

**Cliquer sur "Create"**

**ECS crée le service... [HOURGLASS_WITH_FLOWING_SAND]**

**Durée : 5-10 minutes**

---

**Progression :**

```
1. Creating service...
2. Launching tasks...
3. Tasks running...
4. Registering targets with ALB...
5. Health checks passing...
6. Service active
```

---

**Console ECS -> flask-todo-cluster -> Services -> flask-todo-service**

**État :**

```
Status : Active
Running count : 2
Desired count : 2
```

---

**Onglet "Tasks" :**

**Tu vois 2 tasks :**

```
Task 1 : RUNNING (AZ-a)
Task 2 : RUNNING (AZ-b)
```

---

### ÉTAPE 12 : Tester l'application

**Console EC2 -> Load Balancers -> flask-todo-alb**

**Copier le DNS name :**

```
flask-todo-alb-123456789.eu-west-3.elb.amazonaws.com
```

---

**Ouvrir dans le navigateur :**

```
http://flask-todo-alb-123456789.eu-west-3.elb.amazonaws.com
```

**[BRAVO] TON APPLICATION ECS FARGATE EST EN LIGNE ! [BRAVO]**

---

**Tester le CRUD :**

- [ ] Page d'accueil charge
- [ ] Ajouter une tâche -> Fonctionne
- [ ] Tâche apparaît dans la liste
- [ ] Modifier/Supprimer -> Fonctionne
- [ ] Données persistées dans RDS

---

**Vérifier le health check :**

```
http://flask-todo-alb-123456789.eu-west-3.elb.amazonaws.com/health
```

**Résultat :**

```json
{
  "status": "healthy",
  "database": "connected"
}
```

---

**Console EC2 -> Target Groups -> flask-todo-tg -> Targets**

**Tu vois 2 targets :**

```
Target 1 : 172.31.10.23:32768 (healthy)
Target 2 : 172.31.20.45:32769 (healthy)
```

**Ports dynamiques assignés par Fargate.**

---

### ÉTAPE 13 : Tester l'auto-scaling

**On va simuler une charge pour déclencher l'auto-scaling.**

---

**Installer Apache Bench (si pas déjà fait) :**

```bash
# macOS
brew install httpd

# Linux
sudo apt install apache2-utils -y
```

---

**Générer de la charge :**

```bash
ab -n 10000 -c 100 http://flask-todo-alb-123456789.eu-west-3.elb.amazonaws.com/
```

**Explication :**

**`-n 10000`** = 10 000 requêtes au total

**`-c 100`** = 100 requêtes simultanées

---

**En parallèle, surveiller dans la console :**

**ECS -> Services -> flask-todo-service -> Metrics**

**Regarder "CPU utilization"**

**Si CPU > 70% pendant 3 minutes -> Auto-scaling déclenché**

---

**Après quelques minutes :**

**Onglet "Tasks" :**

```
Running count : 3 (ou 4)
Desired count : 3 (ou 4)
```

**[OK] Auto-scaling fonctionne !**

---

**Arrêter Apache Bench (Ctrl+C)**

**Attendre 15 minutes.**

**CPU redescend < 49% -> Scale-in**

**Running count repasse à 2.**

---

### ÉTAPE 14 : Monitorer avec CloudWatch

**Console CloudWatch -> Container Insights**

**Sélectionner :**

```
ECS Services
flask-todo-cluster
flask-todo-service
```

---

**Métriques disponibles :**

```
CPU Utilization
Memory Utilization
Network RX/TX
Task count
```

---

**Graphiques en temps réel :**

**CPU par task :**

```
Task-1 : 45%
Task-2 : 50%
```

**Memory :**

```
Task-1 : 380 MB / 1024 MB (37%)
Task-2 : 395 MB / 1024 MB (38%)
```

---

**Console CloudWatch -> Log groups**

**Log group :**

```
/ecs/flask-todo-task
```

**Cliquer -> Voir les logs**

**Format :**

```
/ecs/flask-todo-task/ecs/flask-app/abc123def456

Logs :
[2024-12-16 19:00:00 +0000] [1] [INFO] Starting gunicorn 21.2.0
[2024-12-16 19:00:00 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
Database initialized
[2024-12-16 19:00:15] "GET /health HTTP/1.1" 200 -
[2024-12-16 19:00:20] "GET / HTTP/1.1" 200 -
```

---

### ÉTAPE 15 : Mettre à jour l'application (Rolling Update)

**Scénario : Tu veux déployer une nouvelle version.**

---

**1. Modifier le code localement**

**Exemple : Changer le titre dans `templates/base.html` :**

```html
<h1>[NOTE] TODO List - Flask + ECS Fargate v2.0</h1>
```

---

**2. Rebuild l'image Docker**

```bash
docker build -t flask-todo-app:v2 .
```

---

**3. Taguer pour ECR**

```bash
docker tag flask-todo-app:v2 123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app:v2
```

---

**4. Pousser vers ECR**

```bash
docker push 123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app:v2
```

---

**5. Créer une nouvelle révision de Task Definition**

**Console ECS -> Task Definitions -> flask-todo-task**

**Cliquer sur la révision 1 -> Create new revision**

---

**Modifier l'Image URI :**

```
123456789012.dkr.ecr.eu-west-3.amazonaws.com/flask-todo-app:v2
```

**[ATTENTION] Changer le tag `latest` -> `v2`**

---

**Tout le reste : identique**

**Create**

**Nouvelle révision créée : `flask-todo-task:2`**

---

**6. Mettre à jour le Service**

**Console ECS -> Clusters -> flask-todo-cluster -> Services -> flask-todo-service**

**Cliquer sur "Update service"**

---

**Revision :**

```
flask-todo-task:2 (latest)
```

---

**Force new deployment :**

```
[x] Coché
```

**Force le redéploiement même si rien n'a changé.**

---

**Update**

**ECS déclenche un rolling update... [HOURGLASS_WITH_FLOWING_SAND]**

---

**Progression :**

```
1. Lancer 2 nouvelles tasks (revision 2)
2. Attendre qu'elles soient healthy (ALB health checks)
3. Drainer les anciennes tasks (arrêter d'envoyer du trafic)
4. Arrêter les anciennes tasks
5. Terminé
```

**Durée : 3-5 minutes**

**Zero downtime ! [BRAVO]**

---

**Recharger l'URL dans le navigateur :**

```
http://flask-todo-alb-123456789012.eu-west-3.elb.amazonaws.com
```

**Le titre affiche maintenant : "v2.0"**

**[OK] Mise à jour réussie !**

---

### [OK] TESTS DE VALIDATION

**1. Application accessible via ALB**

```
http://flask-todo-alb-123456789.eu-west-3.elb.amazonaws.com
```

- [ ] Page charge
- [ ] CRUD fonctionne
- [ ] Données persistées dans RDS

---

**2. Haute disponibilité**

**Console ECS -> Tasks**

- [ ] 2+ tasks running
- [ ] Tasks dans différentes AZs

---

**3. Health checks**

**Console EC2 -> Target Groups -> flask-todo-tg -> Targets**

- [ ] Toutes les targets "healthy"
- [ ] Health checks passent

---

**4. Auto-scaling configuré**

**Console ECS -> Service -> Auto Scaling**

- [ ] Policy active
- [ ] Min 2, Max 4
- [ ] Target CPU 70%

---

**5. Logs CloudWatch**

**Console CloudWatch -> Log groups -> /ecs/flask-todo-task**

- [ ] Logs présents
- [ ] Logs de chaque task
- [ ] Pas d'erreurs

---

**6. Rolling update fonctionne**

- [ ] Nouvelle révision déployée
- [ ] Zero downtime observé
- [ ] Nouvelles tasks healthy

---

**7. Coûts estimés**

```
Fargate :
- 2 tasks × 0.5 vCPU × 1 GB
- 2 × $18/mois = $36/mois

ALB : $16/mois + $0.008/LCU

ECR : 500 MB gratuits (Free Tier)

Total : ~$52-60/mois
```

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Tasks ne démarrent pas (PENDING -> STOPPED)

**Console ECS -> Tasks -> Task arrêtée -> Stopped reason**

**Causes courantes :**

**1. Image non trouvée**

```
Error: CannotPullContainerError: 
pull image manifest has been retried 5 time(s): failed to resolve ref
```

**Solutions :**
- Vérifier l'URI de l'image dans Task Definition
- Vérifier que l'image existe dans ECR
- Vérifier le tag (latest, v1, etc.)

---

**2. Permissions IAM manquantes**

```
Error: failed to pull image: access denied
```

**Solution :**

Task Execution Role manque la policy `AmazonECSTaskExecutionRolePolicy`

Console IAM -> Roles -> ecsTaskExecutionRole -> Attach policy

---

**3. Ressources insuffisantes**

```
Error: unable to place a task because no container instance met all of its requirements
```

**Causes :**
- Cluster vide (pas de Fargate capacity)
- Combinaison CPU/RAM invalide

**Solution : Vérifier la Task Definition (CPU/RAM valides)**

---

#### Erreur 2 : Tasks unhealthy (ALB)

**Console EC2 -> Target Groups -> Targets -> Status : unhealthy**

**Causes :**

**1. Health check path incorrect**

```
Target Group health check : /health
App n'a pas cette route -> 404 -> unhealthy
```

**Solution :** Vérifier que `/health` existe dans Flask

---

**2. Security Group bloque le trafic**

```
ALB -> Tasks (port 5000) bloqué
```

**Solution :**

Tasks SG Inbound -> Port 5000 depuis ALB SG

---

**3. App ne démarre pas**

Voir les logs CloudWatch pour les erreurs.

---

#### Erreur 3 : Cannot connect to RDS

**Logs CloudWatch :**

```
psycopg2.OperationalError: could not connect to server
```

**Causes :**

**1. Security Groups**

Tasks SG Outbound -> PostgreSQL (5432) autorisé ?

RDS SG Inbound -> PostgreSQL (5432) depuis Tasks SG ?

---

**2. Variables d'environnement incorrectes**

Vérifier `DATABASE_URL` dans Task Definition.

---

**3. RDS dans un VPC différent**

Tasks et RDS doivent être dans le même VPC.

---

#### Erreur 4 : Load Balancer timeout

**Navigateur :**

```
504 Gateway Timeout
```

**Causes :**

**1. App met trop de temps à répondre**

```
Requête > 60s -> ALB timeout
```

**Solution :** Optimiser l'app ou augmenter le timeout ALB

---

**2. App crash**

Voir les logs CloudWatch.

---

#### Erreur 5 : Rolling update bloqué

**Service reste en "UPDATE_IN_PROGRESS" indéfiniment**

**Causes :**

**1. Nouvelles tasks jamais healthy**

Health checks échouent -> Tasks remplacées en boucle

**Solution :** Fixer le problème de health check

---

**2. Grace period trop court**

Tasks tuées avant d'être prêtes.

**Solution :** Augmenter grace period (60-120s)

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. Docker**
- Containerisation = Portabilité
- Image = Template, Container = Instance
- Dockerfile = Recette de construction

**2. ECR**
- Registre Docker privé AWS
- Intégré avec ECS/Fargate
- Scan de vulnérabilités automatique

**3. ECS**
- Orchestrateur de containers AWS
- Task Definition = Blueprint
- Service = Maintient N tasks

**4. Fargate**
- Serverless containers
- Pas de serveurs à gérer
- Pay-per-use (par seconde)

**5. Architecture ECS**
- ALB -> Target Group -> Tasks
- Health checks critiques
- Auto-scaling basé sur CPU/RAM

**6. Déploiement**
- Rolling update = Zero downtime
- Blue/Green = Rollback instantané
- Task Definition versionnée

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. HTTPS avec Certificate Manager**

**Demander un certificat SSL :**

Console ACM -> Request certificate -> `todo.mon-domaine.com`

**Ajouter un listener HTTPS à l'ALB :**

ALB -> Listeners -> Add listener -> HTTPS (443) -> Certificate ACM

---

**2. Service Discovery (Cloud Map)**

**Communication inter-services :**

```
Service A -> Service B (via DNS interne)

Service A appelle : http://service-b.local:8080
```

**Console ECS -> Task Definition -> Service discovery**

---

**3. ECS Exec (SSH dans les containers)**

**Accéder au shell d'un container :**

```bash
aws ecs execute-command \
  --cluster flask-todo-cluster \
  --task abc123... \
  --container flask-app \
  --command "/bin/bash" \
  --interactive
```

**Debugging avancé.**

---

**4. Fargate Spot (économies)**

**Fargate Spot = 70% moins cher**

**Capacity Provider Strategy :**

```
- 50% Fargate (on-demand)
- 50% Fargate Spot (interruptible)
```

**Coût réduit pour workloads tolérants aux interruptions.**

---

**5. CI/CD avec CodePipeline**

**Pipeline automatisé :**

```
GitHub -> CodeBuild -> ECR -> ECS (deploy)

Chaque push -> Build image -> Deploy automatique
```

---

**6. Multi-region avec Route 53**

**Haute disponibilité globale :**

```
Route 53 (failover)
├── Region eu-west-3 (primary)
└── Region us-east-1 (backup)
```

---

## [COURS] CONCLUSION DE L'EXERCICE 6

**[BRAVO] Félicitations ! Tu as déployé Flask sur ECS Fargate ! [BRAVO]**

**Ce que tu as appris :**
- Containerisation avec Docker
- Dockerfile optimisé multi-stage
- ECR (registre Docker AWS)
- ECS Fargate (serverless containers)
- Application Load Balancer
- Target Groups et health checks
- Auto-scaling ECS
- Rolling updates zero-downtime
- Monitoring Container Insights

**Compétences acquises :**
- [OK] Docker (avancé)
- [OK] ECS Fargate
- [OK] ECR
- [OK] Application Load Balancer
- [OK] Auto-scaling
- [OK] Architecture haute disponibilité

**Comparaison des méthodes :**

| Critère | EC2 | Beanstalk | Lambda | **Fargate** |
|---------|-----|-----------|--------|------------|
| Contrôle | ***** | *** | ** | **** |
| Simplicité | * | **** | ***** | **** |
| Coût (constant) | $8-10 | $8-20 | $2 | $36-50 |
| Portabilité | ** | * | * | ***** |
| Cold start | N/A | N/A | 1-2s | 30-60s |

**Fargate = Idéal pour apps containerisées ! [DOCKER]**

**Temps moyen de réalisation :** 4-5 heures

---

**Prochaine étape :** Exercice 7 - ALB + Auto Scaling Groups avancés ! [HAUSSE]

Tu veux continuer ? Je vais créer les exercices 7 à 10 avec le même niveau de détail :

- **Exercice 7** : ALB avancé + Auto Scaling Groups (EC2)
- **Exercice 8** : VPC personnalisé (subnets publics/privés, NAT Gateway)
- **Exercice 9** : CI/CD complet (CodePipeline + CodeBuild + CodeDeploy)
- **Exercice 10** : Architecture complète multi-tier

Veux-tu que je continue ? [RAPIDE]

# [ROUGE] EXERCICE 7 : AUTO SCALING GROUPS + ALB AVANCÉ

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es architecte cloud dans une entreprise e-commerce. Le site subit des pics de trafic imprévisibles (Black Friday, promotions flash). L'équipe a besoin d'une infrastructure qui s'adapte automatiquement à la charge, tout en minimisant les coûts pendant les périodes calmes.

### Cahier des charges

Le client souhaite :
- Scaling automatique basé sur plusieurs métriques
- Haute disponibilité (multi-AZ)
- Coûts optimisés (instances Spot + On-Demand)
- Load balancing intelligent (path-based routing)
- Notifications en cas d'événements critiques
- Déploiements sans downtime
- Monitoring détaillé

### Contraintes techniques

- Compute : EC2 avec Auto Scaling Groups
- Load Balancer : Application Load Balancer (avancé)
- Framework : Flask (même app que précédemment)
- Base de données : RDS PostgreSQL
- Monitoring : CloudWatch + SNS
- Temps estimé : 4-5 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Créer des Launch Templates
- [OK] Configurer des Auto Scaling Groups (ASG)
- [OK] Implémenter différentes scaling policies
- [OK] Utiliser des instances Spot pour réduire les coûts
- [OK] Configurer ALB avancé (path/host routing)
- [OK] Implémenter sticky sessions
- [OK] Créer des CloudWatch Alarms
- [OK] Configurer des notifications SNS
- [OK] Monitoring et debugging avancés

---

## [DOCS] PRÉREQUIS

- Exercice 1 et 3 terminés (EC2, RDS)
- Application Flask fonctionnelle
- Compte AWS actif
- Compréhensions des concepts EC2

---

## [IDEE] CONCEPTS AWS À COMPRENDRE

### Qu'est-ce qu'un Auto Scaling Group ?

**Auto Scaling Group (ASG)** = Groupe d'instances EC2 qui s'ajuste automatiquement

**Analogie :**

```
Restaurant :
- Période calme (midi) : 2 serveurs
- Rush (19h-21h) : 8 serveurs
- Fermeture (23h) : 1 serveur de nettoyage

Auto Scaling Group :
- Trafic faible : 2 instances
- Pic de trafic : 8 instances
- Nuit : 1 instance
```

**Avantages :**
- [ARGENT] Coûts optimisés (payer seulement ce qui est utilisé)
- [RAPIDE] Performance maintenue (toujours assez de capacité)
- [SECURITE] Haute disponibilité (remplace instances défaillantes)
- [RAPIDE] Réactivité (scale en quelques minutes)

---

### Anatomie d'un Auto Scaling Group

**Composants principaux :**

```
Auto Scaling Group
├── Launch Template (ou Launch Configuration)
│   ├── AMI (système d'exploitation)
│   ├── Instance Type (t3.micro, t3.small, etc.)
│   ├── User Data (script de démarrage)
│   ├── Security Groups
│   └── IAM Role
├── Capacity (Min/Desired/Max)
│   ├── Min : 2 instances (toujours au moins 2)
│   ├── Desired : 4 instances (objectif actuel)
│   └── Max : 10 instances (limite haute)
├── Scaling Policies
│   ├── Target Tracking (maintenir CPU à 70%)
│   ├── Step Scaling (règles personnalisées)
│   └── Scheduled Scaling (horaires prédéfinis)
├── Health Checks
│   ├── EC2 Status Checks
│   └── ELB Health Checks
└── Availability Zones
    ├── eu-west-3a (25% des instances)
    ├── eu-west-3b (50% des instances)
    └── eu-west-3c (25% des instances)
```

---

### Launch Template vs Launch Configuration

| Critère | Launch Template | Launch Configuration |
|---------|-----------------|----------------------|
| **Versions** | Multiples versions | Version unique |
| **Modification** | Peut être modifié | Immuable |
| **Spot** | Supporte Spot | Limité |
| **Instance types** | Mix de types | Type unique |
| **Recommandation** | [OK] Nouveau standard | [X] Déprécié |

**Launch Template = Toujours l'utiliser !**

---

### Launch Template - Concepts clés

#### **AMI (Amazon Machine Image)**

**AMI** = Image disque pré-configurée

**Analogie :**

```
AMI = Clone/Snapshot d'un disque dur

Ubuntu 22.04 AMI :
- OS Ubuntu installé
- Packages de base
- Configuration système

AMI personnalisé :
- Ubuntu 22.04
- Python 3.11 installé
- App Flask déployée
- Nginx configuré
-> Instance démarre immédiatement opérationnelle
```

**Types d'AMI :**

**AMI publiques (AWS/Marketplace) :**
- Ubuntu Server 22.04
- Amazon Linux 2023
- Red Hat Enterprise Linux
- Windows Server

**AMI personnalisées (créées par toi) :**
- Instance EC2 configurée
- Actions -> Create Image
- AMI disponible pour lancement

---

#### **User Data**

**User Data** = Script exécuté au premier boot

**Format : Bash script**

**Exemple :**

```bash
#!/bin/bash
# S'exécute en tant que root

apt update
apt install -y python3-pip
pip3 install flask gunicorn

# Télécharger l'app depuis S3
aws s3 cp s3://my-bucket/app.py /home/ubuntu/app.py

# Démarrer l'app
gunicorn --bind 0.0.0.0:5000 app:app &
```

**Cas d'usage :**
- Installer des packages
- Configurer l'application
- Télécharger du code
- Enregistrer l'instance dans un service

**[ATTENTION] User Data ne s'exécute qu'au PREMIER boot**

Pour ré-exécuter : `cloud-init clean && reboot`

---

### Scaling Policies - Types

#### **1. Target Tracking Scaling**

**Principe : Maintenir une métrique à une valeur cible**

**Exemple :**

```
Métrique : Average CPU Utilization
Target : 70%

Comportement :
CPU = 50% -> OK, rien à faire
CPU = 80% -> > 70% -> Scale out (ajouter instances)
CPU = 60% -> < 70% -> Scale in (retirer instances)
```

**Métriques supportées :**
- CPU Utilization
- Network In/Out
- Request Count per Target (ALB)
- Custom metrics (CloudWatch)

**[OK] Recommandé : Simple et efficace**

---

#### **2. Step Scaling**

**Principe : Règles de scaling par paliers**

**Exemple :**

```
Si CPU entre 70-80% -> Ajouter 1 instance
Si CPU entre 80-90% -> Ajouter 2 instances
Si CPU > 90% -> Ajouter 4 instances

Si CPU < 30% pendant 10 min -> Retirer 1 instance
```

**Avantages :**
- Contrôle fin
- Réaction proportionnelle à l'alerte

**Inconvénients :**
- Plus complexe à configurer
- Nécessite des alarmes CloudWatch

---

#### **3. Scheduled Scaling**

**Principe : Scaling basé sur des horaires**

**Exemple :**

```
Lundi-Vendredi 8h-18h : Min 10, Desired 15, Max 20
Lundi-Vendredi 18h-8h : Min 2, Desired 3, Max 5
Samedi-Dimanche : Min 1, Desired 2, Max 5

Black Friday : Min 50, Desired 100, Max 200
```

**Use cases :**
- Pics de trafic prévisibles
- Économies la nuit/week-end
- Événements spéciaux

---

#### **4. Predictive Scaling**

**Principe : ML prédit la charge future**

**AWS analyse l'historique et anticipe :**

```
Historique :
Chaque lundi 9h : Pic de 1000 req/s
Chaque vendredi 17h : Pic de 800 req/s

Prédiction :
Prochain lundi 8h45 : Pré-scale à 10 instances
Prochain vendredi 16h45 : Pré-scale à 8 instances
```

**Avantages :**
- Proactif (pas de retard)
- Basé sur ML

**Inconvénients :**
- Nécessite 14 jours d'historique
- Moins précis pour charges erratiques

---

### Health Checks

**Health Check** = Vérification de santé d'une instance

**Types :**

#### **1. EC2 Status Checks**

**Vérifications AWS automatiques :**

**System Status Check :**
- Hardware sous-jacent OK ?
- Réseau AWS OK ?
- Hôte physique OK ?

**Instance Status Check :**
- OS boote correctement ?
- Kernel panic ?
- Système de fichiers corrompu ?

**Si unhealthy -> ASG remplace l'instance**

---

#### **2. ELB Health Checks**

**Load Balancer vérifie l'application :**

```
GET /health HTTP/1.1

Réponse 200 OK -> Healthy
Réponse 500 ou timeout -> Unhealthy
```

**Plus précis que EC2 checks (vérifie l'app, pas juste l'OS)**

---

**Différence EC2 vs ELB :**

```
Scenario : App crash, OS OK

EC2 Health Check : [OK] Healthy (OS fonctionne)
ELB Health Check : [X] Unhealthy (app ne répond pas)

Avec EC2 checks seuls : Instance jamais remplacée
Avec ELB checks : Instance remplacée automatiquement
```

**Recommandation : Activer les deux**

---

### Lifecycle Hooks

**Lifecycle Hook** = Point d'intervention pendant le cycle de vie

**Cycle de vie d'une instance dans ASG :**

```
1. Pending -> Launching
   [Lifecycle Hook : launch]
   -> Exécuter des actions (installer soft, config)
   
2. InService -> Running
   -> Instance reçoit du trafic
   
3. Terminating -> Shutting down
   [Lifecycle Hook : terminate]
   -> Exécuter des actions (backup, désenregistrement)
   
4. Terminated -> Removed
```

**Use cases :**

**Hook au lancement :**
- Enregistrer l'instance dans un système externe
- Attendre la fin d'une installation complexe
- Réchauffer un cache

**Hook à la terminaison :**
- Sauvegarder des données
- Drainer les connexions proprement
- Notifier des services externes

---

### Instances Spot

**Instances Spot** = Capacité EC2 inutilisée vendue aux enchères

**Principe :**

```
AWS a de la capacité EC2 non utilisée
-> Vend jusqu'à 90% moins cher
-> Mais peut reprendre avec 2 minutes de préavis
```

**Prix :**

```
On-Demand t3.medium : $0.0416/heure
Spot t3.medium : $0.0125/heure (70% de réduction !)
```

**Interruption Spot :**

```
AWS a besoin de capacité
-> Envoie une notification (2 minutes)
-> Instance arrêtée/terminée
```

---

**Quand utiliser Spot ?**

**[OK] Bon pour :**
- Workloads tolérants aux interruptions
- Applications stateless
- Batch processing
- CI/CD
- Web apps avec plusieurs instances

**[X] Éviter pour :**
- Bases de données (stateful)
- Instance unique critique
- Applications temps-réel sensibles

---

**Stratégie mixte On-Demand + Spot :**

```
ASG avec :
- 2 instances On-Demand (base stable)
- 0-8 instances Spot (scaling flexible)

Coût :
2 On-Demand × $0.0416 × 730h = $60.74
4 Spot (moyenne) × $0.0125 × 730h = $36.50

Total : $97.24/mois

Vs tout On-Demand :
6 instances × $0.0416 × 730h = $182.20/mois

Économies : 47% ! [ARGENT]
```

---

### Application Load Balancer - Concepts avancés

#### **Routing avancé**

**1. Path-based routing**

```
monsite.com/api/* -> Target Group API
monsite.com/admin/* -> Target Group Admin
monsite.com/* -> Target Group Frontend
```

**Use case : Microservices**

---

**2. Host-based routing**

```
api.monsite.com -> Target Group API
admin.monsite.com -> Target Group Admin
www.monsite.com -> Target Group Frontend
```

**Use case : Multi-tenant**

---

**3. Header-based routing**

```
Header X-Version: v2 -> Target Group V2 (beta)
Sinon -> Target Group V1 (stable)
```

**Use case : A/B testing**

---

**4. Query string routing**

```
monsite.com?version=beta -> Target Group Beta
monsite.com?version=stable -> Target Group Stable
```

---

#### **Sticky Sessions**

**Sticky Session** = Même utilisateur -> Même instance

**Problème sans sticky sessions :**

```
User fait login -> Instance A (session créée)
User fait requête 2 -> Instance B (pas de session)
-> User re-demande de se connecter
```

**Avec sticky sessions :**

```
User fait login -> Instance A
Toutes les requêtes suivantes -> Instance A
-> Session maintenue
```

**Implémentation :**

```
ALB génère un cookie : AWSALB=abc123...
Durée : 1 heure - 7 jours

User envoie cookie -> ALB route vers la même instance
```

**[ATTENTION] Inconvénient : Distribution de charge moins équilibrée**

---

#### **Connection Draining**

**Connection Draining** = Terminer les connexions proprement

**Problème sans draining :**

```
Instance en cours de terminaison
-> ALB arrête immédiatement d'envoyer du trafic
-> Connexions actives coupées brutalement
-> Utilisateurs voient des erreurs
```

**Avec draining :**

```
Instance marquée "Draining"
-> ALB arrête les nouvelles connexions
-> Connexions actives continuent (max 300s)
-> Quand toutes terminées -> Instance arrêtée
```

**Paramètre : Deregistration delay (défaut 300s)**

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Créer un SNS Topic pour les notifications

**On va créer un système de notifications pour les événements ASG.**

**Console AWS -> SNS (Simple Notification Service)**

---

**Topics -> Create topic**

**Type :**

```
[x] Standard
```

**Standard vs FIFO :**
- Standard : Livraison au moins une fois, ordre non garanti
- FIFO : Ordre strict, exactly-once

**Pour notifications : Standard suffit**

---

**Name :**

```
asg-scaling-notifications
```

---

**Display name (optionnel) :**

```
ASG Scaling
```

**Nom affiché dans les emails/SMS.**

---

**Create topic**

---

**Créer une souscription (subscription) :**

**Create subscription**

**Protocol :**

```
[x] Email
```

**Protocols disponibles :**
- Email
- SMS
- HTTPS (webhook)
- Lambda
- SQS
- Mobile push

---

**Endpoint :**

```
ton-email@example.com
```

---

**Create subscription**

---

**Confirmer la souscription :**

**Tu vas recevoir un email :**

```
Subject: AWS Notification - Subscription Confirmation

You have chosen to subscribe to the topic:
arn:aws:sns:eu-west-3:123456789012:asg-scaling-notifications

To confirm this subscription, click or visit the link below:
https://sns.eu-west-3.amazonaws.com/...
```

**Cliquer sur le lien de confirmation.**

**Subscription confirmée ! [OK]**

---

### ÉTAPE 2 : Créer un Launch Template

**Console EC2 -> Launch Templates -> Create launch template**

---

**Launch template name :**

```
flask-app-launch-template
```

---

**Template version description :**

```
v1 - Initial configuration with Flask app
```

---

#### **Application and OS Images (AMI)**

**Quick Start :**

```
[x] Ubuntu
Ubuntu Server 22.04 LTS (HVM), SSD Volume Type
```

**Architecture :**

```
[x] 64-bit (x86)
```

---

#### **Instance type**

```
t3.micro
```

**Pour le tuto : t3.micro (Free Tier)**

**Production : t3.small ou t3.medium**

---

**Instance type flexibility (pour Spot) :**

**Cliquer sur "Configure instance type options"**

**Ajouter plusieurs types d'instances pour Spot :**

```
[x] t3.micro
[x] t3a.micro
[x] t2.micro
```

**Pourquoi plusieurs types ?**

```
Spot availability varie par instance type
Plus de types = Plus de chances d'obtenir du Spot
```

---

#### **Key pair**

```
ma-cle-ec2 (existante)
```

---

#### **Network settings**

**Subnet :**

```
Don't include in launch template
```

**Pourquoi ?**

ASG gère les subnets (distribution multi-AZ).

---

**Security groups :**

**Créer un nouveau Security Group :**

**Nouvel onglet -> EC2 -> Security Groups -> Create security group**

---

**Security group name :**

```
flask-app-asg-sg
```

---

**Description :**

```
Security group for Flask app instances in ASG
```

---

**VPC :**

```
Default VPC
```

---

**Inbound rules :**

**Rule 1 : HTTP depuis ALB**

```
Type : Custom TCP
Port : 5000
Source : Custom (Security Group ALB)
Description : Allow from ALB
```

**[ATTENTION] Sélectionner le SG de l'ALB qu'on va créer**

**Pour l'instant, mettre :**

```
Source : 0.0.0.0/0 (temporaire)
```

**On restreindra après avoir créé l'ALB.**

---

**Rule 2 : SSH (optionnel, pour debug)**

```
Type : SSH
Port : 22
Source : My IP
Description : SSH access for debugging
```

---

**Outbound rules :**

**Rule 1 : PostgreSQL vers RDS**

```
Type : PostgreSQL
Port : 5432
Destination : Security Group RDS
Description : Database access
```

---

**Rule 2 : HTTPS pour apt/pip**

```
Type : HTTPS
Port : 443
Destination : 0.0.0.0/0
Description : Package downloads
```

---

**Create security group**

---

**Retourner dans Launch Template**

**Security groups :**

```
[x] flask-app-asg-sg
```

---

#### **Advanced details**

**IAM instance profile :**

**Créer un rôle IAM pour les instances :**

**Nouvel onglet -> IAM -> Roles -> Create role**

---

**Trusted entity type :**

```
[x] AWS service
```

**Use case :**

```
[x] EC2
```

**Next**

---

**Permissions policies :**

**Rechercher et attacher :**

```
[x] AmazonSSMManagedInstanceCore (pour Systems Manager)
[x] CloudWatchAgentServerPolicy (pour métriques détaillées)
```

**Next**

---

**Role name :**

```
flask-app-ec2-role
```

---

**Create role**

---

**Retourner dans Launch Template**

**IAM instance profile :**

```
flask-app-ec2-role
```

---

**Monitoring :**

```
[x] Enable (CloudWatch detailed monitoring)
```

**Detailed monitoring = Métriques toutes les 1 minute**

**Coût : $2.10/instance/mois**

**Sans : Métriques toutes les 5 minutes (gratuit)**

---

**User data :**

**Voici le script complet de démarrage :**

```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# USER DATA - FLASK APP AUTO SCALING
# ═══════════════════════════════════════════════════════════════

# Logs du script dans CloudWatch
exec > >(tee /var/log/user-data.log)
exec 2>&1

"""
exec > >(tee /var/log/user-data.log) :
- Redirige stdout vers un fichier ET la console
- tee : Écrit dans le fichier ET affiche
- Permet de voir les logs après : cat /var/log/user-data.log

exec 2>&1 :
- Redirige stderr vers stdout
- Tous les logs (erreurs incluses) dans le même fichier
"""

echo "Starting user data script at $(date)"

# ───────────────────────────────────────────────────────────────
# MISE À JOUR DU SYSTÈME
# ───────────────────────────────────────────────────────────────

apt-get update
apt-get upgrade -y

"""
apt-get update : Met à jour la liste des packages
apt-get upgrade -y : Installe les mises à jour
-y : Répond "yes" automatiquement
"""

# ───────────────────────────────────────────────────────────────
# INSTALLATION DES DÉPENDANCES
# ───────────────────────────────────────────────────────────────

# Python et pip
apt-get install -y python3-pip python3-venv

# PostgreSQL client
apt-get install -y postgresql-client

# Nginx (reverse proxy)
apt-get install -y nginx

# CloudWatch agent (métriques détaillées)
wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
dpkg -i amazon-cloudwatch-agent.deb

"""
CloudWatch agent permet d'envoyer des métriques custom :
- Mémoire utilisée (pas disponible par défaut)
- Espace disque
- Processes
"""

# ───────────────────────────────────────────────────────────────
# DÉPLOIEMENT DE L'APPLICATION
# ───────────────────────────────────────────────────────────────

# Créer le répertoire de l'app
mkdir -p /opt/flask-app
cd /opt/flask-app

# Créer l'application Flask
cat > application.py << 'EOF'
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import os
import socket

application = Flask(__name__)

# Configuration
application.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get(
    'DATABASE_URL',
    'sqlite:///todo.db'
)
application.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
application.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key')

db = SQLAlchemy(application)

# Modèle
class Task(db.Model):
    __tablename__ = 'task'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    description = db.Column(db.Text, nullable=True)
    completed = db.Column(db.Boolean, default=False, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

# Routes
@application.route('/')
def index():
    tasks = Task.query.order_by(Task.created_at.desc()).all()
    # Afficher quelle instance sert la requête
    instance_id = os.environ.get('INSTANCE_ID', 'unknown')
    return render_template('index.html', tasks=tasks, instance_id=instance_id)

@application.route('/health')
def health():
    """Health check pour ALB"""
    try:
        # Tester la connexion DB
        db.session.execute('SELECT 1')
        return jsonify({
            'status': 'healthy',
            'instance': os.environ.get('INSTANCE_ID', 'unknown'),
            'database': 'connected'
        }), 200
    except Exception as e:
        return jsonify({
            'status': 'unhealthy',
            'error': str(e)
        }), 500

@application.route('/api/info')
def info():
    """Info sur l'instance (pour démonstration)"""
    return jsonify({
        'instance_id': os.environ.get('INSTANCE_ID', 'unknown'),
        'hostname': socket.gethostname(),
        'timestamp': datetime.utcnow().isoformat()
    })

# Initialisation DB
with application.app_context():
    db.create_all()

if __name__ == '__main__':
    application.run(debug=True, host='0.0.0.0', port=5000)
EOF

"""
Remarques sur l'application :
1. Route /health pour ALB health checks
2. Route /api/info affiche l'instance ID (utile pour voir le load balancing)
3. Variable d'environnement INSTANCE_ID pour identifier l'instance
"""

# Créer le template simple (pour tester)
mkdir -p templates
cat > templates/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
    <title>TODO List - Auto Scaling</title>
    <style>
        body { font-family: Arial; margin: 50px; }
        .instance-info { background: #e3f2fd; padding: 10px; margin-bottom: 20px; }
        .task { background: #f5f5f5; padding: 10px; margin: 10px 0; }
    </style>
</head>
<body>
    <div class="instance-info">
        [ECRAN] Served by instance: <strong>{{ instance_id }}</strong>
    </div>
    <h1>[NOTE] TODO List - Auto Scaling Demo</h1>
    <p>Total tasks: {{ tasks|length }}</p>
    {% for task in tasks %}
        <div class="task">
            <strong>{{ task.title }}</strong>
            {% if task.description %}<p>{{ task.description }}</p>{% endif %}
        </div>
    {% endfor %}
</body>
</html>
EOF

# Installer les dépendances Python
cat > requirements.txt << 'EOF'
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
psycopg2-binary==2.9.9
gunicorn==21.2.0
EOF

pip3 install -r requirements.txt

# ───────────────────────────────────────────────────────────────
# CONFIGURATION DES VARIABLES D'ENVIRONNEMENT
# ───────────────────────────────────────────────────────────────

# Récupérer l'Instance ID depuis metadata
INSTANCE_ID=$(ec2-metadata --instance-id | cut -d " " -f 2)

"""
ec2-metadata : Utilitaire pour accéder aux métadonnées de l'instance
--instance-id : Retourne l'ID de l'instance
cut -d " " -f 2 : Extrait la 2ème colonne

Résultat : i-0abc123def456789
"""

# Créer le fichier d'environnement
cat > /opt/flask-app/.env << EOF
DATABASE_URL=postgresql://postgres:MOT_DE_PASSE@flask-todo-db.c1234567890.eu-west-3.rds.amazonaws.com:5432/todo_db
SECRET_KEY=ta-cle-secrete-super-longue
INSTANCE_ID=${INSTANCE_ID}
EOF

"""
[ATTENTION] IMPORTANT : REMPLACER ces valeurs !
- MOT_DE_PASSE : Mot de passe RDS
- flask-todo-db.c... : Endpoint RDS
- SECRET_KEY : Clé secrète aléatoire
"""

# ───────────────────────────────────────────────────────────────
# CONFIGURATION DE GUNICORN
# ───────────────────────────────────────────────────────────────

cat > /opt/flask-app/gunicorn_config.py << 'EOF'
import multiprocessing

# Workers
workers = multiprocessing.cpu_count() * 2 + 1

# Bind
bind = '127.0.0.1:5000'

# Logs
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
loglevel = 'info'

# Timeout
timeout = 60
EOF

# Créer le dossier de logs
mkdir -p /var/log/gunicorn
chown ubuntu:ubuntu /var/log/gunicorn

# ───────────────────────────────────────────────────────────────
# CONFIGURATION DE NGINX
# ───────────────────────────────────────────────────────────────

cat > /etc/nginx/sites-available/flask-app << 'EOF'
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://127.0.0.1:5000;
        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;
        
        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # Health check (direct, pas de proxy)
    location /health {
        proxy_pass http://127.0.0.1:5000/health;
    }
}
EOF

# Activer le site
ln -s /etc/nginx/sites-available/flask-app /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default

# Tester et recharger Nginx
nginx -t
systemctl restart nginx

# ───────────────────────────────────────────────────────────────
# CRÉATION DU SERVICE SYSTEMD
# ───────────────────────────────────────────────────────────────

cat > /etc/systemd/system/flask-app.service << 'EOF'
[Unit]
Description=Flask TODO Application
After=network.target

[Service]
Type=notify
User=ubuntu
Group=ubuntu
WorkingDirectory=/opt/flask-app
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
EnvironmentFile=/opt/flask-app/.env
ExecStart=/usr/local/bin/gunicorn -c gunicorn_config.py application:application
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF

# Changer les permissions
chown -R ubuntu:ubuntu /opt/flask-app

# Activer et démarrer le service
systemctl daemon-reload
systemctl enable flask-app
systemctl start flask-app

"""
systemctl enable : Démarre automatiquement au boot
systemctl start : Démarre maintenant
"""

# ───────────────────────────────────────────────────────────────
# VÉRIFICATION
# ───────────────────────────────────────────────────────────────

# Attendre que l'app démarre
sleep 10

# Tester
curl -f http://localhost/health || echo "ERROR: App not responding"

"""
curl -f : Fail silently sur erreur HTTP
|| echo : Si erreur, afficher message
"""

echo "User data script completed at $(date)"

# ═══════════════════════════════════════════════════════════════
```

**[ATTENTION] TRÈS IMPORTANT : Modifier les valeurs suivantes dans le script :**

1. Ligne `DATABASE_URL` : Remplacer par ton endpoint RDS
2. Ligne `SECRET_KEY` : Remplacer par une clé aléatoire

---

**Coller ce script dans le champ "User data"**

---

**Create launch template**

**[OK] Launch Template créé ! [BRAVO]**

---

Je vais continuer avec la création de l'Auto Scaling Group et de l'ALB dans le prochain message. Veux-tu que je continue ? [RAPIDE]


# [ROUGE] EXERCICE 7 : AUTO SCALING GROUPS + ALB AVANCÉ (SUITE)

### ÉTAPE 3 : Créer l'Application Load Balancer

**Console EC2 -> Load Balancers -> Create load balancer**

**Type : Application Load Balancer**

---

**Load balancer name :**

```
flask-app-alb
```

---

**Scheme :**

```
[x] Internet-facing
```

---

**IP address type :**

```
[x] IPv4
```

---

#### **Network mapping**

**VPC :**

```
Default VPC
```

---

**Mappings - Sélectionner AU MOINS 2 AZs :**

```
[x] eu-west-3a
[x] eu-west-3b
[x] eu-west-3c
```

---

#### **Security groups**

**Créer un nouveau Security Group pour l'ALB :**

**Nouvel onglet -> EC2 -> Security Groups -> Create security group**

---

**Security group name :**

```
flask-app-alb-sg
```

---

**Description :**

```
Security group for Flask app ALB
```

---

**VPC :**

```
Default VPC
```

---

**Inbound rules :**

```
Type : HTTP
Port : 80
Source : 0.0.0.0/0
Description : Allow HTTP from Internet
```

---

**Outbound rules :**

```
Type : Custom TCP
Port : 5000
Destination : flask-app-asg-sg (SG des instances)
Description : Forward to app instances
```

---

**Create security group**

---

**[ATTENTION] Mettre à jour le Security Group des instances**

**EC2 -> Security Groups -> flask-app-asg-sg -> Edit inbound rules**

**Modifier la règle temporaire :**

```
Type : Custom TCP
Port : 5000
Source : flask-app-alb-sg (au lieu de 0.0.0.0/0)
Description : Allow from ALB only
```

**Save rules**

---

**Retourner dans l'onglet ALB**

**Security groups :**

```
[x] flask-app-alb-sg
```

---

#### **Listeners and routing**

**Listener HTTP:80**

**Default action : Forward to Target Group**

**Créer un Target Group :**

**Create target group**

---

**Target type :**

```
[x] Instances
```

---

**Target group name :**

```
flask-app-tg
```

---

**Protocol : Port :**

```
HTTP : 80
```

**[ATTENTION] Note : Les instances écoutent sur port 5000, mais on va override**

---

**VPC :**

```
Default VPC
```

---

**Protocol version :**

```
HTTP1
```

---

#### **Health checks**

**Health check protocol :**

```
HTTP
```

---

**Health check path :**

```
/health
```

---

**Advanced health check settings :**

**Port :**

```
Traffic port
```

**[ATTENTION] On va override plus tard pour utiliser le port 5000**

---

**Healthy threshold :**

```
2
```

**2 checks réussis -> Healthy**

---

**Unhealthy threshold :**

```
3
```

**3 checks ratés -> Unhealthy**

---

**Timeout :**

```
5 seconds
```

---

**Interval :**

```
30 seconds
```

---

**Success codes :**

```
200
```

---

**Cliquer sur "Next"**

---

**Register targets :**

**Ne rien sélectionner (ASG ajoutera automatiquement les instances)**

**Cliquer sur "Create target group"**

**[OK] Target Group créé !**

---

**Retourner dans l'onglet ALB**

**Rafraîchir la liste -> Sélectionner :**

```
flask-app-tg
```

---

#### **Attributes (optionnel mais recommandé)**

**En bas de la page, développer "Attributes"**

---

**Deregistration delay :**

```
30 seconds
```

**Connection draining : Attendre 30s avant de terminer une instance**

**Défaut 300s = Trop long pour notre cas**

---

**Stickiness :**

```
[x] Enable
```

**Stickiness type :**

```
[x] Load balancer generated cookie
```

**Stickiness duration :**

```
1 hour (3600 seconds)
```

**Sticky sessions : Même utilisateur -> Même instance**

---

**Create load balancer**

**Durée : 2-3 minutes**

---

**Load Balancer créé ! [BRAVO]**

**DNS name :**

```
flask-app-alb-123456789.eu-west-3.elb.amazonaws.com
```

**Attendre que le statut passe de "Provisioning" à "Active"**

---

### ÉTAPE 4 : Modifier le Target Group pour le bon port

**Console EC2 -> Target Groups -> flask-app-tg**

**Onglet "Health checks" -> Edit**

---

**Override port :**

```
[x] Override
Port : 5000
```

**Maintenant ALB vérifie le port 5000 (où tourne l'app) au lieu du port 80**

---

**Save changes**

---

### ÉTAPE 5 : Créer l'Auto Scaling Group

**Console EC2 -> Auto Scaling Groups -> Create Auto Scaling group**

---

#### **Step 1: Choose launch template**

**Auto Scaling group name :**

```
flask-app-asg
```

---

**Launch template :**

```
flask-app-launch-template
```

**Version :**

```
Latest
```

---

**Next**

---

#### **Step 2: Choose instance launch options**

**Network - VPC :**

```
Default VPC
```

---

**Availability Zones and subnets :**

**Sélectionner les MÊMES AZs que l'ALB :**

```
[x] eu-west-3a
[x] eu-west-3b
[x] eu-west-3c
```

**ASG distribuera les instances équitablement entre ces AZs.**

---

**Next**

---

#### **Step 3: Configure advanced options**

**Load balancing :**

```
[x] Attach to an existing load balancer
```

---

**Choose from your load balancer target groups :**

```
[x] flask-app-tg
```

---

**Health checks :**

**Health check type :**

```
[x] ELB (Elastic Load Balancer)
```

**Recommandé : Vérifie l'application, pas juste l'OS**

---

**Health check grace period :**

```
300 seconds
```

**Grace period** = Temps avant de commencer les health checks

**5 minutes = Temps pour instance de démarrer + app de s'initialiser**

---

**Additional settings :**

**Monitoring :**

```
[x] Enable group metrics collection within CloudWatch
```

**Métriques ASG détaillées dans CloudWatch**

---

**Next**

---

#### **Step 4: Configure group size and scaling policies**

**Group size :**

**Desired capacity :**

```
2
```

**Nombre d'instances à maintenir normalement**

---

**Minimum capacity :**

```
2
```

**Au moins 2 instances (haute disponibilité)**

---

**Maximum capacity :**

```
6
```

**Maximum 6 instances (budget limité)**

---

#### **Scaling policies**

```
[x] Target tracking scaling policy
```

**C'est le type le plus simple et efficace.**

---

**Scaling policy name :**

```
target-tracking-cpu-70
```

---

**Metric type :**

```
[x] Average CPU utilization
```

**Autres métriques disponibles :**
- Average Network In/Out
- Application Load Balancer request count per target

---

**Target value :**

```
70
```

**Maintenir le CPU moyen à 70%**

---

**Instances need :**

```
300 seconds (5 minutes)
```

**Temps de warm-up : Attendre 5 min avant d'inclure l'instance dans les métriques**

**Évite de fausser les métriques pendant le démarrage**

---

**[ATTENTION] Remarque importante sur le scaling :**

```
Comportement du Target Tracking :

CPU > 70% :
- Calcul : instances_needed = current_load / 0.70
- Exemple : Si 2 instances à 90% -> 2 × 0.9 / 0.7 = 2.57 -> 3 instances
- Scale out : Rapide (quelques minutes)

CPU < 70% :
- AWS est conservateur pour scale in
- Attend que CPU reste bas pendant ~15 minutes
- Scale in : Lent (évite le flapping)

Flapping = Monter/descendre en boucle rapide
```

---

**Next**

---

#### **Step 5: Add notifications**

**Notifications - Optional**

**Add notification**

**SNS Topic :**

```
asg-scaling-notifications (créé précédemment)
```

---

**Event types - Sélectionner tous :**

```
[x] Launch
[x] Terminate
[x] Fail to launch
[x] Fail to terminate
```

---

**Next**

---

#### **Step 6: Add tags**

**Tags propagés aux instances :**

**Add tag**

```
Key : Name
Value : flask-app-asg-instance
```

**Tag automatically propagated : [OK]**

---

**Add tag**

```
Key : Environnement
Value : Production
```

---

**Next**

---

#### **Step 7: Review**

**Vérifier la configuration**

**Create Auto Scaling group**

---

**ASG créé ! [BRAVO]**

**ASG lance les instances... [HOURGLASS_WITH_FLOWING_SAND]**

**Durée : 5-7 minutes**

---

**Progression :**

```
1. Launching instances (2 instances)
2. Running user data scripts
3. Installing packages
4. Starting Flask app
5. Registering with target group
6. Health checks
7. Instances in service
```

---

**Console EC2 -> Auto Scaling Groups -> flask-app-asg**

**Onglet "Activity" :**

```
Status Message:
Launching a new EC2 instance: i-0abc123...
Launching a new EC2 instance: i-0def456...

Successfully launched 2 instances
```

---

**Onglet "Instance management" :**

```
Instance ID         Availability Zone    Lifecycle    Health Status
i-0abc123...        eu-west-3a           InService    Healthy
i-0def456...        eu-west-3b           InService    Healthy
```

**[OK] Instances lancées et healthy !**

---

**Console EC2 -> Target Groups -> flask-app-tg -> Targets**

```
Target              Port    Availability Zone    Health status
i-0abc123...        5000    eu-west-3a           healthy
i-0def456...        5000    eu-west-3b           healthy
```

**[OK] Instances enregistrées dans le Target Group !**

---

### ÉTAPE 6 : Tester l'application

**Console EC2 -> Load Balancers -> flask-app-alb**

**Copier le DNS name :**

```
flask-app-alb-123456789.eu-west-3.elb.amazonaws.com
```

---

**Ouvrir dans le navigateur :**

```
http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com
```

**[BRAVO] TON APPLICATION AUTO SCALING EST EN LIGNE ! [BRAVO]**

---

**Tu verras :**

```
[ECRAN] Served by instance: i-0abc123def456789

[NOTE] TODO List - Auto Scaling Demo
```

**Rafraîchir la page plusieurs fois.**

**Avec sticky sessions activées :**
```
1ère visite : i-0abc123... (ALB génère cookie AWSALB)
2ème visite : i-0abc123... (même instance)
3ème visite : i-0abc123... (même instance)
```

**Sans sticky sessions (test en navigation privée) :**
```
1ère visite : i-0abc123...
2ème visite : i-0def456... (différente !)
3ème visite : i-0abc123...
```

**[OK] Load balancing fonctionne !**

---

**Tester le health check :**

```
http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com/health
```

**Résultat :**

```json
{
  "status": "healthy",
  "instance": "i-0abc123def456789",
  "database": "connected"
}
```

---

**Tester l'API info :**

```
http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com/api/info
```

**Résultat :**

```json
{
  "instance_id": "i-0abc123def456789",
  "hostname": "ip-172-31-10-23",
  "timestamp": "2024-12-16T20:00:00"
}
```

---

### ÉTAPE 7 : Configurer des Scaling Policies supplémentaires

**On a déjà Target Tracking (CPU 70%).**

**Ajoutons Step Scaling pour les pics soudains.**

---

**Console EC2 -> Auto Scaling Groups -> flask-app-asg**

**Onglet "Automatic scaling" -> Create dynamic scaling policy**

---

#### **Step Scaling Policy**

**Policy type :**

```
[x] Step scaling
```

---

**Scaling policy name :**

```
step-scaling-cpu-high
```

---

**CloudWatch alarm :**

**Créer une alarme CloudWatch :**

**Create a CloudWatch alarm**

---

**S'ouvre dans CloudWatch :**

**Select metric -> EC2 -> By Auto Scaling Group**

**Sélectionner :**

```
Metric : CPUUtilization
Auto Scaling Group : flask-app-asg
Statistic : Average
```

**Select metric**

---

**Specify metric and conditions :**

**Metric name :**

```
CPUUtilization
```

---

**Statistic :**

```
Average
```

---

**Period :**

```
1 minute
```

**Évaluer toutes les minutes**

---

**Conditions :**

**Threshold type :**

```
[x] Static
```

---

**Whenever CPUUtilization is... :**

```
[x] Greater/Equal
≥ 80
```

**Alarme si CPU ≥ 80%**

---

**Next**

---

**Configure actions :**

**Notification - SNS topic :**

```
asg-scaling-notifications
```

---

**Next**

---

**Alarm name :**

```
flask-app-cpu-high-80
```

---

**Alarm description :**

```
CPU >= 80% for 1 minute
```

---

**Next -> Create alarm**

---

**Retourner dans l'onglet ASG**

**Rafraîchir -> Sélectionner l'alarme :**

```
flask-app-cpu-high-80
```

---

**Take the action :**

```
[x] Add
```

**Instances :**

```
1 capacity units
```

**When 80 <= CPUUtilization < +infinity**

---

**Add another step (optionnel) :**

```
[x] Add
2 capacity units
When 90 <= CPUUtilization < +infinity
```

**Si CPU > 90%, ajouter 2 instances au lieu d'1**

---

**Instance warmup :**

```
300 seconds
```

---

**Create**

---

**Créer aussi la policy de scale in (CPU bas) :**

**Create dynamic scaling policy**

---

**Policy type :**

```
[x] Step scaling
```

---

**Scaling policy name :**

```
step-scaling-cpu-low
```

---

**CloudWatch alarm :**

**Create a CloudWatch alarm**

---

**Créer une alarme similaire mais pour CPU bas :**

```
Metric : CPUUtilization
Auto Scaling Group : flask-app-asg
Statistic : Average
Period : 5 minutes (plus long pour éviter flapping)

Condition : Less than 30
```

---

**Alarm name :**

```
flask-app-cpu-low-30
```

---

**Create alarm**

---

**Retourner dans ASG -> Sélectionner l'alarme**

---

**Take the action :**

```
[x] Remove
```

**Instances :**

```
1 capacity units
```

**Instance warmup :**

```
60 seconds (moins important pour scale in)
```

---

**Create**

---

**[OK] Scaling policies configurées !**

**Récapitulatif :**

```
Policies actives :
1. Target Tracking : Maintient CPU à 70%
2. Step Scaling High : CPU ≥ 80% -> +1, CPU ≥ 90% -> +2
3. Step Scaling Low : CPU < 30% -> -1
```

---

### ÉTAPE 8 : Ajouter Scheduled Scaling

**Pour anticiper les heures de pointe.**

**Console EC2 -> Auto Scaling Groups -> flask-app-asg**

**Onglet "Automatic scaling" -> Create scheduled action**

---

**Scheduled action name :**

```
business-hours-scale-up
```

---

**Desired capacity :**

```
4
```

---

**Min :**

```
3
```

---

**Max :**

```
8
```

---

**Recurrence :**

```
[x] Cron expression
```

**Cron expression :**

```
0 8 * * 1-5
```

**Format : minute hour day month day-of-week**

**Signification :**
- `0` : Minute 0
- `8` : 8h du matin
- `*` : Tous les jours du mois
- `*` : Tous les mois
- `1-5` : Lundi à vendredi

**Résultat : Tous les jours ouvrés à 8h00**

---

**Time zone :**

```
UTC
```

**[ATTENTION] Attention au fuseau horaire !**

**UTC vs Paris (heure d'hiver) : UTC = Paris - 1h**

**Si tu veux 8h Paris -> Utiliser 7 UTC**

---

**Create**

---

**Créer aussi le scale-down en fin de journée :**

**Create scheduled action**

---

**Scheduled action name :**

```
business-hours-scale-down
```

---

**Desired capacity :**

```
2
```

---

**Min :**

```
2
```

---

**Max :**

```
6
```

---

**Recurrence :**

```
0 18 * * 1-5
```

**Tous les jours ouvrés à 18h00**

---

**Create**

---

**[OK] Scheduled actions configurées !**

**Comportement :**

```
Lundi-Vendredi :
- 8h00 : Scale up (min 3, desired 4, max 8)
- 18h00 : Scale down (min 2, desired 2, max 6)

Week-end :
- Configuration normale (min 2, max 6)
```

---

### ÉTAPE 9 : Ajouter des instances Spot

**Pour réduire les coûts de 70% !**

**Console EC2 -> Auto Scaling Groups -> flask-app-asg**

**Onglet "Details" -> Edit**

---

**Développer "Purchase options and instance types"**

---

**Instance type requirements :**

```
[x] Override launch template
```

---

**Instance types :**

**Ajouter plusieurs types :**

```
[x] t3.micro
[x] t3a.micro
[x] t2.micro
```

**Plus de types = Plus de chances d'obtenir du Spot**

---

**Instance distribution :**

```
[x] Combine purchase options and instance types
```

---

**On-Demand base capacity :**

```
2
```

**Toujours garder 2 instances On-Demand (base stable)**

---

**On-Demand percentage above base :**

```
0 %
```

**Tout le reste en Spot (au-delà de la base)**

---

**Spot allocation strategy :**

```
[x] Capacity optimized
```

**Stratégies Spot :**

**Lowest price :**
- Choisit le type le moins cher
- Risque : Interruptions fréquentes

**Capacity optimized :**
- Choisit le type avec le plus de capacité disponible
- [OK] Moins d'interruptions

**Price capacity optimized :**
- Équilibre prix et capacité
- Bon compromis

---

**Max Spot price (optionnel) :**

```
(laisser vide = prix On-Demand)
```

**Limite le prix maximum payé pour Spot**

**Recommandation : Laisser vide (tu payes rarement plus de 30% du On-Demand)**

---

**Update**

---

**[OK] Configuration Spot activée !**

**Résultat :**

```
Capacité désirée : 4 instances

Distribution :
- 2 On-Demand (base)
- 2 Spot (au-dessus de la base)

Coût :
2 On-Demand t3.micro : 2 × $0.0104 × 730h = $15.18
2 Spot t3.micro : 2 × $0.0031 × 730h = $4.53

Total : ~$20/mois

Vs tout On-Demand :
4 × $0.0104 × 730h = $30.37

Économies : 34% ! [ARGENT]
```

---

### ÉTAPE 10 : Configurer Lifecycle Hooks

**Pour exécuter des actions au lancement/terminaison.**

**Console EC2 -> Auto Scaling Groups -> flask-app-asg**

**Onglet "Instance management" -> Lifecycle hooks -> Create lifecycle hook**

---

#### **Hook au lancement**

**Lifecycle hook name :**

```
instance-launching-hook
```

---

**Lifecycle transition :**

```
[x] Instance launch
```

---

**Heartbeat timeout :**

```
300 seconds
```

**Temps maximum pour compléter l'action**

---

**Default result :**

```
[x] CONTINUE
```

**Si timeout -> Continuer quand même**

**Options :**
- CONTINUE : Continuer
- ABANDON : Abandonner le lancement

---

**Notification metadata (optionnel) :**

**SNS Topic :**

```
asg-scaling-notifications
```

---

**Create**

---

#### **Hook à la terminaison**

**Create lifecycle hook**

---

**Lifecycle hook name :**

```
instance-terminating-hook
```

---

**Lifecycle transition :**

```
[x] Instance terminate
```

---

**Heartbeat timeout :**

```
180 seconds
```

**3 minutes pour drainer les connexions**

---

**Default result :**

```
[x] CONTINUE
```

---

**Create**

---

**[OK] Lifecycle hooks créés !**

**Use cases pratiques :**

**Hook au lancement :**
```bash
#!/bin/bash
# Dans user data ou via Lambda

# Enregistrer dans service discovery
curl -X POST https://api.internal.com/register \
  -d "instance_id=$INSTANCE_ID"

# Réchauffer le cache
curl http://localhost:5000/warmup

# Signal de complétion
aws autoscaling complete-lifecycle-action \
  --lifecycle-action-result CONTINUE \
  --lifecycle-hook-name instance-launching-hook \
  --auto-scaling-group-name flask-app-asg \
  --lifecycle-action-token $LIFECYCLE_TOKEN
```

**Hook à la terminaison :**
```bash
# Via Lambda trigger SNS

# Drainer les connexions
# Sauvegarder les logs
# Désenregistrer de services externes

# Puis compléter
aws autoscaling complete-lifecycle-action \
  --lifecycle-action-result CONTINUE \
  --lifecycle-hook-name instance-terminating-hook \
  --auto-scaling-group-name flask-app-asg \
  --lifecycle-action-token $LIFECYCLE_TOKEN
```

---

### ÉTAPE 11 : Tester l'Auto Scaling

**Génération de charge pour déclencher le scaling.**

---

#### **Méthode 1 : Apache Bench (depuis ton ordinateur)**

```bash
# Installer Apache Bench
# macOS
brew install httpd

# Linux
sudo apt install apache2-utils -y
```

---

**Lancer un test de charge :**

```bash
ab -n 50000 -c 100 -t 600 http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com/
```

**Paramètres :**
- `-n 50000` : 50 000 requêtes
- `-c 100` : 100 connexions simultanées
- `-t 600` : Pendant 10 minutes

---

#### **Méthode 2 : Stress test depuis une instance EC2**

**Plus efficace (bande passante AWS) :**

**Lancer une instance EC2 temporaire :**

```bash
# SSH dans l'instance
ssh -i ma-cle-ec2.pem ubuntu@<IP-INSTANCE-TEST>

# Installer Apache Bench
sudo apt update
sudo apt install -y apache2-utils

# Lancer le test
ab -n 100000 -c 200 http://flask-app-alb-internal-dns/
```

---

#### **Méthode 3 : Script Python (charge plus réaliste)**

```python
# stress_test.py
import requests
import time
import concurrent.futures

URL = "http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com/"

def make_request(i):
    try:
        response = requests.get(URL, timeout=10)
        print(f"Request {i}: {response.status_code}")
    except Exception as e:
        print(f"Request {i}: Error - {e}")

# 100 threads, 10 requêtes par thread par seconde
with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
    for i in range(10000):
        executor.submit(make_request, i)
        time.sleep(0.01)  # 100 req/s
```

**Lancer :**

```bash
pip install requests
python stress_test.py
```

---

### ÉTAPE 12 : Surveiller le Scaling en temps réel

**Console CloudWatch -> Dashboards -> Create dashboard**

---

**Dashboard name :**

```
flask-app-autoscaling
```

---

**Create dashboard**

---

**Add widget -> Line**

---

**Data source : Metrics**

**Sélectionner :**

```
EC2 -> By Auto Scaling Group -> CPUUtilization
Auto Scaling Group : flask-app-asg
```

**Create widget**

---

**Ajouter d'autres widgets :**

**Widget 2 : Instance Count**

```
Metric : GroupDesiredCapacity, GroupInServiceInstances
Auto Scaling Group : flask-app-asg
```

---

**Widget 3 : ALB Metrics**

```
ApplicationELB -> Per AppELB Metrics
LoadBalancer : flask-app-alb
Metrics : ActiveConnectionCount, RequestCount, TargetResponseTime
```

---

**Widget 4 : Target Health**

```
ApplicationELB -> Per Target Group Metrics
TargetGroup : flask-app-tg
Metric : HealthyHostCount, UnHealthyHostCount
```

---

**Save dashboard**

---

**Dashboard en temps réel ! [GRAPHIQUE]**

**Pendant le test de charge, tu verras :**

```
Graphique CPU :
0min : ~20% (2 instances au repos)
5min : ~75% (charge augmente)
8min : ~65% (scaling up, 3 instances)
12min : ~55% (scaling up, 4 instances)
15min : ~50% (scaling stabilisé, 5 instances)

Graphique Instances :
0min : 2 instances
8min : 3 instances
12min : 4 instances
15min : 5 instances
```

---

**Console EC2 -> Auto Scaling Groups -> flask-app-asg**

**Onglet "Activity history" :**

```
Status Message                                              End Time
Launching a new EC2 instance: i-0abc123...                 2024-12-16 20:08:00
Successfully set desired capacity from 2 to 3               2024-12-16 20:07:30
Launching a new EC2 instance: i-0def456...                 2024-12-16 20:12:00
Successfully set desired capacity from 3 to 4               2024-12-16 20:11:30
```

---

**Tu recevras aussi des emails (SNS) :**

```
Subject: AWS Notification - Auto Scaling Event

Message:
At 2024-12-16 20:07:30 UTC the capacity of Auto Scaling group 
"flask-app-asg" changed from 2 to 3.

Event: EC2 Instance Launch Successful
Instance ID: i-0abc123def456789
Availability Zone: eu-west-3c
```

---

**Après l'arrêt du test (15-20 minutes plus tard) :**

```
CPU redescend < 30%
-> Scaling in (retrait d'instances)
-> Retour à 2 instances (minimum)
```

**[OK] Auto Scaling fonctionne parfaitement ! [BRAVO]**

---

### [OK] TESTS DE VALIDATION

**1. ASG créé et fonctionnel**

Console EC2 -> Auto Scaling Groups -> flask-app-asg

- [ ] Desired capacity : 2
- [ ] Instances running : 2
- [ ] Health status : Healthy

---

**2. Load Balancer distribue le trafic**

Tester plusieurs fois :
```
http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com/api/info
```

- [ ] Instance ID change (sans sticky sessions)
- [ ] Instance ID reste identique (avec sticky sessions)

---

**3. Health checks passent**

Console EC2 -> Target Groups -> flask-app-tg -> Targets

- [ ] Toutes les cibles : healthy
- [ ] Health check status : 200 OK

---

**4. Scaling policies actives**

Console ASG -> Automatic scaling

- [ ] Target Tracking : Active
- [ ] Step Scaling High : Active
- [ ] Step Scaling Low : Active

---

**5. Auto Scaling réagit à la charge**

Lancer un test de charge

- [ ] CPU augmente > 70%
- [ ] Instances ajoutées automatiquement
- [ ] CPU redescend après scale-up
- [ ] Instances retirées après baisse de charge

---

**6. Notifications SNS fonctionnent**

- [ ] Email reçu au lancement d'instance
- [ ] Email reçu à la terminaison
- [ ] Email reçu en cas d'échec

---

**7. Instances Spot utilisées**

Console ASG -> Instance management

- [ ] Mix On-Demand + Spot
- [ ] 2 On-Demand minimum
- [ ] Spot pour scaling additionnel

---

**8. Scheduled Scaling configuré**

Console ASG -> Scheduled actions

- [ ] Scale-up business hours (8h)
- [ ] Scale-down evening (18h)

---

**9. Monitoring CloudWatch**

Console CloudWatch -> Dashboards -> flask-app-autoscaling

- [ ] CPU visible
- [ ] Instance count visible
- [ ] ALB metrics visibles

---

**10. Coûts estimés**

```
Configuration normale (2 On-Demand + 0 Spot) :
2 × t3.micro × $0.0104/h × 730h = $15.18

ALB : $16.20/mois + $0.008/LCU

Scaling moyen (2 On-Demand + 2 Spot) :
2 × $0.0104 × 730 = $15.18
2 × $0.0031 × 730 = $4.53
Total instances : $19.71

Total estimé : ~$36-40/mois
```

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Instances ne passent pas healthy

**Console Target Group -> Targets : unhealthy**

**Causes :**

**1. Health check path incorrect**

Vérifier : Target Group -> Health checks -> Path = `/health`

Vérifier : L'app répond sur `/health`

---

**2. Port incorrect**

Target Group -> Health checks -> Port override = `5000`

---

**3. Security Group bloque**

Instances SG Inbound -> Port 5000 depuis ALB SG ?

---

**4. App ne démarre pas**

SSH dans une instance :

```bash
sudo systemctl status flask-app

# Voir les logs
sudo journalctl -u flask-app -n 100
```

**Erreurs courantes :**
- Module non trouvé (pip install raté)
- Connexion DB refusée
- Syntax error dans l'app

---

#### Erreur 2 : Instances lancées mais terminées immédiatement

**Console ASG -> Activity :**

```
Launching instance...
Terminating instance (health check failed)
```

**Cause : Health check grace period trop court**

**Solution :**

ASG -> Edit -> Health check grace period = 300s

L'app met du temps à démarrer (user data + installation).

---

#### Erreur 3 : Auto Scaling ne scale pas

**CPU > 70% mais pas de scale up**

**Vérifications :**

**1. Atteint le maximum ?**

Console ASG -> Group details -> Max capacity

Si Desired = Max -> Ne peut pas scaler davantage

---

**2. Cooldown period actif ?**

Target Tracking a un cooldown intégré (~300s).

Attendre 5 minutes entre les scale actions.

---

**3. Instances en Warmup ?**

Les instances en warmup ne sont pas comptées dans les métriques.

Attendre la fin du warmup (300s).

---

**4. Alarme CloudWatch inactive ?**

Console CloudWatch -> Alarms -> flask-app-cpu-high-80

État : In alarm ?

---

#### Erreur 4 : Instances Spot interrompues fréquemment

**Console ASG -> Activity :**

```
EC2 initiated termination due to Spot interruption
```

**Cause : Type d'instance avec peu de capacité Spot**

**Solutions :**

**1. Ajouter plus de types d'instances**

Launch Template -> Instance types -> Ajouter t3a.micro, t2.micro

---

**2. Changer la stratégie**

ASG -> Purchase options -> Spot allocation strategy : Capacity optimized

---

**3. Augmenter la base On-Demand**

ASG -> On-Demand base capacity : 3 (au lieu de 2)

---

#### Erreur 5 : Sticky sessions ne fonctionnent pas

**Instance ID change à chaque requête**

**Causes :**

**1. Sticky sessions désactivées**

Target Group -> Attributes -> Stickiness : Enabled ?

---

**2. Cookies désactivés dans le navigateur**

Tester avec un autre navigateur ou activer les cookies.

---

**3. Requêtes depuis différents IPs (proxies)**

Le sticky fonctionne par cookie, pas par IP.

---

#### Erreur 6 : Connection draining trop long

**Instances prennent 5 minutes à se terminer**

**Cause : Deregistration delay par défaut = 300s**

**Solution :**

Target Group -> Attributes -> Deregistration delay : 30s

Pour une app stateless, 30s suffisent.

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. Auto Scaling Group**
- Ajuste automatiquement le nombre d'instances
- Haute disponibilité (multi-AZ)
- Remplace instances défaillantes
- Minimise les coûts

**2. Launch Template**
- Blueprint des instances
- User Data pour automatisation
- Versionné (modifications faciles)

**3. Scaling Policies**
- Target Tracking : Simple, efficace (CPU 70%)
- Step Scaling : Contrôle fin, réaction rapide
- Scheduled : Anticipation des heures de pointe
- Predictive : ML pour prédire la charge

**4. Health Checks**
- EC2 : Vérifie l'OS
- ELB : Vérifie l'application
- Toujours activer les deux

**5. Instances Spot**
- 70-90% moins cher
- Peut être interrompu (2 min préavis)
- Stratégie : Base On-Demand + Scaling Spot

**6. ALB avancé**
- Sticky sessions : Même user -> Même instance
- Connection draining : Terminaison propre
- Health checks : Application-level

**7. Monitoring**
- CloudWatch métriques détaillées
- Alarmes pour scale up/down
- SNS notifications
- Dashboard temps réel

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Blue/Green Deployment avec ASG**

**Créer 2 ASG :**
```
ASG Blue (v1) -> Target Group Blue
ASG Green (v2) -> Target Group Green

ALB -> 100% Blue
Test Green
ALB -> Switch 100% Green (zéro downtime)
```

---

**2. Canary Deployment**

**ALB Weighted Target Groups :**
```
ALB -> 90% vers TG v1 (stable)
ALB -> 10% vers TG v2 (canary)

Si OK -> Augmenter progressivement à 100% v2
```

---

**3. Warm Pool (pré-chauffage)**

**ASG Warm Pool :**
```
Instances pré-lancées en "stopped"
-> Démarrage instantané (vs 5 min)
-> Scale-out ultra-rapide

Coût : Seulement le disque (~$1/mois/instance)
```

---

**4. Predictive Scaling ML**

**Console ASG -> Predictive scaling**

```
Analyse 14 jours d'historique
-> Prédit la charge future
-> Pré-scale avant le pic

Exemple :
Chaque lundi 9h : Pic
-> Lundi prochain 8h45 : Pré-scale
```

---

**5. Mixed Instance Policy avancée**

**Launch Template avec :**
```
- Instances types : t3.micro, t3.small, t3.medium
- Weights : t3.micro=1, t3.small=2, t3.medium=4
- ASG désire 8 units -> Mix optimal calculé
```

---

**6. Cross-zone Load Balancing**

**ALB distribue équitablement entre AZs :**
```
Sans cross-zone :
AZ-a : 5 instances -> 50% trafic total (10% par instance)
AZ-b : 1 instance -> 50% trafic total (50% par instance)

Avec cross-zone :
Trafic réparti équitablement sur 6 instances (16.6% chacune)
```

---

**7. Lambda + Lifecycle Hooks**

**Action personnalisée au launch/terminate :**

```python
# Lambda déclenchée par SNS (lifecycle hook)

def lambda_handler(event, context):
    instance_id = event['detail']['EC2InstanceId']
    
    if event['detail']['LifecycleTransition'] == 'autoscaling:EC2_INSTANCE_LAUNCHING':
        # Enregistrer dans service discovery
        register_instance(instance_id)
        
        # Réchauffer cache
        warmup_cache(instance_id)
        
    # Compléter le hook
    asg.complete_lifecycle_action(
        LifecycleActionResult='CONTINUE',
        LifecycleHookName=event['detail']['LifecycleHookName'],
        AutoScalingGroupName=event['detail']['AutoScalingGroupName'],
        LifecycleActionToken=event['detail']['LifecycleActionToken']
    )
```

---

## [COURS] CONCLUSION DE L'EXERCICE 7

**[BRAVO] Félicitations ! Tu as maîtrisé l'Auto Scaling et l'ALB avancé ! [BRAVO]**

**Ce que tu as appris :**
- Créer des Launch Templates
- Configurer Auto Scaling Groups
- Implémenter multiples scaling policies
- Utiliser instances Spot pour économiser
- Configurer ALB avec sticky sessions
- Mettre en place health checks robustes
- Configurer notifications SNS
- Monitorer avec CloudWatch
- Déployer sans downtime

**Compétences acquises :**
- [OK] Auto Scaling Groups (expert)
- [OK] Launch Templates
- [OK] Scaling Policies (Target Tracking, Step, Scheduled)
- [OK] Instances Spot
- [OK] Application Load Balancer avancé
- [OK] CloudWatch alarmes et dashboards
- [OK] Lifecycle Hooks
- [OK] High Availability architecture

**Comparaison avec les autres méthodes :**

| Critère | EC2 manuel | Beanstalk | Lambda | Fargate | **ASG** |
|---------|------------|-----------|--------|---------|---------|
| Contrôle | ***** | *** | ** | **** | ***** |
| Coûts (optimisé) | $10 | $10-20 | $2 | $36 | **$20** |
| Scaling | Manuel | Auto | Infini | Auto | **Auto** |
| HA native | [X] | [OK] | [OK] | [OK] | **[OK]** |
| Spot support | [OK] | [X] | N/A | [OK] | **[OK]** |

**ASG = Maximum de flexibilité avec optimisation des coûts ! [HAUSSE]**

**Temps moyen de réalisation :** 4-5 heures

---

**Prochaine étape :** Exercice 8 - VPC personnalisé (architecture réseau avancée) ! [WEB]

Veux-tu que je continue avec les exercices 8 à 10 ?

- **Exercice 8** : VPC custom (subnets publics/privés, NAT Gateway, NACL)
- **Exercice 9** : CI/CD complet (CodePipeline + CodeBuild + CodeDeploy)
- **Exercice 10** : Architecture complète (tous les services intégrés + best practices)

Continue ? [RAPIDE]

# [ROUGE] EXERCICE 8 : VPC PERSONNALISÉ - ARCHITECTURE RÉSEAU AVANCÉE

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es architecte réseau dans une entreprise fintech. L'équipe doit déployer une application sensible avec des exigences de sécurité strictes : isolation réseau, contrôle d'accès granulaire, segmentation par tiers (web/app/data), conformité réglementaire. Le Default VPC d'AWS ne suffit plus.

### Cahier des charges

Le client souhaite :
- Architecture multi-tier (3 niveaux)
- Isolation réseau complète
- Subnets publics (web) et privés (app, DB)
- Accès Internet contrôlé (NAT Gateway)
- Haute disponibilité (multi-AZ)
- Monitoring du trafic réseau
- Conformité sécurité (NACL, Security Groups)
- Pas de Single Point of Failure

### Contraintes techniques

- VPC personnalisé (CIDR /16)
- 6 subnets (2 publics, 2 privés app, 2 privés DB)
- 2 Availability Zones minimum
- NAT Gateway (haute dispo)
- Bastion Host (accès SSH sécurisé)
- VPC Flow Logs (audit)
- Temps estimé : 4-5 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre l'architecture VPC en profondeur
- [OK] Planifier un plan d'adressage IP (CIDR)
- [OK] Créer subnets publics et privés
- [OK] Configurer Internet Gateway et NAT Gateway
- [OK] Gérer les Route Tables
- [OK] Différencier Security Groups et NACLs
- [OK] Implémenter un Bastion Host
- [OK] Configurer VPC Flow Logs
- [OK] Créer une architecture multi-tier sécurisée

---

## [DOCS] PRÉREQUIS

- Exercices précédents terminés
- Compréhensions bases networking (IP, subnets)
- Application Flask fonctionnelle
- Compte AWS actif

---

## [IDEE] CONCEPTS AWS À COMPRENDRE

### Qu'est-ce qu'un VPC ?

**VPC** = Virtual Private Cloud

**Définition simple :**

Un VPC est ton réseau privé virtuel dans le cloud AWS, isolé logiquement des autres réseaux.

**Analogie :**

```
Data Center traditionnel :
[ENTREPRISE] Bâtiment physique
├── Salle réseau
├── Switches
├── Routeurs
├── Firewalls
└── Serveurs

VPC AWS :
[CLOUD] Réseau virtuel
├── Subnets (segments réseau)
├── Route Tables (routage)
├── Internet Gateway (sortie Internet)
├── Security Groups (firewalls instances)
└── EC2 (serveurs virtuels)
```

**Caractéristiques d'un VPC :**
- Isolé des autres VPCs
- Contrôle total du réseau
- Plage d'adresses IP personnalisée
- Subdivision en subnets
- Connexion Internet configurable

---

### Default VPC vs Custom VPC

**Default VPC :**

```
Créé automatiquement par AWS dans chaque région

Caractéristiques :
- CIDR : 172.31.0.0/16
- Subnets publics dans chaque AZ
- Internet Gateway attaché
- Route vers Internet configurée
- Instances ont IP publiques automatiquement

Avantages :
[OK] Prêt à l'emploi
[OK] Simple pour débuter

Inconvénients :
[X] Pas de subnets privés
[X] Pas d'isolation
[X] Tous les comptes AWS ont le même CIDR
[X] Pas de contrôle fin
```

---

**Custom VPC :**

```
Créé par toi

Caractéristiques :
- CIDR : Choix libre (10.0.0.0/16, 192.168.0.0/16, etc.)
- Subnets publics ET privés
- Internet Gateway optionnel
- Routes personnalisées
- Contrôle total

Avantages :
[OK] Architecture sur mesure
[OK] Sécurité renforcée
[OK] Isolation complète
[OK] Production-ready

Inconvénients :
[X] Plus complexe à configurer
[X] Nécessite compréhension réseau
```

**Pour la production : TOUJOURS un Custom VPC !**

---

### CIDR - Adressage IP

**CIDR** = Classless Inter-Domain Routing

**Format : `IP/MASK`**

**Exemple : `10.0.0.0/16`**

```
10.0.0.0 : Adresse réseau
/16 : Masque (nombre de bits pour la partie réseau)
```

---

#### **Calcul des adresses disponibles**

**Formule : `2^(32 - mask) - 2`**

**Pourquoi -2 ?**
- 1ère adresse : Adresse réseau (réservée)
- Dernière adresse : Broadcast (réservée)

---

**Exemples :**

```
/32 : 2^(32-32) - 2 = 2^0 - 2 = 1 - 2 = -1 -> 1 seule adresse (host)
/31 : 2^1 - 2 = 0 -> 2 adresses (point-to-point, pas de -2)
/30 : 2^2 - 2 = 2 -> 2 adresses utilisables
/24 : 2^8 - 2 = 254 -> 254 adresses
/16 : 2^16 - 2 = 65534 -> 65 534 adresses
/8 : 2^24 - 2 = 16 777 214 -> 16 millions d'adresses
```

---

**VPC AWS - Plages CIDR autorisées :**

**Min : /28** (16 adresses, 11 utilisables)

**Max : /16** (65 536 adresses, 65 531 utilisables)

**Recommandation : /16** (flexibilité maximale)

---

#### **Adresses IP privées (RFC 1918)**

**Plages réservées pour réseaux privés :**

```
10.0.0.0/8        : 10.0.0.0 -> 10.255.255.255 (16M adresses)
172.16.0.0/12     : 172.16.0.0 -> 172.31.255.255 (1M adresses)
192.168.0.0/16    : 192.168.0.0 -> 192.168.255.255 (65K adresses)
```

**AWS recommande : 10.0.0.0/8**

**Pourquoi ?**
- Plus d'adresses
- Évite conflits avec réseaux d'entreprise (souvent 192.168.x.x)

---

**Notre architecture VPC :**

```
VPC : 10.0.0.0/16 (65 534 adresses)

Subnets (6 au total) :
- Public A : 10.0.1.0/24 (254 adresses)
- Public B : 10.0.2.0/24 (254 adresses)
- Private App A : 10.0.11.0/24 (254 adresses)
- Private App B : 10.0.12.0/24 (254 adresses)
- Private DB A : 10.0.21.0/24 (254 adresses)
- Private DB B : 10.0.22.0/24 (254 adresses)

Total utilisé : 6 × 256 = 1536 adresses (2.3% du VPC)
Restant : 64 000 adresses pour évolution
```

---

### Subnets - Segmentation réseau

**Subnet** = Subdivision d'un VPC

**Caractéristiques :**
- Chaque subnet dans UNE SEULE Availability Zone
- CIDR subnet ⊂ CIDR VPC
- Les subnets ne se chevauchent pas

---

#### **Subnet Public vs Privé**

**Subnet Public :**

```
Caractéristiques :
- Route vers Internet Gateway (0.0.0.0/0 -> IGW)
- Instances peuvent avoir IP publiques
- Accès Internet direct

Use cases :
- Load Balancers
- Bastion Hosts
- NAT Gateways
- Serveurs web publics
```

---

**Subnet Privé :**

```
Caractéristiques :
- PAS de route directe vers Internet
- IP privées seulement
- Accès Internet via NAT Gateway (si nécessaire)

Use cases :
- Serveurs applicatifs
- Bases de données
- Services internes
- Workers
```

---

**Différence clé :**

```
Public Subnet :
Route Table contient : 0.0.0.0/0 -> Internet Gateway

Private Subnet :
Route Table contient : 0.0.0.0/0 -> NAT Gateway (ou pas de route Internet)
```

---

### Internet Gateway (IGW)

**Internet Gateway** = Passerelle vers Internet

**Rôle :**
- Permet aux instances de communiquer avec Internet
- Fait du NAT 1:1 (IP privée <-> IP publique)
- Hautement disponible et scalable (managé AWS)
- Gratuit

---

**Fonctionnement :**

```
Instance avec IP publique dans subnet public :

Internet <--> [Internet Gateway] <--> [Instance]
                                   IP privée : 10.0.1.10
                                   IP publique : 54.123.45.67

Requête sortante :
Instance (10.0.1.10) -> IGW traduit -> Internet (depuis 54.123.45.67)

Requête entrante :
Internet -> 54.123.45.67 -> IGW traduit -> Instance (10.0.1.10)
```

**C'est transparent pour l'instance (elle ne voit que son IP privée).**

---

**Attachement IGW :**

```
1 VPC = 1 Internet Gateway maximum

VPC sans IGW = Isolé (pas d'Internet)
VPC avec IGW = Connexion Internet possible
```

---

### NAT Gateway

**NAT Gateway** = Network Address Translation Gateway

**Rôle :**
- Permet aux instances **privées** d'accéder à Internet
- Trafic unidirectionnel (sortant seulement)
- Instances privées ne sont PAS accessibles depuis Internet

---

**Fonctionnement :**

```
Instance privée veut télécharger un package :

[Instance Privée] -> [NAT Gateway] -> [Internet Gateway] -> [Internet]
10.0.11.10           10.0.1.20         54.123.45.67

Requête sortante :
1. Instance (10.0.11.10) -> NAT GW (10.0.1.20)
2. NAT GW change l'IP source -> IP publique du NAT GW
3. NAT GW -> IGW -> Internet

Réponse entrante :
1. Internet -> IGW -> NAT GW (54.123.45.67)
2. NAT GW traduit -> Instance (10.0.11.10)
3. Instance reçoit la réponse

Tentative de connexion depuis Internet :
Internet -> NAT GW : BLOQUÉ (NAT GW n'accepte pas de connexions entrantes)
```

---

**NAT Gateway vs NAT Instance :**

| Critère | NAT Gateway | NAT Instance |
|---------|-------------|--------------|
| **Type** | Service managé | EC2 que tu gères |
| **Disponibilité** | HA dans 1 AZ | Tu gères |
| **Performance** | Jusqu'à 45 Gbps | Dépend du type EC2 |
| **Coût** | $0.045/h + data | EC2 + data |
| **Maintenance** | Zero | Patches, monitoring |
| **Recommandation** | [OK] Production | [X] Déprécié |

**Toujours utiliser NAT Gateway en production.**

---

**NAT Gateway - Haute disponibilité :**

```
Architecture NON HA (1 NAT Gateway) :
Public Subnet A : [NAT GW] <- OK
Private Subnet A : -> NAT GW (AZ-A) <- OK
Private Subnet B : -> NAT GW (AZ-A) <- Cross-AZ traffic ($$$)

Si AZ-A tombe -> Tout le trafic privé coupé [X]

Architecture HA (2 NAT Gateways) :
Public Subnet A : [NAT GW-A]
Public Subnet B : [NAT GW-B]
Private Subnet A : -> NAT GW-A (AZ-A) <- OK
Private Subnet B : -> NAT GW-B (AZ-B) <- OK

Si AZ-A tombe -> Subnet B continue via NAT GW-B [OK]
```

**Recommandation : 1 NAT Gateway par AZ (haute dispo + coûts réduits)**

---

### Route Tables

**Route Table** = Table de routage (comme un routeur réseau)

**Rôle :**
- Détermine où envoyer le trafic réseau
- Chaque subnet associé à UNE route table

---

**Anatomie d'une Route Table :**

```
Destination         Target              Description
10.0.0.0/16         local               Trafic interne VPC
0.0.0.0/0           igw-abc123          Internet via IGW
```

**Lecture :**

```
Paquet vers 10.0.5.45 ?
-> Match 10.0.0.0/16 -> local (routage interne)

Paquet vers 8.8.8.8 ?
-> Match 0.0.0.0/0 -> igw-abc123 (Internet)
```

---

**Route Table types :**

**Main Route Table :**
- Créée automatiquement avec le VPC
- Associée par défaut aux nouveaux subnets
- Modifier avec précaution

**Custom Route Tables :**
- Créées par toi
- Associer explicitement aux subnets
- [OK] Recommandé (séparation public/privé)

---

**Notre architecture :**

```
Public Route Table :
10.0.0.0/16    -> local
0.0.0.0/0      -> igw-abc123

Associée à :
- Public Subnet A
- Public Subnet B

Private Route Table A :
10.0.0.0/16    -> local
0.0.0.0/0      -> nat-gateway-a

Associée à :
- Private App Subnet A
- Private DB Subnet A

Private Route Table B :
10.0.0.0/16    -> local
0.0.0.0/0      -> nat-gateway-b

Associée à :
- Private App Subnet B
- Private DB Subnet B
```

---

### Security Groups vs Network ACLs

**Deux niveaux de sécurité réseau dans AWS :**

#### **Security Groups (Stateful)**

**Niveau : Instance (ENI)**

```
[Internet] -> [Security Group] -> [Instance]
```

**Caractéristiques :**

```
Stateful :
Règle inbound autorisée -> Réponse outbound automatique

Exemple :
Inbound : SSH (22) depuis 1.2.3.4
-> Instance peut répondre à 1.2.3.4 (pas besoin de règle outbound)

Défaut :
Inbound : TOUT bloqué
Outbound : TOUT autorisé

Type : Allow only (pas de Deny)
```

---

**Exemple Security Group :**

```
Inbound :
Type        Port    Source          Description
SSH         22      1.2.3.4/32      Admin access
HTTP        80      0.0.0.0/0       Public web
PostgreSQL  5432    sg-app-servers  App -> DB

Outbound :
Type        Port    Destination     Description
All traffic All     0.0.0.0/0       Allow all
```

---

#### **Network ACLs (Stateless)**

**Niveau : Subnet**

```
[Internet] -> [NACL] -> [Subnet] -> [Security Group] -> [Instance]
```

**Caractéristiques :**

```
Stateless :
Règle inbound SÉPARÉE de outbound

Exemple :
Inbound : HTTP (80) autorisé
Outbound : Ports éphémères (1024-65535) nécessaires pour réponses

Défaut :
TOUT autorisé (inbound + outbound)

Type : Allow ET Deny (priorité par numéro de règle)

Ordre d'évaluation :
Règles triées par numéro (100, 200, 300...)
Première règle qui match -> Appliquée
```

---

**Exemple Network ACL :**

```
Inbound :
Rule #  Type        Port        Source          Allow/Deny
100     HTTP        80          0.0.0.0/0       ALLOW
200     HTTPS       443         0.0.0.0/0       ALLOW
300     SSH         22          1.2.3.4/32      ALLOW
*       All         All         0.0.0.0/0       DENY

Outbound :
Rule #  Type        Port            Destination     Allow/Deny
100     HTTP        80              0.0.0.0/0       ALLOW
200     HTTPS       443             0.0.0.0/0       ALLOW
300     Custom TCP  1024-65535      0.0.0.0/0       ALLOW (éphémères)
*       All         All             0.0.0.0/0       DENY
```

---

**Security Groups vs NACLs :**

| Critère | Security Group | Network ACL |
|---------|----------------|-------------|
| **Niveau** | Instance (ENI) | Subnet |
| **État** | Stateful | Stateless |
| **Règles** | Allow only | Allow + Deny |
| **Ordre** | Toutes évaluées | Numéro de règle |
| **Défaut** | Deny in, Allow out | Allow all |
| **Use case** | Sécurité instance | Sécurité subnet |

---

**Quand utiliser quoi ?**

**Security Groups :**
- [OK] Sécurité principale
- [OK] Contrôle par instance/service
- [OK] Règles dynamiques (référence autres SG)

**Network ACLs :**
- [OK] Couche supplémentaire de sécurité
- [OK] Bloquer IPs spécifiques (Deny)
- [OK] Conformité/audit
- [OK] Protection DDoS basique

**Recommandation : Security Groups principalement, NACLs pour règles Deny**

---

### Bastion Host (Jump Box)

**Bastion Host** = Instance dans subnet public pour accéder aux instances privées

**Problème sans Bastion :**

```
Instances privées : Pas d'IP publique
-> Comment SSH dedans pour debug/maintenance ?

Solution naïve : Attacher IP publique -> [X] Défait le but du privé

Solution correcte : Bastion Host
```

---

**Architecture :**

```
Administrateur (Internet)
        v SSH
    [Bastion Host] (subnet public, IP publique)
        v SSH
    [Instance App] (subnet privé, IP privée seulement)
```

---

**Sécurité Bastion :**

```
Security Group Bastion :
Inbound :
  SSH (22) depuis IP admin SEULEMENT (1.2.3.4/32)

Outbound :
  SSH (22) vers Security Group instances privées

Security Group Instances Privées :
Inbound :
  SSH (22) depuis Security Group Bastion SEULEMENT
```

**Principe du moindre privilège : Seul le bastion peut SSH vers le privé.**

---

**Bastion haute disponibilité :**

```
Option 1 : Bastion dans Auto Scaling Group (min 1, max 1)
-> Remplacé automatiquement si échec

Option 2 : Bastion dans 2 AZs avec Elastic IP
-> Failover manuel

Option 3 : AWS Systems Manager Session Manager
-> Pas de bastion nécessaire (accès via console AWS)
```

---

### VPC Flow Logs

**VPC Flow Logs** = Capture du trafic réseau

**Niveau de capture :**
- VPC entier
- Subnet spécifique
- ENI (interface réseau) spécifique

---

**Informations capturées :**

```
Format :
version account-id interface-id srcaddr dstaddr srcport dstport protocol packets bytes start end action log-status

Exemple :
2 123456789012 eni-abc123 10.0.1.10 54.239.28.85 43123 443 6 10 5000 1234567890 1234567895 ACCEPT OK

Lecture :
Version 2
Account 123456789012
ENI eni-abc123
Source 10.0.1.10 (instance privée)
Destination 54.239.28.85 (IP publique)
Port source 43123 (éphémère)
Port destination 443 (HTTPS)
Protocol 6 (TCP)
Packets 10
Bytes 5000
Start 1234567890
End 1234567895
Action ACCEPT (autorisé)
Log-status OK
```

---

**Use cases :**

**1. Debugging :**
```
Instance ne peut pas accéder à la DB ?
-> Flow Logs : REJECT sur port 5432
-> Problème : Security Group bloque
```

**2. Audit de sécurité :**
```
Trafic suspect vers IP malveillante ?
-> Flow Logs montre les connexions
```

**3. Analyse de trafic :**
```
Quelles instances communiquent le plus ?
-> Optimiser l'architecture
```

**4. Conformité :**
```
RGPD/HIPAA nécessite logs réseau
-> Flow Logs obligatoires
```

---

**Destination des logs :**

**CloudWatch Logs :**
- Analyse en temps réel
- Alarmes sur patterns
- Coût : $0.50/GB ingéré

**S3 :**
- Stockage long terme
- Analyse avec Athena
- Coût : $0.023/GB/mois

**Kinesis Data Firehose :**
- Streaming vers analytics
- Intégration temps réel

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Planifier l'architecture

**Avant de créer quoi que ce soit, planifions.**

---

#### **Plan d'adressage IP**

```
VPC CIDR : 10.0.0.0/16 (65 534 adresses)

Availability Zones : eu-west-3a, eu-west-3b

Subnets (6 au total) :

PUBLIC (accès Internet direct) :
- Public-A : 10.0.1.0/24 (eu-west-3a) - 254 IPs
- Public-B : 10.0.2.0/24 (eu-west-3b) - 254 IPs

PRIVATE APP (serveurs applicatifs) :
- Private-App-A : 10.0.11.0/24 (eu-west-3a) - 254 IPs
- Private-App-B : 10.0.12.0/24 (eu-west-3b) - 254 IPs

PRIVATE DB (bases de données) :
- Private-DB-A : 10.0.21.0/24 (eu-west-3a) - 254 IPs
- Private-DB-B : 10.0.22.0/24 (eu-west-3b) - 254 IPs

Réservé pour évolution future :
- 10.0.3.0/24 -> 10.0.10.0/24
- 10.0.13.0/24 -> 10.0.20.0/24
- 10.0.23.0/24 -> 10.0.255.0/24
```

---

#### **Schéma de l'architecture**

```
                            Internet
                               v
                      [Internet Gateway]
                               v
    ┌──────────────────────────┴──────────────────────────┐
    │                       VPC 10.0.0.0/16                 │
    │                                                        │
    │  AZ eu-west-3a                  AZ eu-west-3b        │
    │  ┌───────────────┐              ┌───────────────┐    │
    │  │ Public-A      │              │ Public-B      │    │
    │  │ 10.0.1.0/24   │              │ 10.0.2.0/24   │    │
    │  │               │              │               │    │
    │  │ [NAT GW-A]    │              │ [NAT GW-B]    │    │
    │  │ [Bastion]     │              │               │    │
    │  │ [ALB]         │              │ [ALB]         │    │
    │  └───────┬───────┘              └───────┬───────┘    │
    │          │                              │            │
    │  ┌───────┴───────┐              ┌───────┴───────┐    │
    │  │ Private-App-A │              │ Private-App-B │    │
    │  │ 10.0.11.0/24  │              │ 10.0.12.0/24  │    │
    │  │               │              │               │    │
    │  │ [EC2 Flask]   │              │ [EC2 Flask]   │    │
    │  │ [EC2 Flask]   │              │ [EC2 Flask]   │    │
    │  └───────┬───────┘              └───────┬───────┘    │
    │          │                              │            │
    │  ┌───────┴───────┐              ┌───────┴───────┐    │
    │  │ Private-DB-A  │              │ Private-DB-B  │    │
    │  │ 10.0.21.0/24  │              │ 10.0.22.0/24  │    │
    │  │               │              │               │    │
    │  │ [RDS Primary] │[BLACK_LEFT-POINTING_POINTER]────────────[BLACK_RIGHT-POINTING_POINTER]│ [RDS Standby]│    │
    │  └───────────────┘              └───────────────┘    │
    │                                                        │
    └────────────────────────────────────────────────────────┘
```

---

### ÉTAPE 2 : Créer le VPC

**Console AWS -> VPC -> Your VPCs -> Create VPC**

---

**Resources to create :**

```
[x] VPC only
```

**On va créer les composants un par un pour comprendre.**

---

**Name tag :**

```
production-vpc
```

---

**IPv4 CIDR block :**

```
IPv4 CIDR manual input
```

**IPv4 CIDR :**

```
10.0.0.0/16
```

---

**IPv6 CIDR block :**

```
[ ] No IPv6 CIDR block
```

**Pour simplifier, on reste en IPv4.**

---

**Tenancy :**

```
[x] Default
```

**Tenancy types :**
- **Default** : Instances sur hardware partagé
- **Dedicated** : Instances sur hardware dédié (licensing, compliance)

**Default = Moins cher**

---

**Tags :**

```
Key : Environment | Value : Production
Key : Project | Value : Flask-App
```

---

**Create VPC**

**[OK] VPC créé !**

**Informations affichées :**

```
VPC ID : vpc-0abc123def456789
State : available
IPv4 CIDR : 10.0.0.0/16
Default VPC : No
```

---

### ÉTAPE 3 : Activer DNS Hostnames

**Par défaut, les instances n'ont pas de DNS hostname.**

**VPC -> Sélectionner production-vpc -> Actions -> Edit DNS hostnames**

```
[x] Enable DNS hostnames
```

**Save**

**Pourquoi activer ?**

```
Sans DNS hostname :
Instance privée : 10.0.11.10 (IP seulement)
-> Difficile à identifier

Avec DNS hostname :
Instance privée : ip-10-0-11-10.eu-west-3.compute.internal
-> Plus lisible dans les logs
```

---

### ÉTAPE 4 : Créer un Internet Gateway

**VPC -> Internet Gateways -> Create internet gateway**

---

**Name tag :**

```
production-igw
```

---

**Create internet gateway**

**[OK] Internet Gateway créé !**

**État : Detached (pas encore attaché au VPC)**

---

**Attacher au VPC :**

**Actions -> Attach to VPC**

**Available VPCs :**

```
production-vpc
```

**Attach internet gateway**

**État passe à : Attached**

---

### ÉTAPE 5 : Créer les Subnets

**On va créer les 6 subnets.**

**VPC -> Subnets -> Create subnet**

---

#### **Subnet 1 : Public-A**

**VPC ID :**

```
production-vpc
```

---

**Subnet settings :**

**Subnet name :**

```
Public-A
```

---

**Availability Zone :**

```
eu-west-3a
```

---

**IPv4 CIDR block :**

```
10.0.1.0/24
```

**Validation automatique :**
```
Available IPv4 addresses : 251
(AWS réserve 5 adresses par subnet)
```

**Les 5 adresses réservées :**
```
10.0.1.0 : Adresse réseau
10.0.1.1 : VPC router
10.0.1.2 : DNS server
10.0.1.3 : Réservé AWS
10.0.1.255 : Broadcast
```

---

**Tags :**

```
Key : Name | Value : Public-A
Key : Type | Value : Public
```

---

**Add new subnet** (pour créer les autres en même temps)

---

#### **Subnet 2 : Public-B**

```
Subnet name : Public-B
Availability Zone : eu-west-3b
IPv4 CIDR : 10.0.2.0/24
Tags : Name=Public-B, Type=Public
```

---

#### **Subnet 3 : Private-App-A**

```
Subnet name : Private-App-A
Availability Zone : eu-west-3a
IPv4 CIDR : 10.0.11.0/24
Tags : Name=Private-App-A, Type=Private
```

---

#### **Subnet 4 : Private-App-B**

```
Subnet name : Private-App-B
Availability Zone : eu-west-3b
IPv4 CIDR : 10.0.12.0/24
Tags : Name=Private-App-B, Type=Private
```

---

#### **Subnet 5 : Private-DB-A**

```
Subnet name : Private-DB-A
Availability Zone : eu-west-3a
IPv4 CIDR : 10.0.21.0/24
Tags : Name=Private-DB-A, Type=Private
```

---

#### **Subnet 6 : Private-DB-B**

```
Subnet name : Private-DB-B
Availability Zone : eu-west-3b
IPv4 CIDR : 10.0.22.0/24
Tags : Name=Private-DB-B, Type=Private
```

---

**Create subnet**

**[OK] 6 Subnets créés ! [BRAVO]**

---

### ÉTAPE 6 : Activer Auto-assign Public IP pour subnets publics

**Par défaut, les instances n'ont pas d'IP publique automatiquement.**

**Subnets -> Sélectionner Public-A**

**Actions -> Edit subnet settings**

```
[x] Enable auto-assign public IPv4 address
```

**Save**

---

**Répéter pour Public-B**

---

**Maintenant les instances lancées dans Public-A/B auront automatiquement une IP publique.**

---

### ÉTAPE 7 : Créer les NAT Gateways

**On va créer 2 NAT Gateways (1 par AZ pour haute dispo).**

**VPC -> NAT Gateways -> Create NAT gateway**

---

#### **NAT Gateway A**

**Name :**

```
NAT-Gateway-A
```

---

**Subnet :**

```
Public-A (10.0.1.0/24)
```

**[ATTENTION] NAT Gateway doit être dans un subnet PUBLIC**

---

**Connectivity type :**

```
[x] Public
```

**Types :**
- **Public** : Accès Internet (via IGW)
- **Private** : Accès VPC uniquement (VPC peering, etc.)

---

**Elastic IP allocation ID :**

**Allocate Elastic IP** (cliquer sur le bouton)

**Une IP publique statique est allouée automatiquement**

---

**Create NAT gateway**

**Durée : 2-3 minutes**

**État : Pending -> Available**

---

#### **NAT Gateway B**

**Répéter pour la deuxième AZ :**

```
Name : NAT-Gateway-B
Subnet : Public-B
Connectivity type : Public
Elastic IP : Allocate new
```

**Create NAT gateway**

---

**[OK] 2 NAT Gateways créés !**

---

**[ARGENT] Coût NAT Gateway :**

```
1 NAT Gateway : $0.045/heure = $32.85/mois
Data processed : $0.045/GB

2 NAT Gateways : $65.70/mois + data

[ATTENTION] Coût significatif !

Alternatives pour réduire :
- VPC Endpoints (S3, DynamoDB) : Gratuit
- Grouper plus d'instances par subnet
- 1 NAT Gateway (sans HA) : $32.85/mois
```

---

Je vais continuer avec la configuration des Route Tables et la création du Bastion Host dans le prochain message. Veux-tu que je continue ? [RAPIDE]


# [ROUGE] EXERCICE 8 : VPC PERSONNALISÉ (SUITE)

### ÉTAPE 8 : Créer et configurer les Route Tables

**On a besoin de 3 Route Tables :**
1. Public Route Table (IGW)
2. Private Route Table A (NAT-A)
3. Private Route Table B (NAT-B)

---

**VPC -> Route Tables**

**Tu vois déjà :**
```
Main route table (rtb-xxx)
VPC : production-vpc
Routes : 10.0.0.0/16 -> local
```

**C'est la route table principale (créée automatiquement avec le VPC).**

**[ATTENTION] Bonne pratique : Ne PAS utiliser la Main Route Table**

**Pourquoi ?**
```
Main Route Table = Associée automatiquement aux nouveaux subnets

Si tu ajoutes une route Internet à la Main :
-> Nouveaux subnets = Publics par défaut
-> Risque de sécurité !

Solution : Laisser la Main vide, créer des custom route tables
```

---

#### **Route Table 1 : Public**

**Create route table**

**Name :**
```
Public-Route-Table
```

**VPC :**
```
production-vpc
```

**Tags :**
```
Key : Name | Value : Public-Route-Table
Key : Type | Value : Public
```

**Create route table**

---

**Ajouter la route vers Internet :**

**Routes -> Edit routes -> Add route**

**Destination :**
```
0.0.0.0/0
```

**Target :**
```
[x] Internet Gateway
production-igw
```

**Save changes**

---

**Route Table affiche maintenant :**
```
Destination       Target              Status
10.0.0.0/16       local               Active
0.0.0.0/0         igw-abc123          Active
```

**Explication :**
```
10.0.0.0/16 -> local : Trafic interne VPC (automatique)
0.0.0.0/0 -> IGW : Tout le reste vers Internet
```

---

**Associer aux subnets publics :**

**Subnet associations -> Edit subnet associations**

**Sélectionner :**
```
[x] Public-A (10.0.1.0/24)
[x] Public-B (10.0.2.0/24)
```

**Save associations**

**[OK] Subnets publics configurés !**

---

#### **Route Table 2 : Private-A**

**Create route table**

**Name :**
```
Private-Route-Table-A
```

**VPC :**
```
production-vpc
```

**Tags :**
```
Key : Name | Value : Private-Route-Table-A
Key : Type | Value : Private
Key : AZ | Value : eu-west-3a
```

**Create route table**

---

**Ajouter la route vers NAT Gateway A :**

**Routes -> Edit routes -> Add route**

**Destination :**
```
0.0.0.0/0
```

**Target :**
```
[x] NAT Gateway
NAT-Gateway-A
```

**Save changes**

---

**Routes :**
```
Destination       Target              Status
10.0.0.0/16       local               Active
0.0.0.0/0         nat-abc123          Active
```

---

**Associer aux subnets privés de l'AZ-A :**

**Subnet associations -> Edit subnet associations**

**Sélectionner :**
```
[x] Private-App-A (10.0.11.0/24)
[x] Private-DB-A (10.0.21.0/24)
```

**Save associations**

---

#### **Route Table 3 : Private-B**

**Répéter le processus :**

```
Name : Private-Route-Table-B
VPC : production-vpc
Tags : Name=Private-Route-Table-B, Type=Private, AZ=eu-west-3b

Routes :
0.0.0.0/0 -> NAT-Gateway-B

Subnet associations :
[x] Private-App-B (10.0.12.0/24)
[x] Private-DB-B (10.0.22.0/24)
```

---

**[OK] Route Tables configurées ! [BRAVO]**

**Récapitulatif :**
```
Public Route Table :
- Subnets : Public-A, Public-B
- Internet : Direct via IGW

Private Route Table A :
- Subnets : Private-App-A, Private-DB-A
- Internet : Via NAT-Gateway-A

Private Route Table B :
- Subnets : Private-App-B, Private-DB-B
- Internet : Via NAT-Gateway-B
```

---

### ÉTAPE 9 : Créer les Security Groups

**On va créer 4 Security Groups :**
1. Bastion-SG (accès SSH admin)
2. ALB-SG (load balancer public)
3. App-SG (serveurs Flask)
4. DB-SG (RDS PostgreSQL)

---

**EC2 -> Security Groups -> Create security group**

---

#### **Security Group 1 : Bastion**

**Security group name :**
```
Bastion-SG
```

**Description :**
```
Security group for Bastion Host (SSH access)
```

**VPC :**
```
production-vpc
```

---

**Inbound rules :**

**Add rule :**
```
Type : SSH
Port : 22
Source : My IP (ou ton IP publique : X.X.X.X/32)
Description : SSH access from admin
```

**[ATTENTION] IMPORTANT : Restreindre SSH à TON IP uniquement !**

**Si tu veux autoriser plusieurs IPs admin :**
```
Rule 1 : SSH depuis 1.2.3.4/32 (Admin 1)
Rule 2 : SSH depuis 5.6.7.8/32 (Admin 2)
```

---

**Outbound rules :**

```
Type : SSH
Port : 22
Destination : Custom (on va ajouter App-SG après sa création)
Description : SSH to private instances

Type : HTTPS
Port : 443
Destination : 0.0.0.0/0
Description : Package updates
```

**Pour l'instant, laisser l'outbound par défaut (All traffic -> 0.0.0.0/0)**

---

**Tags :**
```
Key : Name | Value : Bastion-SG
```

**Create security group**

---

#### **Security Group 2 : ALB**

**Security group name :**
```
ALB-SG
```

**Description :**
```
Security group for Application Load Balancer
```

**VPC :**
```
production-vpc
```

---

**Inbound rules :**

```
Type : HTTP
Port : 80
Source : 0.0.0.0/0
Description : HTTP from Internet

Type : HTTPS
Port : 443
Source : 0.0.0.0/0
Description : HTTPS from Internet
```

---

**Outbound rules :**

```
Type : Custom TCP
Port : 5000
Destination : Custom (App-SG à venir)
Description : Forward to Flask app
```

**Pour l'instant, laisser par défaut.**

---

**Create security group**

---

#### **Security Group 3 : App (Flask)**

**Security group name :**
```
App-SG
```

**Description :**
```
Security group for Flask application servers
```

**VPC :**
```
production-vpc
```

---

**Inbound rules :**

```
Type : Custom TCP
Port : 5000
Source : Custom -> ALB-SG
Description : HTTP from ALB

Type : SSH
Port : 22
Source : Custom -> Bastion-SG
Description : SSH from Bastion
```

**Pour sélectionner un autre Security Group :**
1. Dans le champ Source, commencer à taper "sg-"
2. Sélectionner le SG dans la liste

---

**Outbound rules :**

```
Type : PostgreSQL
Port : 5432
Destination : Custom -> DB-SG (à venir)
Description : Database access

Type : HTTPS
Port : 443
Destination : 0.0.0.0/0
Description : Internet access (packages, APIs)
```

---

**Create security group**

---

#### **Security Group 4 : Database**

**Security group name :**
```
DB-SG
```

**Description :**
```
Security group for RDS PostgreSQL database
```

**VPC :**
```
production-vpc
```

---

**Inbound rules :**

```
Type : PostgreSQL
Port : 5432
Source : Custom -> App-SG
Description : Database access from app servers
```

**[ATTENTION] IMPORTANT : Seulement depuis App-SG, pas depuis Internet !**

---

**Outbound rules :**

```
(Laisser par défaut : All traffic)
```

**RDS n'a généralement pas besoin de sortir vers Internet.**

---

**Create security group**

---

**[OK] Security Groups créés !**

---

**Maintenant, mettre à jour les références circulaires :**

**1. Bastion-SG -> Outbound vers App-SG**

EC2 -> Security Groups -> Bastion-SG -> Outbound rules -> Edit outbound rules

Supprimer la règle "All traffic"

Add rule :
```
Type : SSH
Port : 22
Destination : App-SG
Description : SSH to app servers
```

Save rules

---

**2. ALB-SG -> Outbound vers App-SG**

ALB-SG -> Outbound rules -> Edit outbound rules

Supprimer "All traffic"

Add rule :
```
Type : Custom TCP
Port : 5000
Destination : App-SG
Description : Forward to Flask
```

Save rules

---

**3. App-SG -> Outbound vers DB-SG**

App-SG -> Outbound rules -> Edit outbound rules

Modifier la règle PostgreSQL :
```
Destination : DB-SG
```

Save rules

---

**[OK] Security Groups finalisés ! [BRAVO]**

**Architecture de sécurité :**
```
Internet -> ALB-SG (80/443)
           v
        App-SG (5000)
           v
        DB-SG (5432)

Admin -> Bastion-SG (22)
           v
        App-SG (22)
```

---

### ÉTAPE 10 : Configurer les Network ACLs (optionnel mais recommandé)

**Par défaut, tous les subnets utilisent la Default Network ACL (tout autorisé).**

**Pour plus de sécurité, créons des NACLs personnalisées.**

---

**VPC -> Network ACLs -> Create network ACL**

---

#### **NACL 1 : Public**

**Name :**
```
Public-NACL
```

**VPC :**
```
production-vpc
```

**Create network ACL**

---

**Inbound rules -> Edit inbound rules**

**Add new rule :**

**Rule 100 : HTTP**
```
Rule number : 100
Type : HTTP (80)
Source : 0.0.0.0/0
Allow/Deny : Allow
```

**Rule 110 : HTTPS**
```
Rule number : 110
Type : HTTPS (443)
Source : 0.0.0.0/0
Allow/Deny : Allow
```

**Rule 120 : SSH**
```
Rule number : 120
Type : SSH (22)
Source : TON-IP/32
Allow/Deny : Allow
```

**Rule 130 : Ephemeral ports (pour les réponses)**
```
Rule number : 130
Type : Custom TCP
Port range : 1024-65535
Source : 0.0.0.0/0
Allow/Deny : Allow
```

**[ATTENTION] Ports éphémères critiques !**

**Explication :**
```
Client fait requête HTTP :
Client (port 45678) -> Serveur (port 80)

Réponse :
Serveur (port 80) -> Client (port 45678)

Le port 45678 est éphémère (1024-65535)
-> NACL doit autoriser ces ports pour les réponses
```

**Rule * : Deny all**
```
(Automatique, priorité la plus basse)
```

---

**Outbound rules -> Edit outbound rules**

**Rule 100 : HTTP**
```
Rule number : 100
Type : HTTP (80)
Destination : 0.0.0.0/0
Allow/Deny : Allow
```

**Rule 110 : HTTPS**
```
Rule number : 110
Type : HTTPS (443)
Destination : 0.0.0.0/0
Allow/Deny : Allow
```

**Rule 120 : PostgreSQL vers Private**
```
Rule number : 120
Type : PostgreSQL (5432)
Destination : 10.0.0.0/16 (VPC interne)
Allow/Deny : Allow
```

**Rule 130 : Ephemeral ports**
```
Rule number : 130
Type : Custom TCP
Port range : 1024-65535
Destination : 0.0.0.0/0
Allow/Deny : Allow
```

**Rule * : Deny all**

Save changes

---

**Associer aux subnets publics :**

**Subnet associations -> Edit subnet associations**

Sélectionner :
```
[x] Public-A
[x] Public-B
```

Save changes

---

#### **NACL 2 : Private App**

**Name :**
```
Private-App-NACL
```

**Inbound rules :**

```
Rule 100 : Custom TCP (Flask)
Port : 5000
Source : 10.0.1.0/24 (Public-A)
Allow

Rule 110 : Custom TCP (Flask)
Port : 5000
Source : 10.0.2.0/24 (Public-B)
Allow

Rule 120 : SSH
Port : 22
Source : 10.0.1.0/24 (Bastion dans Public-A)
Allow

Rule 130 : Ephemeral
Port : 1024-65535
Source : 0.0.0.0/0
Allow
```

**Outbound rules :**

```
Rule 100 : PostgreSQL
Port : 5432
Destination : 10.0.21.0/24 (DB-A)
Allow

Rule 110 : PostgreSQL
Port : 5432
Destination : 10.0.22.0/24 (DB-B)
Allow

Rule 120 : HTTP
Port : 80
Destination : 0.0.0.0/0
Allow

Rule 130 : HTTPS
Port : 443
Destination : 0.0.0.0/0
Allow

Rule 140 : Ephemeral
Port : 1024-65535
Destination : 0.0.0.0/0
Allow
```

**Subnet associations :**
```
[x] Private-App-A
[x] Private-App-B
```

---

#### **NACL 3 : Private DB**

**Name :**
```
Private-DB-NACL
```

**Inbound rules :**

```
Rule 100 : PostgreSQL
Port : 5432
Source : 10.0.11.0/24 (App-A)
Allow

Rule 110 : PostgreSQL
Port : 5432
Source : 10.0.12.0/24 (App-B)
Allow

Rule 120 : Ephemeral
Port : 1024-65535
Source : 0.0.0.0/0
Allow
```

**Outbound rules :**

```
Rule 100 : Ephemeral
Port : 1024-65535
Destination : 0.0.0.0/0
Allow
```

**Subnet associations :**
```
[x] Private-DB-A
[x] Private-DB-B
```

---

**[OK] Network ACLs configurées !**

**Sécurité à deux niveaux maintenant :**
```
Niveau 1 : Network ACL (subnet-level)
Niveau 2 : Security Group (instance-level)
```

---

### ÉTAPE 11 : Créer le Bastion Host

**Console EC2 -> Instances -> Launch instances**

---

**Name :**
```
Bastion-Host
```

---

**AMI :**
```
Ubuntu Server 22.04 LTS
```

---

**Instance type :**
```
t3.micro
```

**Pour bastion : t3.micro suffit (pas de charge)**

---

**Key pair :**
```
ta-cle-existante
```

---

**Network settings -> Edit**

**VPC :**
```
production-vpc
```

**Subnet :**
```
Public-A (10.0.1.0/24)
```

---

**Auto-assign public IP :**
```
[x] Enable
```

**Le bastion a BESOIN d'une IP publique (accès SSH depuis Internet)**

---

**Firewall (security groups) :**
```
[x] Select existing security group
Bastion-SG
```

---

**Advanced details -> IAM instance profile :**
```
(Aucun pour l'instant)
```

---

**User data (optionnel) :**

```bash
#!/bin/bash
# Mettre à jour le système
apt-get update
apt-get upgrade -y

# Installer des outils utiles
apt-get install -y htop tmux vim

# Configurer le message du jour
cat > /etc/motd << 'EOF'
╔═══════════════════════════════════════╗
║     [EUROPEAN_CASTLE] BASTION HOST - PRODUCTION     ║
║                                       ║
║  [ATTENTION]  Accès sensible - Logs audités   ║
╚═══════════════════════════════════════╝
EOF

echo "Bastion host configured" > /var/log/bastion-init.log
```

---

**Launch instance**

**Instance lance... [HOURGLASS_WITH_FLOWING_SAND]**

---

**Une fois l'instance "Running" :**

**Copier l'IP publique :**
```
Public IPv4: 54.123.45.67
```

---

**Tester la connexion SSH :**

```bash
ssh -i ma-cle-ec2.pem ubuntu@54.123.45.67
```

**Résultat :**
```
╔═══════════════════════════════════════╗
║     [EUROPEAN_CASTLE] BASTION HOST - PRODUCTION     ║
║                                       ║
║  [ATTENTION]  Accès sensible - Logs audités   ║
╚═══════════════════════════════════════╝

ubuntu@bastion-host:~$
```

**[OK] Bastion Host opérationnel ! [BRAVO]**

---

### ÉTAPE 12 : Créer DB Subnet Group pour RDS

**RDS nécessite un DB Subnet Group (subnets où placer la DB).**

**Console RDS -> Subnet groups -> Create DB subnet group**

---

**Name :**
```
production-db-subnet-group
```

---

**Description :**
```
Subnet group for RDS in private DB subnets
```

---

**VPC :**
```
production-vpc
```

---

**Availability Zones :**

Sélectionner :
```
[x] eu-west-3a
[x] eu-west-3b
```

---

**Subnets :**

**Pour eu-west-3a :**
```
[x] Private-DB-A (10.0.21.0/24)
```

**Pour eu-west-3b :**
```
[x] Private-DB-B (10.0.22.0/24)
```

---

**Create**

**[OK] DB Subnet Group créé !**

---

### ÉTAPE 13 : Créer RDS dans le VPC personnalisé

**Console RDS -> Databases -> Create database**

---

**Engine :**
```
[x] PostgreSQL 15.5
```

---

**Templates :**
```
[x] Free tier (ou Dev/Test)
```

---

**DB instance identifier :**
```
production-postgres-db
```

---

**Master username :**
```
postgres
```

---

**Master password :**
```
(Mot de passe sécurisé)
```

---

**DB instance class :**
```
db.t3.micro (Free Tier eligible)
```

---

**Storage :**
```
Allocated storage : 20 GiB
```

---

**Connectivity :**

**VPC :**
```
production-vpc
```

---

**DB subnet group :**
```
production-db-subnet-group
```

---

**Public access :**
```
[ ] No
```

**[ATTENTION] IMPORTANT : DB TOUJOURS privée !**

---

**VPC security group :**
```
[x] Choose existing
DB-SG
```

**Décocher "default"**

---

**Availability Zone :**
```
eu-west-3a (ou No preference)
```

---

**Database authentication :**
```
[x] Password authentication
```

---

**Additional configuration :**

**Initial database name :**
```
production_db
```

---

**Backup :**
```
[x] Enable automated backups
Backup retention period : 7 days
```

---

**Monitoring :**
```
[x] Enable Enhanced Monitoring (optionnel)
```

---

**Create database**

**Durée : 10-15 minutes**

---

**Une fois disponible :**

**RDS -> Databases -> production-postgres-db**

**Endpoint :**
```
production-postgres-db.c1abc2def3.eu-west-3.rds.amazonaws.com
```

**Port :**
```
5432
```

**[OK] RDS créée dans le VPC personnalisé ! [BRAVO]**

---

### ÉTAPE 14 : Déployer l'application dans le VPC personnalisé

**On va déployer l'application Flask dans les subnets Private-App.**

---

#### **Option 1 : Launch Template + Auto Scaling (recommandé)**

**Utiliser le Launch Template de l'exercice 7, mais modifier :**

**EC2 -> Launch Templates -> Créer une nouvelle version**

**Base sur :** `flask-app-launch-template`

---

**Modifier :**

**Network settings :**
- Ne PAS inclure de subnet (ASG gérera)
- Security Groups : `App-SG`

---

**User Data - Mettre à jour DATABASE_URL :**

```bash
# Dans le script user data, remplacer :
DATABASE_URL=postgresql://postgres:MOT_DE_PASSE@production-postgres-db.c1abc2def3.eu-west-3.rds.amazonaws.com:5432/production_db
```

---

**Créer un nouvel Auto Scaling Group :**

**Console EC2 -> Auto Scaling Groups -> Create**

```
Name : production-flask-asg

Launch template : flask-app-launch-template (nouvelle version)

VPC : production-vpc

Subnets : Private-App-A, Private-App-B

Load balancing : Attach to existing ALB (créer ALB après)

Desired : 2
Min : 2
Max : 6

Health checks : ELB
Grace period : 300s

Scaling policies : Target tracking CPU 70%
```

---

#### **Option 2 : Instances manuelles (pour test)**

**EC2 -> Launch instances**

```
Name : Flask-App-1
AMI : Ubuntu 22.04
Instance type : t3.micro

Network :
VPC : production-vpc
Subnet : Private-App-A
Auto-assign public IP : Disable

Security group : App-SG

User data : (script Flask complet)
```

**[ATTENTION] Instance privée : Pas d'IP publique**

**Comment déployer le code ?**

**Méthode 1 : Via Bastion (SSH tunnel)**

```bash
# Sur ton ordinateur
# Copier le code vers Bastion
scp -i ma-cle.pem -r flask-app ubuntu@BASTION-IP:/tmp/

# SSH vers Bastion
ssh -i ma-cle.pem ubuntu@BASTION-IP

# Depuis Bastion, copier vers instance privée
scp -r /tmp/flask-app ubuntu@10.0.11.10:/home/ubuntu/
```

---

**Méthode 2 : User Data (téléchargement depuis S3)**

```bash
#!/bin/bash
# Dans user data

# Télécharger le code depuis S3
aws s3 cp s3://mon-bucket/flask-app.tar.gz /tmp/
tar -xzf /tmp/flask-app.tar.gz -C /opt/

# Ou cloner depuis GitHub (si accès Internet via NAT)
cd /opt
git clone https://github.com/mon-repo/flask-app.git
```

---

**Méthode 3 : AMI personnalisée (meilleure pratique)**

**Créer une instance temporaire dans un subnet public :**
1. Installer tout (Python, Flask, Nginx, etc.)
2. Configurer l'app
3. Actions -> Image and templates -> Create image
4. AMI créée
5. Utiliser cette AMI dans Launch Template

**Avantages :**
- Démarrage instantané (code déjà présent)
- Configuration reproductible
- User Data minimal

---

### ÉTAPE 15 : Créer l'Application Load Balancer dans le VPC

**Console EC2 -> Load Balancers -> Create load balancer**

```
Type : Application Load Balancer

Name : production-alb

Scheme : Internet-facing

IP address type : IPv4

VPC : production-vpc

Mappings :
[x] Public-A
[x] Public-B

Security groups : ALB-SG

Listeners :
HTTP:80 -> Target Group production-flask-tg

Target Group :
Name : production-flask-tg
Type : Instances
Protocol : HTTP
Port : 5000 (override)
VPC : production-vpc

Health check :
Path : /health
Port : Traffic port
```

**Create load balancer**

---

**DNS ALB :**
```
production-alb-123456789.eu-west-3.elb.amazonaws.com
```

---

### ÉTAPE 16 : Configurer VPC Flow Logs

**Pour auditer le trafic réseau.**

**VPC -> Your VPCs -> production-vpc**

**Actions -> Create flow log**

---

**Name :**
```
production-vpc-flow-logs
```

---

**Filter :**
```
[x] All (Accepted + Rejected traffic)
```

**Options :**
- All : Tout le trafic
- Accept : Seulement autorisé
- Reject : Seulement bloqué

**Pour audit complet : All**

---

**Maximum aggregation interval :**
```
1 minute (ou 10 minutes pour réduire coûts)
```

---

**Destination :**
```
[x] Send to CloudWatch Logs
```

---

**Destination log group :**

**Create new log group** :
```
/aws/vpc/production-vpc
```

---

**IAM role :**

**Set Up Permissions** (AWS crée automatiquement le rôle)

---

**Log record format :**
```
[x] AWS default format
```

**Format personnalisé disponible si besoin de champs spécifiques.**

---

**Tags :**
```
Key : Name | Value : Production VPC Flow Logs
```

---

**Create flow log**

**[OK] Flow Logs activés !**

---

**Voir les logs :**

**Console CloudWatch -> Log groups -> /aws/vpc/production-vpc**

**Tu verras des entrées comme :**
```
2 123456789012 eni-abc123 10.0.11.10 10.0.21.15 45678 5432 6 10 5000 1640000000 1640000060 ACCEPT OK
```

**Interprétation :**
```
Version 2
Account 123456789012
ENI eni-abc123
Source IP 10.0.11.10 (instance Flask)
Dest IP 10.0.21.15 (RDS)
Source port 45678
Dest port 5432 (PostgreSQL)
Protocol 6 (TCP)
Packets 10
Bytes 5000
Start timestamp
End timestamp
Action ACCEPT
Log status OK
```

---

### ÉTAPE 17 : Tester l'architecture complète

#### **Test 1 : Connectivité Bastion -> Instance privée**

**SSH vers Bastion :**
```bash
ssh -i ma-cle.pem ubuntu@BASTION-PUBLIC-IP
```

**Depuis Bastion, SSH vers instance privée :**
```bash
ssh ubuntu@10.0.11.10
```

**[ATTENTION] Note : Copier la clé SSH sur Bastion (pas recommandé en prod)**

**Alternative (SSH Agent Forwarding) :**
```bash
# Sur ton ordinateur
ssh-add ma-cle.pem

# SSH avec agent forwarding
ssh -A -i ma-cle.pem ubuntu@BASTION-IP

# Maintenant SSH vers privé fonctionne sans copier la clé
ssh ubuntu@10.0.11.10
```

**[OK] Test 1 réussi si tu peux SSH vers l'instance privée**

---

#### **Test 2 : Instance privée a accès Internet (via NAT)**

**SSH vers instance privée (via Bastion)**

**Tester :**
```bash
# Ping Google DNS
ping -c 3 8.8.8.8

# Télécharger quelque chose
curl -I https://www.google.com

# Installer un package
sudo apt update
sudo apt install -y htop
```

**[OK] Test 2 réussi si les commandes fonctionnent**

**Si échec :**
- Vérifier Route Table (0.0.0.0/0 -> NAT Gateway ?)
- Vérifier NAT Gateway (État : Available ?)
- Vérifier Security Group (Outbound HTTPS autorisé ?)

---

#### **Test 3 : Instance privée peut accéder à RDS**

**SSH vers instance privée**

**Installer PostgreSQL client :**
```bash
sudo apt install -y postgresql-client
```

**Tester connexion RDS :**
```bash
psql -h production-postgres-db.c1abc2def3.eu-west-3.rds.amazonaws.com \
     -U postgres \
     -d production_db
```

**Entrer le mot de passe**

**Si connexion OK :**
```
production_db=> \dt
production_db=> \q
```

**[OK] Test 3 réussi**

---

#### **Test 4 : Application accessible via ALB**

**Navigateur :**
```
http://production-alb-123456789.eu-west-3.elb.amazonaws.com
```

**Page Flask s'affiche**

**[OK] Test 4 réussi**

---

#### **Test 5 : Instance privée n'est PAS accessible depuis Internet**

**Essayer de SSH directement vers l'IP privée (depuis ton ordinateur) :**
```bash
ssh ubuntu@10.0.11.10
```

**Résultat : Timeout / Connection refused**

**[OK] C'est NORMAL et SOUHAITÉ (instance privée protégée)**

---

#### **Test 6 : Security Groups fonctionnent**

**Essayer de SSH vers instance App depuis Internet (pas via Bastion) :**

**Même avec une IP publique temporaire -> Bloqué par App-SG**

**App-SG autorise SSH seulement depuis Bastion-SG**

**[OK] Test 6 réussi**

---

### [OK] TESTS DE VALIDATION

**1. VPC créé avec bon CIDR**

Console VPC -> Your VPCs -> production-vpc

- [ ] CIDR : 10.0.0.0/16
- [ ] DNS hostnames : Enabled

---

**2. Subnets créés dans 2 AZs**

Console VPC -> Subnets

- [ ] 6 subnets total
- [ ] 2 publics (10.0.1.0/24, 10.0.2.0/24)
- [ ] 2 privés app (10.0.11.0/24, 10.0.12.0/24)
- [ ] 2 privés DB (10.0.21.0/24, 10.0.22.0/24)
- [ ] Répartis sur 2 AZs

---

**3. Internet Gateway attaché**

Console VPC -> Internet Gateways

- [ ] IGW créé
- [ ] Attaché à production-vpc

---

**4. NAT Gateways dans subnets publics**

Console VPC -> NAT Gateways

- [ ] 2 NAT Gateways
- [ ] NAT-A dans Public-A
- [ ] NAT-B dans Public-B
- [ ] État : Available

---

**5. Route Tables configurées**

Console VPC -> Route Tables

- [ ] Public RT : 0.0.0.0/0 -> IGW
- [ ] Private RT-A : 0.0.0.0/0 -> NAT-A
- [ ] Private RT-B : 0.0.0.0/0 -> NAT-B
- [ ] Associations correctes

---

**6. Security Groups correctement configurés**

Console EC2 -> Security Groups

- [ ] Bastion-SG : SSH depuis IP admin
- [ ] ALB-SG : HTTP/HTTPS depuis Internet
- [ ] App-SG : Port 5000 depuis ALB, SSH depuis Bastion
- [ ] DB-SG : PostgreSQL depuis App

---

**7. Bastion accessible et fonctionnel**

```bash
ssh -i cle.pem ubuntu@BASTION-IP
```

- [ ] Connexion SSH réussie
- [ ] Peut SSH vers instances privées

---

**8. Instances privées ont Internet via NAT**

Depuis instance privée :

```bash
curl https://www.google.com
```

- [ ] Requête réussie

---

**9. RDS accessible depuis instances privées**

```bash
psql -h RDS-ENDPOINT -U postgres -d production_db
```

- [ ] Connexion réussie

---

**10. Application accessible via ALB**

```
http://ALB-DNS-NAME
```

- [ ] Page charge
- [ ] CRUD fonctionne

---

**11. VPC Flow Logs actifs**

Console CloudWatch -> Log groups -> /aws/vpc/production-vpc

- [ ] Logs présents
- [ ] Trafic enregistré

---

**12. Isolation réseau respectée**

- [ ] Instances privées pas d'IP publique
- [ ] RDS pas accessible depuis Internet
- [ ] SSH vers instances privées seulement via Bastion

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Instance privée n'a pas accès Internet

**Symptômes :**
```bash
curl https://google.com
# Timeout
```

**Causes :**

**1. Pas de route vers NAT Gateway**

Console VPC -> Route Tables -> Private RT

Vérifier :
```
0.0.0.0/0 -> nat-xxx (présent ?)
```

**Solution : Ajouter la route**

---

**2. NAT Gateway dans mauvais subnet**

NAT Gateway doit être dans subnet PUBLIC

Console VPC -> NAT Gateways

Vérifier : Subnet = Public-A ou Public-B

**Solution : Recréer NAT Gateway dans bon subnet**

---

**3. Route Table pas associée**

Console VPC -> Route Tables -> Private RT -> Subnet associations

Vérifier : Subnets privés associés ?

**Solution : Associer les subnets**

---

**4. Security Group bloque**

App-SG Outbound : HTTPS (443) autorisé ?

**Solution : Ajouter règle outbound**

---

#### Erreur 2 : Impossible de SSH vers Bastion

**Symptômes :**
```bash
ssh ubuntu@BASTION-IP
# Connection timeout
```

**Causes :**

**1. Bastion n'a pas d'IP publique**

Console EC2 -> Instances -> Bastion

Vérifier : Public IPv4 address (présente ?)

**Solution :**
- Terminer instance
- Relancer avec "Auto-assign public IP : Enable"

---

**2. Bastion dans subnet privé**

Console EC2 -> Instances -> Bastion -> Networking

Vérifier : Subnet = Public-A ou Public-B ?

**Solution : Relancer dans subnet public**

---

**3. Security Group bloque ton IP**

Bastion-SG -> Inbound rules

Vérifier : SSH (22) depuis TON IP ?

**Solution : Ajouter/modifier la règle**

---

**4. Route Table public n'a pas IGW**

Public RT -> Routes

Vérifier : 0.0.0.0/0 -> igw-xxx ?

**Solution : Ajouter la route**

---

#### Erreur 3 : Instance privée ne peut pas accéder à RDS

**Symptômes :**
```bash
psql -h RDS-ENDPOINT -U postgres
# Connection refused ou timeout
```

**Causes :**

**1. RDS dans mauvais subnet**

Console RDS -> Databases -> production-postgres-db -> Connectivity

Vérifier : Subnets = Private-DB-A/B ?

---

**2. Security Group bloque**

DB-SG -> Inbound rules

Vérifier : PostgreSQL (5432) depuis App-SG ?

**Solution : Ajouter la règle**

---

**3. RDS public access activé (mauvaise config)**

RDS -> Connectivity -> Public access : No ?

**Si Yes : [X] Problème de sécurité !**

---

#### Erreur 4 : ALB ne peut pas atteindre instances

**Symptômes :**

Target Group : All targets unhealthy

**Causes :**

**1. Instances dans mauvais subnet**

Instances doivent être dans Private-App, pas Private-DB

**Solution : Relancer dans bon subnet**

---

**2. Security Group bloque**

App-SG -> Inbound rules

Vérifier : Port 5000 depuis ALB-SG ?

**Solution : Ajouter la règle**

---

**3. Health check path incorrect**

Target Group -> Health checks

Vérifier : Path = /health ?

App a bien une route /health ?

---

**4. Application ne démarre pas**

SSH vers instance -> Vérifier logs :
```bash
sudo systemctl status flask-app
sudo journalctl -u flask-app
```

---

#### Erreur 5 : Coûts élevés

**Symptômes :**

Facture AWS élevée pour VPC

**Causes principales :**

**1. NAT Gateway ($32/mois par NAT)**

2 NAT Gateways = $65/mois

**Solutions :**
- Utiliser 1 seul NAT (sacrifier HA)
- VPC Endpoints pour S3/DynamoDB (gratuit)
- Réduire le trafic sortant

---

**2. Data processing NAT ($0.045/GB)**

Instances privées téléchargent beaucoup ?

**Solutions :**
- Utiliser S3 Gateway Endpoint (gratuit)
- Réduire les téléchargements
- Cacher les packages localement

---

**3. VPC Flow Logs CloudWatch ($0.50/GB)**

Flow Logs génèrent beaucoup de données

**Solutions :**
- Envoyer vers S3 ($0.023/GB vs $0.50/GB)
- Réduire l'intervalle (10 min vs 1 min)
- Filtrer (Accept only vs All)

---

**4. Cross-AZ data transfer ($0.01/GB)**

Trafic entre AZs facturé

**Solutions :**
- Instances dans même AZ quand possible
- Utiliser ALB cross-zone load balancing désactivé

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. VPC**
- Réseau privé virtuel isolé
- CIDR /16 recommandé (65K adresses)
- Plage privée : 10.0.0.0/8

**2. Subnets**
- Public : Route vers IGW, IP publiques
- Privé : Route vers NAT ou aucun Internet
- 1 subnet = 1 AZ

**3. Haute disponibilité**
- Multi-AZ obligatoire (minimum 2)
- 1 NAT Gateway par AZ
- Instances réparties équitablement

**4. Sécurité**
- Security Groups (stateful, instance-level)
- NACLs (stateless, subnet-level)
- Bastion pour accès instances privées
- RDS toujours privé

**5. Internet Access**
- Public : Direct via IGW
- Privé : Sortant via NAT Gateway
- NAT Gateway dans subnet public

**6. Coûts**
- NAT Gateway : ~$32/mois + data
- VPC/Subnets/IGW : Gratuits
- Flow Logs : $0.50/GB (CloudWatch)

**7. Best practices**
- Ne jamais exposer DB publiquement
- Utiliser Bastion pour admin
- Multi-AZ pour HA
- Flow Logs pour audit
- Security Groups avec principe du moindre privilège

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. VPC Peering**

**Connecter 2 VPCs :**
```
VPC Production (10.0.0.0/16)
    ^v VPC Peering
VPC Staging (172.16.0.0/16)

Use case : Communication inter-environnements
```

---

**2. Transit Gateway**

**Hub central pour plusieurs VPCs :**
```
Transit Gateway
├── VPC Prod
├── VPC Staging
├── VPC Dev
└── On-premise (VPN)

Use case : Architecture multi-VPC complexe
```

---

**3. VPC Endpoints**

**Accès AWS services sans Internet :**
```
Instance privée -> VPC Endpoint S3 -> S3
(Pas de NAT Gateway nécessaire)

Types :
- Gateway Endpoints : S3, DynamoDB (gratuit)
- Interface Endpoints : Autres services ($0.01/h)

Économies : Évite data transfer NAT
```

---

**4. AWS PrivateLink**

**Exposer services entre VPCs/comptes :**
```
VPC A : Service Provider
VPC B : Service Consumer

Connexion privée (pas d'Internet)
```

---

**5. VPN Site-to-Site**

**Connecter on-premise à VPC :**
```
Data Center on-premise
    ^v VPN IPsec
AWS VPC

Use case : Migration cloud, architecture hybride
```

---

**6. AWS Direct Connect**

**Connexion dédiée physique :**
```
Data Center
    ^v Ligne dédiée 1-100 Gbps
AWS

Avantages :
- Latence constante
- Plus sécurisé
- Débit garanti

Coût : ~$300-3000/mois selon bande passante
```

---

**7. Network Firewall**

**Firewall managé stateful :**
```
Internet -> Network Firewall -> VPC

Features :
- IDS/IPS
- Filtrage par domaine
- Deep packet inspection

Coût : ~$400/mois + data
```

---

## [COURS] CONCLUSION DE L'EXERCICE 8

**[BRAVO] Félicitations ! Tu as créé une architecture VPC production-ready ! [BRAVO]**

**Ce que tu as appris :**
- Architecture VPC multi-tier
- Planification d'adressage IP (CIDR)
- Subnets publics et privés
- Internet Gateway et NAT Gateway
- Route Tables (routage réseau)
- Security Groups vs Network ACLs
- Bastion Host (accès sécurisé)
- VPC Flow Logs (audit)
- Haute disponibilité multi-AZ
- Isolation et sécurité réseau

**Compétences acquises :**
- [OK] VPC Architecture (expert)
- [OK] Networking AWS
- [OK] CIDR et subnetting
- [OK] Security Groups et NACLs
- [OK] Haute disponibilité
- [OK] Bastion Host
- [OK] VPC Flow Logs
- [OK] Architecture 3-tier

**Architecture finale :**
```
Tier 1 (Public) : Load Balancer, NAT Gateway, Bastion
Tier 2 (Private App) : EC2 Flask instances
Tier 3 (Private DB) : RDS PostgreSQL

Multi-AZ, Sécurisée, Scalable, Production-ready
```

**Temps moyen de réalisation :** 4-5 heures

---

**Prochaine étape :** Exercice 9 - CI/CD complet ! [RAPIDE]

Veux-tu continuer avec les exercices 9 et 10 ?

- **Exercice 9** : CI/CD pipeline (CodePipeline + CodeBuild + CodeDeploy)
- **Exercice 10** : Architecture complète (intégration de tous les services + best practices)

Continue ? [RAPIDE]


# [ROUGE] EXERCICE 9 : CI/CD COMPLET - CODEPIPELINE + CODEBUILD + CODEDEPLOY

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es DevOps Lead dans une startup en croissance rapide. L'équipe déploie actuellement l'application manuellement (SSH, git pull, restart) ce qui prend 30 minutes par déploiement et génère des erreurs. Le CTO veut automatiser complètement le cycle de déploiement : commit -> tests -> déploiement automatique en production.

### Cahier des charges

Le client souhaite :
- Pipeline CI/CD automatisé de bout en bout
- Déploiements automatiques à chaque commit
- Tests automatisés (unitaires, intégration)
- Déploiements sans downtime (Blue/Green)
- Rollback automatique en cas d'erreur
- Notifications sur événements pipeline
- Traçabilité complète des déploiements
- Validation manuelle optionnelle

### Contraintes techniques

- Source : GitHub ou CodeCommit
- Build : CodeBuild (tests + packaging)
- Deploy : CodeDeploy (Blue/Green)
- Orchestration : CodePipeline
- Cible : Auto Scaling Group (exercice 7)
- Notifications : SNS + Email
- Temps estimé : 5-6 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre CI/CD en profondeur
- [OK] Configurer CodeCommit (Git AWS)
- [OK] Créer des builds avec CodeBuild
- [OK] Écrire un buildspec.yml
- [OK] Déployer avec CodeDeploy
- [OK] Écrire un appspec.yml
- [OK] Orchestrer avec CodePipeline
- [OK] Implémenter Blue/Green deployment
- [OK] Configurer rollback automatique
- [OK] Intégrer GitHub avec AWS
- [OK] Monitorer et débugger les pipelines

---

## [DOCS] PRÉREQUIS

- Exercice 7 terminé (Auto Scaling Group)
- Application Flask fonctionnelle
- Compte GitHub (ou utiliser CodeCommit)
- Git installé localement
- Compréhension bases Git

---

## [IDEE] CONCEPTS AWS À COMPRENDRE

### Qu'est-ce que CI/CD ?

**CI/CD** = Continuous Integration / Continuous Deployment

**Continuous Integration (CI) :**

```
Définition :
Intégrer fréquemment le code dans un repository partagé
+ Automatiser les tests à chaque commit

Sans CI :
Dev 1 -> Code pendant 2 semaines
Dev 2 -> Code pendant 2 semaines
Merge -> [HOT] Conflits, bugs, chaos

Avec CI :
Dev 1 -> Commit quotidien -> Tests automatiques
Dev 2 -> Commit quotidien -> Tests automatiques
-> Problèmes détectés tôt, faciles à corriger
```

---

**Continuous Deployment (CD) :**

```
Définition :
Déployer automatiquement chaque changement validé en production

Sans CD :
Code prêt -> Attente release -> Déploiement manuel -> 30 min -> Erreurs

Avec CD :
Code prêt -> Commit -> Tests -> Déploiement automatique -> 5 min -> Production
```

---

**Analogie complète :**

```
Usine de production automobile :

Sans CI/CD :
- Chaque ingénieur fabrique une pièce chez lui (2 semaines)
- On assemble tout à la fin
- [HOT] Pièces incompatibles, défauts découverts tard
- Livraison manuelle au client (2 jours)

Avec CI/CD :
- Chaîne d'assemblage continue
- Chaque pièce testée immédiatement (qualité)
- Assemblage automatique
- Tests finaux automatiques
- Livraison automatique au client
-> Rapide, fiable, répétable
```

---

### Pipeline CI/CD - Vue d'ensemble

**Pipeline** = Série d'étapes automatisées

**Notre pipeline :**

```
┌─────────────┐
│   Source    │  Git push -> Déclenche le pipeline
│  (GitHub)   │
└──────┬──────┘
       │
       v
┌─────────────┐
│    Build    │  Compile, teste, package l'application
│ (CodeBuild) │  -> Crée un artifact (ZIP)
└──────┬──────┘
       │
       v
┌─────────────┐
│   Deploy    │  Déploie sur Auto Scaling Group
│(CodeDeploy) │  -> Blue/Green, zéro downtime
└─────────────┘
```

---

**Durée :**

```
Déploiement manuel : 30 minutes + risques
Pipeline automatisé : 5-10 minutes + zéro erreur humaine
```

---

### AWS CodeCommit

**CodeCommit** = Git repository managé AWS

**Équivalent : GitHub, GitLab, Bitbucket**

**Avantages CodeCommit :**
- [OK] Intégré AWS (IAM, KMS)
- [OK] Pas de limite de taille
- [OK] Haute disponibilité
- [OK] Gratuit (5 users, 50 GB)

**Inconvénients :**
- [X] Moins de features que GitHub
- [X] Pas de CI/CD intégré (vs GitHub Actions)
- [X] UI basique

**Pour ce tuto : On utilisera GitHub (plus populaire)**

**Mais CodeCommit fonctionne exactement pareil.**

---

### AWS CodeBuild

**CodeBuild** = Service de build managé

**Rôle :**
- Compiler le code
- Exécuter les tests
- Créer les artifacts (packages)

**Analogie :**

```
CodeBuild = Ouvrier dans une usine

Input : Code source (GitHub)
Actions : Installer dépendances, tester, packager
Output : Artifact prêt à déployer (ZIP)
```

---

**Environnements de build :**

**Types d'images :**
- **Standard** : Ubuntu avec outils courants (Python, Node.js, Java, etc.)
- **Custom** : Ton propre Docker image

**Compute types :**
```
Small : 3 GB RAM, 2 vCPUs ($0.005/min)
Medium : 7 GB RAM, 4 vCPUs ($0.01/min)
Large : 15 GB RAM, 8 vCPUs ($0.02/min)
2X Large : 145 GB RAM, 72 vCPUs ($0.16/min)
```

**Pour Flask : Small suffit**

---

**buildspec.yml :**

**Configuration du build en YAML**

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.11
    commands:
      - pip install -r requirements.txt
      
  pre_build:
    commands:
      - echo "Running tests..."
      - pytest tests/
      
  build:
    commands:
      - echo "Building application..."
      
  post_build:
    commands:
      - echo "Build completed on $(date)"

artifacts:
  files:
    - '**/*'
```

---

### AWS CodeDeploy

**CodeDeploy** = Service de déploiement managé

**Rôle :**
- Déployer sur EC2, Lambda, ECS
- Stratégies de déploiement
- Rollback automatique

---

**Stratégies de déploiement :**

#### **1. In-place Deployment**

```
Arrêter instances -> Déployer nouvelle version -> Redémarrer

┌─────────┐     ┌─────────┐
│ v1 (ON) │ ->   │ v1 (OFF)│
└─────────┘     └─────────┘
                     v
                ┌─────────┐
                │ v2 (ON) │
                └─────────┘

Avantages :
[OK] Simple
[OK] Moins cher

Inconvénients :
[X] Downtime (quelques minutes)
[X] Rollback = Re-déployer v1
```

---

#### **2. Blue/Green Deployment**

```
Garder v1 (Blue) -> Créer v2 (Green) -> Switch trafic -> Supprimer Blue

┌─────────┐     ┌─────────┐     ┌─────────┐
│Blue (v1)│     │Blue (v1)│     │Blue (v1)│
│  100%   │     │  100%   │     │   OFF   │
└─────────┘     └────┬────┘     └─────────┘
                     │
                ┌────┴────┐     ┌─────────┐
                │Green(v2)│     │Green(v2)│
                │   0%    │     │  100%   │
                └─────────┘     └─────────┘

Avantages :
[OK] Zéro downtime
[OK] Rollback instantané (redirect vers Blue)
[OK] Test en prod avant switch

Inconvénients :
[X] Double capacité temporaire (coûts)
```

---

**appspec.yml :**

**Configuration du déploiement en YAML**

```yaml
version: 0.0
os: linux

files:
  - source: /
    destination: /opt/flask-app

hooks:
  BeforeInstall:
    - location: scripts/install_dependencies.sh
      timeout: 300
      
  ApplicationStart:
    - location: scripts/start_application.sh
      timeout: 300
      
  ValidateService:
    - location: scripts/validate_service.sh
      timeout: 300
```

---

### AWS CodePipeline

**CodePipeline** = Orchestrateur CI/CD

**Rôle :**
- Enchaîner Source -> Build -> Deploy
- Gérer les transitions
- Approvals manuels

**Analogie :**

```
CodePipeline = Chef d'orchestre

Musicians (services) :
- CodeCommit/GitHub (violon)
- CodeBuild (piano)
- CodeDeploy (percussion)

Chef d'orchestre :
- Donne le tempo
- Coordonne les instruments
- Assure l'harmonie
```

---

**Stages (étapes) :**

```
Pipeline : flask-app-pipeline

Stages :
1. Source
   - Action : GitHub
   - Déclencheur : Commit sur branch main
   
2. Build
   - Action : CodeBuild
   - Input : Code source
   - Output : Artifact (ZIP)
   
3. Deploy
   - Action : CodeDeploy
   - Input : Artifact
   - Target : Auto Scaling Group
```

---

**Transitions :**

```
Automatiques :
Source -> Build (dès que code disponible)
Build -> Deploy (dès que build réussi)

Manuelles (approval) :
Build -> Manual Approval -> Deploy
-> Review avant prod
```

---

### Lifecycle Hooks CodeDeploy

**Hooks** = Scripts exécutés à différentes étapes

**Ordre d'exécution :**

```
Blue/Green Deployment :

1. BeforeInstall
   -> Préparer l'environnement
   -> Installer dépendances système

2. AfterInstall
   -> Copier fichiers de config
   -> Créer répertoires

3. ApplicationStart
   -> Démarrer l'application
   -> systemctl start flask-app

4. ValidateService
   -> Tester l'application
   -> curl http://localhost:5000/health

5. BeforeAllowTraffic
   -> Vérifications finales
   -> Réchauffer cache

6. AllowTraffic
   -> ALB commence à envoyer du trafic
   (Automatique, géré par CodeDeploy)

7. AfterAllowTraffic
   -> Monitoring post-déploiement
   -> Logs

8. BeforeBlockTraffic (sur anciennes instances)
   -> Drainer connexions

9. BlockTraffic
   -> ALB arrête le trafic
   (Automatique)

10. AfterBlockTraffic
    -> Cleanup
    -> Supprimer anciennes instances
```

---

### Rollback automatique

**CodeDeploy surveille les métriques CloudWatch**

**Configuration :**

```
Rollback si :
- Déploiement échoue (ValidateService raté)
- Alarmes CloudWatch déclenchées
  -> Exemple : ErrorRate > 5%
  -> Exemple : TargetResponseTime > 2s

Action :
Rediriger le trafic vers l'ancien environment (Blue)
-> Instantané (secondes)
```

---

**Pourquoi rollback automatique ?**

```
Sans rollback automatique :
Déploiement v2 -> Bugs -> Utilisateurs voient erreurs
-> DevOps réveillé (3h du matin)
-> Rollback manuel (10 minutes)
-> 10 minutes d'erreurs

Avec rollback automatique :
Déploiement v2 -> Bugs détectés (2 secondes)
-> Rollback automatique (5 secondes)
-> 7 secondes d'erreurs
-> DevOps dort paisiblement [SLEEPING_FACE]
```

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Préparer le code pour CI/CD

**On va organiser le code avec les fichiers nécessaires au pipeline.**

---

**Structure du projet :**

```
flask-app/
├── application.py
├── requirements.txt
├── templates/
│   └── *.html
├── tests/
│   ├── __init__.py
│   ├── test_app.py
│   └── test_health.py
├── scripts/
│   ├── install_dependencies.sh
│   ├── start_application.sh
│   ├── stop_application.sh
│   └── validate_service.sh
├── buildspec.yml
├── appspec.yml
└── .gitignore
```

---

**Créer le dossier de projet :**

```bash
mkdir flask-app-cicd
cd flask-app-cicd
```

---

**Copier l'application Flask :**

```bash
cp -r ../exercice-3/application.py .
cp -r ../exercice-3/requirements.txt .
cp -r ../exercice-3/templates/ .
```

---

**Créer le dossier tests :**

```bash
mkdir tests
```

**Créer `tests/__init__.py` (vide) :**

```bash
touch tests/__init__.py
```

---

**Créer `tests/test_app.py` :**

```bash
nano tests/test_app.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# TESTS UNITAIRES - FLASK APPLICATION
# ═══════════════════════════════════════════════════════════════

import pytest
import sys
import os

# Ajouter le répertoire parent au path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from application import application, db

@pytest.fixture
def client():
    """
    Fixture pytest : Client de test Flask
    
    Crée une instance de test de l'application
    Configure une base SQLite en mémoire
    """
    application.config['TESTING'] = True
    application.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    
    with application.test_client() as client:
        with application.app_context():
            db.create_all()
        yield client
        
    # Cleanup
    with application.app_context():
        db.drop_all()

def test_home_page(client):
    """Test : Page d'accueil charge"""
    response = client.get('/')
    assert response.status_code == 200
    assert b'TODO' in response.data

def test_health_endpoint(client):
    """Test : Health check retourne 200"""
    response = client.get('/health')
    assert response.status_code == 200
    
    # Vérifier le JSON
    json_data = response.get_json()
    assert json_data['status'] == 'healthy'

def test_api_list_tasks_empty(client):
    """Test : API liste vide au départ"""
    response = client.get('/api/tasks')
    assert response.status_code == 200
    
    json_data = response.get_json()
    assert json_data['count'] == 0
    assert json_data['tasks'] == []

def test_api_create_task(client):
    """Test : API création de tâche"""
    response = client.post('/api/tasks', json={
        'title': 'Test task',
        'description': 'Test description'
    })
    
    assert response.status_code == 201
    
    json_data = response.get_json()
    assert json_data['title'] == 'Test task'
    assert json_data['description'] == 'Test description'
    assert json_data['completed'] == False

def test_api_create_task_missing_title(client):
    """Test : API création sans titre -> Erreur 400"""
    response = client.post('/api/tasks', json={
        'description': 'Test description'
    })
    
    assert response.status_code == 400

def test_database_integration(client):
    """Test : Intégration base de données"""
    # Créer une tâche
    response = client.post('/api/tasks', json={
        'title': 'Integration test'
    })
    assert response.status_code == 201
    
    # Vérifier qu'elle apparaît dans la liste
    response = client.get('/api/tasks')
    json_data = response.get_json()
    assert json_data['count'] == 1
    assert json_data['tasks'][0]['title'] == 'Integration test'

# ═══════════════════════════════════════════════════════════════
```

---

**Ajouter pytest dans requirements.txt :**

```bash
nano requirements.txt
```

**Ajouter à la fin :**

```
pytest==7.4.3
pytest-flask==1.3.0
```

---

**Créer le dossier scripts :**

```bash
mkdir scripts
```

---

**Créer `scripts/install_dependencies.sh` :**

```bash
nano scripts/install_dependencies.sh
```

**Contenu :**

```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# SCRIPT : INSTALLATION DES DÉPENDANCES
# ═══════════════════════════════════════════════════════════════

set -e  # Arrêter si une commande échoue

echo "===== Starting install_dependencies.sh ====="

# Mettre à jour le système
echo "Updating system packages..."
sudo apt-get update

# Installer Python et pip
echo "Installing Python and pip..."
sudo apt-get install -y python3-pip python3-venv

# Installer PostgreSQL client
echo "Installing PostgreSQL client..."
sudo apt-get install -y postgresql-client

# Installer Nginx
echo "Installing Nginx..."
sudo apt-get install -y nginx

echo "===== Dependencies installed successfully ====="
```

**Rendre exécutable :**

```bash
chmod +x scripts/install_dependencies.sh
```

---

**Créer `scripts/start_application.sh` :**

```bash
nano scripts/start_application.sh
```

**Contenu :**

```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# SCRIPT : DÉMARRAGE DE L'APPLICATION
# ═══════════════════════════════════════════════════════════════

set -e

echo "===== Starting start_application.sh ====="

APP_DIR="/opt/flask-app"

# Aller dans le répertoire de l'app
cd $APP_DIR

# Créer l'environnement virtuel si nécessaire
if [ ! -d "venv" ]; then
    echo "Creating virtual environment..."
    python3 -m venv venv
fi

# Activer l'environnement virtuel
source venv/bin/activate

# Installer les dépendances Python
echo "Installing Python dependencies..."
pip install -r requirements.txt

# Récupérer l'Instance ID (pour affichage dans l'app)
INSTANCE_ID=$(ec2-metadata --instance-id | cut -d " " -f 2)

# Créer le fichier .env avec les variables d'environnement
echo "Creating .env file..."
cat > .env << EOF
DATABASE_URL=${DATABASE_URL:-sqlite:///todo.db}
SECRET_KEY=${SECRET_KEY:-dev-secret-key}
INSTANCE_ID=${INSTANCE_ID}
FLASK_ENV=production
EOF

# Créer la configuration Gunicorn
cat > gunicorn_config.py << 'EOF'
import multiprocessing

workers = multiprocessing.cpu_count() * 2 + 1
bind = '127.0.0.1:5000'
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
loglevel = 'info'
timeout = 60
EOF

# Créer le dossier de logs Gunicorn
sudo mkdir -p /var/log/gunicorn
sudo chown ubuntu:ubuntu /var/log/gunicorn

# Créer le service systemd
echo "Creating systemd service..."
sudo tee /etc/systemd/system/flask-app.service > /dev/null << EOF
[Unit]
Description=Flask TODO Application
After=network.target

[Service]
Type=notify
User=ubuntu
Group=ubuntu
WorkingDirectory=$APP_DIR
Environment="PATH=$APP_DIR/venv/bin"
EnvironmentFile=$APP_DIR/.env
ExecStart=$APP_DIR/venv/bin/gunicorn -c gunicorn_config.py application:application
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF

# Configurer Nginx
echo "Configuring Nginx..."
sudo tee /etc/nginx/sites-available/flask-app > /dev/null << 'EOF'
server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://127.0.0.1:5000;
        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_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    location /health {
        proxy_pass http://127.0.0.1:5000/health;
    }
}
EOF

# Activer le site Nginx
sudo ln -sf /etc/nginx/sites-available/flask-app /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default

# Tester et redémarrer Nginx
sudo nginx -t
sudo systemctl restart nginx

# Recharger systemd
sudo systemctl daemon-reload

# Démarrer le service Flask
echo "Starting Flask application..."
sudo systemctl enable flask-app
sudo systemctl restart flask-app

# Attendre que l'app démarre
sleep 5

echo "===== Application started successfully ====="
```

**Rendre exécutable :**

```bash
chmod +x scripts/start_application.sh
```

---

**Créer `scripts/stop_application.sh` :**

```bash
nano scripts/stop_application.sh
```

**Contenu :**

```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# SCRIPT : ARRÊT DE L'APPLICATION
# ═══════════════════════════════════════════════════════════════

set -e

echo "===== Starting stop_application.sh ====="

# Arrêter le service Flask
if sudo systemctl is-active --quiet flask-app; then
    echo "Stopping Flask application..."
    sudo systemctl stop flask-app
else
    echo "Flask application is not running"
fi

echo "===== Application stopped successfully ====="
```

**Rendre exécutable :**

```bash
chmod +x scripts/stop_application.sh
```

---

**Créer `scripts/validate_service.sh` :**

```bash
nano scripts/validate_service.sh
```

**Contenu :**

```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# SCRIPT : VALIDATION DU SERVICE
# ═══════════════════════════════════════════════════════════════

set -e

echo "===== Starting validate_service.sh ====="

# Attendre que le service soit prêt
MAX_ATTEMPTS=30
ATTEMPT=0

while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
    echo "Attempt $((ATTEMPT+1))/$MAX_ATTEMPTS : Testing health endpoint..."
    
    # Tester le health check
    if curl -f http://localhost/health; then
        echo "[OK] Health check passed!"
        
        # Vérifier que le JSON est valide
        RESPONSE=$(curl -s http://localhost/health)
        if echo "$RESPONSE" | grep -q "healthy"; then
            echo "[OK] Application is healthy!"
            echo "===== Validation successful ====="
            exit 0
        fi
    fi
    
    ATTEMPT=$((ATTEMPT+1))
    sleep 2
done

echo "[X] Validation failed after $MAX_ATTEMPTS attempts"
echo "===== Validation failed ====="
exit 1
```

**Rendre exécutable :**

```bash
chmod +x scripts/validate_service.sh
```

---

**Créer `buildspec.yml` :**

```bash
nano buildspec.yml
```

**Contenu :**

```yaml
# ═══════════════════════════════════════════════════════════════
# AWS CODEBUILD - BUILD SPECIFICATION
# ═══════════════════════════════════════════════════════════════

version: 0.2

# Variables d'environnement
env:
  variables:
    PYTHON_VERSION: "3.11"

# Phases du build
phases:
  # Phase d'installation
  install:
    runtime-versions:
      python: 3.11
    commands:
      - echo "===== Install Phase ====="
      - echo "Python version:"
      - python --version
      - pip --version
      
  # Phase avant le build
  pre_build:
    commands:
      - echo "===== Pre-Build Phase ====="
      - echo "Installing dependencies..."
      - pip install -r requirements.txt
      
      - echo "Running tests..."
      - pytest tests/ -v --tb=short
      
      - echo "Code quality checks..."
      - pip install flake8
      - flake8 application.py --max-line-length=120 --ignore=E501 || true
      
  # Phase de build
  build:
    commands:
      - echo "===== Build Phase ====="
      - echo "Build completed on $(date)"
      - echo "Creating deployment package..."
      
  # Phase après le build
  post_build:
    commands:
      - echo "===== Post-Build Phase ====="
      - echo "Build completed successfully!"

# Artifacts à conserver
artifacts:
  files:
    - '**/*'
  name: flask-app-$(date +%Y%m%d-%H%M%S)

# Cache (optionnel, accélère les builds)
cache:
  paths:
    - '/root/.cache/pip/**/*'
```

---

**Créer `appspec.yml` :**

```bash
nano appspec.yml
```

**Contenu :**

```yaml
# ═══════════════════════════════════════════════════════════════
# AWS CODEDEPLOY - APPLICATION SPECIFICATION
# ═══════════════════════════════════════════════════════════════

version: 0.0
os: linux

# Fichiers à copier
files:
  - source: /
    destination: /opt/flask-app
    
# Permissions des fichiers
permissions:
  - object: /opt/flask-app
    owner: ubuntu
    group: ubuntu
    mode: 755
    type:
      - directory
      
  - object: /opt/flask-app
    owner: ubuntu
    group: ubuntu
    mode: 644
    type:
      - file

# Hooks (scripts lifecycle)
hooks:
  # Avant installation
  BeforeInstall:
    - location: scripts/install_dependencies.sh
      timeout: 300
      runas: root
      
  # Après installation
  AfterInstall:
    - location: scripts/stop_application.sh
      timeout: 60
      runas: root
      
  # Démarrage application
  ApplicationStart:
    - location: scripts/start_application.sh
      timeout: 300
      runas: ubuntu
      
  # Validation
  ValidateService:
    - location: scripts/validate_service.sh
      timeout: 300
      runas: ubuntu
```

---

**Créer `.gitignore` :**

```bash
nano .gitignore
```

**Contenu :**

```
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
venv/
env/
*.db
*.sqlite
*.sqlite3
.env
*.log
.DS_Store
.vscode/
.idea/
```

---

**Structure finale :**

```bash
tree -L 2
```

**Résultat :**

```
flask-app-cicd/
├── application.py
├── requirements.txt
├── templates/
│   ├── base.html
│   ├── index.html
│   ├── add_task.html
│   └── edit_task.html
├── tests/
│   ├── __init__.py
│   └── test_app.py
├── scripts/
│   ├── install_dependencies.sh
│   ├── start_application.sh
│   ├── stop_application.sh
│   └── validate_service.sh
├── buildspec.yml
├── appspec.yml
└── .gitignore
```

**[OK] Code préparé pour CI/CD ! [BRAVO]**

---

### ÉTAPE 2 : Créer un repository GitHub

**Option A : Utiliser GitHub (recommandé)**

**Aller sur GitHub.com -> New repository**

```
Repository name : flask-app-cicd
Description : Flask TODO app with CI/CD
Visibility : Public (ou Private)

[ ] Initialize with README
[ ] Add .gitignore
[ ] Choose a license
```

**Create repository**

---

**Cloner et pousser le code :**

```bash
cd flask-app-cicd

# Initialiser Git
git init

# Ajouter tous les fichiers
git add .

# Premier commit
git commit -m "Initial commit - Flask app with CI/CD config"

# Ajouter l'origin GitHub
git remote add origin https://github.com/TON-USERNAME/flask-app-cicd.git

# Pousser
git branch -M main
git push -u origin main
```

**[OK] Code sur GitHub !**

---

**Option B : Utiliser CodeCommit (alternative)**

**Console AWS -> CodeCommit -> Repositories -> Create repository**

```
Repository name : flask-app-cicd
Description : Flask TODO app with CI/CD
```

**Create**

---

**Configurer les credentials :**

**IAM -> Users -> Ton user -> Security credentials**

**HTTPS Git credentials for AWS CodeCommit -> Generate credentials**

**Télécharger les credentials (username + password)**

---

**Cloner et pousser :**

```bash
git clone https://git-codecommit.eu-west-3.amazonaws.com/v1/repos/flask-app-cicd

cd flask-app-cicd

# Copier les fichiers de l'app

git add .
git commit -m "Initial commit"
git push
```

---

**Pour ce tuto, on continue avec GitHub.**

---

Je vais continuer avec la configuration de CodeBuild, CodeDeploy et CodePipeline dans le prochain message. Veux-tu que je continue ? [RAPIDE]


# [ROUGE] EXERCICE 9 : CI/CD COMPLET (SUITE)

### ÉTAPE 3 : Créer un projet CodeBuild

**Console AWS -> CodeBuild -> Build projects -> Create build project**

---

**Project name :**

```
flask-app-build
```

---

**Description :**

```
Build and test Flask application
```

---

#### **Source**

**Source provider :**

```
[x] GitHub
```

**Alternatives :**
- CodeCommit
- Bitbucket
- S3

---

**Repository :**

```
[x] Connect using OAuth
```

**Cliquer sur "Connect to GitHub"**

**Autoriser AWS CodeBuild dans la fenêtre popup GitHub**

---

**Une fois connecté :**

**GitHub repository :**

```
[x] Repository in my GitHub account
```

**Repository URL :**

```
https://github.com/TON-USERNAME/flask-app-cicd
```

**Ou sélectionner dans la liste déroulante**

---

**Source version (optionnel) :**

```
main
```

**Branch par défaut**

---

#### **Environment**

**Environment image :**

```
[x] Managed image
```

**AWS fournit des images pré-configurées**

---

**Operating system :**

```
[x] Ubuntu
```

**Alternatives : Amazon Linux 2, Windows Server**

---

**Runtime(s) :**

```
[x] Standard
```

---

**Image :**

```
aws/codebuild/standard:7.0
```

**Version la plus récente (Ubuntu 22.04, Python 3.11, Node.js 18, etc.)**

---

**Image version :**

```
[x] Always use the latest image for this runtime version
```

**Met à jour automatiquement l'image**

---

**Environment type :**

```
[x] Linux
```

---

**Privileged :**

```
[ ] No
```

**Privilèges Docker (pas nécessaire pour nous)**

---

**Service role :**

```
[x] New service role
```

**Role name (auto-généré) :**

```
codebuild-flask-app-build-service-role
```

**AWS crée automatiquement un rôle avec les permissions nécessaires :**
- CloudWatch Logs (écriture)
- S3 (lecture/écriture artifacts)

---

**Additional configuration :**

**Timeout :**

```
15 minutes
```

**Temps max pour un build**

**Pour Flask : 5 minutes suffisent, mais on laisse de la marge**

---

**Compute :**

```
[x] 3 GB memory, 2 vCPUs
```

**Coût : $0.005/minute**

**Pour Flask : Suffisant**

---

**Environment variables (optionnel) :**

**Ajouter si besoin :**

```
Name : DATABASE_URL
Value : postgresql://...
Type : Plaintext

Name : SECRET_KEY
Value : stored-in-secrets-manager
Type : Secrets Manager (plus sécurisé)
```

**Pour les tests unitaires, pas nécessaire (SQLite en mémoire)**

---

#### **Buildspec**

**Build specifications :**

```
[x] Use a buildspec file
```

**Buildspec name :**

```
buildspec.yml
```

**CodeBuild va lire `buildspec.yml` à la racine du repo**

---

#### **Artifacts**

**Type :**

```
[x] Amazon S3
```

---

**Bucket name :**

**Créer un bucket S3 d'abord :**

**Nouvel onglet -> S3 -> Create bucket**

```
Bucket name : flask-app-artifacts-123456 (unique globalement)
Region : eu-west-3
Block all public access : [OK] (privé)
```

**Create bucket**

---

**Retourner dans CodeBuild**

**Bucket name :**

```
flask-app-artifacts-123456
```

---

**Name (optionnel) :**

```
(laisser vide = nom automatique avec timestamp)
```

---

**Path (optionnel) :**

```
builds/
```

**Sous-dossier dans le bucket**

---

**Packaging :**

```
[x] Zip
```

**Package les artifacts en ZIP**

---

**Artifacts encryption :**

```
[x] Default AWS managed key
```

---

#### **Logs**

**CloudWatch Logs :**

```
[x] Enabled
```

**Group name :**

```
/aws/codebuild/flask-app-build
```

**Stream name :**

```
(laisser vide = auto-généré)
```

---

**S3 logs (optionnel) :**

```
[ ] Disabled
```

**Logs aussi dans S3 (pour archivage long terme)**

---

**Create build project**

**[OK] Projet CodeBuild créé ! [BRAVO]**

---

### ÉTAPE 4 : Tester le build manuellement

**Avant d'intégrer dans le pipeline, testons le build.**

**CodeBuild -> Build projects -> flask-app-build**

**Cliquer sur "Start build"**

---

**Start build configuration :**

**Source version :**

```
main
```

---

**Environment variables override (optionnel) :**

```
(laisser vide)
```

---

**Buildspec override (optionnel) :**

```
[ ] No
```

---

**Artifacts override :**

```
[ ] No
```

---

**Start build**

**Le build démarre... [HOURGLASS_WITH_FLOWING_SAND]**

---

**Phase en cours :**

```
Phase : SUBMITTED
Phase : QUEUED
Phase : PROVISIONING (création environnement)
Phase : DOWNLOAD_SOURCE (clone GitHub)
Phase : INSTALL (installation runtimes)
Phase : PRE_BUILD (tests)
Phase : BUILD
Phase : POST_BUILD
Phase : UPLOAD_ARTIFACTS
Phase : FINALIZING
Phase : COMPLETED
```

**Durée totale : 2-5 minutes**

---

**Voir les logs en temps réel :**

**Onglet "Build logs"**

**Logs :**

```
[Container] 2024/12/17 10:00:00 Running command python --version
Python 3.11.6

[Container] 2024/12/17 10:00:05 Running command pip install -r requirements.txt
Collecting Flask==3.0.0
...
Successfully installed Flask-3.0.0 ...

[Container] 2024/12/17 10:00:30 Running command pytest tests/ -v --tb=short
============================= test session starts ==============================
tests/test_app.py::test_home_page PASSED                                [ 14%]
tests/test_app.py::test_health_endpoint PASSED                          [ 28%]
tests/test_app.py::test_api_list_tasks_empty PASSED                     [ 42%]
tests/test_app.py::test_api_create_task PASSED                          [ 57%]
tests/test_app.py::test_api_create_task_missing_title PASSED            [ 71%]
tests/test_app.py::test_database_integration PASSED                     [ 85%]
============================== 7 passed in 2.34s ===============================

[Container] 2024/12/17 10:01:00 Phase complete: PRE_BUILD State: SUCCEEDED
[Container] 2024/12/17 10:01:05 Phase complete: BUILD State: SUCCEEDED
[Container] 2024/12/17 10:01:10 Phase complete: POST_BUILD State: SUCCEEDED
```

**[OK] Build succeeded !**

---

**Vérifier l'artifact dans S3 :**

**S3 -> Buckets -> flask-app-artifacts-123456 -> builds/**

**Tu vois un fichier ZIP :**

```
flask-app-20241217-100110.zip
```

**Télécharger et vérifier le contenu :**

```bash
unzip flask-app-20241217-100110.zip
ls -la
```

**Contenu :**

```
application.py
requirements.txt
templates/
scripts/
appspec.yml
...
```

**[OK] Artifact créé correctement !**

---

### ÉTAPE 5 : Créer une application CodeDeploy

**Console AWS -> CodeDeploy -> Applications -> Create application**

---

**Application name :**

```
flask-app-deploy
```

---

**Compute platform :**

```
[x] EC2/On-premises
```

**Options :**
- EC2/On-premises
- AWS Lambda
- Amazon ECS

---

**Create application**

**[OK] Application créée !**

---

### ÉTAPE 6 : Créer un Deployment Group

**Application créée -> Deployment groups -> Create deployment group**

---

**Deployment group name :**

```
flask-app-production
```

---

**Service role :**

**CodeDeploy nécessite un rôle IAM pour gérer les instances.**

**Créer le rôle IAM :**

**Nouvel onglet -> IAM -> Roles -> Create role**

---

**Trusted entity type :**

```
[x] AWS service
```

**Use case :**

```
[x] CodeDeploy
[x] CodeDeploy (EC2/On-premises)
```

**Next**

---

**Permissions :**

**Automatiquement attachée :**

```
AWSCodeDeployRole
```

**Cette policy permet à CodeDeploy de :**
- Lire les tags EC2
- Gérer les Auto Scaling Groups
- Interagir avec Elastic Load Balancers

**Next**

---

**Role name :**

```
CodeDeployServiceRole
```

---

**Create role**

---

**Retourner dans CodeDeploy**

**Service role ARN :**

```
arn:aws:iam::123456789012:role/CodeDeployServiceRole
```

**Ou chercher "CodeDeployServiceRole" dans la liste**

---

#### **Deployment type**

**Choose a deployment type :**

```
[x] Blue/green
```

**Blue/green = Zero downtime !**

---

**Automatically copy Auto Scaling group :**

```
[x] Automatically copy Amazon EC2 Auto Scaling group
```

**CodeDeploy va créer automatiquement un ASG temporaire (Green)**

---

#### **Environment configuration**

**Amazon EC2 Auto Scaling groups :**

**Choose an Auto Scaling group :**

```
flask-app-asg (de l'exercice 7)
```

---

#### **Deployment settings**

**Deployment configuration :**

```
[x] CodeDeployDefault.AllAtOnce
```

**Options :**

**AllAtOnce :**
- Déploie sur toutes les instances simultanément
- Plus rapide
- [OK] Recommandé pour petites flottes

**HalfAtATime :**
- 50% des instances à la fois
- Plus lent mais plus sûr

**OneAtATime :**
- 1 instance à la fois
- Très lent mais ultra-sûr

**Custom :**
- Configuration personnalisée

---

**Load balancer :**

```
[x] Application Load Balancer or Network Load Balancer
```

---

**Choose a load balancer :**

```
flask-app-alb (de l'exercice 7)
```

---

**Target group 1 (Blue) :**

```
flask-app-tg
```

**Target group actuel (production)**

---

**Target group 2 (Green) :**

**Créer un nouveau Target Group pour Green :**

**Nouvel onglet -> EC2 -> Target Groups -> Create target group**

```
Target type : Instances
Target group name : flask-app-tg-green
Protocol : HTTP
Port : 80
VPC : production-vpc (ou default)

Health checks :
Protocol : HTTP
Path : /health
Port : Traffic port

Port override : 5000

Advanced settings :
Healthy threshold : 2
Unhealthy threshold : 2
Timeout : 5
Interval : 30
Success codes : 200
```

**Create target group**

---

**Retourner dans CodeDeploy**

**Target group 2 (Green) :**

```
flask-app-tg-green
```

---

**Deployment configuration :**

**Reroute traffic immediately :**

```
[x] Yes
```

**Switch le trafic dès que les instances Green sont healthy**

---

**Terminate the original instances in the deployment group :**

```
[x] 5 minutes
```

**Attendre 5 minutes après le switch avant de terminer les Blue**

**Pourquoi attendre ?**

```
Si problème détecté -> Rollback possible vers Blue
Après 5 min -> Tout va bien -> Supprimer Blue (économiser coûts)
```

---

**Deployment group options :**

**Choose how instances are identified :**

```
[x] Amazon EC2 Auto Scaling groups
```

**CodeDeploy détecte automatiquement les instances via l'ASG**

---

#### **Advanced (optionnel mais recommandé)**

**Alarms (rollback automatique) :**

**Créer une alarme CloudWatch d'abord :**

**Nouvel onglet -> CloudWatch -> Alarms -> Create alarm**

---

**Select metric -> ApplicationELB -> Per AppELB, per TG Metrics**

**Chercher :**

```
TargetResponseTime
Load Balancer : flask-app-alb
Target Group : flask-app-tg-green
```

**Select metric**

---

**Specify metric and conditions :**

**Statistic :**

```
Average
```

**Period :**

```
1 minute
```

---

**Conditions :**

```
Threshold type : Static
Whenever TargetResponseTime is : Greater
than : 2 (secondes)
```

**Si le temps de réponse > 2s -> Alarme**

---

**Next**

---

**Configure actions :**

**Notification (optionnel) :**

```
SNS topic : asg-scaling-notifications (de l'exercice 7)
```

---

**Next**

---

**Alarm name :**

```
flask-app-high-response-time-green
```

---

**Create alarm**

---

**Retourner dans CodeDeploy**

**CloudWatch alarms :**

```
[x] Use CloudWatch alarms
```

**Add alarm :**

```
flask-app-high-response-time-green
```

---

**Automatic rollback :**

```
[x] Roll back when a deployment fails
[x] Roll back when alarm thresholds are met
```

**Rollback automatique si :**
- Déploiement échoue (ValidateService raté)
- Alarme CloudWatch déclenchée

---

**Create deployment group**

**[OK] Deployment Group créé ! [BRAVO]**

---

### ÉTAPE 7 : Installer l'agent CodeDeploy sur les instances

**CodeDeploy nécessite un agent sur chaque instance EC2.**

**2 méthodes :**

---

#### **Méthode 1 : Mettre à jour le Launch Template (recommandé)**

**EC2 -> Launch Templates -> flask-app-launch-template**

**Actions -> Modify template (Create new version)**

---

**Ajouter dans User Data (au début du script) :**

```bash
#!/bin/bash

# ═══════════════════════════════════════════════════════════════
# INSTALLER LE CODEDEPLOY AGENT
# ═══════════════════════════════════════════════════════════════

# Mettre à jour le système
apt-get update

# Installer Ruby (requis par l'agent)
apt-get install -y ruby wget

# Télécharger l'agent CodeDeploy
cd /tmp
wget https://aws-codedeploy-eu-west-3.s3.eu-west-3.amazonaws.com/latest/install

# Installer
chmod +x ./install
./install auto

# Vérifier que l'agent tourne
systemctl status codedeploy-agent

# Activer au démarrage
systemctl enable codedeploy-agent

echo "CodeDeploy agent installed successfully"

# ═══════════════════════════════════════════════════════════════
# RESTE DU USER DATA (application Flask)
# ═══════════════════════════════════════════════════════════════

# ... (ton user data existant)
```

**[ATTENTION] IMPORTANT : Remplacer `eu-west-3` par ta région !**

---

**Create template version**

---

**Mettre à jour l'ASG :**

**Auto Scaling Groups -> flask-app-asg**

**Actions -> Edit**

**Launch template :**

```
Version : Latest
```

**Update**

---

**Remplacer les instances existantes :**

**Instance refresh**

**Start instance refresh :**

```
Minimum healthy percentage : 90%
```

**Les instances sont remplacées progressivement avec la nouvelle version**

**Durée : 10-15 minutes**

---

#### **Méthode 2 : Installation manuelle (pour test)**

**SSH vers une instance (via Bastion) :**

```bash
ssh ubuntu@10.0.11.10
```

**Installer l'agent :**

```bash
sudo apt-get update
sudo apt-get install -y ruby wget

cd /tmp
wget https://aws-codedeploy-eu-west-3.s3.eu-west-3.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto

# Vérifier
sudo systemctl status codedeploy-agent
```

**[OK] Agent installé !**

---

### ÉTAPE 8 : Ajouter les permissions IAM aux instances

**Les instances doivent pouvoir lire les artifacts S3.**

**IAM -> Roles -> flask-app-ec2-role (de l'exercice 7)**

**Add permissions -> Attach policies**

---

**Chercher et attacher :**

```
[x] AmazonS3ReadOnlyAccess
```

**Ou créer une policy custom plus restrictive :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:GetObjectVersion"
      ],
      "Resource": "arn:aws:s3:::flask-app-artifacts-123456/*"
    }
  ]
}
```

---

**Attach policy**

---

### ÉTAPE 9 : Créer le pipeline CodePipeline

**Console AWS -> CodePipeline -> Pipelines -> Create pipeline**

---

**Pipeline name :**

```
flask-app-pipeline
```

---

**Service role :**

```
[x] New service role
```

**Role name (auto-généré) :**

```
AWSCodePipelineServiceRole-eu-west-3-flask-app-pipeline
```

---

**Advanced settings :**

**Artifact store :**

```
[x] Default location
```

**S3 bucket auto-créé pour les artifacts du pipeline**

---

**Encryption key :**

```
[x] Default AWS Managed Key
```

---

**Next**

---

#### **Add source stage**

**Source provider :**

```
[x] GitHub (Version 2)
```

**Version 2 = OAuth App avec webhooks automatiques**

---

**Connection :**

**Créer une connexion GitHub :**

**Cliquer sur "Connect to GitHub"**

---

**Connection name :**

```
github-connection
```

---

**Connect to GitHub**

**Autoriser AWS Connector for GitHub dans la popup**

**Sélectionner :**

```
[x] Only select repositories
Repository : flask-app-cicd
```

**Install**

---

**Connexion créée !**

**Connection :**

```
github-connection
```

---

**Repository name :**

```
flask-app-cicd
```

---

**Branch name :**

```
main
```

---

**Change detection options :**

```
[x] Start the pipeline on source code change
```

**Webhook GitHub -> Pipeline se déclenche automatiquement à chaque commit**

---

**Output artifact format :**

```
[x] CodePipeline default
```

---

**Next**

---

#### **Add build stage**

**Build provider :**

```
[x] AWS CodeBuild
```

---

**Region :**

```
Europe (Paris) eu-west-3
```

---

**Project name :**

```
flask-app-build
```

---

**Build type :**

```
[x] Single build
```

---

**Next**

---

#### **Add deploy stage**

**Deploy provider :**

```
[x] AWS CodeDeploy
```

---

**Region :**

```
Europe (Paris) eu-west-3
```

---

**Application name :**

```
flask-app-deploy
```

---

**Deployment group :**

```
flask-app-production
```

---

**Next**

---

#### **Review**

**Vérifier la configuration :**

```
Pipeline name : flask-app-pipeline

Stages :
1. Source : GitHub - flask-app-cicd (main)
2. Build : CodeBuild - flask-app-build
3. Deploy : CodeDeploy - flask-app-deploy (flask-app-production)
```

---

**Create pipeline**

**Pipeline créé ! [BRAVO]**

**Le pipeline se lance automatiquement... [HOURGLASS_WITH_FLOWING_SAND]**

---

### ÉTAPE 10 : Observer l'exécution du pipeline

**Le pipeline commence immédiatement :**

```
Stage : Source
Status : In Progress
Action : GitHub
```

**Clique GitHub dans le stage Source pour voir les détails**

---

**Après ~10 secondes :**

```
Stage : Source
Status : Succeeded [OK]
```

**Le code est récupéré depuis GitHub**

---

```
Stage : Build
Status : In Progress
Action : CodeBuild
```

**Clique CodeBuild pour voir les logs en temps réel**

**Logs (résumé) :**

```
PROVISIONING
DOWNLOAD_SOURCE
INSTALL
PRE_BUILD - Running tests
  [OK] 7 tests passed
BUILD
POST_BUILD
UPLOAD_ARTIFACTS
COMPLETED
```

**Durée : 3-5 minutes**

---

```
Stage : Build
Status : Succeeded [OK]
```

---

```
Stage : Deploy
Status : In Progress
Action : CodeDeploy
```

**Clique CodeDeploy pour voir les détails du déploiement**

---

**Nouvelle fenêtre -> CodeDeploy -> Deployments -> d-XXXXXXXXX**

**Deployment lifecycle events :**

```
Step 1 : Provision replacement instances
  Status : Creating Auto Scaling group for Green environment
  [HOURGLASS_WITH_FLOWING_SAND] Creating instances...
  Duration : 5-7 minutes

Step 2 : Install application on replacement instances
  Events (par instance) :
    [OK] BeforeInstall : install_dependencies.sh
    [OK] AfterInstall : stop_application.sh
    [OK] ApplicationStart : start_application.sh
    [OK] ValidateService : validate_service.sh
  Duration : 3-5 minutes

Step 3 : Reroute traffic to replacement instances
  Status : Registering instances with target group
  [HOURGLASS_WITH_FLOWING_SAND] Waiting for health checks to pass...
  Status : Switching traffic to Green
  [OK] Traffic switched!
  Duration : 2-3 minutes

Step 4 : Terminate original instances
  Status : Waiting 5 minutes...
  [HOURGLASS_WITH_FLOWING_SAND] Monitoring for issues...
  (Si alarme -> Rollback automatique)
  Status : Terminating Blue instances
  [OK] Blue instances terminated
  Duration : 5-6 minutes
```

**Durée totale déploiement : 15-20 minutes**

---

```
Stage : Deploy
Status : Succeeded [OK]
```

---

**Pipeline complet :**

```
Pipeline : flask-app-pipeline
Status : Succeeded [OK]

Stages :
[OK] Source (10s)
[OK] Build (4 min)
[OK] Deploy (18 min)

Total : ~22 minutes
```

**[BRAVO] PREMIER DÉPLOIEMENT AUTOMATISÉ RÉUSSI ! [BRAVO]**

---

### ÉTAPE 11 : Tester le déploiement continu

**Faisons un changement dans le code et observons le pipeline.**

---

**Sur ton ordinateur :**

**Modifier `application.py` :**

```python
# Trouver la route /
@application.route('/')
def index():
    tasks = Task.query.order_by(Task.created_at.desc()).all()
    instance_id = os.environ.get('INSTANCE_ID', 'unknown')
    
    # AJOUT : Version de l'app
    app_version = "v2.0.0"
    
    return render_template('index.html', 
                          tasks=tasks, 
                          instance_id=instance_id,
                          app_version=app_version)
```

---

**Modifier `templates/index.html` :**

```html
<!-- En haut de la page -->
<div class="version-info">
    [RAPIDE] Version: {{ app_version }}
</div>
```

---

**Commiter et pousser :**

```bash
git add .
git commit -m "feat: Add version display v2.0.0"
git push origin main
```

---

**Retourner dans CodePipeline**

**Le pipeline se déclenche automatiquement ! [RAPIDE]**

```
Execution #2
Trigger : Commit by ton-username (feat: Add version display v2.0.0)
Started : Just now

Stages :
[HOURGLASS_WITH_FLOWING_SAND] Source : In Progress
[HOURGLASS_WITH_FLOWING_SAND] Build : Waiting
[HOURGLASS_WITH_FLOWING_SAND] Deploy : Waiting
```

---

**Après quelques secondes :**

```
[OK] Source : Succeeded
[HOURGLASS_WITH_FLOWING_SAND] Build : In Progress
```

---

**3-4 minutes plus tard :**

```
[OK] Source : Succeeded
[OK] Build : Succeeded
[HOURGLASS_WITH_FLOWING_SAND] Deploy : In Progress
```

---

**15-18 minutes plus tard :**

```
[OK] Source : Succeeded
[OK] Build : Succeeded
[OK] Deploy : Succeeded

Total : ~22 minutes
```

---

**Tester l'application :**

**Navigateur :**

```
http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com
```

**Tu vois maintenant :**

```
[RAPIDE] Version: v2.0.0
```

**[OK] DÉPLOIEMENT CONTINU FONCTIONNE ! [BRAVO]**

---

**Recap du workflow :**

```
1. Dev commit -> GitHub
   (10 secondes)

2. Webhook déclenche CodePipeline
   (instantané)

3. CodeBuild clone, teste, package
   (4 minutes)

4. CodeDeploy Blue/Green deployment
   (18 minutes)
   
5. Production mise à jour
   (Total : 22 minutes, zéro downtime)
```

---

### ÉTAPE 12 : Ajouter une étape d'approbation manuelle

**Pour les déploiements en production, on veut souvent une validation humaine.**

**CodePipeline -> flask-app-pipeline -> Edit**

---

**Entre Build et Deploy, ajouter un stage :**

**Cliquer sur "Add stage" entre Build et Deploy**

---

**Stage name :**

```
Approval
```

---

**Add stage**

---

**Dans le stage Approval -> Add action group**

---

**Action name :**

```
ManualApproval
```

---

**Action provider :**

```
[x] Manual approval
```

---

**SNS topic ARN (optionnel mais recommandé) :**

```
arn:aws:sns:eu-west-3:123456789012:asg-scaling-notifications
```

**Email envoyé aux approvers**

---

**URL for review (optionnel) :**

```
http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com
```

**URL de staging pour tester avant approbation**

---

**Comments (instructions pour approver) :**

```
Please review the build artifacts and test the application before approving deployment to production.

Build details: #{codepipeline.PipelineExecutionId}
Commit: #{SourceVariables.CommitMessage}
```

---

**Done**

---

**Save**

**Pipeline mis à jour ! [OK]**

---

**Nouveau workflow :**

```
Stages :
1. Source (GitHub)
2. Build (CodeBuild)
3. [DOUBLE_VERTICAL_BAR] Approval (Manuel)
4. Deploy (CodeDeploy)
```

---

**Prochain commit :**

**Le pipeline s'arrête au stage Approval :**

```
[OK] Source : Succeeded
[OK] Build : Succeeded
[DOUBLE_VERTICAL_BAR] Approval : Waiting for approval
[HOURGLASS_WITH_FLOWING_SAND] Deploy : Waiting
```

**Email reçu :**

```
Subject: APPROVAL NEEDED: Pipeline flask-app-pipeline awaiting approval

Message:
Please review the build artifacts and test the application before approving deployment to production.

Build details: abc123-def456-ghi789
Commit: feat: Add new feature

Actions:
[Approve] [Reject]
```

---

**Cliquer sur "Review" dans la console**

**Ou dans l'email**

---

**Review approval :**

```
[x] Approve

Comments :
Tested successfully, ready for production deployment.
```

**Submit**

---

**Pipeline continue :**

```
[OK] Source : Succeeded
[OK] Build : Succeeded
[OK] Approval : Approved by ton-nom
[HOURGLASS_WITH_FLOWING_SAND] Deploy : In Progress
```

---

### ÉTAPE 13 : Tester le rollback automatique

**Simulons un déploiement défaillant.**

---

**Modifier `scripts/validate_service.sh` pour échouer :**

```bash
# Temporairement, forcer l'échec
nano scripts/validate_service.sh
```

**Ajouter au début :**

```bash
#!/bin/bash

echo "===== Simulating validation failure ====="
exit 1  # Forcer l'échec
```

---

**Commiter :**

```bash
git add .
git commit -m "test: Simulate deployment failure"
git push origin main
```

---

**Pipeline se lance...**

```
[OK] Source : Succeeded
[OK] Build : Succeeded (tests passent toujours)
[HOURGLASS_WITH_FLOWING_SAND] Deploy : In Progress
```

---

**Pendant le déploiement :**

**CodeDeploy -> Deployments -> d-XXXXXXXXX**

**Lifecycle events :**

```
Step 1 : Provision replacement instances
  [OK] Succeeded

Step 2 : Install application
  [OK] BeforeInstall : Succeeded
  [OK] AfterInstall : Succeeded
  [OK] ApplicationStart : Succeeded
  [X] ValidateService : FAILED (exit code 1)
  
Status : Deployment failed
```

---

**Rollback automatique déclenché ! [SYNC]**

```
Rollback triggered : Deployment lifecycle event failed

Actions :
1. Stop routing traffic to Green instances
2. Route all traffic back to Blue instances
3. Terminate Green instances
```

**Durée rollback : 30 secondes - 1 minute**

---

**Pipeline :**

```
[OK] Source : Succeeded
[OK] Build : Succeeded
[X] Deploy : FAILED

Error : Deployment d-XXXXXXXXX failed: Lifecycle event 'ValidateService' failed
```

---

**L'application en production continue de fonctionner normalement ! [OK]**

```
http://flask-app-alb-123456789.eu-west-3.elb.amazonaws.com
-> Version v2.0.0 (version précédente stable)
-> Aucun downtime
-> Utilisateurs ne voient rien
```

**[BRAVO] ROLLBACK AUTOMATIQUE FONCTIONNE ! [BRAVO]**

---

**Corriger le problème :**

```bash
# Restaurer validate_service.sh
git revert HEAD
git push origin main
```

**Pipeline se relance -> Déploiement réussit**

---

### [OK] TESTS DE VALIDATION

**1. Pipeline créé et fonctionnel**

Console CodePipeline -> flask-app-pipeline

- [ ] 3+ stages (Source, Build, Deploy)
- [ ] Webhook GitHub configuré
- [ ] Exécutions visibles

---

**2. Build automatique sur commit**

Faire un commit -> Push

- [ ] Pipeline se déclenche automatiquement
- [ ] Build s'exécute
- [ ] Tests passent

---

**3. Tests automatisés**

CodeBuild -> Build logs

- [ ] Tests unitaires exécutés
- [ ] Tous les tests passent
- [ ] Échec de test -> Build échoue

---

**4. Artifacts créés**

S3 -> flask-app-artifacts-123456

- [ ] ZIP créé après chaque build
- [ ] Contient tous les fichiers nécessaires

---

**5. Déploiement Blue/Green**

CodeDeploy -> Deployments

- [ ] Green environment créé
- [ ] Instances provisionées
- [ ] Traffic switché
- [ ] Blue terminé après délai

---

**6. Zéro downtime**

Pendant un déploiement, tester l'URL ALB

- [ ] Application toujours accessible
- [ ] Pas d'erreurs 5xx
- [ ] Transition imperceptible

---

**7. Rollback automatique**

Simuler un échec (validate_service)

- [ ] Déploiement détecte l'échec
- [ ] Rollback vers Blue
- [ ] Application reste opérationnelle

---

**8. Approbation manuelle (si configuré)**

Faire un commit

- [ ] Pipeline s'arrête à Approval
- [ ] Email de notification reçu
- [ ] Approbation débloque Deploy

---

**9. Notifications**

- [ ] Email à chaque déploiement
- [ ] Email en cas d'échec
- [ ] Email pour approbations

---

**10. CodeDeploy agent sur instances**

SSH vers une instance

```bash
sudo systemctl status codedeploy-agent
```

- [ ] Service actif
- [ ] Logs présents

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Pipeline ne se déclenche pas automatiquement

**Symptômes :**

Commit/Push -> Pipeline ne démarre pas

---

**Causes :**

**1. Webhook GitHub pas configuré**

GitHub -> Repo -> Settings -> Webhooks

Vérifier : Webhook AWS présent ?

**Solution :** Recréer la connexion GitHub dans CodePipeline

---

**2. Branch incorrect**

CodePipeline -> Source stage

Vérifier : Branch = main ?

Commit sur branch différente -> Pas de déclenchement

---

**3. Change detection désactivé**

CodePipeline -> Source stage -> Edit

Vérifier : "Start the pipeline on source code change" coché ?

---

#### Erreur 2 : Build échoue

**CodeBuild logs :**

```
Error: Command did not exit successfully
Exit code: 1
```

---

**Causes courantes :**

**1. Tests échouent**

Logs :

```
FAILED tests/test_app.py::test_something
```

**Solution :** Corriger le test ou le code

---

**2. Dépendances manquantes**

```
ModuleNotFoundError: No module named 'flask'
```

**Solution :** Vérifier `requirements.txt`

---

**3. Erreur de syntaxe Python**

```
SyntaxError: invalid syntax
```

**Solution :** Corriger le code

---

**4. buildspec.yml invalide**

```
YAML_FILE_ERROR: Phase name is not valid
```

**Solution :** Vérifier la syntaxe YAML (indentation !)

---

#### Erreur 3 : CodeDeploy agent non trouvé

**CodeDeploy logs :**

```
The overall deployment failed because too many individual instances failed deployment.
```

**Détail instance :**

```
The deployment failed because the CodeDeploy agent did not respond.
```

---

**Causes :**

**1. Agent pas installé**

SSH vers instance :

```bash
sudo systemctl status codedeploy-agent
# Unit codedeploy-agent.service could not be found
```

**Solution :** Installer l'agent (voir Étape 7)

---

**2. Agent arrêté**

```bash
sudo systemctl status codedeploy-agent
# Active: inactive (dead)
```

**Solution :**

```bash
sudo systemctl start codedeploy-agent
sudo systemctl enable codedeploy-agent
```

---

**3. Permissions IAM manquantes**

Instance role n'a pas accès S3

**Solution :** Attacher `AmazonS3ReadOnlyAccess` au rôle EC2

---

#### Erreur 4 : Lifecycle event échoue

**CodeDeploy :**

```
[X] ApplicationStart : Script at specified location: scripts/start_application.sh failed with exit code 1
```

---

**Debug :**

**SSH vers instance Green :**

```bash
# Logs CodeDeploy
sudo cat /var/log/aws/codedeploy-agent/codedeploy-agent.log

# Logs des scripts
sudo cat /opt/codedeploy-agent/deployment-root/.../logs/scripts.log
```

---

**Causes courantes :**

**1. Script n'existe pas**

```
Script does not exist at specified location
```

**Solution :** Vérifier `appspec.yml` (chemins corrects ?)

---

**2. Script pas exécutable**

```
Permission denied
```

**Solution :**

```bash
chmod +x scripts/*.sh
git add scripts/
git commit -m "fix: Make scripts executable"
git push
```

---

**3. Erreur dans le script**

Voir les logs du script spécifique

**Solution :** Corriger le script

---

#### Erreur 5 : Rollback impossible

**CodeDeploy :**

```
Cannot perform rollback: No previous deployment available
```

---

**Cause :**

Premier déploiement -> Pas de "Blue" vers lequel rollback

---

**Solution :**

Accepter l'échec du premier déploiement

Ou : Déployer manuellement une version stable d'abord

---

#### Erreur 6 : Target Group reste unhealthy

**CodeDeploy attend indéfiniment :**

```
Waiting for instances to become healthy...
```

---

**Causes :**

**1. Health check path incorrect**

Target Group -> Health checks -> Path = `/health` ?

App a cette route ?

---

**2. Port incorrect**

Target Group -> Health checks -> Port override = 5000 ?

---

**3. Security Group bloque**

App SG -> Inbound -> Port 5000 depuis ALB SG ?

---

**4. Application ne démarre pas**

SSH vers instance -> Vérifier :

```bash
sudo systemctl status flask-app
sudo journalctl -u flask-app -n 50
```

---

### [IMPORTANT] POINTS CLÉS À RETENIR

**1. CI/CD**
- Automatisation complète : Commit -> Prod
- Tests automatiques (détection bugs précoce)
- Déploiements fréquents et fiables

**2. Pipeline stages**
- Source : GitHub/CodeCommit
- Build : CodeBuild (tests + package)
- Deploy : CodeDeploy (Blue/Green)

**3. Blue/Green deployment**
- Zéro downtime
- Rollback instantané
- Test en prod avant switch

**4. CodeBuild**
- buildspec.yml : Configuration build
- Environnements managés
- Artifacts vers S3

**5. CodeDeploy**
- appspec.yml : Configuration déploiement
- Lifecycle hooks : Scripts personnalisés
- Agent sur instances

**6. Rollback automatique**
- Surveillance CloudWatch
- Détection échecs
- Retour version stable

**7. Monitoring**
- Logs CloudWatch
- Métriques déploiements
- Notifications SNS

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Tests avancés**

**Ajouter des tests d'intégration :**

```yaml
# buildspec.yml
pre_build:
  commands:
    - pytest tests/unit/ -v
    - pytest tests/integration/ -v
    - coverage run -m pytest
    - coverage report
```

---

**2. Déploiement Canary**

**CodeDeploy Linear/Canary :**

```
CodeDeployDefault.LambdaCanary10Percent5Minutes
-> 10% trafic vers nouvelle version
-> Attendre 5 minutes
-> Si OK, 100%
-> Sinon, rollback
```

---

**3. Multi-environment**

**Pipeline avec plusieurs stages :**

```
Source -> Build -> Deploy Dev -> Deploy Staging -> Approval -> Deploy Prod
```

---

**4. Infrastructure as Code**

**Définir le pipeline en code (CloudFormation/CDK) :**

```typescript
// AWS CDK
const pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
  stages: [
    { stageName: 'Source', actions: [sourceAction] },
    { stageName: 'Build', actions: [buildAction] },
    { stageName: 'Deploy', actions: [deployAction] }
  ]
});
```

---

**5. Feature Flags**

**Déployer le code sans activer la feature :**

```python
if feature_flags.is_enabled('new_ui'):
    return render_template('new_ui.html')
else:
    return render_template('old_ui.html')
```

**Activation progressive sans redéploiement**

---

**6. Smoke Tests post-déploiement**

**Ajouter un stage de tests après Deploy :**

```
Deploy -> Smoke Tests (API tests, load tests) -> Approval final
```

---

**7. Notifications Slack/Teams**

**Webhook dans appspec.yml :**

```bash
# AfterAllowTraffic hook
curl -X POST https://hooks.slack.com/... \
  -d '{"text":"[OK] Deployment successful: v2.0.0"}'
```

---

**8. Blue/Green avec database migrations**

**Problème : Migrations DB incompatibles**

**Solution : Migrations backward-compatible :**

```
v1 : Column A exists
v2 : Add Column B (A still works)
v3 : Remove Column A (B only)

Deploy : v1 -> v2 (works), v2 -> v3 (works)
Never : v1 -> v3 (breaks)
```

---

## [COURS] CONCLUSION DE L'EXERCICE 9

**[BRAVO] Félicitations ! Tu as créé un pipeline CI/CD complet ! [BRAVO]**

**Ce que tu as appris :**
- Pipeline CI/CD automatisé
- CodeBuild (build + tests)
- CodeDeploy (Blue/Green)
- CodePipeline (orchestration)
- Déploiements sans downtime
- Rollback automatique
- Tests automatisés
- Approbations manuelles
- Monitoring et debugging

**Compétences acquises :**
- [OK] CI/CD architecture
- [OK] CodeBuild (buildspec.yml)
- [OK] CodeDeploy (appspec.yml)
- [OK] CodePipeline
- [OK] Blue/Green deployment
- [OK] Automated testing
- [OK] Rollback strategies
- [OK] DevOps best practices

**Avant/Après :**

| Aspect | Manuel | CI/CD Pipeline |
|--------|--------|----------------|
| Déploiement | 30 min | 22 min (automatique) |
| Tests | Manuels | Automatiques |
| Erreurs humaines | Fréquentes | Éliminées |
| Downtime | 2-5 min | 0 seconde |
| Rollback | 10 min | 30 secondes |
| Confiance | [FACE_WITH_OPEN_MOUTH_AND_COLD_SWEAT] | [SMILING_FACE_WITH_SUNGLASSES] |

**Temps moyen de réalisation :** 5-6 heures

---

**Dernière étape :** Exercice 10 - Architecture complète ! [CONSTRUCTION]

**On va intégrer TOUS les services des 9 exercices dans une architecture production-ready complète avec tous les best practices AWS !**

Veux-tu continuer ? [RAPIDE]


# [ROUGE] EXERCICE 10 : ARCHITECTURE COMPLÈTE PRODUCTION-READY

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es Solutions Architect Senior. L'entreprise a décidé de migrer complètement vers AWS. Tu dois concevoir et implémenter une architecture cloud complète, évolutive, hautement disponible, sécurisée et optimisée pour les coûts. Cette architecture doit intégrer tous les best practices AWS et servir de référence pour les futurs projets.

### Cahier des charges

Le client souhaite une architecture qui :
- **Haute disponibilité** : 99.95% uptime (4h20 downtime/an max)
- **Scalabilité** : De 10 à 10 000 utilisateurs sans intervention
- **Sécurité** : Conformité SOC 2, isolation complète, chiffrement
- **Performance** : < 200ms temps de réponse global
- **Disaster Recovery** : RPO 1h, RTO 4h
- **Observabilité** : Monitoring complet, alertes proactives
- **Coûts optimisés** : Budget contrôlé, utilisation efficace
- **DevOps** : Déploiements automatisés, infrastructure as code

### Contraintes techniques

- Toutes les régions AWS (focus EU)
- Multi-AZ obligatoire
- CI/CD complet
- Backup automatisé
- Logging centralisé
- Budget : ~$300-500/mois
- Temps estimé : 8-10 heures (implémentation complète)

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Concevoir une architecture AWS complète
- [OK] Intégrer tous les services AWS (compute, storage, network, security)
- [OK] Implémenter les best practices AWS Well-Architected
- [OK] Configurer la haute disponibilité
- [OK] Optimiser les coûts et performances
- [OK] Mettre en place observabilité complète
- [OK] Implémenter disaster recovery
- [OK] Sécuriser l'infrastructure
- [OK] Automatiser avec Infrastructure as Code
- [OK] Maintenir et faire évoluer l'architecture

---

## [DOCS] PRÉREQUIS

- **TOUS les exercices 1-9 terminés**
- Compréhension des services AWS
- Notion d'architecture cloud
- Budget AWS disponible (~$300-500)

---

## [IDEE] ARCHITECTURE FINALE - VUE D'ENSEMBLE

### Schéma d'architecture complet

```
                                 ┌─────────────────┐
                                 │   Route 53      │
                                 │   (DNS Global)  │
                                 └────────┬────────┘
                                          │
                        ┌─────────────────┴─────────────────┐
                        │                                   │
                  ┌─────[BLACK_DOWN-POINTING_TRIANGLE]──────┐                    ┌──────[BLACK_DOWN-POINTING_TRIANGLE]─────┐
                  │ CloudFront │                    │    WAF     │
                  │   (CDN)    │                    │ (Firewall) │
                  └─────┬──────┘                    └──────┬─────┘
                        │                                   │
                        └─────────────────┬─────────────────┘
                                          │
                              ┌───────────[BLACK_DOWN-POINTING_TRIANGLE]───────────┐
                              │         ALB           │
                              │  (Load Balancer)      │
                              └───────────┬───────────┘
                                          │
        ┌─────────────────────────────────┴─────────────────────────────────┐
        │                     VPC (10.0.0.0/16)                              │
        │                                                                     │
        │  ┌──────────────────────────────────────────────────────────────┐ │
        │  │              Availability Zone A (eu-west-3a)                 │ │
        │  │                                                                │ │
        │  │  ┌────────────────┐  ┌────────────────┐  ┌─────────────────┐│ │
        │  │  │ Public Subnet  │  │ Private App    │  │  Private DB     ││ │
        │  │  │                │  │                │  │                 ││ │
        │  │  │ [NAT Gateway]  │  │ [EC2 Auto     │  │ [RDS Primary]   ││ │
        │  │  │ [Bastion]      │  │  Scaling]     │  │ [ElastiCache]   ││ │
        │  │  └────────────────┘  └────────────────┘  └─────────────────┘│ │
        │  └──────────────────────────────────────────────────────────────┘ │
        │                                                                     │
        │  ┌──────────────────────────────────────────────────────────────┐ │
        │  │              Availability Zone B (eu-west-3b)                 │ │
        │  │                                                                │ │
        │  │  ┌────────────────┐  ┌────────────────┐  ┌─────────────────┐│ │
        │  │  │ Public Subnet  │  │ Private App    │  │  Private DB     ││ │
        │  │  │                │  │                │  │                 ││ │
        │  │  │ [NAT Gateway]  │  │ [EC2 Auto     │  │ [RDS Standby]   ││ │
        │  │  │                │  │  Scaling]     │  │ [ElastiCache]   ││ │
        │  │  └────────────────┘  └────────────────┘  └─────────────────┘│ │
        │  └──────────────────────────────────────────────────────────────┘ │
        │                                                                     │
        │  ┌──────────────────────────────────────────────────────────────┐ │
        │  │              Availability Zone C (eu-west-3c)                 │ │
        │  │                                                                │ │
        │  │  ┌────────────────┐  ┌────────────────┐  ┌─────────────────┐│ │
        │  │  │ Public Subnet  │  │ Private App    │  │  Private DB     ││ │
        │  │  │                │  │                │  │                 ││ │
        │  │  │ [NAT Gateway]  │  │ [EC2 Auto     │  │ [ElastiCache]   ││ │
        │  │  │                │  │  Scaling]     │  │                 ││ │
        │  │  └────────────────┘  └────────────────┘  └─────────────────┘│ │
        │  └──────────────────────────────────────────────────────────────┘ │
        └─────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────────────┐
│                          Services Additionnels                            │
├──────────────────────────────────────────────────────────────────────────┤
│ • S3 (Backups, Static Assets, Logs)                                      │
│ • CloudWatch (Monitoring, Logs, Alarms)                                  │
│ • SNS (Notifications, Alertes)                                           │
│ • Systems Manager (Gestion instances)                                    │
│ • Secrets Manager (Credentials DB)                                       │
│ • CodePipeline/Build/Deploy (CI/CD)                                      │
│ • CloudTrail (Audit logs)                                                │
│ • Config (Compliance)                                                    │
│ • Cost Explorer (Optimisation coûts)                                     │
└──────────────────────────────────────────────────────────────────────────┘
```

---

## [CONSTRUCTION] COMPOSANTS DE L'ARCHITECTURE

### 1. Networking Layer (Réseau)

```
VPC Custom : 10.0.0.0/16
├── 3 Availability Zones
│   ├── Public Subnets (10.0.1-3.0/24)
│   ├── Private App Subnets (10.0.11-13.0/24)
│   └── Private DB Subnets (10.0.21-23.0/24)
├── Internet Gateway
├── 3 NAT Gateways (HA)
├── Route Tables (Public, Private-A, Private-B, Private-C)
├── Security Groups (Layered security)
└── Network ACLs (Subnet-level firewall)
```

**Pourquoi 3 AZs ?**

```
2 AZs : 99.95% disponibilité
3 AZs : 99.99% disponibilité

Calcul :
1 AZ down (rare) : App continue (2 AZs restantes)
2 AZs down (extrêmement rare) : App continue (1 AZ)
3 AZs down : Catastrophe régionale AWS (jamais arrivé)
```

---

### 2. Compute Layer (Calcul)

```
Auto Scaling Group
├── Launch Template (AMI optimisée)
├── Min : 3 instances (1 par AZ)
├── Desired : 6 instances (2 par AZ)
├── Max : 18 instances (6 par AZ)
├── Instance Types : t3.micro, t3a.micro (Spot + On-Demand)
├── Spot : 50% (économies)
├── Scaling Policies
│   ├── Target Tracking (CPU 70%)
│   ├── Step Scaling (CPU > 90%)
│   └── Scheduled (Business hours)
└── Health Checks : EC2 + ELB
```

---

### 3. Load Balancing Layer

```
Application Load Balancer
├── Scheme : Internet-facing
├── 3 Availability Zones
├── Target Groups
│   ├── Blue (Production)
│   └── Green (Déploiements)
├── Listeners
│   ├── HTTP:80 -> HTTPS redirect
│   └── HTTPS:443 -> Target Group
├── SSL/TLS Certificate (ACM)
├── Sticky Sessions : Enabled
├── Connection Draining : 30s
└── Health Checks : /health (5s interval)
```

---

### 4. Database Layer

```
RDS PostgreSQL
├── Instance Class : db.t3.small (production)
├── Multi-AZ : Enabled (failover automatique)
├── Storage : 100 GB gp3 (auto-scaling enabled)
├── Backups
│   ├── Automated : 7 days retention
│   ├── Manual Snapshots : Avant deployments
│   └── S3 Export : Mensuel (compliance)
├── Encryption : AES-256 (at rest + in transit)
├── Performance Insights : Enabled
├── Enhanced Monitoring : 60s interval
└── Read Replica (optionnel, si lecture élevée)

ElastiCache Redis
├── Node Type : cache.t3.micro
├── Cluster Mode : Enabled
├── Replicas : 2 (1 par AZ)
├── Use case : Session store, cache DB queries
└── Automatic Failover : Enabled
```

---

### 5. Storage Layer

```
S3 Buckets
├── Static Assets Bucket
│   ├── Lifecycle : Archive > 90 days -> Glacier
│   ├── Versioning : Enabled
│   └── CloudFront Distribution
├── Application Logs Bucket
│   ├── Lifecycle : Delete > 180 days
│   └── Intelligent-Tiering
├── Backup Bucket
│   ├── Lifecycle : Archive > 30 days -> Glacier Deep Archive
│   ├── Versioning : Enabled
│   └── Cross-Region Replication (DR)
└── Artifacts Bucket (CodePipeline)
    └── Lifecycle : Delete > 30 days
```

---

### 6. Content Delivery (CDN)

```
CloudFront Distribution
├── Origins
│   ├── ALB (dynamic content)
│   └── S3 (static assets)
├── Cache Behaviors
│   ├── /static/* -> S3 (TTL 1 day)
│   ├── /api/* -> ALB (No cache)
│   └── /* -> ALB (TTL 5 min)
├── Price Class : Use All Edge Locations
├── SSL Certificate : *.domain.com
├── WAF : Attached
├── Custom Domain : app.domain.com
└── Compression : Gzip + Brotli
```

---

### 7. Security Layer

```
AWS WAF (Web Application Firewall)
├── Managed Rules
│   ├── Core Rule Set (OWASP Top 10)
│   ├── Known Bad Inputs
│   ├── SQL Injection
│   └── IP Reputation
├── Custom Rules
│   ├── Rate Limiting : 2000 req/5min per IP
│   ├── Geo-blocking : Block certain countries
│   └── Bot Control
└── Logging : Send to S3

Security Groups (Layered)
├── ALB-SG : 80, 443 from 0.0.0.0/0
├── App-SG : 5000 from ALB-SG, 22 from Bastion-SG
├── DB-SG : 5432 from App-SG
├── Cache-SG : 6379 from App-SG
└── Bastion-SG : 22 from Admin IPs

Secrets Manager
├── Database Credentials (auto-rotation)
├── API Keys
└── SSL Certificates

IAM Roles (Least Privilege)
├── EC2 Instance Role
│   ├── CloudWatch Logs Write
│   ├── S3 Read (artifacts)
│   ├── Secrets Manager Read
│   └── Systems Manager Session Manager
├── CodePipeline Role
├── CodeBuild Role
├── CodeDeploy Role
└── Lambda Execution Roles
```

---

### 8. CI/CD Pipeline

```
CodePipeline
├── Source : GitHub (main branch)
├── Build : CodeBuild
│   ├── Unit Tests
│   ├── Integration Tests
│   ├── Security Scan (Snyk/Trivy)
│   ├── Code Quality (SonarQube)
│   └── Artifact Creation
├── Test : Deploy to Staging
│   └── Automated Smoke Tests
├── Approval : Manual (Production)
└── Deploy : CodeDeploy Blue/Green
    ├── Create Green Environment
    ├── Deploy Application
    ├── Run Validation Tests
    ├── Switch Traffic (10% -> 50% -> 100%)
    ├── Monitor Metrics (5 min)
    └── Terminate Blue
```

---

### 9. Monitoring & Observability

```
CloudWatch
├── Metrics
│   ├── EC2 : CPU, Memory, Disk, Network
│   ├── ALB : Request Count, Latency, HTTP Errors
│   ├── RDS : Connections, IOPS, CPU
│   ├── Cache : Hit Rate, Memory
│   └── Custom Metrics : Business KPIs
├── Alarms
│   ├── Critical (PagerDuty)
│   │   ├── RDS CPU > 90%
│   │   ├── ALB 5xx > 5%
│   │   └── Disk Space < 10%
│   ├── Warning (Email)
│   │   ├── RDS CPU > 70%
│   │   ├── ALB Latency > 2s
│   │   └── ASG Max Capacity
│   └── Info (Slack)
│       └── Deployments, Scaling Events
├── Dashboards
│   ├── Infrastructure Overview
│   ├── Application Performance
│   ├── Database Metrics
│   └── Cost Analytics
└── Logs
    ├── Application Logs (CloudWatch Logs)
    ├── VPC Flow Logs
    ├── ALB Access Logs -> S3
    ├── CloudTrail (API Audit)
    └── Log Insights (Queries)

X-Ray (Distributed Tracing)
├── Trace requests end-to-end
├── Identify bottlenecks
├── Service Map
└── Error Analysis
```

---

### 10. Disaster Recovery

```
Backup Strategy
├── RDS Automated Backups : 7 days
├── RDS Manual Snapshots : Weekly
├── EC2 AMIs : Weekly (golden images)
├── S3 Cross-Region Replication : Enabled
└── Database Exports : Monthly (compliance)

Recovery Procedures
├── RTO (Recovery Time Objective) : 4 hours
├── RPO (Recovery Point Objective) : 1 hour
└── DR Region : eu-west-1 (Ireland)

Disaster Scenarios
├── AZ Failure : Automatic (Multi-AZ)
├── Region Failure : Manual switch (4h)
│   ├── Restore RDS from snapshot
│   ├── Update Route 53
│   └── Launch instances
└── Data Corruption : Restore from backup
```

---

## [OK] PLAN D'IMPLÉMENTATION COMPLET

### Phase 1 : Fondations (Jour 1 - 2h)

**Objectif : Infrastructure réseau de base**

#### Étape 1.1 : Créer le VPC optimisé

**Amélioration par rapport à l'exercice 8 :**

Console VPC -> Create VPC

```
Name : production-vpc-v2
CIDR : 10.0.0.0/16
Enable DNS hostnames : [OK]
Enable DNS resolution : [OK]

Tags :
Environment : Production
Project : Flask-TODO-V2
ManagedBy : Terraform (futur)
CostCenter : Engineering
```

---

#### Étape 1.2 : Créer les subnets (3 AZs)

**9 subnets au total :**

```
Public Subnets (Internet access)
├── Public-A : 10.0.1.0/24 (eu-west-3a)
├── Public-B : 10.0.2.0/24 (eu-west-3b)
└── Public-C : 10.0.3.0/24 (eu-west-3c)

Private App Subnets (Application tier)
├── Private-App-A : 10.0.11.0/24 (eu-west-3a)
├── Private-App-B : 10.0.12.0/24 (eu-west-3b)
└── Private-App-C : 10.0.13.0/24 (eu-west-3c)

Private DB Subnets (Data tier)
├── Private-DB-A : 10.0.21.0/24 (eu-west-3a)
├── Private-DB-B : 10.0.22.0/24 (eu-west-3b)
└── Private-DB-C : 10.0.23.0/24 (eu-west-3c)
```

---

#### Étape 1.3 : Créer 3 NAT Gateways (HA complète)

```
NAT-Gateway-A -> Public-A (Elastic IP-A)
NAT-Gateway-B -> Public-B (Elastic IP-B)
NAT-Gateway-C -> Public-C (Elastic IP-C)
```

**Coût : 3 × $32.85/mois = $98.55/mois**

**Justification :**
- Haute disponibilité totale
- Pas de cross-AZ traffic (économies data transfer)
- Production-grade

---

#### Étape 1.4 : Configurer les Route Tables

**4 Route Tables :**

```
1. Public-RT
   Routes : 0.0.0.0/0 -> IGW
   Subnets : Public-A, Public-B, Public-C

2. Private-RT-A
   Routes : 0.0.0.0/0 -> NAT-A
   Subnets : Private-App-A, Private-DB-A

3. Private-RT-B
   Routes : 0.0.0.0/0 -> NAT-B
   Subnets : Private-App-B, Private-DB-B

4. Private-RT-C
   Routes : 0.0.0.0/0 -> NAT-C
   Subnets : Private-App-C, Private-DB-C
```

---

#### Étape 1.5 : Activer VPC Flow Logs

```
Destination : CloudWatch Logs
Log Group : /aws/vpc/production-vpc-v2
Aggregation Interval : 10 minutes (coûts réduits)
Filter : All traffic
```

---

**[OK] Phase 1 terminée : Infrastructure réseau prête**

**Durée : 2 heures**

---

### Phase 2 : Base de données (Jour 1 - 2h)

**Objectif : RDS Multi-AZ + ElastiCache**

#### Étape 2.1 : Créer le Subnet Group RDS

```
Name : production-db-subnet-group-v2
VPC : production-vpc-v2
Subnets :
  - Private-DB-A (eu-west-3a)
  - Private-DB-B (eu-west-3b)
  - Private-DB-C (eu-west-3c)
```

---

#### Étape 2.2 : Créer RDS PostgreSQL (Production-grade)

Console RDS -> Create database

```
Engine : PostgreSQL 15.5
Template : Production

DB instance identifier : flask-todo-production-db
Master username : postgres
Master password : (Générer et stocker dans Secrets Manager)

DB instance class : db.t3.small (2 vCPU, 2 GB RAM)
  -> Plus puissant que t3.micro pour production

Storage :
  Type : General Purpose SSD (gp3)
  Allocated : 100 GB
  IOPS : 3000 (provisionné)
  Throughput : 125 MB/s
  Storage autoscaling : Enable (max 500 GB)

Availability : Multi-AZ deployment [OK]
  -> Automatic failover

Connectivity :
  VPC : production-vpc-v2
  Subnet group : production-db-subnet-group-v2
  Public access : No
  Security group : DB-SG-Production

Database authentication : Password + IAM database authentication

Monitoring :
  Enhanced monitoring : Enable (60 seconds)
  Performance Insights : Enable
  Retention : 7 days

Backup :
  Automated backups : Enable
  Retention : 7 days
  Backup window : 03:00-04:00 UTC (nuit)
  Copy tags to snapshots : [OK]

Maintenance :
  Auto minor version upgrade : Enable
  Maintenance window : Sun 04:00-05:00 UTC

Deletion protection : Enable [OK]
```

**Coût : ~$60/mois (t3.small Multi-AZ)**

---

#### Étape 2.3 : Stocker les credentials dans Secrets Manager

Console Secrets Manager -> Store a new secret

```
Secret type : Credentials for RDS database
Username : postgres
Password : (celui généré)
Encryption key : aws/secretsmanager (default)

Database : flask-todo-production-db
Secret name : production/db/postgres

Rotation : Enable automatic rotation
  Rotation schedule : 30 days
  Lambda function : Create new (auto-créée par AWS)
```

**Coût : $0.40/mois + $0.05 per 10K API calls**

---

#### Étape 2.4 : Créer ElastiCache Redis Cluster

Console ElastiCache -> Redis clusters -> Create

```
Cluster mode : Enabled [OK]

Cluster info :
  Name : flask-todo-production-cache
  Engine version : 7.0

Location :
  Multi-AZ : Enabled
  Number of shards : 3 (1 par AZ)
  Replicas per shard : 1

Node type : cache.t3.micro

Subnet group :
  Name : production-cache-subnet-group
  VPC : production-vpc-v2
  Subnets : Private-DB-A, Private-DB-B, Private-DB-C

Security :
  Security groups : Cache-SG-Production
  Encryption at rest : Enable
  Encryption in transit : Enable

Backup :
  Enable automatic backups : [OK]
  Retention : 7 days
  Backup window : 04:00-05:00 UTC

Maintenance window : Sun 05:00-06:00 UTC
```

**Coût : ~$25/mois (3 shards + 3 replicas)**

---

**[OK] Phase 2 terminée : Base de données prête**

**Durée : 2 heures**

---

### Phase 3 : Compute & Load Balancing (Jour 2 - 3h)

**Objectif : ASG 3-AZ + ALB avec HTTPS**

#### Étape 3.1 : Créer une AMI optimisée (Golden Image)

**Lancer une instance temporaire :**

```
AMI : Ubuntu 22.04
Instance type : t3.small
Subnet : Private-App-A (via Session Manager, pas de Bastion)
Security group : App-SG-Production
IAM Role : EC2-Production-Role
```

---

**SSH via Session Manager :**

```bash
# Pas besoin de SSH key, utiliser Session Manager
# Console Systems Manager -> Session Manager -> Start session
```

---

**Installer et configurer tout :**

```bash
#!/bin/bash
# Installation complète

# Système
sudo apt update && sudo apt upgrade -y

# Python et dépendances
sudo apt install -y python3-pip python3-venv
sudo apt install -y postgresql-client redis-tools
sudo apt install -y nginx

# CloudWatch Agent
wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
sudo dpkg -i amazon-cloudwatch-agent.deb

# CodeDeploy Agent
cd /tmp
wget https://aws-codedeploy-eu-west-3.s3.eu-west-3.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto
sudo systemctl enable codedeploy-agent

# Node Exporter (Prometheus, optionnel)
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
tar xvfz node_exporter-1.6.1.linux-amd64.tar.gz
sudo mv node_exporter-1.6.1.linux-amd64/node_exporter /usr/local/bin/

# Configurer CloudWatch Agent
sudo tee /opt/aws/amazon-cloudwatch-agent/etc/config.json > /dev/null << 'EOF'
{
  "metrics": {
    "namespace": "FlaskTODO/Production",
    "metrics_collected": {
      "mem": {
        "measurement": [
          {"name": "mem_used_percent", "rename": "MemoryUtilization", "unit": "Percent"}
        ],
        "metrics_collection_interval": 60
      },
      "disk": {
        "measurement": [
          {"name": "used_percent", "rename": "DiskUtilization", "unit": "Percent"}
        ],
        "metrics_collection_interval": 60,
        "resources": ["*"]
      }
    }
  },
  "logs": {
    "logs_collected": {
      "files": {
        "collect_list": [
          {
            "file_path": "/var/log/flask-app/*.log",
            "log_group_name": "/aws/ec2/flask-app",
            "log_stream_name": "{instance_id}/{ip_address}"
          },
          {
            "file_path": "/var/log/nginx/access.log",
            "log_group_name": "/aws/ec2/nginx",
            "log_stream_name": "{instance_id}/access"
          },
          {
            "file_path": "/var/log/nginx/error.log",
            "log_group_name": "/aws/ec2/nginx",
            "log_stream_name": "{instance_id}/error"
          }
        ]
      }
    }
  }
}
EOF

# Démarrer CloudWatch Agent
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config \
  -m ec2 \
  -s \
  -c file:/opt/aws/amazon-cloudwatch-agent/etc/config.json

# Optimisations système
sudo tee -a /etc/sysctl.conf > /dev/null << EOF
# Network optimizations
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.ip_local_port_range = 10240 65535
EOF

sudo sysctl -p

# Cleanup
sudo apt autoremove -y
sudo apt clean

# Créer la structure de l'app
sudo mkdir -p /opt/flask-app
sudo chown ubuntu:ubuntu /opt/flask-app
```

---

**Créer l'AMI :**

```
Console EC2 -> Instance -> Actions -> Image and templates -> Create image

Image name : flask-todo-production-ami-v1.0
Image description : Production-ready AMI with all dependencies
Reboot instance : No (si app pas encore installée)
```

**Attendre 5-10 minutes**

**AMI créée : ami-0abc123def456789**

---

**Terminer l'instance temporaire**

---

#### Étape 3.2 : Demander un certificat SSL (ACM)

Console ACM (Certificate Manager) -> Request certificate

```
Certificate type : Public certificate

Domain names :
  - app.tondomaine.com
  - *.tondomaine.com (wildcard)

Validation method : DNS validation

Key algorithm : RSA 2048
```

**Request**

---

**Valider via DNS :**

```
ACM affiche des enregistrements CNAME à ajouter dans ton DNS

Exemple :
_abc123.app.tondomaine.com CNAME _xyz789.acm-validations.aws.
```

**Ajouter ces enregistrements dans Route 53 ou ton DNS provider**

**Attendre la validation (5-30 minutes)**

**[OK] Certificate issued**

---

#### Étape 3.3 : Créer le Launch Template (Version Production)

```
Name : flask-todo-production-lt
AMI : flask-todo-production-ami-v1.0 (ton AMI)
Instance type : t3.micro
Key pair : (Optionnel, Session Manager recommandé)

Network :
  Don't include in launch template

Security groups : App-SG-Production

IAM instance profile : EC2-Production-Role

Monitoring : Enable CloudWatch detailed monitoring

User data :
```

```bash
#!/bin/bash
# User data minimal (app déjà dans l'AMI)

# Récupérer les secrets depuis Secrets Manager
DB_SECRET=$(aws secretsmanager get-secret-value \
  --secret-id production/db/postgres \
  --region eu-west-3 \
  --query SecretString \
  --output text)

DB_HOST=$(echo $DB_SECRET | jq -r .host)
DB_PASSWORD=$(echo $DB_SECRET | jq -r .password)

# Créer .env
cat > /opt/flask-app/.env << EOF
DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@${DB_HOST}:5432/production_db
REDIS_URL=redis://flask-todo-production-cache.abc123.cache.amazonaws.com:6379/0
SECRET_KEY=$(openssl rand -base64 32)
FLASK_ENV=production
ENVIRONMENT=production
INSTANCE_ID=$(ec2-metadata --instance-id | cut -d " " -f 2)
INSTANCE_AZ=$(ec2-metadata --availability-zone | cut -d " " -f 2)
EOF

# Démarrer CloudWatch Agent (au cas où)
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
  -a fetch-config \
  -m ec2 \
  -s \
  -c file:/opt/aws/amazon-cloudwatch-agent/etc/config.json
```

---

#### Étape 3.4 : Créer les Target Groups

**Target Group 1 : Blue (Production)**

```
Name : flask-todo-production-tg-blue
Target type : Instances
Protocol : HTTP
Port : 80
VPC : production-vpc-v2

Health check :
  Protocol : HTTP
  Path : /health
  Port : Traffic port
  Healthy threshold : 2
  Unhealthy threshold : 3
  Timeout : 5
  Interval : 30
  Success codes : 200

Port override : 5000

Tags :
  Environment : Blue
  Type : Production
```

---

**Target Group 2 : Green (Déploiements)**

```
Name : flask-todo-production-tg-green
(Même configuration que Blue)

Tags :
  Environment : Green
  Type : Deployment
```

---

#### Étape 3.5 : Créer l'Application Load Balancer

```
Name : flask-todo-production-alb
Scheme : Internet-facing
IP address type : IPv4

Network mapping :
  VPC : production-vpc-v2
  AZs :
    [x] eu-west-3a (Public-A)
    [x] eu-west-3b (Public-B)
    [x] eu-west-3c (Public-C)

Security groups : ALB-SG-Production

Listeners :
  1. HTTP:80
     Default action : Redirect to HTTPS
     
  2. HTTPS:443
     Default action : Forward to flask-todo-production-tg-blue
     SSL certificate : app.tondomaine.com (ACM)
     Security policy : ELBSecurityPolicy-TLS13-1-2-2021-06

Attributes :
  Idle timeout : 60 seconds
  Drop invalid headers : Enable
  HTTP/2 : Enable
  Cross-zone load balancing : Enable
  Deletion protection : Enable
  Access logs : Enable
    S3 bucket : flask-todo-production-logs/alb/
```

---

#### Étape 3.6 : Créer l'Auto Scaling Group (Production)

```
Name : flask-todo-production-asg

Launch template : flask-todo-production-lt (Latest version)

VPC : production-vpc-v2
Subnets :
  [x] Private-App-A
  [x] Private-App-B
  [x] Private-App-C

Load balancing :
  [x] Attach to existing load balancer
  Target group : flask-todo-production-tg-blue

Health checks :
  [x] ELB
  Health check grace period : 300 seconds

Group size :
  Desired : 6 (2 par AZ)
  Minimum : 3 (1 par AZ)
  Maximum : 18 (6 par AZ)

Scaling policies :
  1. Target Tracking - CPU 70%
     Instance warmup : 300 seconds
     
  2. Target Tracking - ALB Request Count
     Target value : 1000 requests per target
     
  3. Scheduled Action - Business Hours
     Monday-Friday 08:00 UTC :
       Min : 6, Desired : 9, Max : 24
     Monday-Friday 18:00 UTC :
       Min : 3, Desired : 6, Max : 18

Instance purchase options :
  On-Demand base : 3
  On-Demand % above base : 33%
  Spot allocation : Capacity optimized
  Instance types : t3.micro, t3a.micro, t2.micro

Notifications : SNS topic (production-alerts)

Tags (propagated) :
  Name : FlaskTODO-Production-Instance
  Environment : Production
  ManagedBy : ASG
```

---

**[OK] Phase 3 terminée : Compute layer prêt**

**Durée : 3 heures**

---

Je continue avec les phases 4-7 (CDN, CI/CD, Monitoring, Security) dans le prochain message. Veux-tu que je continue ? [RAPIDE]

# [ROUGE] EXERCICE 10 : ARCHITECTURE COMPLÈTE (SUITE)

### Phase 4 : CDN & DNS (Jour 3 - 2h)

**Objectif : CloudFront + Route 53 + WAF**

#### Étape 4.1 : Créer les buckets S3

**Bucket 1 : Static Assets**

Console S3 -> Create bucket

```
Bucket name : flask-todo-production-assets-123456
Region : eu-west-3

Block Public Access : Uncheck (CloudFront aura accès)

Bucket Versioning : Enable

Server-side encryption : Enable (SSE-S3)

Lifecycle rules :
  Name : Archive old versions
  Scope : All objects
  Transitions :
    - After 90 days -> Glacier Flexible Retrieval
    - After 180 days -> Glacier Deep Archive
  Expiration :
    - Delete noncurrent versions after 365 days

Tags :
  Environment : Production
  Type : Static Assets
```

---

**Bucket 2 : Application Logs**

```
Bucket name : flask-todo-production-logs-123456
Region : eu-west-3

Block Public Access : [OK] (logs privés)

Lifecycle rules :
  Name : Intelligent tiering and deletion
  Transitions :
    - After 30 days -> Intelligent-Tiering
  Expiration :
    - Delete objects after 180 days

Object Lock : Enable (compliance)
  Retention mode : Compliance
  Default retention : 90 days
```

---

**Bucket 3 : Backups**

```
Bucket name : flask-todo-production-backups-123456
Region : eu-west-3

Block Public Access : [OK]

Versioning : Enable

Cross-Region Replication :
  Destination bucket : flask-todo-dr-backups-123456 (eu-west-1)
  IAM role : Create new (auto)
  Replicate : All objects
  Replication time control : Enable

Lifecycle rules :
  Name : Move to Glacier
  Transitions :
    - After 30 days -> Glacier Flexible Retrieval
    - After 90 days -> Glacier Deep Archive
  Expiration :
    - Never (compliance requirement)

MFA Delete : Enable (protection suppression accidentelle)
```

---

#### Étape 4.2 : Créer une distribution CloudFront

Console CloudFront -> Create distribution

```
Origin domain :
  flask-todo-production-alb-123456.eu-west-3.elb.amazonaws.com

Origin path : (vide)

Name : ALB-Production-Origin

Protocol : HTTPS only

Minimum origin SSL protocol : TLSv1.2

Origin shield : Enable
  Origin Shield Region : eu-west-3
  (Cache supplémentaire, réduit load sur origin)

Additional settings :
  Connection attempts : 3
  Connection timeout : 10 seconds
  Response timeout : 60 seconds
  Keep-alive timeout : 5 seconds

Add custom header :
  X-CloudFront-Secret : generate-random-secret-123456
  (Pour que ALB accepte seulement CloudFront)
```

---

**Default cache behavior :**

```
Path pattern : Default (*)

Compress objects automatically : Yes

Viewer protocol policy : Redirect HTTP to HTTPS

Allowed HTTP methods : GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE

Cache key and origin requests :
  Cache policy : CachingOptimized
  Origin request policy : AllViewer

Response headers policy : 
  SecurityHeadersPolicy (CORS + Security headers)

Function associations :
  Viewer request : (optionnel) Add security headers
```

---

**Add additional origins :**

**Origin 2 : S3 Static Assets**

```
Origin domain : flask-todo-production-assets-123456.s3.eu-west-3.amazonaws.com
Origin access : Origin access control (OAC)
  Create new OAC

Name : S3-Static-Assets-Origin
```

---

**Add behavior for static assets :**

```
Path pattern : /static/*

Origin : S3-Static-Assets-Origin

Viewer protocol policy : Redirect HTTP to HTTPS

Cache policy : CachingOptimized
  TTL : Min 1 day, Max 365 days, Default 1 day

Compress : Yes
```

---

**Settings :**

```
Price class : Use all edge locations

Alternate domain name (CNAME) :
  - app.tondomaine.com
  - www.tondomaine.com

Custom SSL certificate : *.tondomaine.com (ACM)
  [ATTENTION] Certificate doit être dans us-east-1 !

Supported HTTP versions : HTTP/2 and HTTP/3

Standard logging : On
  S3 bucket : flask-todo-production-logs-123456
  Log prefix : cloudfront/

IPv6 : On

Default root object : (vide, géré par ALB)

WAF : Enable (on va créer après)
```

---

**Create distribution**

**[HOURGLASS_WITH_FLOWING_SAND] Durée déploiement : 15-20 minutes**

**Une fois déployée :**

**Domain name : d1234567890abc.cloudfront.net**

---

#### Étape 4.3 : Configurer AWS WAF

Console WAF -> Web ACLs -> Create web ACL

```
Name : flask-todo-production-waf
CloudWatch metric name : FlaskTODOWAF
Resource type : CloudFront
Region : Global (CloudFront est global)

Associated AWS resources : (on ajoutera après)
```

---

**Add rules :**

**Rule 1 : AWS Managed Rules - Core Rule Set**

```
Name : AWS-AWSManagedRulesCommonRuleSet
Type : Managed rule group
Managed rule group : Core rule set (CRS)
  Protège contre : OWASP Top 10, injections, XSS, etc.
Priority : 1
```

---

**Rule 2 : AWS Managed Rules - Known Bad Inputs**

```
Name : AWS-AWSManagedRulesKnownBadInputsRuleSet
Type : Managed rule group
Managed rule group : Known bad inputs
Priority : 2
```

---

**Rule 3 : Rate Limiting**

```
Name : RateLimitRule
Type : Rate-based rule
Rate limit : 2000 requests per 5 minutes
Scope : IP address
Action : Block
Priority : 3

Raison :
2000 req/5min = 400 req/min par IP
-> Usage normal : ~10-50 req/min
-> Attaque DDoS bloquée
```

---

**Rule 4 : Geo-Blocking (optionnel)**

```
Name : GeoBlockRule
Type : Regular rule
Statement :
  Originates from a country in : [CN, RU, KP, ...]
  (Pays avec beaucoup de bots)
Action : Block
Priority : 4
```

---

**Rule 5 : SQL Injection Protection**

```
Name : AWS-AWSManagedRulesSQLiRuleSet
Type : Managed rule group
Managed rule group : SQL database
Priority : 5
```

---

**Default action : Allow**

**CloudWatch metrics : Enable all**

**Logging : Enable**

```
Logging destination : S3
S3 bucket : flask-todo-production-logs-123456
Log prefix : waf/
```

---

**Create web ACL**

---

**Associer à CloudFront :**

**WAF -> Web ACLs -> flask-todo-production-waf -> Associated AWS resources -> Add AWS resources**

```
Resource type : CloudFront distribution
Resource : d1234567890abc.cloudfront.net
```

**Add**

**[HOURGLASS_WITH_FLOWING_SAND] Propagation : 5-10 minutes**

---

#### Étape 4.4 : Mettre à jour ALB Security Group

**Maintenant que CloudFront est devant, sécuriser ALB :**

**EC2 -> Security Groups -> ALB-SG-Production -> Edit inbound rules**

**Supprimer les règles "0.0.0.0/0"**

**Ajouter :**

```
Type : HTTP
Port : 80
Source : CloudFront Managed Prefix List (com.amazonaws.global.cloudfront.origin-facing)
Description : CloudFront only

Type : HTTPS
Port : 443
Source : CloudFront Managed Prefix List
Description : CloudFront only
```

**[ATTENTION] Alternative : Vérifier le header custom**

**Dans ALB, ajouter une règle listener :**

```
IF Request header X-CloudFront-Secret != "generate-random-secret-123456"
THEN Return fixed response 403 Forbidden
```

---

#### Étape 4.5 : Configurer Route 53

**Console Route 53 -> Hosted zones -> tondomaine.com (si tu as un domaine)**

**Create record**

---

**Record 1 : app.tondomaine.com**

```
Record name : app
Record type : A
Alias : Yes
Route traffic to :
  Alias to CloudFront distribution
  Distribution : d1234567890abc.cloudfront.net

Routing policy : Simple
Evaluate target health : No
```

**Create records**

---

**Record 2 : www.tondomaine.com**

```
Record name : www
Record type : A
Alias : Yes
Route traffic to :
  Alias to CloudFront distribution
  Distribution : d1234567890abc.cloudfront.net
```

---

**[ATTENTION] Si tu n'as pas de domaine :**

**Utiliser directement l'URL CloudFront :**

```
https://d1234567890abc.cloudfront.net
```

---

**Tester l'application :**

```
https://app.tondomaine.com
```

**[OK] Application accessible via CloudFront + WAF + HTTPS !**

---

**[OK] Phase 4 terminée : CDN et DNS configurés**

**Durée : 2 heures**

---

### Phase 5 : CI/CD Avancé (Jour 3 - 2h)

**Objectif : Pipeline complet avec staging + tests + sécurité**

#### Étape 5.1 : Créer un environnement Staging

**On va créer un mini-environnement staging pour tester avant prod.**

---

**Créer un ASG Staging :**

```
Name : flask-todo-staging-asg
Launch template : flask-todo-production-lt (même)
VPC : production-vpc-v2
Subnets : Private-App-A seulement (1 AZ suffit pour staging)

Group size :
  Desired : 1
  Min : 1
  Max : 2

Load balancing :
  Target group : flask-todo-staging-tg (créer)

Health checks : ELB
```

---

**Créer Target Group Staging :**

```
Name : flask-todo-staging-tg
(Même config que production)
```

---

**Créer Listener Rule dans ALB :**

**ALB -> Listeners -> HTTPS:443 -> Add rules**

```
Priority : 1 (avant default)

Conditions :
  Host header : staging.tondomaine.com

Actions :
  Forward to : flask-todo-staging-tg
```

---

**Route 53 record staging :**

```
Record name : staging
Type : A (Alias to CloudFront)
```

---

**Maintenant on a :**

```
staging.tondomaine.com -> Staging environment (1 instance)
app.tondomaine.com -> Production environment (6 instances)
```

---

#### Étape 5.2 : Améliorer le buildspec.yml

**Mettre à jour `buildspec.yml` dans le repo :**

```yaml
version: 0.2

env:
  variables:
    PYTHON_VERSION: "3.11"
    
  parameter-store:
    SONAR_TOKEN: /production/sonarqube/token (optionnel)
    SNYK_TOKEN: /production/snyk/token (optionnel)

phases:
  install:
    runtime-versions:
      python: 3.11
    commands:
      - echo "===== Install Phase ====="
      - pip install --upgrade pip
      
  pre_build:
    commands:
      - echo "===== Pre-Build Phase ====="
      
      # Installer les dépendances
      - pip install -r requirements.txt
      
      # Installer les outils de test
      - pip install pytest pytest-cov pytest-xdist
      - pip install flake8 black isort bandit safety
      
      # Code formatting check
      - echo "Checking code formatting..."
      - black --check application.py tests/
      - isort --check-only application.py tests/
      
      # Linting
      - echo "Running linting..."
      - flake8 application.py --max-line-length=120 --exclude=venv,__pycache__
      
      # Security scan - Dependencies
      - echo "Scanning dependencies for vulnerabilities..."
      - safety check --json || true
      
      # Security scan - Code
      - echo "Running Bandit security scan..."
      - bandit -r application.py -f json -o bandit-report.json || true
      
      # Unit tests with coverage
      - echo "Running unit tests with coverage..."
      - pytest tests/ -v --cov=application --cov-report=xml --cov-report=term --junitxml=test-results.xml
      
      # Coverage threshold
      - |
        COVERAGE=$(coverage report | grep TOTAL | awk '{print $4}' | sed 's/%//')
        echo "Code coverage: ${COVERAGE}%"
        if (( $(echo "$COVERAGE < 70" | bc -l) )); then
          echo "ERROR: Coverage ${COVERAGE}% is below threshold 70%"
          exit 1
        fi
      
  build:
    commands:
      - echo "===== Build Phase ====="
      - echo "Build completed on $(date)"
      - echo "Commit: $CODEBUILD_RESOLVED_SOURCE_VERSION"
      
      # Créer un fichier de version
      - echo $CODEBUILD_RESOLVED_SOURCE_VERSION > VERSION
      
  post_build:
    commands:
      - echo "===== Post-Build Phase ====="
      
      # Upload coverage to CodeCov (optionnel)
      # - bash <(curl -s https://codecov.io/bash)
      
      - echo "Build completed successfully!"

artifacts:
  files:
    - '**/*'
  name: flask-app-$(date +%Y%m%d-%H%M%S)

reports:
  pytest_reports:
    files:
      - test-results.xml
    file-format: JUNITXML
    
  coverage_reports:
    files:
      - coverage.xml
    file-format: COBERTURAXML

cache:
  paths:
    - '/root/.cache/pip/**/*'
```

---

#### Étape 5.3 : Créer un pipeline complet avec staging

**CodePipeline -> Create pipeline**

```
Pipeline name : flask-todo-production-pipeline-v2
```

---

**Stages :**

**Stage 1 : Source (GitHub)**

```
Source provider : GitHub (Version 2)
Repository : flask-app-cicd
Branch : main
Trigger : Webhook (on push)
```

---

**Stage 2 : Build (CodeBuild)**

```
Project : flask-app-build
Input : SourceArtifact
Output : BuildArtifact

Environment variables override :
  ENVIRONMENT : staging
```

---

**Stage 3 : Deploy to Staging**

```
Deploy provider : CodeDeploy
Application : flask-app-deploy
Deployment group : flask-app-staging (créer)
Input : BuildArtifact
```

---

**Stage 4 : Integration Tests**

```
Action provider : CodeBuild
Project : flask-app-integration-tests (créer)

buildspec-integration.yml :
```

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.11
      
  pre_build:
    commands:
      - pip install requests pytest
      
  build:
    commands:
      - echo "Running integration tests against staging..."
      - |
        cat > test_staging.py << 'EOF'
        import requests
        import pytest
        
        BASE_URL = "https://staging.tondomaine.com"
        
        def test_health():
            r = requests.get(f"{BASE_URL}/health")
            assert r.status_code == 200
            assert r.json()["status"] == "healthy"
            
        def test_home():
            r = requests.get(BASE_URL)
            assert r.status_code == 200
            assert "TODO" in r.text
            
        def test_api_list():
            r = requests.get(f"{BASE_URL}/api/tasks")
            assert r.status_code == 200
            assert "tasks" in r.json()
            
        def test_api_create():
            r = requests.post(
                f"{BASE_URL}/api/tasks",
                json={"title": "Integration test"}
            )
            assert r.status_code == 201
            assert r.json()["title"] == "Integration test"
        EOF
      
      - pytest test_staging.py -v
```

---

**Stage 5 : Manual Approval**

```
Action provider : Manual approval
SNS topic : production-approvals
URL for review : https://staging.tondomaine.com
Comments :
  Please review the staging deployment:
  - Functional tests passed
  - Security scans completed
  - Ready for production?
```

---

**Stage 6 : Deploy to Production (Blue/Green)**

```
Deploy provider : CodeDeploy
Application : flask-app-deploy
Deployment group : flask-app-production
Input : BuildArtifact

Deployment config : CodeDeployDefault.AllAtOnce
```

---

**Stage 7 : Post-Deployment Smoke Tests**

```
Action provider : CodeBuild
Project : flask-app-smoke-tests (créer)

buildspec-smoke.yml :
```

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.11
  pre_build:
    commands:
      - pip install requests
  build:
    commands:
      - echo "Running smoke tests against production..."
      - |
        python << 'EOF'
        import requests
        import sys
        
        PROD_URL = "https://app.tondomaine.com"
        
        tests = [
            ("Health check", f"{PROD_URL}/health"),
            ("Home page", PROD_URL),
            ("API list", f"{PROD_URL}/api/tasks"),
        ]
        
        failed = 0
        for name, url in tests:
            try:
                r = requests.get(url, timeout=10)
                if r.status_code == 200:
                    print(f"[OK] {name} : OK")
                else:
                    print(f"[X] {name} : FAILED ({r.status_code})")
                    failed += 1
            except Exception as e:
                print(f"[X] {name} : ERROR ({e})")
                failed += 1
        
        if failed > 0:
            sys.exit(1)
        print(f"All {len(tests)} smoke tests passed!")
        EOF
```

---

**Create pipeline**

---

**[OK] Pipeline complet créé !**

**Workflow :**

```
1. Git push
2. Build + Unit tests (3 min)
3. Deploy to Staging (15 min)
4. Integration tests (2 min)
5. [DOUBLE_VERTICAL_BAR] Manual approval
6. Deploy to Production (Blue/Green, 20 min)
7. Smoke tests (1 min)

Total : ~41 minutes (dont 5-10 min approbation)
```

---

**[OK] Phase 5 terminée : CI/CD avancé opérationnel**

**Durée : 2 heures**

---

### Phase 6 : Monitoring & Observability (Jour 4 - 3h)

**Objectif : Monitoring complet + alertes proactives**

#### Étape 6.1 : Créer des CloudWatch Dashboards

Console CloudWatch -> Dashboards -> Create dashboard

---

**Dashboard 1 : Infrastructure Overview**

```
Dashboard name : Production-Infrastructure

Widgets :
```

**Widget 1 : Instances métriques (Line graph)**

```
Metrics :
- EC2 > By Auto Scaling Group > CPUUtilization (flask-todo-production-asg)
- EC2 > By Auto Scaling Group > NetworkIn
- EC2 > By Auto Scaling Group > NetworkOut

Statistic : Average
Period : 5 minutes
```

---

**Widget 2 : ALB métriques (Line graph)**

```
Metrics :
- ApplicationELB > Per AppELB Metrics > RequestCount (sum)
- ApplicationELB > TargetResponseTime (average)
- ApplicationELB > HTTPCode_Target_2XX_Count (sum)
- ApplicationELB > HTTPCode_Target_4XX_Count (sum)
- ApplicationELB > HTTPCode_Target_5XX_Count (sum)

Period : 1 minute
```

---

**Widget 3 : ASG Instance Count (Number)**

```
Metrics :
- AutoScaling > GroupDesiredCapacity
- AutoScaling > GroupInServiceInstances
- AutoScaling > GroupMinSize
- AutoScaling > GroupMaxSize

Statistic : Maximum
```

---

**Widget 4 : RDS métriques (Line graph)**

```
Metrics :
- RDS > DatabaseConnections
- RDS > CPUUtilization
- RDS > FreeableMemory
- RDS > ReadLatency
- RDS > WriteLatency

Statistic : Average
Period : 1 minute
```

---

**Widget 5 : ElastiCache métriques (Line graph)**

```
Metrics :
- ElastiCache > CacheHitRate
- ElastiCache > CurrConnections
- ElastiCache > EngineCPUUtilization
- ElastiCache > NetworkBytesIn
- ElastiCache > NetworkBytesOut

Period : 1 minute
```

---

**Widget 6 : CloudFront métriques (Line graph)**

```
Metrics :
- CloudFront > Requests
- CloudFront > BytesDownloaded
- CloudFront > 4xxErrorRate
- CloudFront > 5xxErrorRate

Period : 5 minutes
```

---

**Widget 7 : Logs Insights (Query)**

```
Log groups : /aws/ec2/flask-app

Query :
fields @timestamp, @message
| filter @message like /ERROR/
| stats count() by bin(5m)
| sort @timestamp desc
| limit 20
```

---

**Save dashboard**

---

**Dashboard 2 : Application Performance**

```
Dashboard name : Production-Application-Performance

Widgets :
```

**Widget 1 : Request latency percentiles**

```
Metrics :
- ApplicationELB > TargetResponseTime
  - p50
  - p90
  - p95
  - p99

Period : 1 minute
```

---

**Widget 2 : Throughput**

```
Metrics :
- ApplicationELB > RequestCount (sum per minute)

Math expression : m1 / 60 (requests per second)
```

---

**Widget 3 : Error rate**

```
Metrics :
- ALB 5xx count
- ALB 4xx count

Math expression :
(5xx + 4xx) / total_requests * 100 (error percentage)
```

---

**Widget 4 : Database query performance**

```
Metrics :
- RDS > ReadLatency (p99)
- RDS > WriteLatency (p99)
- RDS > DatabaseConnections

Period : 1 minute
```

---

**Widget 5 : Cache performance**

```
Metrics :
- ElastiCache > CacheHitRate
- ElastiCache > CacheMissRate

Period : 1 minute
```

---

**Dashboard 3 : Cost Analytics**

```
Dashboard name : Production-Costs

Use AWS Cost Explorer embeds
```

---

#### Étape 6.2 : Créer des alarmes CloudWatch stratégiques

**Alarmes CRITIQUES (PagerDuty/Phone) :**

---

**Alarme 1 : RDS CPU élevé**

```
Name : Production-RDS-CPU-Critical
Metric : RDS > CPUUtilization
Statistic : Average
Period : 5 minutes
Threshold : >= 90%
Datapoints : 2 out of 2

Actions :
  ALARM : SNS topic "production-critical-alerts"
  OK : SNS topic "production-ok-notifications"

Treat missing data : notBreaching
```

---

**Alarme 2 : ALB 5xx errors élevés**

```
Name : Production-ALB-5xx-Critical
Metric : ALB > HTTPCode_Target_5XX_Count
Statistic : Sum
Period : 1 minute
Threshold : >= 10 (10 erreurs/minute)
Datapoints : 2 out of 2

Actions : SNS "production-critical-alerts"
```

---

**Alarme 3 : Disk space faible**

```
Name : Production-EC2-DiskSpace-Critical
Metric : CWAgent > DiskUtilization
Statistic : Maximum
Period : 5 minutes
Threshold : >= 85%
Datapoints : 1 out of 1

Actions : SNS "production-critical-alerts"
```

---

**Alarme 4 : RDS Storage faible**

```
Name : Production-RDS-Storage-Critical
Metric : RDS > FreeStorageSpace
Statistic : Average
Period : 5 minutes
Threshold : <= 10 GB
Datapoints : 1 out of 1

Actions : SNS "production-critical-alerts"
```

---

**Alarmes WARNING (Email/Slack) :**

---

**Alarme 5 : RDS CPU élevé (warning)**

```
Name : Production-RDS-CPU-Warning
Threshold : >= 70%
Actions : SNS "production-warnings"
```

---

**Alarme 6 : ALB Latency élevée**

```
Name : Production-ALB-Latency-Warning
Metric : ALB > TargetResponseTime
Statistic : Average
Period : 5 minutes
Threshold : >= 2 seconds
Datapoints : 3 out of 3

Actions : SNS "production-warnings"
```

---

**Alarme 7 : ASG à capacité max**

```
Name : Production-ASG-MaxCapacity-Warning
Metric : ASG > GroupDesiredCapacity
Statistic : Maximum
Period : 1 minute
Threshold : >= 18 (max capacity)
Datapoints : 1 out of 1

Actions : SNS "production-warnings"
```

---

**Alarme 8 : Cache Hit Rate faible**

```
Name : Production-Cache-HitRate-Warning
Metric : ElastiCache > CacheHitRate
Statistic : Average
Period : 5 minutes
Threshold : <= 70%
Datapoints : 3 out of 3

Actions : SNS "production-warnings"
```

---

**Alarmes INFO (Notifications légères) :**

---

**Alarme 9 : Scaling events**

```
Name : Production-ASG-ScalingEvent-Info
Metric : ASG > GroupDesiredCapacity
Change detection : Any change

Actions : SNS "production-info"
```

---

#### Étape 6.3 : Configurer SNS Topics pour notifications

**Topic 1 : Critical Alerts**

```
Name : production-critical-alerts

Subscriptions :
  - Protocol : Email
    Endpoint : oncall@company.com
    
  - Protocol : SMS
    Endpoint : +33612345678
    
  - Protocol : HTTPS
    Endpoint : https://events.pagerduty.com/... (webhook)
```

---

**Topic 2 : Warnings**

```
Name : production-warnings

Subscriptions :
  - Protocol : Email
    Endpoint : devops-team@company.com
    
  - Protocol : HTTPS
    Endpoint : https://hooks.slack.com/... (Slack webhook)
```

---

**Topic 3 : Info**

```
Name : production-info

Subscriptions :
  - Protocol : HTTPS
    Endpoint : https://hooks.slack.com/... (channel #deployments)
```

---

#### Étape 6.4 : Activer AWS X-Ray (Distributed Tracing)

**Pour tracer les requêtes de bout en bout.**

**Console X-Ray -> Getting Started**

---

**Installer le daemon X-Ray sur EC2 :**

**Ajouter au user data du Launch Template :**

```bash
# Installer X-Ray daemon
curl https://s3.amazonaws.com/aws-xray-assets.us-east-1/xray-daemon/aws-xray-daemon-3.x.deb -o /tmp/xray.deb
sudo dpkg -i /tmp/xray.deb
sudo systemctl enable xray
sudo systemctl start xray
```

---

**Instrumenter l'application Flask :**

**Ajouter dans `requirements.txt` :**

```
aws-xray-sdk==2.12.0
```

---

**Modifier `application.py` :**

```python
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.ext.flask.middleware import XRayMiddleware

# ...

application = Flask(__name__)

# Configurer X-Ray
xray_recorder.configure(service='Flask-TODO-Production')
XRayMiddleware(application, xray_recorder)

# ... (reste du code)
```

---

**Redéployer l'application**

---

**Console X-Ray -> Service Map**

**Tu verras :**

```
CloudFront -> ALB -> Flask App -> RDS
                            -> ElastiCache
```

**Cliquer sur un segment pour voir les détails, latences, erreurs.**

---

#### Étape 6.5 : Configurer Container Insights (pour métriques avancées)

**Console CloudWatch -> Settings -> Container Insights**

**Install Container Insights for EC2 :**

```
Cluster : flask-todo-production-asg

Installation method : CloudWatch agent
```

**Suivre les instructions pour installer l'agent sur les instances**

---

**Métriques supplémentaires disponibles :**

```
- Process-level CPU/Memory
- Disk I/O per process
- Network connections
- TCP connections
- File descriptors
```

---

**[OK] Phase 6 terminée : Observabilité complète**

**Durée : 3 heures**

---

### Phase 7 : Security & Compliance (Jour 4 - 2h)

**Objectif : Auditing, compliance, détection menaces**

#### Étape 7.1 : Activer AWS CloudTrail

Console CloudTrail -> Trails -> Create trail

```
Trail name : production-audit-trail

Storage location :
  Create new S3 bucket : flask-todo-production-cloudtrail-123456
  
Log file SSE-KMS encryption : Enable
  New KMS key : cloudtrail-encryption-key

Log file validation : Enable
  (Détecte si logs modifiés)

CloudWatch Logs : Enable
  Log group : /aws/cloudtrail/production
  IAM role : Create new

Management events :
  Read : [OK]
  Write : [OK]

Data events :
  S3 :
    [x] All current and future S3 buckets
    Read : [OK]
    Write : [OK]

Insights events :
  [x] API call rate
  [x] API error rate

Tags :
  Environment : Production
  Compliance : SOC2
```

**Create trail**

---

**CloudTrail enregistre maintenant tous les appels API AWS.**

**Use case :**
- Qui a modifié le Security Group ?
- Qui a supprimé une instance ?
- Audit de conformité
- Investigation post-incident

---

#### Étape 7.2 : Activer AWS Config

Console AWS Config -> Get started

```
Resource types to record :
  [x] All resources

Configuration recorder :
  Name : production-config-recorder
  IAM role : Create new

Delivery method :
  S3 bucket : flask-todo-production-config-123456
  SNS topic : production-config-changes

Rules :
  Add managed rules :
    - ec2-instance-managed-by-ssm (SSM agent installé ?)
    - encrypted-volumes (Volumes EBS chiffrés ?)
    - rds-multi-az-support (RDS Multi-AZ ?)
    - s3-bucket-public-read-prohibited (S3 pas public ?)
    - vpc-flow-logs-enabled (VPC Flow Logs actifs ?)
    - cloudtrail-enabled (CloudTrail actif ?)
    - iam-password-policy (Politique mot de passe IAM ?)
    - root-account-mfa-enabled (MFA sur root ?)
```

**Confirm**

---

**Config évalue maintenant la conformité continue.**

**Dashboard Config -> Compliance**

**Tu vois :**

```
Resources : 156
Compliant : 142
Non-compliant : 14
```

**Cliquer sur "Non-compliant" pour voir les ressources problématiques.**

---

#### Étape 7.3 : Activer AWS GuardDuty (Détection menaces)

Console GuardDuty -> Get Started

```
Enable GuardDuty
```

**C'est tout ! GuardDuty analyse automatiquement :**
- VPC Flow Logs
- CloudTrail logs
- DNS logs

**Détecte :**
- Instances compromises
- Reconnaissance réseau
- Accès non autorisés
- Bitcoin mining
- Comportements anormaux

---

**Configurer les notifications :**

**GuardDuty -> Settings -> Findings**

**CloudWatch Events rule :**

```
Event pattern :
{
  "source": ["aws.guardduty"],
  "detail-type": ["GuardDuty Finding"],
  "detail": {
    "severity": [7, 8, 9]  // High severity only
  }
}

Target : SNS topic "production-security-alerts"
```

---

**Email immédiat si menace détectée !**

---

#### Étape 7.4 : Activer AWS Security Hub

Console Security Hub -> Enable Security Hub

```
Security standards :
  [x] AWS Foundational Security Best Practices
  [x] CIS AWS Foundations Benchmark
  [x] PCI DSS (si applicable)

Integrations :
  [x] GuardDuty
  [x] AWS Config
  [x] IAM Access Analyzer
  [x] Inspector
  [x] Macie (optionnel, scan S3 pour données sensibles)
```

**Enable Security Hub**

---

**Security Hub centralise toutes les alertes de sécurité.**

**Dashboard -> Security score**

```
Overall security score : 72/100

Critical findings : 2
High : 8
Medium : 24
Low : 45
```

**Corriger les findings critiques/high en priorité.**

---

#### Étape 7.5 : Configurer AWS Secrets Manager rotation

**Console Secrets Manager -> production/db/postgres**

**Edit rotation**

```
Enable automatic rotation : [OK]

Rotation schedule :
  Days : 30
  Window : 8 hours

Rotation function :
  Create new Lambda function
  Function name : SecretsManagerRDSPostgreSQLRotation
```

**Save**

---

**Maintenant le mot de passe RDS change automatiquement chaque mois !**

**Lambda function gère :**
1. Créer nouveau mot de passe
2. Tester connexion
3. Mettre à jour RDS
4. Vérifier que tout fonctionne

---

#### Étape 7.6 : Activer MFA Delete sur S3 backups

**Protection contre suppression accidentelle/malveillante.**

**[ATTENTION] Nécessite AWS CLI avec root account**

```bash
# Activer versioning (si pas déjà fait)
aws s3api put-bucket-versioning \
  --bucket flask-todo-production-backups-123456 \
  --versioning-configuration Status=Enabled,MFADelete=Enabled \
  --mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device XXXXXX"
```

**Maintenant, pour supprimer une version :**

```bash
# Nécessite MFA token
aws s3api delete-object \
  --bucket flask-todo-production-backups-123456 \
  --key backup.sql \
  --version-id VERSION_ID \
  --mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device YYYYYY"
```

**Protection ultime contre ransomware/erreur humaine.**

---

**[OK] Phase 7 terminée : Sécurité et conformité renforcées**

**Durée : 2 heures**

---

Je vais maintenant créer la phase finale (8) avec les optimisations, le récapitulatif complet, les coûts et les best practices dans le prochain message. Veux-tu que je continue ? [RAPIDE]


# [ROUGE] EXERCICE 10 : ARCHITECTURE COMPLÈTE (SUITE FINALE)

### Phase 8 : Optimisations & Best Practices (Jour 5 - 3h)

**Objectif : Optimiser coûts, performances, et résilience**

#### Étape 8.1 : Optimisations des coûts

**Cost Optimization 1 : Reserved Instances**

Console EC2 -> Reserved Instances -> Purchase Reserved Instances

```
Instance type : t3.micro
Platform : Linux/UNIX
Tenancy : Default
Offering class : Standard
Term : 1 year
Payment option : All Upfront

Quantity : 3 (base instances)

Pricing :
On-Demand : 3 × $0.0104/h × 8760h = $273/an
Reserved (1yr all upfront) : 3 × $60 = $180/an
Économies : $93/an (34%)
```

---

**Cost Optimization 2 : Savings Plans**

Console Cost Management -> Savings Plans -> Purchase

```
Type : Compute Savings Plans
Term : 1 year
Payment option : All Upfront
Hourly commitment : $0.02/hour ($175/an)

Savings : 20-30% sur compute (EC2, Lambda, Fargate)
```

---

**Cost Optimization 3 : S3 Intelligent-Tiering**

**Modifier les buckets S3 :**

S3 -> flask-todo-production-assets-123456 -> Management -> Create lifecycle rule

```
Rule name : Intelligent-Tiering

Transitions :
  Day 0 : Move to Intelligent-Tiering

Intelligent-Tiering archive :
  After 90 days no access : Archive Access tier
  After 180 days no access : Deep Archive Access tier

Économies :
Standard : $0.023/GB/month
Intelligent-Tiering : $0.0125/GB/month (accès rare)
Deep Archive : $0.00099/GB/month

100 GB -> Économies : ~$15/mois
```

---

**Cost Optimization 4 : NAT Gateway -> NAT Instance (optionnel)**

**[ATTENTION] Trade-off : Coût vs Simplicité**

**NAT Gateway : $98.55/mois (3 NAT)**

**Alternative NAT Instance :**

```
Instance type : t3.nano
Coût : 3 × $0.0052/h × 730h = $11.40/mois

Économies : $87/mois

Inconvénients :
- Tu gères les instances
- Moins de bande passante
- Moins de HA native

Recommandation production : Garder NAT Gateway
Recommandation dev/staging : NAT Instance
```

---

**Cost Optimization 5 : RDS Reserved Instances**

```
RDS Reserved Instance (1 year, all upfront) :
db.t3.small Multi-AZ : $380/an vs $720/an On-Demand
Économies : $340/an (47%)
```

---

**Cost Optimization 6 : CloudFront savings bundle**

Console CloudFront -> Security savings bundle

```
Commitment : 100 TB/month transfer
Term : 1 year
Discount : 30% sur data transfer
```

---

**Résumé des optimisations :**

```
Avant optimisations : ~$500/mois
Après optimisations : ~$320/mois
Économies : $180/mois (36%)
```

---

#### Étape 8.2 : Optimisations des performances

**Performance Optimization 1 : ElastiCache pour session store**

**Modifier l'application Flask pour utiliser Redis :**

```python
from flask import Flask, session
from flask_session import Session
import redis

application = Flask(__name__)

# Configuration session Redis
application.config['SESSION_TYPE'] = 'redis'
application.config['SESSION_REDIS'] = redis.from_url(
    os.environ.get('REDIS_URL')
)
application.config['SESSION_PERMANENT'] = False
application.config['SESSION_USE_SIGNER'] = True

Session(application)

# Maintenant les sessions sont dans Redis (partagées entre instances)
```

**Avantages :**
- Sessions persistentes entre instances
- Sticky sessions ALB pas nécessaires
- Meilleure distribution de charge
- Performance améliorée

---

**Performance Optimization 2 : Query caching**

```python
from functools import wraps
import json

def cache_query(timeout=300):
    """Cache decorator pour queries DB"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            # Créer une clé de cache
            cache_key = f"query:{f.__name__}:{json.dumps(args)}:{json.dumps(kwargs)}"
            
            # Vérifier le cache
            cached_result = redis_client.get(cache_key)
            if cached_result:
                return json.loads(cached_result)
            
            # Exécuter la query
            result = f(*args, **kwargs)
            
            # Mettre en cache
            redis_client.setex(
                cache_key,
                timeout,
                json.dumps(result)
            )
            
            return result
        return decorated_function
    return decorator

# Utilisation
@application.route('/api/tasks')
@cache_query(timeout=60)  # Cache 60 secondes
def list_tasks():
    tasks = Task.query.all()
    return jsonify([t.to_dict() for t in tasks])
```

**Résultats :**
- Latence DB : 50ms -> 2ms (cache hit)
- Load DB réduite de 80%
- Throughput augmenté ×5

---

**Performance Optimization 3 : Database indexes**

**SSH vers bastion -> psql vers RDS :**

```sql
-- Analyser les requêtes lentes
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;

-- Créer des indexes stratégiques
CREATE INDEX idx_task_created_at ON task(created_at DESC);
CREATE INDEX idx_task_completed ON task(completed);
CREATE INDEX idx_task_user_id ON task(user_id);  -- Si multi-user

-- Analyser les plans d'exécution
EXPLAIN ANALYZE SELECT * FROM task WHERE completed = false ORDER BY created_at DESC;
```

**Résultats :**
- Query time : 120ms -> 8ms
- Index scan au lieu de sequential scan

---

**Performance Optimization 4 : RDS Read Replica (si charge lecture élevée)**

Console RDS -> flask-todo-production-db -> Actions -> Create read replica

```
Read replica identifier : flask-todo-production-db-read-1
Region : Same region
Instance class : db.t3.small
Multi-AZ : No (read replica déjà HA via primary)
```

**Modifier l'application :**

```python
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# Write DB (primary)
write_engine = create_engine(os.environ.get('DATABASE_WRITE_URL'))
WriteSession = sessionmaker(bind=write_engine)

# Read DB (replica)
read_engine = create_engine(os.environ.get('DATABASE_READ_URL'))
ReadSession = sessionmaker(bind=read_engine)

# Utilisation
@application.route('/api/tasks')
def list_tasks():
    # Lecture depuis replica
    session = ReadSession()
    tasks = session.query(Task).all()
    session.close()
    return jsonify(tasks)

@application.route('/api/tasks', methods=['POST'])
def create_task():
    # Écriture vers primary
    session = WriteSession()
    task = Task(...)
    session.add(task)
    session.commit()
    session.close()
    return jsonify(task)
```

**Résultats :**
- Load primary DB réduite de 60%
- Latence queries lecture : 50ms -> 20ms (réplica plus proche)

---

**Performance Optimization 5 : ALB connection settings**

Console EC2 -> Load Balancers -> flask-todo-production-alb -> Edit attributes

```
Idle timeout : 300 seconds (pour long-polling/WebSockets)

Cross-zone load balancing : Enable
  -> Meilleure distribution même si instances déséquilibrées par AZ

HTTP/2 : Enable
  -> Multiplexing, meilleure performance

gRPC : Enable (si applicable)
```

---

**Performance Optimization 6 : CloudFront optimizations**

Console CloudFront -> Distribution -> Edit

```
Compress objects automatically : Yes

Supported HTTP versions : HTTP/3 (QUIC)
  -> Plus rapide que HTTP/2

Origin response timeout : 30 seconds (augmenté de 10s)

Cache behaviors :
  /static/* : TTL 1 day -> 7 days (static assets changent rarement)
  
Price class : All edge locations
  -> Meilleure latence globale

Lambda@Edge (optionnel) :
  - Image optimization à la volée
  - A/B testing
  - Header manipulation
```

---

**Résumé optimisations performances :**

```
Avant :
- Latence p95 : 500ms
- Throughput : 200 req/s
- DB load : 80%

Après :
- Latence p95 : 150ms (70% amélioration)
- Throughput : 1000 req/s (5× augmentation)
- DB load : 20% (cache + read replica)
```

---

#### Étape 8.3 : Disaster Recovery - Procédures et tests

**DR Strategy : Pilot Light (compromis coût/RTO)**

**Configuration actuelle :**

```
Primary Region : eu-west-3 (Paris)
DR Region : eu-west-1 (Ireland)

RPO (Recovery Point Objective) : 1 hour
RTO (Recovery Time Objective) : 4 hours
```

---

**DR Setup :**

**1. Cross-Region RDS Snapshot copy**

Console RDS -> Automated backups -> Snapshot copy

```
Destination region : eu-west-1
Frequency : Daily
Retention : 7 days
Encryption : aws/rds
```

---

**2. S3 Cross-Region Replication (déjà configuré)**

**Vérifier :**

```
Bucket : flask-todo-production-backups-123456
Replication destination : flask-todo-dr-backups-123456 (eu-west-1)
```

---

**3. AMI copy to DR region**

**Script Lambda pour copier AMI automatiquement :**

```python
import boto3

def lambda_handler(event, context):
    """
    Copie la dernière AMI production vers DR region
    Trigger : Weekly (CloudWatch Events)
    """
    ec2_source = boto3.client('ec2', region_name='eu-west-3')
    ec2_dest = boto3.client('ec2', region_name='eu-west-1')
    
    # Trouver la dernière AMI production
    images = ec2_source.describe_images(
        Filters=[
            {'Name': 'tag:Environment', 'Values': ['Production']},
            {'Name': 'tag:Type', 'Values': ['Golden-Image']}
        ],
        Owners=['self']
    )
    
    latest_image = sorted(
        images['Images'],
        key=lambda x: x['CreationDate'],
        reverse=True
    )[0]
    
    # Copier vers DR region
    response = ec2_dest.copy_image(
        Name=f"{latest_image['Name']}-DR",
        SourceImageId=latest_image['ImageId'],
        SourceRegion='eu-west-3',
        Description=f"DR copy of {latest_image['ImageId']}"
    )
    
    print(f"AMI copied: {response['ImageId']}")
    return response
```

---

**4. Infrastructure as Code (CloudFormation/Terraform)**

**Créer un template CloudFormation pour recréer l'infra en DR :**

**dr-stack.yaml (simplifié) :**

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: DR Stack for Flask TODO App

Parameters:
  LatestAMI:
    Type: AWS::EC2::Image::Id
    Description: Latest DR AMI ID
  
  DBSnapshotIdentifier:
    Type: String
    Description: RDS snapshot to restore

Resources:
  # VPC
  DRVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.1.0.0/16
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: DR-VPC
  
  # RDS from snapshot
  DRDatabase:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: flask-todo-dr-db
      DBSnapshotIdentifier: !Ref DBSnapshotIdentifier
      DBInstanceClass: db.t3.small
      MultiAZ: true
      VPCSecurityGroups:
        - !Ref DRDBSecurityGroup
  
  # ASG
  DRAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      MinSize: 2
      MaxSize: 10
      DesiredCapacity: 4
      LaunchTemplate:
        LaunchTemplateId: !Ref DRLaunchTemplate
        Version: $Latest
      VPCZoneIdentifier:
        - !Ref DRPrivateSubnetA
        - !Ref DRPrivateSubnetB
  
  # ... (ALB, etc.)
```

---

**DR Runbook (Procédure manuelle) :**

**Fichier : `DR-RUNBOOK.md`**

```markdown
# Disaster Recovery Runbook

## Scénario : Primary Region (eu-west-3) indisponible

### Phase 1 : Validation (15 minutes)

1. Confirmer que primary region est DOWN
   - Vérifier AWS Service Health Dashboard
   - Ping endpoints : FAIL
   - CloudWatch metrics : No data

2. Activer l'équipe DR
   - Notifier : CTO, DevOps Lead, On-call engineer
   - Ouvrir bridge call
   - Ouvrir incident ticket

### Phase 2 : Restore Database (1 heure)

1. Trouver le dernier snapshot RDS
   ```bash
   aws rds describe-db-snapshots \
     --region eu-west-1 \
     --db-instance-identifier flask-todo-production-db \
     --query 'reverse(sort_by(DBSnapshots, &SnapshotCreateTime))[0]'
   ```

2. Restaurer RDS en eu-west-1
   ```bash
   aws rds restore-db-instance-from-db-snapshot \
     --region eu-west-1 \
     --db-instance-identifier flask-todo-dr-db \
     --db-snapshot-identifier SNAPSHOT_ID \
     --db-instance-class db.t3.small \
     --multi-az
   ```
   
   Durée : 30-45 minutes

3. Vérifier RDS disponible
   ```bash
   aws rds describe-db-instances \
     --region eu-west-1 \
     --db-instance-identifier flask-todo-dr-db \
     --query 'DBInstances[0].DBInstanceStatus'
   ```

### Phase 3 : Launch Infrastructure (2 heures)

1. Deploy CloudFormation stack
   ```bash
   aws cloudformation create-stack \
     --region eu-west-1 \
     --stack-name flask-todo-dr-stack \
     --template-body file://dr-stack.yaml \
     --parameters \
       ParameterKey=LatestAMI,ParameterValue=ami-dr-123456 \
       ParameterKey=DBSnapshotIdentifier,ParameterValue=SNAPSHOT_ID
   ```

2. Attendre stack CREATE_COMPLETE
   Durée : 20-30 minutes

3. Vérifier instances healthy
   - ASG : 4 instances running
   - Target Group : 4/4 healthy
   - ALB : Active

### Phase 4 : DNS Failover (30 minutes)

1. Mettre à jour Route 53
   ```bash
   # Pointer app.tondomaine.com vers ALB DR
   aws route53 change-resource-record-sets \
     --hosted-zone-id Z123456 \
     --change-batch file://dns-failover.json
   ```

2. Vérifier propagation DNS
   ```bash
   dig app.tondomaine.com
   # Doit pointer vers eu-west-1 ALB
   ```

3. Tester l'application
   - https://app.tondomaine.com -> OK
   - Login -> OK
   - CRUD operations -> OK

### Phase 5 : Communication (15 minutes)

1. Status page update
   "Service restored on backup infrastructure. 
    Performance may be reduced. 
    Data loss: < 1 hour."

2. Notifier stakeholders
   - Customers (email/in-app notification)
   - Internal teams (Slack #incidents)
   - Management (executive summary)

### Phase 6 : Monitoring (continue)

1. Surveiller métriques DR
   - CloudWatch Dashboard DR
   - Error rates
   - Latency
   - Database performance

2. Préparer le failback
   - Attendre que primary region soit restored
   - Planifier fenêtre de maintenance
   - Sync data depuis DR -> Primary

## Temps total : 4 heures [OK]
## Data loss : < 1 heure [OK]
```

---

**DR Test (GameDay) :**

**Planifier un test DR annuel :**

```
Date : Q1 2025
Participants : Toute l'équipe DevOps
Durée : 4 heures
Objectif : Valider que DR fonctionne réellement

Scénario :
1. Simuler indisponibilité eu-west-3
   - Détacher Internet Gateway (break connectivity)
   
2. Activer DR playbook
   
3. Mesurer temps de restoration
   
4. Identifier les gaps
   
5. Mettre à jour le runbook

Post-Mortem :
- Ce qui a marché
- Ce qui n'a pas marché
- Action items pour améliorer
```

---

#### Étape 8.4 : Backup & Restore - Procédures détaillées

**Backup Strategy complète :**

```
Niveau 1 : RDS Automated Backups
  Fréquence : Quotidien
  Rétention : 7 jours
  RPO : 5 minutes (transaction logs)
  RTO : 30 minutes

Niveau 2 : RDS Manual Snapshots
  Fréquence : Avant chaque déploiement + Hebdomadaire
  Rétention : 30 jours
  RPO : Point-in-time du snapshot
  RTO : 30 minutes

Niveau 3 : S3 Data Export
  Fréquence : Mensuel
  Rétention : 1 an
  Format : SQL dump + JSON
  Use case : Compliance, archives

Niveau 4 : AMI Golden Images
  Fréquence : Hebdomadaire
  Rétention : 4 versions
  RTO : 10 minutes (launch instances)
```

---

**Script backup complet :**

**`scripts/backup.sh`**

```bash
#!/bin/bash
# ════════════════════════════════════════════════════════════════
# SCRIPT BACKUP COMPLET - PRODUCTION
# ════════════════════════════════════════════════════════════════

set -e

DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_BUCKET="s3://flask-todo-production-backups-123456"

echo "===== Starting backup process at $(date) ====="

# ────────────────────────────────────────────────────────────────
# 1. RDS SNAPSHOT
# ────────────────────────────────────────────────────────────────

echo "Creating RDS snapshot..."

SNAPSHOT_ID="flask-todo-production-manual-${DATE}"

aws rds create-db-snapshot \
  --region eu-west-3 \
  --db-instance-identifier flask-todo-production-db \
  --db-snapshot-identifier ${SNAPSHOT_ID} \
  --tags Key=Type,Value=Manual Key=Date,Value=${DATE}

echo "[OK] RDS snapshot created: ${SNAPSHOT_ID}"

# Attendre que snapshot soit disponible
aws rds wait db-snapshot-completed \
  --region eu-west-3 \
  --db-snapshot-identifier ${SNAPSHOT_ID}

echo "[OK] RDS snapshot ready"

# ────────────────────────────────────────────────────────────────
# 2. SQL EXPORT (pour compliance)
# ────────────────────────────────────────────────────────────────

echo "Exporting database to SQL..."

# Récupérer credentials depuis Secrets Manager
DB_SECRET=$(aws secretsmanager get-secret-value \
  --secret-id production/db/postgres \
  --region eu-west-3 \
  --query SecretString \
  --output text)

DB_HOST=$(echo $DB_SECRET | jq -r .host)
DB_PASSWORD=$(echo $DB_SECRET | jq -r .password)

# Export SQL
PGPASSWORD=$DB_PASSWORD pg_dump \
  -h $DB_HOST \
  -U postgres \
  -d production_db \
  -F c \
  -f backup-${DATE}.dump

# Compresser
gzip backup-${DATE}.dump

# Upload vers S3
aws s3 cp backup-${DATE}.dump.gz \
  ${BACKUP_BUCKET}/database/backup-${DATE}.dump.gz

# Cleanup local
rm backup-${DATE}.dump.gz

echo "[OK] SQL export uploaded to S3"

# ────────────────────────────────────────────────────────────────
# 3. AMI BACKUP
# ────────────────────────────────────────────────────────────────

echo "Creating AMI from running instance..."

# Trouver une instance running
INSTANCE_ID=$(aws ec2 describe-instances \
  --region eu-west-3 \
  --filters "Name=tag:Name,Values=FlaskTODO-Production-Instance" \
            "Name=instance-state-name,Values=running" \
  --query 'Reservations[0].Instances[0].InstanceId' \
  --output text)

# Créer AMI
AMI_ID=$(aws ec2 create-image \
  --region eu-west-3 \
  --instance-id ${INSTANCE_ID} \
  --name "flask-todo-production-ami-${DATE}" \
  --description "Production AMI backup ${DATE}" \
  --no-reboot \
  --tag-specifications \
    "ResourceType=image,Tags=[{Key=Type,Value=Backup},{Key=Date,Value=${DATE}}]" \
  --query 'ImageId' \
  --output text)

echo "[OK] AMI created: ${AMI_ID}"

# ────────────────────────────────────────────────────────────────
# 4. CONFIGURATION BACKUP
# ────────────────────────────────────────────────────────────────

echo "Backing up configurations..."

# Créer dossier temporaire
TEMP_DIR="/tmp/backup-config-${DATE}"
mkdir -p ${TEMP_DIR}

# Export Security Groups
aws ec2 describe-security-groups \
  --region eu-west-3 \
  --filters "Name=tag:Environment,Values=Production" \
  > ${TEMP_DIR}/security-groups.json

# Export ASG configurations
aws autoscaling describe-auto-scaling-groups \
  --region eu-west-3 \
  --auto-scaling-group-names flask-todo-production-asg \
  > ${TEMP_DIR}/asg-config.json

# Export ALB configuration
aws elbv2 describe-load-balancers \
  --region eu-west-3 \
  --names flask-todo-production-alb \
  > ${TEMP_DIR}/alb-config.json

# Export RDS configuration
aws rds describe-db-instances \
  --region eu-west-3 \
  --db-instance-identifier flask-todo-production-db \
  > ${TEMP_DIR}/rds-config.json

# Tar et upload
tar -czf config-backup-${DATE}.tar.gz -C ${TEMP_DIR} .
aws s3 cp config-backup-${DATE}.tar.gz \
  ${BACKUP_BUCKET}/configs/config-backup-${DATE}.tar.gz

# Cleanup
rm -rf ${TEMP_DIR}
rm config-backup-${DATE}.tar.gz

echo "[OK] Configuration backup uploaded"

# ────────────────────────────────────────────────────────────────
# 5. BACKUP VERIFICATION
# ────────────────────────────────────────────────────────────────

echo "Verifying backups..."

# Vérifier RDS snapshot
SNAPSHOT_STATUS=$(aws rds describe-db-snapshots \
  --region eu-west-3 \
  --db-snapshot-identifier ${SNAPSHOT_ID} \
  --query 'DBSnapshots[0].Status' \
  --output text)

if [ "$SNAPSHOT_STATUS" != "available" ]; then
  echo "[X] ERROR: RDS snapshot not available"
  exit 1
fi

# Vérifier S3 files
aws s3 ls ${BACKUP_BUCKET}/database/backup-${DATE}.dump.gz || {
  echo "[X] ERROR: SQL dump not found in S3"
  exit 1
}

aws s3 ls ${BACKUP_BUCKET}/configs/config-backup-${DATE}.tar.gz || {
  echo "[X] ERROR: Config backup not found in S3"
  exit 1
}

echo "[OK] All backups verified"

# ────────────────────────────────────────────────────────────────
# 6. CLEANUP OLD BACKUPS
# ────────────────────────────────────────────────────────────────

echo "Cleaning up old backups..."

# Supprimer snapshots > 30 jours
aws rds describe-db-snapshots \
  --region eu-west-3 \
  --snapshot-type manual \
  --query "DBSnapshots[?SnapshotCreateTime<='$(date -d '30 days ago' --iso-8601)'].DBSnapshotIdentifier" \
  --output text | while read SNAP; do
    echo "Deleting old snapshot: $SNAP"
    aws rds delete-db-snapshot \
      --region eu-west-3 \
      --db-snapshot-identifier $SNAP || true
done

# Supprimer AMIs > 4 versions
OLD_AMIS=$(aws ec2 describe-images \
  --region eu-west-3 \
  --owners self \
  --filters "Name=tag:Type,Values=Backup" \
  --query 'sort_by(Images, &CreationDate)[:-4].ImageId' \
  --output text)

for AMI in $OLD_AMIS; do
  echo "Deregistering old AMI: $AMI"
  aws ec2 deregister-image \
    --region eu-west-3 \
    --image-id $AMI || true
done

echo "[OK] Old backups cleaned"

# ────────────────────────────────────────────────────────────────
# 7. NOTIFICATION
# ────────────────────────────────────────────────────────────────

echo "Sending notification..."

aws sns publish \
  --region eu-west-3 \
  --topic-arn arn:aws:sns:eu-west-3:123456789012:production-info \
  --subject "[OK] Production Backup Completed" \
  --message "Backup completed successfully at $(date)
  
RDS Snapshot: ${SNAPSHOT_ID}
AMI: ${AMI_ID}
SQL Dump: backup-${DATE}.dump.gz
  
All backups verified and uploaded to S3."

echo "===== Backup process completed at $(date) ====="
```

---

**Scheduler le backup :**

**CloudWatch Events -> Create rule**

```
Name : production-backup-daily
Schedule : cron(0 3 * * ? *)  # Tous les jours à 3h UTC

Target : Lambda function
  Function : TriggerBackupScript
  
Lambda function :
  Trigger : CloudWatch Events
  Action : Execute backup.sh on bastion via SSM
```

---

**Script restore complet :**

**`scripts/restore.sh`**

```bash
#!/bin/bash
# ════════════════════════════════════════════════════════════════
# SCRIPT RESTORE - PRODUCTION
# ════════════════════════════════════════════════════════════════

set -e

# Arguments
SNAPSHOT_ID=$1
RESTORE_TIME=$2  # Format: 2024-12-17T10:30:00Z (optionnel)

if [ -z "$SNAPSHOT_ID" ]; then
  echo "Usage: ./restore.sh SNAPSHOT_ID [RESTORE_TIME]"
  echo "Example: ./restore.sh flask-todo-production-manual-20241217-100000"
  echo "Example: ./restore.sh latest 2024-12-17T10:30:00Z"
  exit 1
fi

echo "===== Starting restore process ====="

# ────────────────────────────────────────────────────────────────
# 1. TROUVER LE SNAPSHOT
# ────────────────────────────────────────────────────────────────

if [ "$SNAPSHOT_ID" == "latest" ]; then
  echo "Finding latest snapshot..."
  SNAPSHOT_ID=$(aws rds describe-db-snapshots \
    --region eu-west-3 \
    --snapshot-type manual \
    --query 'reverse(sort_by(DBSnapshots, &SnapshotCreateTime))[0].DBSnapshotIdentifier' \
    --output text)
  
  echo "Latest snapshot: ${SNAPSHOT_ID}"
fi

# ────────────────────────────────────────────────────────────────
# 2. CONFIRMATION
# ────────────────────────────────────────────────────────────────

echo ""
echo "[ATTENTION]  WARNING: This will restore the database!"
echo "Snapshot: ${SNAPSHOT_ID}"
[ -n "$RESTORE_TIME" ] && echo "Point-in-time: ${RESTORE_TIME}"
echo ""
read -p "Are you sure? (yes/no): " CONFIRM

if [ "$CONFIRM" != "yes" ]; then
  echo "Restore cancelled"
  exit 0
fi

# ────────────────────────────────────────────────────────────────
# 3. BACKUP ACTUEL (avant restore)
# ────────────────────────────────────────────────────────────────

echo "Creating pre-restore backup..."

PRE_RESTORE_SNAPSHOT="pre-restore-$(date +%Y%m%d-%H%M%S)"

aws rds create-db-snapshot \
  --region eu-west-3 \
  --db-instance-identifier flask-todo-production-db \
  --db-snapshot-identifier ${PRE_RESTORE_SNAPSHOT}

aws rds wait db-snapshot-completed \
  --region eu-west-3 \
  --db-snapshot-identifier ${PRE_RESTORE_SNAPSHOT}

echo "[OK] Pre-restore backup: ${PRE_RESTORE_SNAPSHOT}"

# ────────────────────────────────────────────────────────────────
# 4. CRÉER NOUVELLE INSTANCE RDS
# ────────────────────────────────────────────────────────────────

NEW_DB_ID="flask-todo-production-db-restored"

echo "Restoring RDS instance..."

if [ -n "$RESTORE_TIME" ]; then
  # Point-in-time restore
  aws rds restore-db-instance-to-point-in-time \
    --region eu-west-3 \
    --source-db-instance-identifier flask-todo-production-db \
    --target-db-instance-identifier ${NEW_DB_ID} \
    --restore-time ${RESTORE_TIME} \
    --db-instance-class db.t3.small \
    --multi-az
else
  # Snapshot restore
  aws rds restore-db-instance-from-db-snapshot \
    --region eu-west-3 \
    --db-instance-identifier ${NEW_DB_ID} \
    --db-snapshot-identifier ${SNAPSHOT_ID} \
    --db-instance-class db.t3.small \
    --multi-az
fi

echo "Waiting for RDS instance to be available..."
aws rds wait db-instance-available \
  --region eu-west-3 \
  --db-instance-identifier ${NEW_DB_ID}

echo "[OK] RDS instance restored: ${NEW_DB_ID}"

# ────────────────────────────────────────────────────────────────
# 5. VALIDATION
# ────────────────────────────────────────────────────────────────

echo "Validating restored database..."

# Récupérer endpoint
NEW_ENDPOINT=$(aws rds describe-db-instances \
  --region eu-west-3 \
  --db-instance-identifier ${NEW_DB_ID} \
  --query 'DBInstances[0].Endpoint.Address' \
  --output text)

echo "New endpoint: ${NEW_ENDPOINT}"

# Test connexion
DB_SECRET=$(aws secretsmanager get-secret-value \
  --secret-id production/db/postgres \
  --region eu-west-3 \
  --query SecretString \
  --output text)

DB_PASSWORD=$(echo $DB_SECRET | jq -r .password)

PGPASSWORD=$DB_PASSWORD psql \
  -h $NEW_ENDPOINT \
  -U postgres \
  -d production_db \
  -c "SELECT COUNT(*) FROM task;" || {
    echo "[X] ERROR: Cannot connect to restored database"
    exit 1
  }

echo "[OK] Database connection validated"

# ────────────────────────────────────────────────────────────────
# 6. SWITCH ENDPOINT
# ────────────────────────────────────────────────────────────────

echo ""
echo "To complete the restore:"
echo "1. Update application config with new endpoint: ${NEW_ENDPOINT}"
echo "2. Test thoroughly"
echo "3. If OK, rename instances:"
echo "   - Rename current -> flask-todo-production-db-old"
echo "   - Rename restored -> flask-todo-production-db"
echo "4. Update DNS/endpoints"
echo "5. Delete old instance"
echo ""
echo "Pre-restore backup: ${PRE_RESTORE_SNAPSHOT}"
echo "(Keep for rollback if needed)"
```

---

**[OK] Phase 8 terminée : Optimisations et DR prêts**

**Durée : 3 heures**

---

## [GRAPHIQUE] RÉCAPITULATIF COMPLET DE L'ARCHITECTURE

### Architecture finale - Composants

```
┌─────────────────────────────────────────────────────────────┐
│                    INTERNET USERS                            │
└─────────────────────┬───────────────────────────────────────┘
                      │
         ┌────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐
         │      Route 53 DNS       │
         │   app.tondomaine.com    │
         └────────────┬────────────┘
                      │
         ┌────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐
         │    AWS WAF (Firewall)   │
         │  Rate limiting, Rules   │
         └────────────┬────────────┘
                      │
         ┌────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐
         │   CloudFront (CDN)      │
         │   Global edge caching   │
         └────────────┬────────────┘
                      │
         ┌────────────[BLACK_DOWN-POINTING_TRIANGLE]────────────┐
         │   ALB (Load Balancer)   │
         │   3 AZs, HTTPS          │
         └────────────┬────────────┘
                      │
    ┌─────────────────┴─────────────────┐
    │          VPC 10.0.0.0/16          │
    │                                    │
    │  ┌──────────────────────────────┐ │
    │  │  Auto Scaling Group          │ │
    │  │  Min:3 Desired:6 Max:18      │ │
    │  │                               │ │
    │  │  ┌─────┐ ┌─────┐ ┌─────┐    │ │
    │  │  │EC2  │ │EC2  │ │EC2  │    │ │
    │  │  │Flask│ │Flask│ │Flask│    │ │
    │  │  │(AZ-A││(AZ-B││(AZ-C│    │ │
    │  │  └──┬──┘ └──┬──┘ └──┬──┘    │ │
    │  └─────┼──────┼──────┼─────────┘ │
    │        │      │      │            │
    │     ┌──┴──────┴──────┴──┐        │
    │     │   ElastiCache      │        │
    │     │   Redis Cluster    │        │
    │     │   3 shards + HA    │        │
    │     └──────────┬─────────┘        │
    │                │                   │
    │     ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]─────────┐        │
    │     │   RDS PostgreSQL   │        │
    │     │   Multi-AZ         │        │
    │     │   Primary + Standby│        │
    │     └────────────────────┘        │
    └────────────────────────────────────┘

External Services:
├── S3 (Static assets, backups, logs)
├── CloudWatch (Monitoring, Logs, Alarms)
├── SNS (Notifications)
├── Secrets Manager (Credentials)
├── CloudTrail (Audit logs)
├── Config (Compliance)
├── GuardDuty (Threat detection)
├── Security Hub (Security posture)
├── X-Ray (Distributed tracing)
├── Systems Manager (Instance management)
└── CodePipeline/Build/Deploy (CI/CD)
```

---

### Flux de données

**1. User Request Flow :**

```
User -> Route 53 (DNS) -> CloudFront (Cache check)
  ├─ Cache HIT -> Return cached content (10-50ms)
  └─ Cache MISS -> WAF (Security check) -> ALB
                                        └─ EC2 Instance
                                           ├─ Check Redis (Session/Cache)
                                           │  ├─ Cache HIT -> Return
                                           │  └─ Cache MISS -> Query RDS
                                           └─ Return response
```

**Latency totale :**
- Cache hit (static) : 10-50ms
- Cache hit (Redis) : 50-100ms
- Database query : 150-300ms

---

**2. Deployment Flow :**

```
Dev -> Git push -> GitHub -> Webhook -> CodePipeline
                                  └─ CodeBuild (Tests, Build)
                                     └─ Artifact S3
                                        └─ CodeDeploy
                                           ├─ Create Green Environment
                                           ├─ Deploy + Validate
                                           ├─ Switch Traffic (Blue->Green)
                                           └─ Terminate Blue
```

**Durée : 22 minutes, Zero downtime**

---

**3. Monitoring Flow :**

```
All Services -> CloudWatch Metrics
            -> CloudWatch Logs
            -> X-Ray Traces
            
CloudWatch -> Alarms -> SNS -> Email/SMS/PagerDuty/Slack

GuardDuty -> Security Findings -> Security Hub -> SNS
Config -> Compliance Checks -> Dashboard
CloudTrail -> API Logs -> S3 -> Athena (Analysis)
```

---

## [ARGENT] COÛTS MENSUELS DÉTAILLÉS

### Compute (EC2 + ASG)

```
Instances :
  Reserved (base) : 3 × t3.micro × $5/month = $15
  On-Demand : 1 × t3.micro × $7.50/month = $7.50
  Spot : 2 × t3.micro × $2/month = $4

Total Compute : $26.50/month
```

### Load Balancing

```
ALB :
  Base : $16.20/month
  LCU : 10 LCU × $0.008 × 730h = $58.40/month
  
Total ALB : $74.60/month
```

### Networking

```
NAT Gateway :
  3 × $0.045/h × 730h = $98.55/month
  Data processing : 100 GB × $0.045 = $4.50/month

CloudFront :
  Data transfer out : 500 GB × $0.085 = $42.50/month
  Requests : 10M × $0.0075/10K = $7.50/month

Route 53 :
  Hosted zone : $0.50/month
  Queries : 10M × $0.40/1M = $4/month

Total Networking : $157.55/month
```

### Database

```
RDS PostgreSQL :
  db.t3.small Multi-AZ : $60/month (with RI)
  Storage : 100 GB gp3 × $0.138 = $13.80/month
  Backup storage : 50 GB × $0.095 = $4.75/month

ElastiCache Redis :
  3 shards : 3 × cache.t3.micro × $0.034/h × 730h = $74.46/month
  3 replicas : Included in Multi-AZ pricing

Total Database : $153/month
```

### Storage

```
S3 :
  Static assets : 10 GB Standard × $0.023 = $0.23/month
  Logs : 50 GB Intelligent-Tiering × $0.0125 = $0.63/month
  Backups : 100 GB Glacier × $0.004 = $0.40/month
  
  Requests : 1M GET × $0.0004/1K = $0.40/month
  Data transfer : Included (via CloudFront)

Total S3 : $1.66/month
```

### Security & Monitoring

```
CloudWatch :
  Metrics : 50 custom × $0.30 = $15/month
  Logs ingestion : 10 GB × $0.50 = $5/month
  Logs storage : 50 GB × $0.03 = $1.50/month
  Alarms : 20 × $0.10 = $2/month
  Dashboards : 3 × $3 = $9/month

WAF :
  Web ACL : $5/month
  Rules : 10 × $1 = $10/month
  Requests : 10M × $0.60/1M = $6/month

GuardDuty :
  CloudTrail analysis : $4.40/month
  VPC Flow Logs : 10 GB × $1.00 = $10/month

CloudTrail :
  Management events : First copy free
  Data events S3 : 1M × $0.10/100K = $1/month

Config :
  Configuration items : 100 × $0.003 = $0.30/month
  Rules : 10 × $2 = $20/month

Secrets Manager :
  Secrets : 3 × $0.40 = $1.20/month
  API calls : 100K × $0.05/10K = $0.50/month

X-Ray :
  Traces : 1M × $5/1M = $5/month

Total Security & Monitoring : $90.50/month
```

### CI/CD

```
CodePipeline :
  Active pipeline : $1/month

CodeBuild :
  Build minutes : 100 min/month × $0.005 = $0.50/month

CodeDeploy :
  Free for EC2

Total CI/CD : $1.50/month
```

### Backup & DR

```
RDS Snapshots :
  Storage : 200 GB × $0.095 = $19/month

AMI Storage :
  4 AMIs × 10 GB × $0.05 = $2/month

Cross-Region Replication :
  Data transfer : 50 GB × $0.02 = $1/month

Total Backup & DR : $22/month
```

---

### [ARGENT] TOTAL MENSUEL

```
┌─────────────────────────────┬──────────┐
│ Catégorie                   │ Coût     │
├─────────────────────────────┼──────────┤
│ Compute (EC2)               │ $26.50   │
│ Load Balancing (ALB)        │ $74.60   │
│ Networking (NAT+CDN+DNS)    │ $157.55  │
│ Database (RDS+ElastiCache)  │ $153.00  │
│ Storage (S3)                │ $1.66    │
│ Security & Monitoring       │ $90.50   │
│ CI/CD                       │ $1.50    │
│ Backup & DR                 │ $22.00   │
├─────────────────────────────┼──────────┤
│ TOTAL                       │ $527.31  │
└─────────────────────────────┴──────────┘

Avec optimisations (Reserved Instances, Savings Plans) :
Total optimisé : ~$350-400/month
```

---

**Répartition par catégorie :**

```
Networking : 30% (NAT Gateway + CloudFront)
Database : 29% (RDS + ElastiCache)
Monitoring : 17% (CloudWatch + Security services)
Compute : 14% (EC2 instances)
Load Balancing : 14% (ALB)
Storage : 3% (S3)
CI/CD : 1%
Backup : 4%
```

---

### Optimisations de coûts possibles

**Option 1 : Réduire NAT Gateways (dev/staging)**

```
3 NAT -> 1 NAT
Économies : $65/month
Trade-off : Moins de HA
```

**Option 2 : Plus de Spot Instances**

```
50% Spot -> 75% Spot
Économies : $15/month
```

**Option 3 : S3 Lifecycle aggressive**

```
Archives plus rapides vers Glacier
Économies : $5/month
```

**Option 4 : RDS Reserved Instance (3 ans)**

```
db.t3.small 3-year RI : $30/month vs $60/month
Économies : $30/month
```

**Total économies potentielles : ~$115/month**

**Nouveau total : ~$400/month**

---

## [OK] TESTS DE VALIDATION FINAUX

### Test 1 : Haute Disponibilité

```bash
# Simuler défaillance AZ-A
aws ec2 terminate-instances \
  --instance-ids $(aws ec2 describe-instances \
    --filters "Name=availability-zone,Values=eu-west-3a" \
              "Name=tag:aws:autoscaling:groupName,Values=flask-todo-production-asg" \
    --query 'Reservations[].Instances[].InstanceId' \
    --output text)

# Observer :
- [ ] ASG détecte instances terminated
- [ ] Nouvelles instances lancées en AZ-A
- [ ] Application reste accessible
- [ ] Latence p95 < 500ms pendant incident
- [ ] Aucune erreur 5xx

Durée recovery : < 5 minutes
```

---

### Test 2 : Scalabilité

```bash
# Load test
ab -n 100000 -c 500 -t 300 https://app.tondomaine.com/

# Observer :
- [ ] ASG scale de 6 -> 12+ instances
- [ ] CPU reste < 80%
- [ ] Latence p95 < 300ms
- [ ] Taux erreur < 0.1%
- [ ] Scale-in après arrêt du test

Throughput atteint : > 1000 req/s
```

---

### Test 3 : DR (Disaster Recovery)

```bash
# Simuler indisponibilité région
# (Test sur environnement staging)

./scripts/dr-failover.sh

# Vérifier :
- [ ] RDS restore < 1h
- [ ] Infrastructure up < 4h
- [ ] DNS switch < 30min
- [ ] Data loss < 1h
- [ ] Application fonctionnelle

RTO : < 4h [OK]
RPO : < 1h [OK]
```

---

### Test 4 : Sécurité

```bash
# Scan vulnérabilités
aws inspector start-assessment-run \
  --assessment-template-arn arn:aws:inspector:...

# Vérifier :
- [ ] Aucune vulnérabilité critique
- [ ] Ports non-nécessaires fermés
- [ ] IMDSv2 activé
- [ ] Chiffrement at-rest + in-transit
- [ ] Pas d'accès public DB/Cache

# Test WAF
curl -X POST https://app.tondomaine.com/api/tasks \
  -d "title=<script>alert('XSS')</script>"

# Attendu : 403 Forbidden (WAF bloque)
```

---

### Test 5 : CI/CD

```bash
# Faire un commit
echo "v3.0.0" > VERSION
git add VERSION
git commit -m "feat: Version 3.0.0"
git push origin main

# Observer pipeline :
- [ ] Source trigger < 10s
- [ ] Build + tests < 5min
- [ ] Deploy staging < 20min
- [ ] Integration tests < 3min
- [ ] Approval manuel
- [ ] Deploy production (Blue/Green) < 25min
- [ ] Smoke tests < 2min
- [ ] Zero downtime

Total : ~55 minutes
```

---

### Test 6 : Monitoring & Alertes

```bash
# Déclencher une alarme (CPU élevé)
stress-ng --cpu 4 --timeout 600s

# Vérifier :
- [ ] CloudWatch détecte CPU > 70%
- [ ] Alarme triggered
- [ ] Email reçu < 1min
- [ ] Slack notification reçue
- [ ] ASG commence à scale out

# Déclencher alarme critique (5xx errors)
# Simuler erreurs dans l'app

# Vérifier :
- [ ] Alarme critique triggered
- [ ] PagerDuty alerte
- [ ] SMS reçu < 2min
```

---

### Test 7 : Backup & Restore

```bash
# Test restore
./scripts/restore.sh latest

# Vérifier :
- [ ] Snapshot trouvé
- [ ] RDS restored < 30min
- [ ] Données intègres
- [ ] Application se connecte
- [ ] Aucune perte de données

# Test backup automatique
# Attendre backup scheduled (3h UTC)

# Vérifier :
- [ ] Snapshot créé
- [ ] SQL dump dans S3
- [ ] AMI créée
- [ ] Configs backupées
- [ ] Notification envoyée
```

---

### Test 8 : Performance

```bash
# Test latence
for i in {1..100}; do
  curl -w "%{time_total}\n" -o /dev/null -s https://app.tondomaine.com/
done | sort -n | awk '{sum+=$1} END {print "Average:", sum/NR}'

# Objectifs :
- [ ] p50 < 100ms
- [ ] p95 < 200ms
- [ ] p99 < 500ms

# Test cache
# Hit rate Redis > 80%
aws cloudwatch get-metric-statistics \
  --namespace AWS/ElastiCache \
  --metric-name CacheHitRate \
  --dimensions Name=CacheClusterId,Value=flask-todo-production-cache \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 3600 \
  --statistics Average
```

---

## [COURS] CONCLUSION GÉNÉRALE

### [BRAVO] Félicitations ! Tu as terminé les 10 exercices AWS ! [BRAVO]

**Parcours complet :**

```
Exercice 1 : EC2 Manuel (Fondations)
Exercice 2 : S3 + CloudFront (Static hosting)
Exercice 3 : EC2 + RDS + Nginx (Application complète)
Exercice 4 : Elastic Beanstalk (PaaS)
Exercice 5 : Lambda + API Gateway (Serverless)
Exercice 6 : ECS Fargate + ECR (Containers)
Exercice 7 : Auto Scaling + ALB (Haute disponibilité)
Exercice 8 : VPC Custom (Networking avancé)
Exercice 9 : CI/CD Pipeline (DevOps)
Exercice 10 : Architecture Production Complète (Enterprise-grade)
```

**Durée totale : ~50-60 heures**

---

### Compétences acquises

**Infrastructure :**
- [OK] EC2 (instances, AMIs, user data)
- [OK] VPC (subnets, routing, security)
- [OK] Load Balancers (ALB, target groups)
- [OK] Auto Scaling (policies, lifecycle)
- [OK] NAT Gateway, Internet Gateway

**Compute :**
- [OK] Lambda (serverless)
- [OK] ECS Fargate (containers)
- [OK] Elastic Beanstalk (PaaS)

**Storage & Database :**
- [OK] S3 (buckets, lifecycle, replication)
- [OK] RDS (PostgreSQL, Multi-AZ, backups)
- [OK] ElastiCache (Redis, clustering)

**Networking & CDN :**
- [OK] Route 53 (DNS)
- [OK] CloudFront (CDN, edge caching)
- [OK] VPC (custom networking)

**Security :**
- [OK] IAM (roles, policies, least privilege)
- [OK] Security Groups & NACLs
- [OK] WAF (web application firewall)
- [OK] Secrets Manager
- [OK] GuardDuty, Security Hub
- [OK] CloudTrail, Config

**DevOps & CI/CD :**
- [OK] CodePipeline (orchestration)
- [OK] CodeBuild (build automation)
- [OK] CodeDeploy (Blue/Green deployment)
- [OK] GitHub integration

**Monitoring & Observability :**
- [OK] CloudWatch (metrics, logs, alarms)
- [OK] X-Ray (distributed tracing)
- [OK] SNS (notifications)
- [OK] Dashboards

**Cost Optimization :**
- [OK] Reserved Instances
- [OK] Savings Plans
- [OK] Spot Instances
- [OK] S3 Lifecycle policies

**Disaster Recovery :**
- [OK] Backup strategies
- [OK] Cross-region replication
- [OK] RTO/RPO planning
- [OK] DR runbooks

---

### Architecture avant/après

**Exercice 1 (Début) :**
```
1 EC2 instance
Manual deployment
No HA
No monitoring
```

**Exercice 10 (Fin) :**
```
Multi-AZ, Multi-tier architecture
18 instances max (auto-scaling)
CI/CD automatisé
Blue/Green deployments
Monitoring complet
99.99% uptime
< 200ms latency
Security complète
DR ready (4h RTO, 1h RPO)
Enterprise-grade *
```

---

### Next Steps - Évolutions possibles

**1. Infrastructure as Code (IaC)**

```hcl
# Terraform
terraform init
terraform plan
terraform apply

-> Infrastructure reproductible
-> Version control
-> Multi-environments
```

**2. Kubernetes (EKS)**

```
Migrer de ECS -> EKS
-> Portabilité
-> Ecosystem plus large
-> GitOps (ArgoCD, Flux)
```

**3. Observability avancée**

```
-> Grafana + Prometheus
-> Datadog / New Relic
-> OpenTelemetry
-> Distributed tracing avancé
```

**4. Microservices**

```
Monolithe Flask -> Microservices
-> Frontend : React/Next.js
-> API Gateway : Kong/API Gateway
-> Services : Flask/FastAPI/Go
-> Message Queue : SQS/SNS
```

**5. Multi-Region**

```
Active-Active multi-region
-> Route 53 Geolocation routing
-> DynamoDB Global Tables
-> Aurora Global Database
-> < 100ms latency worldwide
```

**6. FinOps avancé**

```
-> Cost Explorer automatisé
-> Budget alerts
-> Rightsiziing recommandations
-> Reserved Instance optimization
-> Spot Fleet management
```

**7. Security avancée**

```
-> Zero Trust Architecture
-> Service Mesh (Istio)
-> Certificate rotation automatique
-> Penetration testing
-> SOC 2 / ISO 27001 compliance
```

**8. ML/AI Integration**

```
-> SageMaker (ML models)
-> Recommendations engine
-> Predictive scaling
-> Anomaly detection
```

---

### Ressources pour continuer

**Certifications AWS :**
- [OK] AWS Certified Cloud Practitioner (fondations)
- [OK] AWS Certified Solutions Architect Associate (architecture)
- * AWS Certified Solutions Architect Professional (avancé)
- * AWS Certified DevOps Engineer Professional (DevOps)

**Documentation :**
- https://docs.aws.amazon.com
- https://aws.amazon.com/architecture/well-architected/
- https://aws.amazon.com/blogs/architecture/

**Communauté :**
- Reddit : r/aws, r/devops
- AWS Community Builders
- AWS User Groups (meetups locaux)

**Practice :**
- AWS Workshops : https://workshops.aws
- AWS re:Invent sessions (YouTube)
- Build real projects!

---

### [TROPHEE] FÉLICITATIONS FINALES

**Tu maîtrises maintenant :**

[OK] Architecture cloud production-ready
[OK] 50+ services AWS
[OK] DevOps best practices
[OK] Security & Compliance
[OK] Cost optimization
[OK] Disaster Recovery
[OK] CI/CD automation
[OK] Monitoring & Observability

**Tu es prêt pour :**

[RAPIDE] Architecte Solutions AWS
[RAPIDE] DevOps Engineer
[RAPIDE] Cloud Engineer
[RAPIDE] SRE (Site Reliability Engineer)
[RAPIDE] Projets personnels/entreprise

---

**Temps total investi : ~50-60 heures**

**Valeur acquise : Inestimable ! [GEM_STONE]**

**Continue à builder, à expérimenter, à casser (dans un sandbox!), et à apprendre ! [COURS]**

---

**Merci d'avoir suivi ce parcours complet AWS ! [MERCI]**

**Questions ? Feedback ? N'hésite pas ! [SPEECH_BALLOON]**

---

## [DOCS] ANNEXES

**Document complet disponible :**
- Architecture diagrams (draw.io)
- Scripts complets (backup, restore, monitoring)
- CloudFormation templates
- Terraform modules
- Runbooks (operations, DR)
- Cost calculator spreadsheet

**Repository GitHub suggéré :**
```
flask-todo-aws-production/
├── docs/
│   ├── architecture.md
│   ├── runbooks/
│   └── diagrams/
├── infrastructure/
│   ├── terraform/
│   └── cloudformation/
├── scripts/
│   ├── backup.sh
│   ├── restore.sh
│   └── monitoring.sh
├── application/
│   └── (Flask app)
└── README.md
```

---

**[BRAVO] BRAVO ! Tu es maintenant un AWS Expert ! [BRAVO]**