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

## [LIVRE] INTRODUCTION

Ce document contient **10 exercices pratiques corrigés** sur Amazon Web Services (AWS), allant du niveau débutant au niveau expert. Chaque exercice est conçu pour :

- **Renforcer** tes compétences cloud pratiques
- **Consolider** les concepts AWS fondamentaux et avancés
- **Simuler** des situations réelles en entreprise
- **Te préparer** aux certifications AWS (Solutions Architect, Developer, SysOps)

**Niveau de progression :**
- [VERT] Exercice 1 : Débutant - Infrastructure de base
- [VERT] Exercice 2 : Débutant - Stockage et sauvegarde
- [JAUNE] Exercice 3 : Intermédiaire - Base de données managée
- [JAUNE] Exercice 4 : Intermédiaire - Architecture serverless
- [JAUNE] Exercice 5 : Intermédiaire - Réseau avancé
- [ROUGE] Exercice 6 : Avancé - Haute disponibilité
- [ROUGE] Exercice 7 : Avancé - CDN et distribution globale
- [ROUGE] Exercice 8 : Avancé - Monitoring et observabilité
- [ROUGE] Exercice 9 : Expert - Infrastructure as Code
- [ROUGE] Exercice 10 : Expert - Architecture microservices

**Chaque exercice contient :**
- [OK] Énoncé détaillé avec contexte professionnel
- [OK] Architecture cible avec diagrammes
- [OK] Prérequis et objectifs pédagogiques
- [OK] Solution complète étape par étape
- [OK] Explications approfondies (Pourquoi ? Comment ? Quand ?)
- [OK] Code commenté ligne par ligne
- [OK] Tests de validation
- [OK] Erreurs courantes et comment les éviter
- [OK] Calcul des coûts AWS
- [OK] Points clés à retenir
- [OK] Pour aller plus loin
- [OK] Bonnes pratiques AWS Well-Architected

**Conseils avant de commencer :**
1. Crée un compte AWS gratuit (Free Tier 12 mois)
2. Active la MFA (authentification multi-facteurs) sur le compte root
3. Ne jamais utiliser le compte root pour les opérations quotidiennes
4. Surveille tes coûts avec AWS Budgets
5. Nettoie tes ressources après chaque exercice pour éviter les frais

**[ATTENTION] IMPORTANT - COÛTS AWS :**
- La plupart des services utilisent le Free Tier
- Certains exercices avancés peuvent générer des coûts minimes (< 5$)
- TOUJOURS arrêter/supprimer les ressources après l'exercice
- Configurer des alertes de facturation

**Bon courage ! [RAPIDE]**

---

---

# [VERT] EXERCICE 1 : INFRASTRUCTURE DE BASE AWS (EC2 + VPC + IAM)

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu es ingénieur DevOps junior dans une startup qui veut migrer son infrastructure vers le cloud. La direction t'a confié la création de l'infrastructure de base pour héberger une application web Flask.

L'application est un **gestionnaire de tâches** (todo list) avec une base de données SQLite simple. Elle doit être accessible publiquement sur internet mais de manière sécurisée.

### Cahier des charges

Le CTO exige :
- **Une instance EC2** pour héberger l'application Flask
- **Un VPC dédié** (réseau isolé) avec sous-réseaux publics et privés
- **IAM** : Séparation des responsabilités (principe du moindre privilège)
- **Security Groups** : Uniquement les ports nécessaires ouverts
- **Accès SSH sécurisé** avec clés (pas de mot de passe)
- **IP élastique** pour l'instance (IP publique fixe)
- **Monitoring de base** avec CloudWatch
- **Budget total** : Rester dans le Free Tier AWS

### Architecture cible

```
┌─────────────────────────────────────────────────────────────┐
│                         INTERNET                             │
└──────────────────────────┬──────────────────────────────────┘
                           │
                  ┌────────[BLACK_DOWN-POINTING_TRIANGLE]─────────┐
                  │  Internet Gateway │
                  └────────┬─────────┘
                           │
┌──────────────────────────┼──────────────────────────────────┐
│                      VPC (10.0.0.0/16)                       │
│  ┌───────────────────────┼───────────────────────────────┐  │
│  │  Public Subnet (10.0.1.0/24)                          │  │
│  │  ┌────────────────────[BLACK_DOWN-POINTING_TRIANGLE]─────────────────┐             │  │
│  │  │  EC2 Instance                         │             │  │
│  │  │  - Ubuntu 22.04                       │             │  │
│  │  │  - Flask App (port 5000)              │             │  │
│  │  │  - Elastic IP (IP publique fixe)      │             │  │
│  │  │  - Security Group:                    │             │  │
│  │  │    * SSH (22) depuis ton IP           │             │  │
│  │  │    * HTTP (80) depuis partout         │             │  │
│  │  │    * Flask (5000) depuis partout      │             │  │
│  │  └───────────────────────────────────────┘             │  │
│  └──────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  Private Subnet (10.0.2.0/24)                        │  │
│  │  (pour futures bases de données)                     │  │
│  └──────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘

IAM :
├── Groupe "Admins" -> Accès complet
└── Groupe "Developers" -> Accès EC2/S3 uniquement
```

### Contraintes techniques

- Région AWS : `eu-west-1` (Irlande) - la plus proche de l'Afrique de l'Ouest
- Type d'instance : `t2.micro` (Free Tier)
- OS : Ubuntu 22.04 LTS
- Application : Flask avec SQLite
- Temps estimé : 3-4 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Créer et configurer un VPC avec subnets
- [OK] Comprendre le routage réseau dans AWS
- [OK] Lancer et configurer une instance EC2
- [OK] Gérer les clés SSH pour l'accès sécurisé
- [OK] Configurer des Security Groups (pare-feu cloud)
- [OK] Attacher une Elastic IP à une instance
- [OK] Créer des utilisateurs et groupes IAM
- [OK] Appliquer des politiques IAM (policies)
- [OK] Déployer une application Flask sur EC2
- [OK] Surveiller les coûts et l'utilisation

---

## [DOCS] PRÉREQUIS

- Compte AWS créé (Free Tier)
- Carte bancaire enregistrée (nécessaire même pour Free Tier)
- MFA activée sur le compte root
- Connaissances de base en Linux
- Connaissances de base en Python/Flask
- Client SSH installé (PuTTY sur Windows, ssh sur Linux/Mac)

---

## [ARGENT] ESTIMATION DES COÛTS

**Services utilisés :**

| Service | Type | Coût Free Tier | Coût après Free Tier |
|---------|------|----------------|----------------------|
| EC2 t2.micro | Instance | 750h/mois (1 an) | ~0.0116$/h (~8.5$/mois) |
| EBS (stockage) | 30 GB | Inclus | 0.10$/GB-mois |
| Elastic IP | IP publique | Gratuit si attaché | 0.005$/h si non utilisé |
| Data transfer OUT | Bande passante | 1 GB/mois | 0.09$/GB |
| VPC | Réseau | Gratuit | Gratuit |
| CloudWatch | Monitoring basique | Gratuit | Gratuit (métriques basiques) |

**Coût total estimé :** 
- **Dans Free Tier (12 premiers mois) : 0$/mois** [OK]
- **Après Free Tier : ~9-10$/mois**

**[ATTENTION] Pour éviter les frais :**
- Stopper l'instance quand tu ne l'utilises pas
- Libérer l'Elastic IP si non utilisée
- Supprimer les snapshots et volumes inutilisés

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 0 : Préparation du compte AWS

#### Se connecter à la console AWS

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

**2. Cliquer sur "Sign In to the Console"**

**3. Se connecter avec le compte root (première fois)**

**[ATTENTION] IMPORTANT : Après cette première connexion, NE PLUS UTILISER LE COMPTE ROOT !**

---

#### Activer la MFA sur le compte root

**Pourquoi ?**
- Le compte root a accès à TOUT (facturation, suppression de compte, etc.)
- Si quelqu'un vole tes identifiants root -> Catastrophe
- MFA = Multi-Factor Authentication (double authentification)

**Comment ?**

**1. Dans la console AWS, cliquer sur ton nom (en haut à droite) -> Security credentials**

**2. Section "Multi-factor authentication (MFA)" -> Assign MFA device**

**3. Choisir "Virtual MFA device" (application mobile)**

**4. Installer une app d'authentification :**
- Google Authenticator (iOS/Android)
- Microsoft Authenticator (iOS/Android)
- Authy (iOS/Android/Desktop)

**5. Scanner le QR code avec l'app**

**6. Entrer deux codes MFA consécutifs (attendre 30 secondes entre les deux)**

**7. Cliquer "Assign MFA"**

**[OK] Compte root sécurisé !**

**Maintenant, ferme cette session root et ne reviens JAMAIS ici (sauf urgence absolue).**

---

#### Créer un utilisateur IAM pour toi-même

**Pourquoi ?**
- Principe du moindre privilège
- Séparation des responsabilités
- Traçabilité des actions

**Comment ?**

**1. Se reconnecter avec le compte root (dernière fois)**

**2. Aller dans "Services" -> "IAM" (ou rechercher "IAM" dans la barre de recherche)**

**3. Dans le menu de gauche -> "Users" -> "Create user"**

**Étape 1 : User details**
```
User name: sirrdev-admin
[x] Provide user access to the AWS Management Console
[x] I want to create an IAM user

Console password: Custom password
  -> Entre un mot de passe fort : SirrDev2024!Admin@AWS

[ ] User must create a new password at next sign-in
  (Décocher si c'est pour toi)
```

**Cliquer "Next"**

---

**Étape 2 : Set permissions**

**Pour l'instant, on va donner accès admin complet (on affinera après)**

```
[x] Attach policies directly

Rechercher : AdministratorAccess
[x] AdministratorAccess
```

**Explication de la politique AdministratorAccess :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    }
  ]
}
```

**`"Action": "*"` = Toutes les actions autorisées**
**`"Resource": "*"` = Sur toutes les ressources**

**[ATTENTION] En production, jamais donner AdministratorAccess à tout le monde !**

**Cliquer "Next"**

---

**Étape 3 : Review and create**

**Vérifier et cliquer "Create user"**

**Résultat :**

```
Success!
User sirrdev-admin was created successfully.

Console sign-in URL:
https://123456789012.signin.aws.amazon.com/console

User name: sirrdev-admin
Password: (le mot de passe que tu as défini)
```

**[ATTENTION] NOTER CETTE URL ! C'est ton URL de connexion personnelle.**

**Cliquer "Return to users list"**

---

**4. Activer MFA pour cet utilisateur aussi**

**Cliquer sur "sirrdev-admin" -> Onglet "Security credentials"**

**Section "Multi-factor authentication (MFA)" -> "Assign MFA device"**

**Répéter le processus (scanner QR code, entrer 2 codes)**

**[OK] Utilisateur admin créé et sécurisé !**

---

**5. Se déconnecter du compte root et se reconnecter avec sirrdev-admin**

**1. Déconnexion (en haut à droite)**

**2. Aller sur l'URL personnelle :**

```
https://123456789012.signin.aws.amazon.com/console
```

**Ou utiliser l'alias de compte (plus facile à retenir) :**

**Dans IAM -> Dashboard -> Customize sign-in URL**

```
Account Alias: sirrdev-company
```

**L'URL devient :**

```
https://sirrdev-company.signin.aws.amazon.com/console
```

**3. Se connecter :**

```
IAM user name: sirrdev-admin
Password: SirrDev2024!Admin@AWS
MFA code: (de l'app d'authentification)
```

**[OK] Connecté avec l'utilisateur IAM !**

---

#### Configurer une alerte de facturation

**Pourquoi ?**
- Éviter les mauvaises surprises
- Être alerté si dépassement du Free Tier
- Prévenir les attaques (ex: quelqu'un lance 1000 instances à ton insu)

**Comment ?**

**1. Aller dans "Services" -> "Billing and Cost Management"**

**OU cliquer sur ton nom -> "Billing and Cost Management"**

**2. Dans le menu de gauche -> "Budgets"**

**3. Cliquer "Create budget"**

**4. Choisir "Use a template (simplified)"**

**5. Sélectionner "Zero spend budget"**

```
Budget name: Free-Tier-Alert
Email recipients: ton-email@example.com
```

**Cliquer "Create budget"**

**Explication :**
- Cette alerte se déclenche dès que tu dépasses 0.01$
- Tu recevras un email immédiatement
- Permet de détecter tout dépassement du Free Tier

---

**6. (Optionnel) Créer un budget mensuel**

**Créer un autre budget :**

```
Type: Cost budget
Amount: 10.00 USD (ou ton budget max)
Alert threshold: 80% (alerte à 8$)
Email: ton-email@example.com
```

---

**7. Activer les alertes de facturation dans CloudWatch**

**Billing and Cost Management -> Preferences**

```
[x] Receive Free Tier Usage Alerts
Email: ton-email@example.com

[x] Receive Billing Alerts
```

**Cliquer "Save preferences"**

**[OK] Alertes configurées !**

---

### ÉTAPE 1 : Créer un VPC (Virtual Private Cloud)

#### Comprendre le VPC

**Qu'est-ce qu'un VPC ?**

VPC = Virtual Private Cloud = Réseau virtuel isolé dans AWS

**Analogie :**
- VPC = Ta maison avec son réseau local
- Subnets = Les pièces de ta maison
- Internet Gateway = La porte d'entrée vers l'extérieur
- Route Tables = Les panneaux de direction à l'intérieur
- Security Groups = Les serrures et alarmes

**Pourquoi créer un VPC ?**

| Critère | VPC par défaut | VPC personnalisé |
|---------|---------------|------------------|
| Isolation | Partagé | [OK] Totalement isolé |
| Contrôle CIDR | Limité | [OK] Choix libre |
| Subnets | Créés auto | [OK] Contrôle total |
| Sécurité | Basique | [OK] Avancée |
| Production | [X] Non recommandé | [OK] Recommandé |

**En entreprise : TOUJOURS créer un VPC dédié.**

---

#### Planification du réseau

**Choix du bloc CIDR (plage d'adresses IP) :**

```
VPC CIDR: 10.0.0.0/16
```

**Explication du CIDR :**

**10.0.0.0/16 signifie :**
- Réseau : 10.0.0.0
- Masque : /16 = 255.255.0.0
- Plage : 10.0.0.0 -> 10.0.255.255
- Nombre d'IPs : 2^(32-16) = 65,536 adresses

**Pourquoi 10.0.0.0/16 ?**
- Plage privée (RFC 1918) : 10.0.0.0 - 10.255.255.255
- Jamais routée sur internet public
- Large (65k IPs) mais pas trop (gaspillage)
- Standard en entreprise

**Autres plages privées :**
- 10.0.0.0/8 (très grand : 16 millions d'IPs)
- 172.16.0.0/12 (moyen : 1 million d'IPs)
- 192.168.0.0/16 (petit : 65k IPs - souvent utilisé en réseau domestique)

---

**Découpage en subnets :**

```
Public Subnet 1:  10.0.1.0/24  (256 IPs, zone eu-west-1a)
Public Subnet 2:  10.0.2.0/24  (pour future haute dispo)
Private Subnet 1: 10.0.11.0/24 (zone eu-west-1a)
Private Subnet 2: 10.0.12.0/24 (pour future haute dispo)
```

**Explication du /24 :**
- /24 = 255.255.255.0
- Plage : x.x.x.0 -> x.x.x.255
- Nombre d'IPs : 256 (mais AWS en réserve 5)
- IPs utilisables : 251

**Les 5 IPs réservées par AWS :**
```
10.0.1.0   -> Adresse réseau
10.0.1.1   -> Gateway VPC
10.0.1.2   -> DNS AWS
10.0.1.3   -> Réservé futur
10.0.1.255 -> Broadcast
```

**Public vs Private Subnet :**

| Type | Accès internet sortant | Accès internet entrant | Usage |
|------|------------------------|------------------------|-------|
| Public | [OK] Via Internet Gateway | [OK] Avec IP publique | Serveurs web, bastion |
| Private | [OK] Via NAT Gateway | [X] Jamais | Bases de données, workers |

---

#### Créer le VPC dans la console

**1. Aller dans "Services" -> "VPC"**

**2. Dans le menu de gauche -> "Your VPCs" -> "Create VPC"**

**3. Choisir "VPC and more" (assistant complet)**

**Configuration :**

```
Name tag auto-generation: sirrdev-vpc

IPv4 CIDR block: 10.0.0.0/16

IPv6 CIDR block: No IPv6 CIDR block
  (On n'utilise pas IPv6 pour cet exercice)

Tenancy: Default
  (Shared hardware - moins cher que Dedicated)
```

---

```
Number of Availability Zones (AZs): 2
  (Haute disponibilité - minimum 2 AZs)

Number of public subnets: 2
Number of private subnets: 2

Customize subnets CIDR blocks:
  Public subnet 1:  10.0.1.0/24  (eu-west-1a)
  Public subnet 2:  10.0.2.0/24  (eu-west-1b)
  Private subnet 1: 10.0.11.0/24 (eu-west-1a)
  Private subnet 2: 10.0.12.0/24 (eu-west-1b)

NAT gateways ($): None
  (Coûte cher ~0.045$/h, on n'en a pas besoin pour cet exercice)

VPC endpoints: None
  (Pas nécessaire pour l'instant)
```

**Explication des choix :**

**Availability Zones (AZs) :**
- AZ = Centre de données AWS physique
- eu-west-1a et eu-west-1b = 2 datacenters différents en Irlande
- Si eu-west-1a tombe -> eu-west-1b continue de fonctionner
- Résilience et haute disponibilité

**NAT Gateway :**
- Permet aux instances en subnet privé d'accéder à internet (sortie seulement)
- Coûte ~0.045$/h (~33$/mois) + data transfer
- Pas nécessaire ici (on mettra tout en public pour l'exercice)

---

**4. Cliquer "Create VPC"**

**AWS va créer automatiquement :**
- 1 VPC
- 4 Subnets (2 publics + 2 privés)
- 1 Internet Gateway (attaché au VPC)
- 2 Route Tables (1 pour public, 1 pour privé)
- Network ACLs (par défaut)

**Durée : ~2 minutes**

**[OK] VPC créé !**

---

#### Vérifier les ressources créées

**1. Your VPCs**

```
VPC ID: vpc-0abc123...
Name: sirrdev-vpc
IPv4 CIDR: 10.0.0.0/16
State: Available [OK]
```

---

**2. Subnets**

```
sirrdev-vpc-subnet-public1-eu-west-1a
  Subnet ID: subnet-0def456...
  IPv4 CIDR: 10.0.1.0/24
  AZ: eu-west-1a
  Available IPs: 251

sirrdev-vpc-subnet-public2-eu-west-1b
  10.0.2.0/24, eu-west-1b

sirrdev-vpc-subnet-private1-eu-west-1a
  10.0.11.0/24, eu-west-1a

sirrdev-vpc-subnet-private2-eu-west-1b
  10.0.12.0/24, eu-west-1b
```

---

**3. Internet Gateways**

```
Name: sirrdev-vpc-igw
IGW ID: igw-0ghi789...
State: Attached [OK]
VPC: vpc-0abc123... (sirrdev-vpc)
```

**Rôle de l'Internet Gateway :**
- Passerelle entre le VPC et internet
- Traduit les IPs privées en IPs publiques (NAT)
- Nécessaire pour que les instances publiques soient accessibles

---

**4. Route Tables**

**Route table publique :**

```
Name: sirrdev-vpc-rtb-public
Routes:
  Destination     Target
  10.0.0.0/16     local          (Trafic interne VPC)
  0.0.0.0/0       igw-0ghi789... (Internet via IGW)

Associated subnets:
  - sirrdev-vpc-subnet-public1-eu-west-1a
  - sirrdev-vpc-subnet-public2-eu-west-1b
```

**Explication des routes :**

**10.0.0.0/16 -> local**
- Tout le trafic vers 10.0.x.x reste dans le VPC
- Communication interne entre subnets

**0.0.0.0/0 -> igw-...**
- Tout le reste (internet) passe par l'Internet Gateway
- 0.0.0.0/0 = n'importe quelle IP (route par défaut)

---

**Route table privée :**

```
Name: sirrdev-vpc-rtb-private
Routes:
  Destination     Target
  10.0.0.0/16     local          (Trafic interne seulement)

Associated subnets:
  - sirrdev-vpc-subnet-private1-eu-west-1a
  - sirrdev-vpc-subnet-private2-eu-west-1b
```

**Pas de route vers internet -> Les subnets privés sont VRAIMENT privés.**

**Pour donner accès internet sortant aux subnets privés, il faudrait :**
- Créer un NAT Gateway dans un subnet public
- Ajouter une route `0.0.0.0/0 -> nat-gateway-...`

---

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

**Pourquoi ?**
- Accès SSH sécurisé aux instances EC2
- Pas de mot de passe (plus sécurisé)
- Clé privée = comme un mot de passe super long

**Comment ?**

**1. Dans la console EC2 -> "Key Pairs" (menu gauche) -> "Create key pair"**

```
Name: sirrdev-ec2-key

Key pair type: 
  [x] RSA (standard)
  [ ] ED25519 (plus moderne, mais moins compatible)

Private key file format:
  Linux/Mac: .pem
  Windows (PuTTY): .ppk
  
  -> Choisir .pem (compatible partout)
```

**Cliquer "Create key pair"**

**Le fichier `sirrdev-ec2-key.pem` se télécharge automatiquement.**

**[ATTENTION] IMPORTANT :**
- Ce fichier NE PEUT PAS être re-téléchargé
- Si tu le perds, tu perds l'accès SSH à tes instances
- Le stocker dans un endroit sûr

---

**2. Sécuriser la clé (permissions)**

**Sur Linux/Mac :**

```bash
# Déplacer dans ~/.ssh/
mv ~/Downloads/sirrdev-ec2-key.pem ~/.ssh/

# Définir les bonnes permissions (obligatoire)
chmod 400 ~/.ssh/sirrdev-ec2-key.pem
```

**Pourquoi 400 ?**
- 4 = r-- (read only pour le propriétaire)
- 0 = --- (aucun accès pour le groupe)
- 0 = --- (aucun accès pour les autres)

**SSH refuse de fonctionner si les permissions sont trop permissives (sécurité).**

---

**Sur Windows :**

**Si tu utilises PuTTY :**

1. Télécharger PuTTYgen : https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html
2. Ouvrir PuTTYgen
3. Load -> Sélectionner `sirrdev-ec2-key.pem`
4. Save private key -> `sirrdev-ec2-key.ppk`

**Si tu utilises OpenSSH sur Windows (Git Bash, WSL, PowerShell) :**

```powershell
# Déplacer dans %USERPROFILE%\.ssh\
Move-Item sirrdev-ec2-key.pem $env:USERPROFILE\.ssh\

# Windows gère les permissions différemment (via GUI)
# Clic droit sur le fichier -> Propriétés -> Sécurité
# Supprimer tous les utilisateurs sauf toi
```

---

### ÉTAPE 3 : Créer un Security Group

**Qu'est-ce qu'un Security Group ?**

Security Group = Pare-feu virtuel pour les instances EC2

**Fonctionnement :**
- **Stateful** : Si tu autorises une requête entrante, la réponse sortante est automatiquement autorisée
- **Deny by default** : Tout est bloqué par défaut, on autorise explicitement
- **Layer 4** : Fonctionne au niveau TCP/UDP/ICMP

**Différence avec un pare-feu classique (iptables) :**

| Critère | Security Group | iptables |
|---------|----------------|----------|
| Niveau | Instance | OS |
| Configuration | Console AWS | Ligne de commande |
| Stateful | [OK] Oui | Optionnel |
| Default | Deny all | Varies |

---

**Créer le Security Group :**

**1. Console EC2 -> "Security Groups" -> "Create security group"**

```
Security group name: sirrdev-web-sg
Description: Security group for Flask web application
VPC: sirrdev-vpc (sélectionner le VPC créé)
```

---

**2. Inbound rules (règles entrantes)**

**Ajouter 3 règles :**

**Règle 1 : SSH**

```
Type: SSH
Protocol: TCP
Port range: 22
Source: My IP
  (AWS détecte automatiquement ton IP publique)
Description: SSH access from my IP
```

**Explication :**
- Port 22 = SSH
- Source "My IP" = Seulement depuis ton IP actuelle
- [ATTENTION] Si ton IP change (4G, changement de réseau), tu ne pourras plus te connecter
- Solution : Utiliser "My IP" + une IP fixe de secours, ou un VPN

---

**Règle 2 : HTTP**

```
Type: HTTP
Protocol: TCP
Port range: 80
Source: Anywhere-IPv4 (0.0.0.0/0)
Description: HTTP access from internet
```

**Explication :**
- Port 80 = HTTP standard
- 0.0.0.0/0 = Toutes les IPs du monde
- L'application Flask sera accessible publiquement

---

**Règle 3 : Flask (custom)**

```
Type: Custom TCP
Protocol: TCP
Port range: 5000
Source: Anywhere-IPv4 (0.0.0.0/0)
Description: Flask app access
```

**Explication :**
- Port 5000 = Port par défaut de Flask (mode dev)
- En production, Flask tournerait derrière Nginx/Apache sur le port 80

---

**3. Outbound rules (règles sortantes)**

**Par défaut, tout le trafic sortant est autorisé :**

```
Type: All traffic
Protocol: All
Port range: All
Destination: 0.0.0.0/0
```

**Explication :**
- L'instance peut contacter n'importe quel serveur externe
- Nécessaire pour apt update, pip install, etc.
- En environnement ultra-sécurisé, on restreindrait (ex: seulement HTTPS vers certains domaines)

---

**4. Cliquer "Create security group"**

**[OK] Security Group créé !**

```
Security group ID: sg-0jkl012...
Name: sirrdev-web-sg
```

---

### ÉTAPE 4 : Lancer une instance EC2

**Qu'est-ce qu'une instance EC2 ?**

EC2 = Elastic Compute Cloud = Serveur virtuel dans le cloud

**Analogie :**
- Instance = Un ordinateur virtuel
- AMI (Amazon Machine Image) = Image disque (OS préinstallé)
- Type d'instance = Puissance (CPU, RAM)
- EBS (Elastic Block Store) = Disque dur

---

#### Choisir l'AMI (système d'exploitation)

**1. Console EC2 -> "Instances" -> "Launch instances"**

**Étape 1 : Name and tags**

```
Name: sirrdev-flask-server
Additional tags (optional):
  Environment: Development
  Project: TodoApp
```

**Pourquoi les tags ?**
- Organisation (tri, recherche)
- Facturation (coût par projet/environnement)
- Automatisation (scripts qui agissent sur certains tags)

---

**Étape 2 : Application and OS Images (AMI)**

**Quick Start :**

```
[x] Ubuntu

Ubuntu Server 22.04 LTS (HVM), SSD Volume Type
  64-bit (x86)
  Free tier eligible [OK]
  
  AMI ID: ami-0d71ea30463e0ff8d (peut varier selon la région)
```

**Pourquoi Ubuntu 22.04 LTS ?**
- LTS = Long Term Support (5 ans de mises à jour)
- Stable et populaire
- Bonne compatibilité avec Python/Flask
- Documentation abondante

**Alternatives :**
- Amazon Linux 2023 (optimisé AWS, gratuit)
- Red Hat Enterprise Linux (payant)
- Windows Server (payant, plus cher)

---

**Étape 3 : Instance type**

```
[x] t2.micro

  Family: t2 (Burstable performance)
  vCPUs: 1
  Memory: 1 GiB
  Network: Low to Moderate
  
  Free tier eligible: 750 hours/month [OK]
```

**Explication des types d'instances :**

**t2.micro (Burstable) :**
- CPU de base : ~10% d'un cœur
- Peut "burster" jusqu'à 100% temporairement
- Crédits CPU accumulés quand utilisation < baseline
- Parfait pour dev/test, sites à faible trafic

**Autres familles :**

| Famille | Type | Usage | Prix (h) |
|---------|------|-------|----------|
| t2/t3 | Burstable | Dev, test, sites légers | 0.0116$ |
| m5 | General purpose | Apps standards | 0.096$ |
| c5 | Compute optimized | Calcul intensif | 0.085$ |
| r5 | Memory optimized | Bases de données | 0.126$ |
| p3 | GPU | Machine learning | 3.06$ |

**Pour cet exercice : t2.micro (Free Tier) [OK]**

---

**Étape 4 : Key pair**

```
Key pair name: sirrdev-ec2-key (créé précédemment)
```

**Si tu ne l'as pas créé, cliquer "Create new key pair"**

---

**Étape 5 : Network settings**

```
VPC: sirrdev-vpc
Subnet: sirrdev-vpc-subnet-public1-eu-west-1a (Public)
Auto-assign public IP: Enable [OK]

Firewall (security groups):
  [x] Select existing security group
  Security groups: sirrdev-web-sg
```

**Explication :**

**Auto-assign public IP : Enable**
- L'instance recevra une IP publique automatiquement
- Nécessaire pour accès internet et SSH depuis l'extérieur
- [ATTENTION] Cette IP change à chaque redémarrage (on attachera une Elastic IP après)

---

**Étape 6 : Configure storage**

```
Volume 1 (Root):
  Size: 8 GiB (minimum)
  Volume type: gp3 (General Purpose SSD v3)
  IOPS: 3000
  Throughput: 125 MB/s
  
  [x] Delete on termination (par défaut)
  [ ] Encrypted (pas nécessaire pour cet exercice)
```

**Explication des types de volumes EBS :**

| Type | IOPS | Débit | Prix (GB/mois) | Usage |
|------|------|-------|----------------|-------|
| gp3 | 3000-16000 | 125-1000 MB/s | 0.08$ | * Standard (recommandé) |
| gp2 | 100-16000 | Variable | 0.10$ | Ancien (moins bien) |
| io2 | Jusqu'à 64000 | Jusqu'à 1000 MB/s | 0.125$ + IOPS | Bases critiques |
| st1 | 500 | 500 MB/s | 0.045$ | Big data (HDD) |

**gp3 = Meilleur rapport qualité/prix**

**8 GiB = Minimum (Free Tier jusqu'à 30 GB)**

---

**Étape 7 : Advanced details**

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

**On reviendra ici pour :**
- User data (script de démarrage automatique)
- IAM instance profile (permissions)
- Placement groups (haute performance)
- Tenancy (dédié vs partagé)

---

**Étape 8 : Summary**

**Vérifier le récapitulatif :**

```
Number of instances: 1
AMI: Ubuntu Server 22.04 LTS
Instance type: t2.micro (Free tier eligible)
Key pair: sirrdev-ec2-key
Network: sirrdev-vpc / public subnet
Security group: sirrdev-web-sg
Storage: 8 GiB gp3
```

**Cliquer "Launch instance"**

**Résultat :**

```
Successfully initiated launch of instance (i-0mno345...)
```

**Cliquer "View all instances"**

---

#### Surveiller le lancement

**État de l'instance :**

```
Instance ID: i-0mno345...
Name: sirrdev-flask-server
Instance state: Pending -> Running (après ~30 secondes)
Status check: Initializing -> 2/2 checks passed (après ~2 minutes)
Public IPv4: 34.245.123.45 (exemple)
Private IPv4: 10.0.1.234
```

**Explication des états :**

**Pending** : Allocation des ressources, démarrage de la VM
**Running** : VM démarrée, OS en cours de boot
**Status checks** :
- Check 1/2 : Infrastructure AWS (hyperviseur, réseau)
- Check 2/2 : OS (le système a bien démarré)

**Attendre que "Status check" = "2/2 checks passed" avant de se connecter.**

---

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

**1. Récupérer l'IP publique**

```
Public IPv4: 34.245.123.45 (remplacer par la tienne)
```

---

**2. Se connecter**

**Linux/Mac :**

```bash
ssh -i ~/.ssh/sirrdev-ec2-key.pem ubuntu@34.245.123.45
```

**Windows (Git Bash / WSL) :**

```bash
ssh -i ~/.ssh/sirrdev-ec2-key.pem ubuntu@34.245.123.45
```

**Windows (PuTTY) :**

```
Host: ubuntu@34.245.123.45
Port: 22
Connection -> SSH -> Auth -> Private key: sirrdev-ec2-key.ppk
```

---

**Première connexion :**

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

**Taper `yes` (vérifie l'empreinte si important)**

---

**Résultat :**

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

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

  System information as of Mon Dec 16 10:00:00 UTC 2024

  System load:  0.0               Processes:             95
  Usage of /:   20.1% of 7.57GB   Users logged in:       0
  Memory usage: 21%               IPv4 address for ens5: 10.0.1.234
  Swap usage:   0%

ubuntu@ip-10-0-1-234:~$
```

**[OK] Connecté à l'instance EC2 !**

---

**3. Vérifier la connectivité internet**

```bash
ping -c 3 google.com
```

**Résultat :**

```
PING google.com (142.250.185.46) 56(84) bytes of data.
64 bytes from par21s17-in-f14.1e100.net (142.250.185.46): icmp_seq=1 ttl=115 time=1.23 ms
64 bytes from par21s17-in-f14.1e100.net (142.250.185.46): icmp_seq=2 ttl=115 time=1.45 ms
64 bytes from par21s17-in-f14.1e100.net (142.250.185.46): icmp_seq=3 ttl=115 time=1.67 ms

--- google.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
```

**[OK] Internet fonctionne !**

---

### ÉTAPE 6 : Attacher une Elastic IP

**Problème :**
- L'IP publique actuelle (34.245.123.45) change à chaque arrêt/démarrage
- Pas pratique pour DNS, configuration

**Solution : Elastic IP**
- IP publique FIXE
- Peut être détachée et réattachée à une autre instance
- Gratuite SI attachée à une instance en cours d'exécution
- Coûte 0.005$/h (~3.6$/mois) si non utilisée

---

**1. Console EC2 -> "Elastic IPs" -> "Allocate Elastic IP address"**

```
Network Border Group: eu-west-1
Public IPv4 address pool: Amazon's pool of IPv4 addresses

Tags (optional):
  Name: sirrdev-eip
```

**Cliquer "Allocate"**

**Résultat :**

```
Allocated IPv4 address: 54.194.78.90 (exemple)
```

**[OK] IP allouée, mais pas encore attachée !**

---

**2. Attacher l'Elastic IP à l'instance**

**Sélectionner l'Elastic IP -> Actions -> Associate Elastic IP address**

```
Resource type: Instance

Instance: i-0mno345... (sirrdev-flask-server)

Private IP address: 10.0.1.234 (auto-sélectionné)

[x] Allow this Elastic IP address to be reassociated
```

**Cliquer "Associate"**

**Résultat :**

```
Successfully associated Elastic IP address 54.194.78.90 with instance i-0mno345...
```

---

**3. Vérifier dans la console EC2**

**Instances -> sirrdev-flask-server**

```
Public IPv4: 54.194.78.90 (Elastic IP)
  <- Ne changera plus jamais (sauf si tu la détaches)
```

---

**4. Se reconnecter avec la nouvelle IP**

**Déconnexion de la session SSH actuelle (Ctrl+D)**

**Nouvelle connexion :**

```bash
ssh -i ~/.ssh/sirrdev-ec2-key.pem ubuntu@54.194.78.90
```

**[OK] Connexion avec Elastic IP !**

**Maintenant, tu peux arrêter/démarrer l'instance, l'IP restera la même.**

---

### ÉTAPE 7 : Installer l'application Flask

#### Mettre à jour le système

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

# Mettre à jour les paquets installés
sudo apt upgrade -y
```

**`-y` = Répondre "yes" automatiquement à toutes les questions**

**Durée : ~2-5 minutes**

---

#### Installer Python et pip

**Vérifier Python :**

```bash
python3 --version
```

**Résultat :**

```
Python 3.10.12
```

**[OK] Python 3.10 déjà installé sur Ubuntu 22.04**

---

**Installer pip et venv :**

```bash
sudo apt install python3-pip python3-venv -y
```

**Explication :**
- `python3-pip` : Gestionnaire de paquets Python
- `python3-venv` : Création d'environnements virtuels

---

#### Créer le projet Flask

**1. Créer le dossier de l'application :**

```bash
mkdir ~/flask-todo-app
cd ~/flask-todo-app
```

---

**2. Créer un environnement virtuel :**

```bash
python3 -m venv venv
```

**Explication :**
- Environnement Python isolé
- Évite les conflits de dépendances
- Bonne pratique en développement Python

---

**3. Activer l'environnement virtuel :**

```bash
source venv/bin/activate
```

**Le prompt change :**

```
(venv) ubuntu@ip-10-0-1-234:~/flask-todo-app$
```

**`(venv)` indique que l'environnement virtuel est actif.**

---

**4. Installer Flask :**

```bash
pip install flask
```

**Résultat :**

```
Collecting flask
  Downloading Flask-3.0.0-py3-none-any.whl (...)
Collecting Werkzeug>=3.0.0
  Downloading Werkzeug-3.0.1-py3-none-any.whl (...)
...
Successfully installed Flask-3.0.0 Jinja2-3.1.2 MarkupSafe-2.1.3 Werkzeug-3.0.1 ...
```

---

#### Créer l'application Flask

**1. Créer le fichier principal :**

```bash
nano app.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# APPLICATION FLASK - GESTIONNAIRE DE TÂCHES (TODO LIST)
# ═══════════════════════════════════════════════════════════════
# Description : Application web simple pour gérer des tâches
# Base de données : SQLite (fichier local)
# ═══════════════════════════════════════════════════════════════

from flask import Flask, render_template, request, redirect, url_for
import sqlite3
from datetime import datetime

app = Flask(__name__)

# ───────────────────────────────────────────────────────────────
# CONFIGURATION
# ───────────────────────────────────────────────────────────────

DATABASE = 'todo.db'

# ───────────────────────────────────────────────────────────────
# FONCTIONS DE BASE DE DONNÉES
# ───────────────────────────────────────────────────────────────

def get_db():
    """Connexion à la base de données SQLite"""
    conn = sqlite3.connect(DATABASE)
    # Row factory pour récupérer des dictionnaires au lieu de tuples
    conn.row_factory = sqlite3.Row
    return conn

def init_db():
    """Initialiser la base de données (créer la table)"""
    conn = get_db()
    conn.execute('''
        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            description TEXT,
            completed BOOLEAN NOT NULL DEFAULT 0,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    conn.commit()
    conn.close()

# ───────────────────────────────────────────────────────────────
# ROUTES
# ───────────────────────────────────────────────────────────────

@app.route('/')
def index():
    """Page d'accueil - Liste des tâches"""
    conn = get_db()
    tasks = conn.execute(
        'SELECT * FROM tasks ORDER BY created_at DESC'
    ).fetchall()
    conn.close()
    
    return render_template('index.html', tasks=tasks)

@app.route('/add', methods=['POST'])
def add_task():
    """Ajouter une nouvelle tâche"""
    title = request.form.get('title')
    description = request.form.get('description', '')
    
    if title:
        conn = get_db()
        conn.execute(
            'INSERT INTO tasks (title, description) VALUES (?, ?)',
            (title, description)
        )
        conn.commit()
        conn.close()
    
    return redirect(url_for('index'))

@app.route('/complete/<int:task_id>')
def complete_task(task_id):
    """Marquer une tâche comme terminée"""
    conn = get_db()
    conn.execute(
        'UPDATE tasks SET completed = 1 WHERE id = ?',
        (task_id,)
    )
    conn.commit()
    conn.close()
    
    return redirect(url_for('index'))

@app.route('/delete/<int:task_id>')
def delete_task(task_id):
    """Supprimer une tâche"""
    conn = get_db()
    conn.execute('DELETE FROM tasks WHERE id = ?', (task_id,))
    conn.commit()
    conn.close()
    
    return redirect(url_for('index'))

@app.route('/health')
def health():
    """Endpoint de santé pour monitoring"""
    return {
        'status': 'OK',
        'timestamp': datetime.now().isoformat(),
        'service': 'Flask Todo App'
    }

# ───────────────────────────────────────────────────────────────
# DÉMARRAGE DE L'APPLICATION
# ───────────────────────────────────────────────────────────────

if __name__ == '__main__':
    # Initialiser la base de données
    init_db()
    
    # Démarrer le serveur Flask
    # host='0.0.0.0' = Écouter sur toutes les interfaces réseau
    # port=5000 = Port par défaut Flask
    # debug=False = Mode production (pas de rechargement auto)
    app.run(host='0.0.0.0', port=5000, debug=False)
```

**Sauvegarde : Ctrl+O, Entrée, Ctrl+X**

---

**2. Créer le dossier pour les templates :**

```bash
mkdir templates
```

---

**3. Créer le template HTML :**

```bash
nano templates/index.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>[NOTE] Gestionnaire de Tâches AWS</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 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;
        }
        .add-form {
            background: #f8f9fa;
            padding: 1.5rem;
            border-radius: 10px;
            margin-bottom: 2rem;
        }
        .form-group {
            margin-bottom: 1rem;
        }
        label {
            display: block;
            margin-bottom: 0.5rem;
            font-weight: 600;
            color: #333;
        }
        input[type="text"],
        textarea {
            width: 100%;
            padding: 0.75rem;
            border: 2px solid #ddd;
            border-radius: 5px;
            font-size: 1rem;
        }
        textarea {
            resize: vertical;
            min-height: 80px;
        }
        button {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            padding: 0.75rem 1.5rem;
            border-radius: 5px;
            font-size: 1rem;
            cursor: pointer;
            transition: transform 0.2s;
        }
        button:hover {
            transform: translateY(-2px);
        }
        .task-list {
            list-style: none;
        }
        .task-item {
            background: #f8f9fa;
            padding: 1rem;
            border-radius: 10px;
            margin-bottom: 1rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .task-item.completed {
            opacity: 0.6;
        }
        .task-item.completed .task-title {
            text-decoration: line-through;
        }
        .task-content {
            flex: 1;
        }
        .task-title {
            font-weight: 600;
            color: #333;
            margin-bottom: 0.25rem;
        }
        .task-description {
            color: #666;
            font-size: 0.9rem;
        }
        .task-actions {
            display: flex;
            gap: 0.5rem;
        }
        .btn-small {
            padding: 0.5rem 1rem;
            font-size: 0.875rem;
        }
        .btn-success {
            background: #28a745;
        }
        .btn-danger {
            background: #dc3545;
        }
        .empty-state {
            text-align: center;
            padding: 3rem;
            color: #999;
        }
        .info-box {
            background: #e7f3ff;
            border-left: 4px solid #667eea;
            padding: 1rem;
            margin-bottom: 2rem;
            border-radius: 5px;
        }
        .info-box strong {
            color: #667eea;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>[NOTE] Gestionnaire de Tâches</h1>
        
        <div class="info-box">
            <strong>[RAPIDE] Application déployée sur AWS EC2</strong><br>
            Instance : {{ request.host }}<br>
            Hébergé dans le VPC : sirrdev-vpc
        </div>
        
        <form class="add-form" action="/add" method="POST">
            <div class="form-group">
                <label for="title">Titre de la tâche *</label>
                <input type="text" id="title" name="title" required placeholder="Ex: Apprendre AWS">
            </div>
            <div class="form-group">
                <label for="description">Description (optionnel)</label>
                <textarea id="description" name="description" placeholder="Détails sur la tâche..."></textarea>
            </div>
            <button type="submit">+ Ajouter la tâche</button>
        </form>
        
        {% if tasks %}
            <ul class="task-list">
                {% for task in tasks %}
                <li class="task-item {% if task.completed %}completed{% endif %}">
                    <div class="task-content">
                        <div class="task-title">{{ task.title }}</div>
                        {% if task.description %}
                        <div class="task-description">{{ task.description }}</div>
                        {% endif %}
                    </div>
                    <div class="task-actions">
                        {% if not task.completed %}
                        <a href="/complete/{{ task.id }}">
                            <button class="btn-small btn-success">[OK] Terminé</button>
                        </a>
                        {% endif %}
                        <a href="/delete/{{ task.id }}">
                            <button class="btn-small btn-danger">[SUPPRIMER] Supprimer</button>
                        </a>
                    </div>
                </li>
                {% endfor %}
            </ul>
        {% else %}
            <div class="empty-state">
                <p>[BRAVO] Aucune tâche pour le moment !</p>
                <p>Ajoutez votre première tâche ci-dessus.</p>
            </div>
        {% endif %}
    </div>
</body>
</html>
```

**Sauvegarde.**

---

#### Démarrer l'application

```bash
# S'assurer que l'environnement virtuel est actif
source venv/bin/activate

# Lancer l'application
python app.py
```

**Résultat :**

```
 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5000
 * Running on http://10.0.1.234:5000
Press CTRL+C to quit
```

**[OK] Application Flask démarrée !**

---

**Tester depuis ton navigateur :**

```
http://54.194.78.90:5000
```

**(Remplace par ton Elastic IP)**

**[BRAVO] L'application devrait s'afficher !**

---

**Tester les fonctionnalités :**
- [OK] Ajouter une tâche
- [OK] Marquer comme terminée
- [OK] Supprimer une tâche

---

#### Problème : L'application s'arrête si on ferme SSH

**Solution : Utiliser `screen` ou `tmux`**

**Installer screen :**

```bash
sudo apt install screen -y
```

---

**Utiliser screen :**

```bash
# Créer une nouvelle session screen
screen -S flask-app

# Dans la session screen, lancer Flask
cd ~/flask-todo-app
source venv/bin/activate
python app.py
```

---

**Détacher la session (laisser tourner en arrière-plan) :**

**Appuyer sur : `Ctrl+A` puis `D`**

**La session continue en arrière-plan même si tu te déconnectes !**

---

**Réattacher la session plus tard :**

```bash
screen -r flask-app
```

---

**Lister les sessions screen :**

```bash
screen -ls
```

---

**Tuer une session :**

```bash
screen -X -S flask-app quit
```

---

### ÉTAPE 8 : Créer des groupes et utilisateurs IAM

#### Comprendre IAM

**IAM = Identity and Access Management**

**3 concepts clés :**
1. **Users** : Personnes ou services
2. **Groups** : Ensemble d'utilisateurs avec permissions communes
3. **Policies** : Règles JSON définissant les autorisations

**Meilleure pratique :**
Users -> Groups -> Policies

**Pas Users -> Policies directement**

---

#### Créer un groupe "Developers"

**1. Console IAM -> "User groups" -> "Create group"**

```
User group name: Developers
Description: Developers with EC2 and S3 access
```

---

**2. Attach permissions policies**

**Rechercher et sélectionner :**

```
[x] AmazonEC2ReadOnlyAccess
[x] AmazonS3FullAccess
```

**Cliquer "Create user group"**

---

**Explication des politiques :**

**AmazonEC2ReadOnlyAccess :**
- Voir les instances EC2
- Voir les volumes, snapshots, AMIs
- Ne peut PAS démarrer/arrêter/modifier

**AmazonS3FullAccess :**
- Créer/supprimer des buckets S3
- Uploader/télécharger des objets
- Gérer les permissions S3

---

#### Créer un utilisateur developer

**1. IAM -> Users -> Create user**

```
User name: dev-alice
[x] Provide user access to the AWS Management Console
[x] I want to create an IAM user

Console password: Custom password
  -> AliceDevSecure2024!

[ ] User must create a new password at next sign-in
```

**Next**

---

**2. Set permissions**

```
[x] Add user to group
[x] Developers
```

**Next -> Create user**

---

**3. Tester l'accès d'Alice**

**Se déconnecter et se reconnecter avec :**

```
URL: https://sirrdev-company.signin.aws.amazon.com/console
Username: dev-alice
Password: AliceDevSecure2024!
```

---

**Tester les permissions :**

**Aller dans EC2 -> Instances**

[OK] Alice peut voir les instances

**Essayer de stopper une instance**

[X] Alice reçoit une erreur : "You are not authorized to perform this operation"

**[OK] Les permissions fonctionnent !**

---

### ÉTAPE 9 : Surveiller les coûts et l'utilisation

#### Configurer Cost Explorer

**1. Billing and Cost Management -> Cost Explorer**

**2. Activer Cost Explorer (si première fois)**

**Gratuit, mais prend ~24h à activer**

---

**3. Créer un rapport de coûts mensuel**

```
Time range: Last 3 months
Granularity: Monthly
Group by: Service
```

**Graphique montrant les coûts par service (EC2, S3, etc.)**

---

#### Surveiller le Free Tier

**Billing -> Free Tier**

**Affiche :**
```
EC2 t2.micro instances:
  Usage: 45 hours of 750 hours (6%)
  Remaining: 705 hours

EBS storage:
  Usage: 8 GB of 30 GB (27%)
  Remaining: 22 GB

Data transfer OUT:
  Usage: 0.5 GB of 1 GB (50%)
  Remaining: 0.5 GB
```

**Vérifier régulièrement pour éviter les surprises !**

---

### [OK] TESTS DE VALIDATION

**Infrastructure VPC :**
- [ ] VPC créé avec CIDR 10.0.0.0/16
- [ ] 2 subnets publics + 2 subnets privés
- [ ] Internet Gateway attaché
- [ ] Routes configurées correctement
- [ ] Subnets dans 2 AZs différentes

**Instance EC2 :**
- [ ] Instance t2.micro lancée (Free Tier)
- [ ] Ubuntu 22.04 LTS installé
- [ ] Clés SSH fonctionnelles
- [ ] Connexion SSH réussie
- [ ] Elastic IP attachée (IP fixe)

**Security Groups :**
- [ ] Port 22 (SSH) ouvert depuis ton IP uniquement
- [ ] Port 80 (HTTP) ouvert publiquement
- [ ] Port 5000 (Flask) ouvert publiquement
- [ ] Trafic sortant autorisé

**Application Flask :**
- [ ] Flask installé dans environnement virtuel
- [ ] Base de données SQLite créée
- [ ] Application accessible sur http://ELASTIC_IP:5000
- [ ] Ajout de tâches fonctionne
- [ ] Marquage terminé fonctionne
- [ ] Suppression fonctionne
- [ ] Application tourne en arrière-plan (screen)

**IAM :**
- [ ] Compte root sécurisé avec MFA
- [ ] Utilisateur admin créé (sirrdev-admin)
- [ ] Groupe Developers créé
- [ ] Utilisateur dev-alice créé
- [ ] Permissions testées (lecture OK, écriture bloquée)

**Monitoring :**
- [ ] Alertes de facturation configurées
- [ ] Budget configuré
- [ ] Free Tier surveillé
- [ ] Cost Explorer activé

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Connection timeout lors du SSH

**Symptôme :**

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

**Causes possibles :**

**1. Security Group mal configuré**

Vérifier : EC2 -> Security Groups -> sirrdev-web-sg -> Inbound rules

Il doit y avoir :
```
SSH (22) depuis ton IP actuelle
```

**Solution :**
- Éditer la règle
- Source : "My IP" (AWS détecte ton IP actuelle)

---

**2. Ton IP a changé**

Si tu es en 4G/5G ou sur un réseau changeant :

**Solution temporaire :**
```
Source: 0.0.0.0/0 (Partout)
```

**[ATTENTION] DANGEREUX en production ! Utilise un VPN ou bastion host.**

---

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

Vérifier : EC2 -> Instances -> État

Si "Stopped" -> Démarrer l'instance

---

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

**Symptôme :**

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

**Causes :**

**1. Mauvaise clé utilisée**

```bash
# Vérifier le chemin
ssh -i ~/.ssh/sirrdev-ec2-key.pem ubuntu@54.194.78.90
```

---

**2. Permissions incorrectes sur la clé**

```bash
# Sur Linux/Mac, la clé doit être en 400
chmod 400 ~/.ssh/sirrdev-ec2-key.pem
```

---

**3. Mauvais utilisateur**

Pour Ubuntu : `ubuntu@...`
Pour Amazon Linux : `ec2-user@...`
Pour Red Hat : `ec2-user@...`

---

#### Erreur 3 : L'application Flask n'est pas accessible

**Symptôme :**

```
http://54.194.78.90:5000 -> Connection refused
```

**Vérifications :**

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

```bash
# Se connecter en SSH
ps aux | grep python
```

Si rien -> Relancer Flask

---

**2. Port 5000 ouvert dans le Security Group ?**

EC2 -> Security Groups -> Inbound rules

Il doit y avoir :
```
Custom TCP, Port 5000, Source: 0.0.0.0/0
```

---

**3. Flask écoute sur toutes les interfaces ?**

Dans `app.py` :

```python
app.run(host='0.0.0.0', port=5000)  # [OK]
# PAS :
# app.run(host='127.0.0.1', port=5000)  # [X]
```

---

#### Erreur 4 : Coûts inattendus

**Symptôme :**

Facturation > 0$ alors que tout devrait être gratuit

**Causes fréquentes :**

**1. Elastic IP non utilisée**

Si tu crées une Elastic IP mais ne l'attaches PAS à une instance en cours -> 0.005$/h

**Solution :**
- Attacher à une instance
- OU libérer l'Elastic IP

---

**2. Instance non Free Tier**

Si tu lances t3.micro (au lieu de t2.micro) -> Pas Free Tier

**Solution :**
- Arrêter l'instance
- Créer un AMI
- Relancer avec t2.micro

---

**3. Data transfer dépassé**

Free Tier = 1 GB/mois sortant

**Solution :**
- Limiter les téléchargements
- Surveiller avec CloudWatch

---

**4. Snapshot oubliés**

Les snapshots EBS coûtent 0.05$/GB-mois

**Solution :**
- EC2 -> Snapshots
- Supprimer les anciens snapshots

---

**5. Volumes EBS détachés**

Un volume EBS existe même si l'instance est supprimée (sauf si "Delete on termination")

**Solution :**
- EC2 -> Volumes
- Supprimer les volumes inutilisés

---

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

**VPC (Réseau) :**
- VPC = Réseau isolé dans AWS
- Subnets publics = Accès internet via Internet Gateway
- Subnets privés = Pas d'accès direct internet (via NAT Gateway)
- CIDR /16 pour VPC, /24 pour subnets
- Toujours créer dans 2+ AZs (haute disponibilité)

**EC2 (Instances) :**
- t2.micro = Free Tier (750h/mois pendant 12 mois)
- AMI = Image disque (OS + logiciels préinstallés)
- Clés SSH = Authentification sécurisée
- Elastic IP = IP publique fixe (gratuite si utilisée)
- User data = Script de démarrage automatique

**Security Groups :**
- Pare-feu virtuel au niveau instance
- Stateful (réponse auto-autorisée)
- Deny by default
- Une instance peut avoir plusieurs SG

**IAM :**
- Jamais utiliser le compte root
- Toujours activer MFA
- Users -> Groups -> Policies (hiérarchie)
- Principe du moindre privilège
- Politiques JSON (Effect, Action, Resource)

**Coûts :**
- Free Tier = 12 mois (certains services)
- Surveiller avec AWS Budgets et Cost Explorer
- Arrêter/Supprimer les ressources inutilisées
- Elastic IP non utilisée = Coût !
- Snapshots et volumes = Coûts continus

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Utiliser Nginx comme reverse proxy**

```bash
# Installer Nginx
sudo apt install nginx -y

# Configuration Nginx pour Flask
sudo nano /etc/nginx/sites-available/flask-app

# Contenu :
server {
    listen 80;
    server_name 54.194.78.90;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# Activer
sudo ln -s /etc/nginx/sites-available/flask-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
```

**Avantages :**
- Performance (Nginx gère les connexions statiques)
- HTTPS facile avec Let's Encrypt
- Load balancing (plusieurs instances Flask)

---

**2. Configurer un domaine personnalisé avec Route 53**

**Acheter un domaine :**
- Route 53 -> Registered domains -> Register domain
- Exemple : `mon-app-aws.com` (~12$/an)

**Créer une zone hébergée :**
- Route 53 -> Hosted zones -> Create hosted zone
- Domain name : `mon-app-aws.com`

**Créer un enregistrement A :**
```
Name: mon-app-aws.com
Type: A
Value: 54.194.78.90 (Elastic IP)
TTL: 300
```

**Après propagation DNS (~15 minutes) :**
```
http://mon-app-aws.com -> Ton application !
```

---

**3. Activer HTTPS avec Let's Encrypt**

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

# Obtenir un certificat
sudo certbot --nginx -d mon-app-aws.com

# Renouvellement automatique (cron)
sudo certbot renew --dry-run
```

**Résultat : https://mon-app-aws.com [OK]**

---

**4. Automatiser le déploiement avec User Data**

**Lors du lancement de l'instance, dans "Advanced details" -> User data :**

```bash
#!/bin/bash
apt update -y
apt install python3-pip python3-venv nginx -y

# Cloner le projet (si Git)
cd /home/ubuntu
git clone https://github.com/ton-user/flask-todo-app.git
cd flask-todo-app

# Installer dépendances
python3 -m venv venv
source venv/bin/activate
pip install flask gunicorn

# Configurer systemd pour Flask
cat > /etc/systemd/system/flask-app.service << EOF
[Unit]
Description=Flask Todo App
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/flask-todo-app
Environment="PATH=/home/ubuntu/flask-todo-app/venv/bin"
ExecStart=/home/ubuntu/flask-todo-app/venv/bin/gunicorn -w 4 -b 0.0.0.0:5000 app:app

[Install]
WantedBy=multi-user.target
EOF

systemctl enable flask-app
systemctl start flask-app

# Configurer Nginx
# ... (config nginx ci-dessus)
```

**L'instance démarre avec l'app préinstallée et configurée !**

---

**5. Migrer vers une base de données RDS (PostgreSQL)**

**Au lieu de SQLite local -> PostgreSQL managé par AWS**

**Avantages :**
- Backups automatiques
- Haute disponibilité (Multi-AZ)
- Scalabilité (read replicas)
- Monitoring intégré

**(Ceci sera couvert en détail dans l'Exercice 3)**

---

**6. Créer un AMI personnalisé**

**Créer une image de ton instance configurée :**

```
EC2 -> Instances -> sirrdev-flask-server
  -> Actions -> Image and templates -> Create image

Image name: flask-app-ami-v1.0
Description: Flask app avec SQLite configuré
```

**Utilité :**
- Lancer rapidement des instances identiques
- Backup de la configuration
- Disaster recovery

---

**7. Ajouter un bastion host pour SSH sécurisé**

**Architecture :**

```
Internet -> Bastion (subnet public) -> Instance privée
```

**Créer un subnet privé** et y déplacer l'instance Flask

**Créer un bastion (petite instance t2.nano) dans subnet public**

**SSH en deux étapes :**
```bash
# 1. Se connecter au bastion
ssh -i key.pem ubuntu@bastion-ip

# 2. Depuis le bastion, se connecter à l'instance privée
ssh -i key.pem ubuntu@private-ip
```

**Avantage :** L'instance Flask n'est jamais exposée directement à internet

---

**8. Configurer CloudWatch pour monitoring avancé**

**Métriques personnalisées :**

```bash
# Installer CloudWatch agent
wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
sudo dpkg -i amazon-cloudwatch-agent.deb
```

**Surveiller :**
- Utilisation mémoire (pas par défaut)
- Espace disque
- Métriques applicatives (requêtes Flask/min)

---

**9. Sauvegardes automatiques avec Lambda + Snapshots**

**Créer une fonction Lambda qui :**
1. S'exécute tous les jours à minuit
2. Crée un snapshot du volume EBS
3. Supprime les snapshots > 7 jours

**(Ceci sera couvert dans l'Exercice 4 - Lambda)**

---

**10. Implémenter une architecture Multi-AZ**

**Lancer une 2ème instance dans eu-west-1b**

**Ajouter un Load Balancer (ELB) devant les 2 instances**

**Si une AZ tombe -> L'autre continue**

**(Ceci sera couvert dans l'Exercice 6 - Haute disponibilité)**

---

## [COURS] CONCLUSION DE L'EXERCICE 1

**[BRAVO] FÉLICITATIONS ! Tu as créé ton infrastructure AWS de base ! [BRAVO]**

**Ce que tu as appris :**
- [OK] Créer un VPC avec subnets publics et privés
- [OK] Comprendre le routage réseau AWS (route tables, IGW)
- [OK] Lancer et configurer une instance EC2
- [OK] Gérer les clés SSH et l'accès sécurisé
- [OK] Configurer des Security Groups (pare-feu cloud)
- [OK] Attacher une Elastic IP (IP publique fixe)
- [OK] Déployer une application Flask sur EC2
- [OK] Créer des utilisateurs et groupes IAM
- [OK] Appliquer des politiques IAM
- [OK] Surveiller les coûts et rester dans le Free Tier

**Compétences acquises :**
- [OK] Réseau cloud (VPC, subnets, routing)
- [OK] Infrastructure as a Service (IaaS)
- [OK] Sécurité cloud (IAM, Security Groups)
- [OK] Déploiement d'applications web
- [OK] Gestion des coûts AWS

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

**Architecture déployée :**
```
[OK] 1 VPC dédié
[OK] 4 Subnets (2 publics + 2 privés)
[OK] 1 Internet Gateway
[OK] 1 Instance EC2 t2.micro
[OK] 1 Elastic IP
[OK] 1 Security Group configuré
[OK] 1 Application Flask fonctionnelle
[OK] 3 Utilisateurs IAM avec permissions granulaires
```

**Coût total : 0$/mois (Free Tier) [OK]**

**Prochaine étape :** Exercice 2 - Stockage et sauvegarde avec S3 + Glacier ! [PACKAGE]

---

**[ATTENTION] IMPORTANT : NETTOYAGE DES RESSOURCES**

**Si tu veux arrêter et nettoyer pour éviter tout frais :**

```bash
# 1. Supprimer l'application Flask (pas de coût de toute façon)
# Se connecter en SSH, Ctrl+C pour arrêter Flask

# 2. Arrêter l'instance EC2 (stoppe les coûts de calcul)
EC2 -> Instances -> sirrdev-flask-server -> Instance state -> Stop

# 3. (Optionnel) Libérer l'Elastic IP si non utilisée
EC2 -> Elastic IPs -> Sélectionner -> Actions -> Release Elastic IP

# 4. (Optionnel) Supprimer l'instance complètement
EC2 -> Instances -> sirrdev-flask-server -> Instance state -> Terminate

# 5. (Optionnel) Supprimer le VPC et toutes ses ressources
VPC -> Your VPCs -> sirrdev-vpc -> Actions -> Delete VPC
  (Supprime automatiquement subnets, IGW, route tables, etc.)

# 6. Garder les utilisateurs IAM (pas de coût)
```

**[IDEE] TIP : Si tu continues avec l'Exercice 2, garde tout et arrête juste l'instance EC2.**

---

**FIN DE L'EXERCICE 1**

═══════════════════════════════════════════════════════════════

Veux-tu que je continue avec l'**Exercice 2 : Stockage et sauvegarde avec S3 + Glacier** ?

# [VERT] EXERCICE 2 : STOCKAGE ET SAUVEGARDE AWS (S3 + GLACIER)

## [LISTE] ÉNONCÉ

### Contexte professionnel

Tu continues ton travail dans la startup. L'application de gestion de tâches (Exercice 1) connaît un succès initial et le CTO souhaite ajouter de nouvelles fonctionnalités :

1. **Upload de fichiers** : Les utilisateurs veulent attacher des fichiers aux tâches (images, PDFs, documents)
2. **Sauvegardes automatiques** : La base de données SQLite doit être sauvegardée quotidiennement
3. **Archivage long terme** : Les anciennes sauvegardes doivent être archivées après 30 jours (coût réduit)
4. **Partage de fichiers** : Certains fichiers doivent être accessibles publiquement via URL
5. **Optimisation des coûts** : Utiliser les classes de stockage appropriées selon les besoins

Le directeur technique insiste sur la **sécurité** (encryption) et la **résilience** (versioning, réplication).

### Cahier des charges

**Fonctionnalités requises :**
- [OK] Bucket S3 pour stocker les fichiers uploadés
- [OK] Bucket S3 pour les backups de la base de données
- [OK] Upload de fichiers depuis l'interface Flask
- [OK] Versioning activé (récupération en cas de suppression accidentelle)
- [OK] Lifecycle policies (transition vers Glacier après 30 jours)
- [OK] Encryption au repos (AES-256)
- [OK] Sauvegarde automatique quotidienne de la base SQLite
- [OK] Politiques d'accès granulaires (IAM + Bucket policies)
- [OK] Monitoring de l'utilisation et des coûts

### Architecture cible

```
┌─────────────────────────────────────────────────────────────────┐
│                      UTILISATEURS                                │
└────────────────┬────────────────────────────────────────────────┘
                 │
                 │ Upload fichier via Flask
                 [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│              Instance EC2 (Flask App)                            │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Flask Application                                        │   │
│  │  - boto3 (AWS SDK Python)                                │   │
│  │  - IAM Role attaché (permissions S3)                     │   │
│  └────────────────┬─────────────────────────────────────────┘   │
└─────────────────┬─┴─────────────────────────────────────────────┘
                  │
                  │ AWS SDK
                  [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│                        S3 BUCKETS                                │
│                                                                  │
│  ┌────────────────────────────────────────────────────────┐    │
│  │  Bucket 1: sirrdev-todo-uploads                        │    │
│  │  - Fichiers utilisateurs (images, PDFs)               │    │
│  │  - Versioning: Activé                                  │    │
│  │  - Encryption: AES-256 (SSE-S3)                        │    │
│  │  - Lifecycle:                                          │    │
│  │    * Standard (0-30 jours)                             │    │
│  │    * Standard-IA (30-90 jours)                         │    │
│  │    * Glacier (90+ jours)                               │    │
│  │  - Public access: Bloqué (sauf quelques fichiers)      │    │
│  └────────────────────────────────────────────────────────┘    │
│                                                                  │
│  ┌────────────────────────────────────────────────────────┐    │
│  │  Bucket 2: sirrdev-todo-backups                        │    │
│  │  - Sauvegardes base de données                         │    │
│  │  - Versioning: Activé                                  │    │
│  │  - Encryption: AES-256                                 │    │
│  │  - Lifecycle:                                          │    │
│  │    * Standard (0-7 jours)                              │    │
│  │    * Glacier Instant Retrieval (7-30 jours)            │    │
│  │    * Glacier Deep Archive (30+ jours)                  │    │
│  │  - Public access: Complètement bloqué                  │    │
│  └────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

Automatisation :
├── Lambda Function (quotidienne) -> Backup DB vers S3
└── EventBridge Rule -> Déclenche Lambda tous les jours à 2h
```

### Contraintes techniques

- Région : `eu-west-1` (même que l'Exercice 1)
- Noms de buckets : Uniques globalement
- Encryption : Server-Side Encryption (SSE-S3)
- SDK : boto3 (Python)
- Budget : Rester dans le Free Tier autant que possible
- Temps estimé : 4-5 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre S3 en profondeur (buckets, objets, métadonnées)
- [OK] Créer et configurer des buckets S3
- [OK] Uploader/télécharger des objets via console, CLI et SDK
- [OK] Activer et gérer le versioning
- [OK] Configurer des lifecycle policies (transition entre classes)
- [OK] Mettre en place l'encryption au repos
- [OK] Gérer les permissions (IAM policies, bucket policies, ACLs)
- [OK] Intégrer S3 avec une application Flask (boto3)
- [OK] Créer des sauvegardes automatiques
- [OK] Optimiser les coûts avec les classes de stockage appropriées
- [OK] Monitorer l'utilisation de S3
- [OK] Comprendre Glacier et l'archivage long terme

---

## [DOCS] PRÉREQUIS

- Exercice 1 terminé (instance EC2 + Flask app)
- Compte AWS avec Free Tier actif
- Connaissances Python de base
- Compréhension des concepts de stockage objet

---

## [ARGENT] ESTIMATION DES COÛTS

**S3 Free Tier (12 premiers mois) :**

| Service | Quota gratuit | Après Free Tier |
|---------|---------------|-----------------|
| Stockage Standard | 5 GB | 0.023$/GB-mois |
| Requêtes PUT/POST | 2,000 | 0.005$/1000 requêtes |
| Requêtes GET | 20,000 | 0.0004$/1000 requêtes |
| Data transfer OUT | 15 GB/mois | 0.09$/GB |
| Glacier storage | N/A | 0.004$/GB-mois |
| Glacier Deep Archive | N/A | 0.00099$/GB-mois |

**Scénario de cet exercice :**
```
Uploads utilisateurs : ~500 MB
Backups quotidiens : ~10 MB × 365 jours = ~3.6 GB/an
Total : ~4 GB -> Dans le Free Tier [OK]

Coût estimé : 0$/mois (12 premiers mois)
Après Free Tier : ~0.15$/mois
```

**Classes de stockage S3 :**

| Classe | Cas d'usage | Prix stockage | Prix récupération | Latence |
|--------|-------------|---------------|-------------------|---------|
| **Standard** | Données fréquemment accédées | 0.023$/GB | Gratuit | ms |
| **Standard-IA** | Données rarement accédées | 0.0125$/GB | 0.01$/GB | ms |
| **One Zone-IA** | Données non-critiques | 0.01$/GB | 0.01$/GB | ms |
| **Glacier Instant** | Archives avec accès rare | 0.004$/GB | 0.03$/GB | ms |
| **Glacier Flexible** | Archives long terme | 0.0036$/GB | Variable | minutes-heures |
| **Glacier Deep Archive** | Archives très long terme | 0.00099$/GB | 0.02$/GB | 12-48h |

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Comprendre Amazon S3

#### Qu'est-ce que S3 ?

**S3 = Simple Storage Service**

**Stockage objet** (pas block storage comme EBS)

**Analogie :**
- **S3 = Google Drive / Dropbox** (stockage de fichiers dans le cloud)
- **EBS = Disque dur d'un ordinateur** (attaché à une instance)
- **EFS = Dossier partagé réseau** (monté par plusieurs instances)

---

#### Concepts fondamentaux

**1. Bucket**

Conteneur pour stocker des objets

**Caractéristiques :**
- Nom **unique globalement** (dans tout AWS, pas juste ton compte)
- Créé dans une région spécifique (eu-west-1)
- Pas de limite de taille (stockage infini)
- Pas de hiérarchie (flat structure)

**Limites :**
- 100 buckets par compte (par défaut, peut être augmenté)
- Nom : 3-63 caractères, minuscules, tirets autorisés

---

**2. Objet**

Fichier stocké dans un bucket

**Composants d'un objet :**
```
Key (chemin) : "photos/2024/vacation.jpg"
Value (données) : Le fichier binaire
Metadata : Content-Type, taille, date, etc.
Version ID : Si versioning activé
```

**Caractéristiques :**
- Taille : 0 bytes à 5 TB (par objet)
- Key = chemin complet (simule une hiérarchie)
- Métadonnées : Key-value pairs

---

**3. Hiérarchie simulée avec les "préfixes"**

**S3 n'a PAS de dossiers réels !**

```
Bucket: mon-bucket
├── fichier1.txt                  (key: "fichier1.txt")
├── dossier/fichier2.txt          (key: "dossier/fichier2.txt")
└── photos/2024/vacances/img.jpg  (key: "photos/2024/vacances/img.jpg")
```

**C'est juste une illusion visuelle dans la console.**

**En réalité : 3 objets avec des keys différents.**

**Pourquoi c'est important ?**
- Listing : `aws s3 ls s3://mon-bucket/photos/` -> Liste les objets avec préfixe "photos/"
- Performance : Pas de limite de "fichiers par dossier"

---

**4. S3 vs autres systèmes de fichiers**

| Critère | S3 (Objet) | EBS (Block) | EFS (File) |
|---------|------------|-------------|------------|
| **Type** | Stockage objet | Disque bloc | Système fichiers |
| **Attachement** | Via API/SDK | 1 instance | Plusieurs instances |
| **Accès** | HTTP/HTTPS | POSIX | NFS |
| **Durabilité** | 99.999999999% (11 nines) | 99.999% | 99.999999999% |
| **Coût** | ~0.023$/GB | ~0.08$/GB | ~0.30$/GB |
| **Usage** | Fichiers, backups, static | OS, DB | Fichiers partagés |

---

#### Durabilité vs Disponibilité

**Durabilité** = Probabilité de NE PAS perdre un objet

```
S3 Standard : 99.999999999% (11 nines)
= 1 objet perdu tous les 10 millions d'années pour 10M objets
```

**Comment ?**
- Réplication automatique sur **3+ zones de disponibilité**
- Checksums pour détecter corruption
- Auto-réparation

---

**Disponibilité** = Pourcentage de temps où le service est accessible

```
S3 Standard : 99.99%
= ~52 minutes d'indisponibilité par an
```

---

### ÉTAPE 2 : Installer et configurer AWS CLI

**Pourquoi AWS CLI ?**
- Automatisation (scripts)
- Plus rapide que la console pour certaines opérations
- Nécessaire pour beaucoup de commandes avancées

---

#### Installer AWS CLI sur l'instance EC2

**Se connecter en SSH à l'instance :**

```bash
ssh -i ~/.ssh/sirrdev-ec2-key.pem ubuntu@54.194.78.90
```

**(Remplace par ton Elastic IP)**

---

**Installer AWS CLI v2 :**

```bash
# Télécharger l'installateur
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"

# Installer unzip si nécessaire
sudo apt install unzip -y

# Décompresser
unzip awscliv2.zip

# Installer
sudo ./aws/install

# Vérifier l'installation
aws --version
```

**Résultat :**

```
aws-cli/2.15.0 Python/3.11.6 Linux/6.2.0-1018-aws exe/x86_64.ubuntu.22
```

**[OK] AWS CLI installé !**

---

#### Configurer AWS CLI (MAUVAISE méthode - on utilisera IAM Role)

**[ATTENTION] NE PAS FAIRE ÇA (je montre pour expliquer pourquoi) :**

```bash
# [X] MAUVAISE PRATIQUE
aws configure

AWS Access Key ID: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
```

**Problèmes :**
- Clés en clair dans `~/.aws/credentials`
- Risque de commit dans Git
- Rotation manuelle des clés
- Si l'instance est compromise -> Toutes les permissions sont volées

---

**[OK] BONNE PRATIQUE : IAM Role**

**Un rôle IAM pour l'instance EC2 avec permissions S3**

**Avantages :**
- Pas de clés à gérer
- Rotation automatique des credentials
- Permissions granulaires
- Révocation facile

---

### ÉTAPE 3 : Créer un IAM Role pour EC2

#### Créer le rôle

**1. Console IAM -> Roles -> Create role**

```
Trusted entity type: AWS service
Use case: EC2
```

**Cliquer "Next"**

---

**2. Add permissions**

**Pour l'instant, on va donner accès complet S3 (on affinera après) :**

```
[x] AmazonS3FullAccess
```

**Cliquer "Next"**

---

**3. Name, review, and create**

```
Role name: EC2-S3-Full-Access
Description: Allow EC2 instances to access S3 buckets

Tags (optional):
  Environment: Development
  Project: TodoApp
```

**Cliquer "Create role"**

**[OK] Rôle créé !**

---

#### Attacher le rôle à l'instance EC2

**1. Console EC2 -> Instances -> sirrdev-flask-server**

**2. Actions -> Security -> Modify IAM role**

```
IAM role: EC2-S3-Full-Access
```

**Cliquer "Update IAM role"**

**[OK] Rôle attaché !**

---

**3. Vérifier sur l'instance**

**Dans SSH :**

```bash
# Tester l'accès S3 sans configuration
aws s3 ls
```

**Si aucun bucket n'existe encore :**

```
(Rien ne s'affiche, mais pas d'erreur d'authentification)
```

**Si erreur :**

```
Unable to locate credentials. You can configure credentials by running "aws configure".
```

**-> Le rôle n'est pas correctement attaché, réessayer.**

---

**Explication de la "magie" :**

**Quand tu fais `aws s3 ls` :**

1. AWS CLI cherche des credentials dans cet ordre :
   - Variables d'environnement
   - `~/.aws/credentials`
   - **IAM Role de l'instance (metadata service)**

2. L'instance contacte le service de métadonnées :
   ```
   http://169.254.169.254/latest/meta-data/iam/security-credentials/EC2-S3-Full-Access
   ```

3. Ce service retourne des credentials temporaires :
   ```json
   {
     "AccessKeyId": "ASIA...",
     "SecretAccessKey": "...",
     "Token": "...",
     "Expiration": "2024-12-16T18:00:00Z"
   }
   ```

4. Ces credentials sont valides quelques heures, puis renouvelés automatiquement

**Résultat : Accès S3 sans aucune clé en dur ! [OK]**

---

### ÉTAPE 4 : Créer le premier bucket S3

#### Nommage du bucket

**Contraintes de nommage :**
- 3 à 63 caractères
- Minuscules, chiffres, tirets `-` uniquement
- Pas de points `.` (sauf pour sites web statiques)
- Doit commencer par une lettre ou un chiffre
- **Unique GLOBALEMENT dans tout AWS**

**Exemple de noms :**
```
[OK] sirrdev-todo-uploads
[OK] my-company-backups-2024
[OK] data-analytics-prod-eu

[X] SirrDev-Uploads (majuscules)
[X] sirrdev_uploads (underscore)
[X] -sirrdev-uploads (commence par tiret)
[X] myapp (trop court, probablement déjà pris)
```

**Convention recommandée :**
```
{organisation}-{projet}-{usage}-{environnement}-{region}

Exemples :
sirrdev-todo-uploads-dev-eu
acme-corp-backups-prod-us
```

---

#### Créer le bucket via la console

**1. Console S3 -> Buckets -> Create bucket**

```
Bucket name: sirrdev-todo-uploads
  (Remplace "sirrdev" par ton nom/entreprise)

AWS Region: EU (Ireland) eu-west-1
  (Même région que l'instance EC2)
```

**Pourquoi choisir cette région ?**
- Latence minimale (proche de l'instance)
- Compliance (RGPD pour données européennes)
- Coûts de transfert (transfert gratuit dans la même région)

---

**2. Object Ownership**

```
[x] ACLs disabled (recommended)
  Bucket owner enforced
```

**Explication :**

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

**2 modes :**

**ACLs disabled (recommandé) :**
- Permissions gérées uniquement via IAM + Bucket Policies
- Plus simple et sécurisé
- Toujours "bucket owner" propriétaire des objets

**ACLs enabled :**
- Permet à d'autres comptes AWS de gérer les permissions
- Complexe, risques de sécurité
- Utilisé pour cas très spécifiques (logs CloudFront, etc.)

**Pour cet exercice : ACLs disabled [OK]**

---

**3. Block Public Access settings**

```
[x] Block all public access

  [x] Block public access to buckets and objects granted through new ACLs
  [x] Block public access to buckets and objects granted through any ACLs
  [x] Block public access to buckets and objects granted through new public bucket or access point policies
  [x] Block public and cross-account access to buckets and objects through any public bucket or access point policies
```

**Explication :**

**4 niveaux de protection contre l'accès public accidentel**

**Pourquoi bloquer par défaut ?**
- Évite les fuites de données (très fréquentes)
- Principe du moindre privilège
- On débloquera de manière granulaire si nécessaire

**Cas réels de fuites S3 :**
- Capital One (2019) : 100M de clients exposés
- Uber (2016) : 57M d'utilisateurs
- Dow Jones (2017) : 2.2M de clients

**Toutes dues à des buckets publics non intentionnels !**

**Pour cet exercice : Tout bloquer [OK]**

**(On verra comment rendre certains fichiers publics plus tard)**

---

**4. Bucket Versioning**

```
[x] Enable
```

**Explication :**

**Versioning = Garder l'historique de toutes les modifications**

**Exemple :**

```
Upload : fichier.txt (version 1)
Modifie : fichier.txt (version 2, version 1 conservée)
Supprime : fichier.txt (marqueur de suppression, versions 1 et 2 conservées)
```

**Avantages :**
- Protection contre suppression accidentelle
- Récupération de versions antérieures
- Audit trail (historique des changements)

**Inconvénients :**
- Coût : Chaque version stockée = facturée
- Espace : Peut croître rapidement si modifications fréquentes

**Quand activer ?**
- [OK] Données critiques (backups, configuration)
- [OK] Collaboration (plusieurs personnes modifient)
- [X] Données temporaires (logs, cache)
- [X] Très gros fichiers rarement modifiés

**Pour cet exercice : Enable [OK]**

---

**5. Tags (optionnel)**

```
Key: Environment | Value: Development
Key: Project | Value: TodoApp
Key: Owner | Value: sirrdev
```

**Utilité des tags :**
- Facturation par projet/environnement
- Automatisation (scripts agissant sur tags)
- Recherche et organisation

---

**6. Default encryption**

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

Bucket Key: Enable
```

**Explication :**

**3 types d'encryption S3 :**

| Type | Gestion des clés | Coût | Contrôle |
|------|-----------------|------|----------|
| **SSE-S3** | AWS gère tout | Gratuit | Aucun |
| **SSE-KMS** | AWS KMS (Key Management Service) | 0.03$/10k requêtes | Élevé |
| **SSE-C** | Tu fournis la clé à chaque requête | Gratuit | Total |

**SSE-S3 (choix pour cet exercice) :**
- AWS génère une clé unique par objet
- Encryption AES-256
- Clés gérées automatiquement
- Transparent (aucune action de ta part)

**SSE-KMS (cas avancés) :**
- Plus de contrôle (rotation, audit, révocation)
- Intégration CloudTrail (logs de chaque accès)
- Coûteux pour gros volume de requêtes

**SSE-C (rare) :**
- Tu fournis ta propre clé de chiffrement
- AWS ne stocke JAMAIS la clé
- Complexe à gérer

**Bucket Key :**
- Réduit les coûts KMS (si utilisé)
- Recommandé : Enable

**Pour cet exercice : SSE-S3 + Bucket Key enabled [OK]**

---

**7. Advanced settings (laisser par défaut pour l'instant)**

```
Object Lock: Disable
  (WORM - Write Once Read Many - pour conformité légale)
```

---

**8. Cliquer "Create bucket"**

**[OK] Bucket créé !**

```
sirrdev-todo-uploads

Region: eu-west-1
Creation date: December 16, 2024
```

---

#### Créer le bucket de backups (même processus)

**Répéter pour créer :**

```
Bucket name: sirrdev-todo-backups
Region: eu-west-1
ACLs: Disabled
Block public access: Tout bloquer [x]
Versioning: Enable [x]
Encryption: SSE-S3 [x]
```

**[OK] 2 buckets créés !**

---

### ÉTAPE 5 : Uploader des objets dans S3

#### Via la console

**1. S3 -> Buckets -> sirrdev-todo-uploads -> Upload**

**2. Add files ou Add folder**

**Créer un fichier de test sur ton PC :**

```bash
# Sur ton PC (pas l'instance EC2)
echo "Ceci est un fichier de test pour S3" > test-upload.txt
```

**Drag & drop ou Browse pour sélectionner `test-upload.txt`**

---

**3. Destination**

```
Destination bucket: sirrdev-todo-uploads
Destination folder: (laisser vide ou taper "tests/")
```

**Si tu mets "tests/" -> Le fichier sera stocké avec la clé `tests/test-upload.txt`**

---

**4. Permissions (laisser par défaut)**

```
Predefined ACLs: (None)
Access control list (ACL): (Disabled)
```

---

**5. Properties (optionnel)**

```
Storage class: Standard (défaut)

Server-side encryption: Inherit from bucket
  (Utilise SSE-S3 configuré précédemment)

Object tags: (optionnel)
  Key: Type | Value: Test
```

---

**6. Cliquer "Upload"**

**Résultat :**

```
[OK] Upload succeeded
  test-upload.txt
  Size: 39 bytes
```

**Cliquer "Close"**

---

**7. Vérifier dans la liste des objets**

```
Name: test-upload.txt
Type: txt
Size: 39 B
Storage class: Standard
Last modified: 2024-12-16 ...
```

**Cliquer sur le nom du fichier pour voir les détails**

---

**8. Obtenir l'URL de l'objet**

**Onglet "Properties" -> Object URL :**

```
https://sirrdev-todo-uploads.s3.eu-west-1.amazonaws.com/test-upload.txt
```

**Essayer d'accéder à cette URL dans le navigateur :**

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

**C'est normal ! Public access est bloqué.**

---

#### Via AWS CLI (depuis l'instance EC2)

**Se connecter en SSH à l'instance :**

```bash
ssh -i ~/.ssh/sirrdev-ec2-key.pem ubuntu@54.194.78.90
```

---

**Créer un fichier de test :**

```bash
echo "Upload depuis EC2 via AWS CLI" > test-cli.txt
```

---

**Uploader vers S3 :**

```bash
aws s3 cp test-cli.txt s3://sirrdev-todo-uploads/
```

**Résultat :**

```
upload: ./test-cli.txt to s3://sirrdev-todo-uploads/test-cli.txt
```

**[OK] Fichier uploadé !**

---

**Lister les objets du bucket :**

```bash
aws s3 ls s3://sirrdev-todo-uploads/
```

**Résultat :**

```
2024-12-16 10:30:00         39 test-upload.txt
2024-12-16 10:35:00         32 test-cli.txt
```

---

**Uploader dans un "dossier" (préfixe) :**

```bash
aws s3 cp test-cli.txt s3://sirrdev-todo-uploads/backups/cli-test.txt
```

---

**Lister récursivement :**

```bash
aws s3 ls s3://sirrdev-todo-uploads/ --recursive
```

**Résultat :**

```
2024-12-16 10:30:00         39 test-upload.txt
2024-12-16 10:35:00         32 test-cli.txt
2024-12-16 10:40:00         32 backups/cli-test.txt
```

---

**Télécharger un fichier depuis S3 :**

```bash
aws s3 cp s3://sirrdev-todo-uploads/test-upload.txt downloaded.txt

cat downloaded.txt
```

**Résultat :**

```
Ceci est un fichier de test pour S3
```

**[OK] Download fonctionne !**

---

**Synchroniser un dossier local vers S3 :**

```bash
# Créer un dossier avec plusieurs fichiers
mkdir local-folder
echo "Fichier 1" > local-folder/file1.txt
echo "Fichier 2" > local-folder/file2.txt
echo "Fichier 3" > local-folder/file3.txt

# Synchroniser vers S3
aws s3 sync local-folder/ s3://sirrdev-todo-uploads/synced/
```

**Résultat :**

```
upload: local-folder/file1.txt to s3://sirrdev-todo-uploads/synced/file1.txt
upload: local-folder/file2.txt to s3://sirrdev-todo-uploads/synced/file2.txt
upload: local-folder/file3.txt to s3://sirrdev-todo-uploads/synced/file3.txt
```

**`sync` = Upload seulement les fichiers nouveaux/modifiés (intelligent)**

---

**Supprimer un objet :**

```bash
aws s3 rm s3://sirrdev-todo-uploads/test-cli.txt
```

**Résultat :**

```
delete: s3://sirrdev-todo-uploads/test-cli.txt
```

---

**Supprimer récursivement :**

```bash
aws s3 rm s3://sirrdev-todo-uploads/synced/ --recursive
```

---

### ÉTAPE 6 : Intégrer S3 avec Flask (upload de fichiers)

#### Installer boto3 (AWS SDK pour Python)

**Sur l'instance EC2, dans l'environnement virtuel Flask :**

```bash
cd ~/flask-todo-app
source venv/bin/activate
pip install boto3
```

**Résultat :**

```
Collecting boto3
  Downloading boto3-1.34.15-py3-none-any.whl (...)
Collecting botocore>=1.34.15
  Downloading botocore-1.34.15-py3-none-any.whl (...)
...
Successfully installed boto3-1.34.15 botocore-1.34.15 ...
```

---

#### Modifier l'application Flask pour permettre l'upload

**Éditer `app.py` :**

```bash
nano app.py
```

**Ajouter au début du fichier (après les imports existants) :**

```python
import boto3
from werkzeug.utils import secure_filename
import os
from datetime import datetime
```

---

**Ajouter la configuration S3 (après `DATABASE = 'todo.db'`) :**

```python
# ───────────────────────────────────────────────────────────────
# CONFIGURATION S3
# ───────────────────────────────────────────────────────────────

S3_BUCKET = 'sirrdev-todo-uploads'
S3_REGION = 'eu-west-1'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx'}

# Initialiser le client S3
s3_client = boto3.client('s3', region_name=S3_REGION)

def allowed_file(filename):
    """Vérifier si l'extension du fichier est autorisée"""
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
```

**Explications :**

**`secure_filename()` :**
- Nettoie les noms de fichiers dangereux
- Exemple : `../../../etc/passwd` -> `etc_passwd`

**`S3_BUCKET` :**
- Nom du bucket (remplace par le tien si différent)

**`boto3.client('s3')` :**
- Crée un client S3 pour interagir avec l'API
- Utilise automatiquement le rôle IAM de l'instance (pas de clés !)

**`allowed_file()` :**
- Vérifie l'extension (sécurité)
- Bloque les fichiers exécutables (.exe, .sh, etc.)

---

**Modifier la base de données pour ajouter une colonne "attachment" :**

**Modifier la fonction `init_db()` :**

```python
def init_db():
    """Initialiser la base de données (créer la table)"""
    conn = get_db()
    conn.execute('''
        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            description TEXT,
            completed BOOLEAN NOT NULL DEFAULT 0,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            attachment_key TEXT,
            attachment_url TEXT
        )
    ''')
    conn.commit()
    conn.close()
```

**Explications des nouvelles colonnes :**
- `attachment_key` : Clé S3 du fichier (ex: "uploads/2024/file.pdf")
- `attachment_url` : URL présignée pour télécharger (temporaire)

---

**Ajouter une route pour uploader un fichier :**

```python
@app.route('/upload/<int:task_id>', methods=['POST'])
def upload_file(task_id):
    """Uploader un fichier attaché à une tâche"""
    
    # Vérifier qu'un fichier est présent
    if 'file' not in request.files:
        return redirect(url_for('index'))
    
    file = request.files['file']
    
    # Vérifier que le fichier n'est pas vide
    if file.filename == '':
        return redirect(url_for('index'))
    
    # Vérifier l'extension
    if file and allowed_file(file.filename):
        # Sécuriser le nom de fichier
        filename = secure_filename(file.filename)
        
        # Créer une clé S3 unique
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        s3_key = f"task-attachments/{task_id}/{timestamp}_{filename}"
        
        try:
            # Uploader vers S3
            s3_client.upload_fileobj(
                file,
                S3_BUCKET,
                s3_key,
                ExtraArgs={
                    'ContentType': file.content_type,
                    'ServerSideEncryption': 'AES256'
                }
            )
            
            # Mettre à jour la base de données
            conn = get_db()
            conn.execute(
                'UPDATE tasks SET attachment_key = ? WHERE id = ?',
                (s3_key, task_id)
            )
            conn.commit()
            conn.close()
            
        except Exception as e:
            print(f"Erreur S3: {e}")
            # En production : logger l'erreur
    
    return redirect(url_for('index'))
```

**Explications ligne par ligne :**

**`request.files['file']` :**
- Récupère le fichier uploadé depuis le formulaire HTML
- `'file'` = nom de l'input dans le HTML

**`secure_filename()` :**
```python
# Avant : "../../etc/passwd"
# Après : "etc_passwd"
```

**`s3_key = f"task-attachments/{task_id}/{timestamp}_{filename}"` :**
- Organise les fichiers par tâche
- Exemple : `task-attachments/5/20241216_103045_document.pdf`
- Timestamp évite les collisions de noms

**`s3_client.upload_fileobj()` :**
- Upload un objet de type file-like (le fichier reçu de Flask)
- `ExtraArgs` : Métadonnées supplémentaires

**`ContentType` :**
- Type MIME (image/png, application/pdf, etc.)
- Important pour que le navigateur affiche correctement

**`ServerSideEncryption: 'AES256'` :**
- Force l'encryption (même si bucket encryption activée)
- Bonne pratique : toujours chiffrer explicitement

---

**Ajouter une route pour télécharger/voir le fichier :**

```python
@app.route('/download/<int:task_id>')
def download_file(task_id):
    """Générer une URL présignée pour télécharger le fichier"""
    
    conn = get_db()
    task = conn.execute(
        'SELECT attachment_key FROM tasks WHERE id = ?',
        (task_id,)
    ).fetchone()
    conn.close()
    
    if task and task['attachment_key']:
        try:
            # Générer une URL présignée (valide 1 heure)
            presigned_url = s3_client.generate_presigned_url(
                'get_object',
                Params={
                    'Bucket': S3_BUCKET,
                    'Key': task['attachment_key']
                },
                ExpiresIn=3600  # 1 heure en secondes
            )
            
            return redirect(presigned_url)
            
        except Exception as e:
            print(f"Erreur génération URL: {e}")
            return "Erreur lors du téléchargement", 500
    
    return redirect(url_for('index'))
```

**Explications :**

**URL présignée (presigned URL) :**
- URL temporaire avec permissions intégrées
- Valide pendant une durée limitée (ici 1 heure)
- Permet d'accéder à un objet privé sans changer les permissions

**Exemple d'URL présignée :**
```
https://sirrdev-todo-uploads.s3.eu-west-1.amazonaws.com/task-attachments/5/file.pdf?
  X-Amz-Algorithm=AWS4-HMAC-SHA256&
  X-Amz-Credential=ASIA.../20241216/eu-west-1/s3/aws4_request&
  X-Amz-Date=20241216T103000Z&
  X-Amz-Expires=3600&
  X-Amz-SignedHeaders=host&
  X-Amz-Signature=abcd1234...
```

**Pourquoi utiliser des URLs présignées ?**
- Bucket reste privé (sécurisé)
- Accès temporaire (pas de permissions permanentes)
- Auditabilité (CloudTrail peut tracer les accès)

**Alternative (pas recommandée) :**
- Rendre le fichier public -> Risque de fuite
- ACL sur l'objet -> Complexe à gérer

---

**Modifier le template HTML pour ajouter l'upload :**

```bash
nano templates/index.html
```

**Dans le bloc de la tâche (`.task-item`), ajouter :**

```html
<div class="task-actions">
    {% if not task.completed %}
    <a href="/complete/{{ task.id }}">
        <button class="btn-small btn-success">[OK] Terminé</button>
    </a>
    {% endif %}
    
    <!-- NOUVEAU : Formulaire d'upload -->
    {% if not task.attachment_key %}
    <form action="/upload/{{ task.id }}" method="POST" enctype="multipart/form-data" style="display: inline-block; margin: 0;">
        <input type="file" name="file" id="file-{{ task.id }}" style="display: none;" onchange="this.form.submit()">
        <label for="file-{{ task.id }}">
            <button type="button" class="btn-small" style="background: #17a2b8;" onclick="document.getElementById('file-{{ task.id }}').click()">
                [PAPERCLIP] Joindre
            </button>
        </label>
    </form>
    {% else %}
    <a href="/download/{{ task.id }}">
        <button class="btn-small" style="background: #6c757d;">[ENTREE] Télécharger</button>
    </a>
    {% endif %}
    
    <a href="/delete/{{ task.id }}">
        <button class="btn-small btn-danger">[SUPPRIMER] Supprimer</button>
    </a>
</div>
```

**Explications :**

**`enctype="multipart/form-data"` :**
- **OBLIGATOIRE** pour uploader des fichiers
- Sans ça, seul le nom du fichier est envoyé (pas le contenu)

**`<input type="file" style="display: none;">` :**
- Cache l'input file par défaut (moche)
- Le label déclenche le clic sur l'input caché

**`onchange="this.form.submit()"` :**
- Soumet automatiquement le formulaire dès qu'un fichier est sélectionné
- Pas besoin de bouton "Upload"

**`{% if not task.attachment_key %}` :**
- Affiche "Joindre" si pas de fichier
- Affiche "Télécharger" si fichier déjà uploadé

---

**Sauvegarder et relancer Flask :**

```bash
# Arrêter Flask (Ctrl+C si en avant-plan, ou :)
screen -r flask-app
# Ctrl+C pour arrêter

# Relancer
python app.py
# Ctrl+A puis D pour détacher
```

---

**Tester dans le navigateur :**

```
http://54.194.78.90:5000
```

**Créer une nouvelle tâche, puis cliquer "[PAPERCLIP] Joindre"**

**Sélectionner un fichier (image, PDF, etc.)**

**Le fichier est uploadé automatiquement !**

**Cliquer "[ENTREE] Télécharger" pour récupérer le fichier**

**[OK] Upload/Download S3 fonctionnel dans Flask !**

---

### ÉTAPE 7 : Configurer Lifecycle Policies

#### Comprendre les Lifecycle Policies

**Lifecycle Policy = Règles automatiques pour gérer le cycle de vie des objets**

**Actions possibles :**
1. **Transition** : Déplacer vers une classe de stockage moins chère
2. **Expiration** : Supprimer après X jours

**Exemple de scénario :**

```
Jour 0 : Upload fichier -> S3 Standard
Jour 30 : Transition -> S3 Standard-IA (moins cher)
Jour 90 : Transition -> Glacier Flexible Retrieval (très peu cher)
Jour 365 : Expiration -> Suppression définitive
```

**Cas d'usage :**
- Logs : Standard 7 jours -> IA 30 jours -> Supprimer 90 jours
- Backups : Standard 7 jours -> Glacier 30 jours -> Conserver 5 ans
- Données analytics : Standard 30 jours -> IA indéfiniment

---

#### Créer une Lifecycle Policy pour les uploads

**1. S3 -> Buckets -> sirrdev-todo-uploads -> Management -> Create lifecycle rule**

```
Lifecycle rule name: transition-to-glacier

[x] Apply to all objects in the bucket
  (Ou définir un filtre par préfixe/tags)
```

---

**2. Lifecycle rule actions**

```
[x] Transition current versions of objects between storage classes
[x] Transition noncurrent versions of objects between storage classes
[x] Permanently delete noncurrent versions of objects
```

**Explications :**

**Current versions** : Dernière version de chaque objet
**Noncurrent versions** : Anciennes versions (si versioning activé)

---

**3. Transition current versions**

```
Transition to Standard-IA:
  Days after object creation: 30

Transition to Glacier Flexible Retrieval:
  Days after object creation: 90

Transition to Glacier Deep Archive:
  Days after object creation: 365
```

**Schéma :**

```
Upload -> S3 Standard (0-29 jours) -> 0.023$/GB-mois
  v
30 jours -> S3 Standard-IA -> 0.0125$/GB-mois
  v
90 jours -> Glacier Flexible -> 0.0036$/GB-mois
  v
365 jours -> Glacier Deep Archive -> 0.00099$/GB-mois
```

**Économies :**
- Après 30 jours : ~46% moins cher
- Après 90 jours : ~84% moins cher
- Après 1 an : ~96% moins cher

---

**4. Transition noncurrent versions (anciennes versions)**

```
Transition to Glacier Flexible Retrieval:
  Days after objects become noncurrent: 30

Permanently delete:
  Days after objects become noncurrent: 90
```

**Explications :**

**Noncurrent = Quand une nouvelle version est uploadée, l'ancienne devient "noncurrent"**

**Exemple :**
```
Jour 0 : Upload file.txt (v1)
Jour 10 : Upload file.txt (v2) -> v1 devient noncurrent
Jour 40 : v1 passe en Glacier (30 jours après devenir noncurrent)
Jour 100 : v1 est supprimée (90 jours après devenir noncurrent)
```

**Pourquoi supprimer les anciennes versions ?**
- Coûts : Chaque version = stockage facturé
- Utile : Garder 2-3 mois d'historique suffisant

---

**5. Cliquer "Create rule"**

**[OK] Lifecycle policy créée !**

---

**Vérifier la policy :**

```
Management -> Lifecycle rules -> transition-to-glacier

Status: Enabled [OK]
```

---

#### Créer une Lifecycle Policy pour les backups

**Pour le bucket `sirrdev-todo-backups` :**

```
Lifecycle rule name: backup-archival

Apply to: All objects

Transition current versions:
  Standard-IA: 7 days
  Glacier Instant Retrieval: 30 days
  Glacier Deep Archive: 90 days

Permanently delete current versions: 1825 days (5 ans)

Transition noncurrent versions:
  Glacier Deep Archive: 1 day

Permanently delete noncurrent versions: 30 days
```

**Logique :**
- Backups récents (7 jours) : Accès rapide
- Backups moyens (30 jours) : Archivés mais récupérables rapidement
- Backups anciens (90+ jours) : Archivage ultra-économique
- Après 5 ans : Suppression (conformité/loi)

---

### ÉTAPE 8 : Créer des sauvegardes automatiques de la base

#### Script de backup

**Sur l'instance EC2, créer un script de backup :**

```bash
nano ~/backup-db.sh
```

**Contenu :**

```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# SCRIPT DE BACKUP DE LA BASE DE DONNÉES VERS S3
# ═══════════════════════════════════════════════════════════════

# Configuration
DB_PATH="/home/ubuntu/flask-todo-app/todo.db"
S3_BUCKET="sirrdev-todo-backups"
BACKUP_PREFIX="daily-backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="todo_backup_${TIMESTAMP}.db"

# Créer le backup
echo "[$(date)] Début du backup..."

# Copier la base de données
cp "$DB_PATH" "/tmp/$BACKUP_FILE"

# Uploader vers S3
aws s3 cp "/tmp/$BACKUP_FILE" "s3://${S3_BUCKET}/${BACKUP_PREFIX}/${BACKUP_FILE}" \
  --storage-class STANDARD \
  --server-side-encryption AES256

# Vérifier le succès
if [ $? -eq 0 ]; then
    echo "[$(date)] [OK] Backup réussi : ${BACKUP_FILE}"
    # Supprimer le fichier temporaire
    rm "/tmp/$BACKUP_FILE"
else
    echo "[$(date)] [X] Erreur lors du backup"
    exit 1
fi

# Lister les backups existants
echo "[$(date)] Backups existants :"
aws s3 ls "s3://${S3_BUCKET}/${BACKUP_PREFIX}/" | tail -5

echo "[$(date)] Backup terminé."
```

**Rendre le script exécutable :**

```bash
chmod +x ~/backup-db.sh
```

---

**Tester le script :**

```bash
~/backup-db.sh
```

**Résultat :**

```
[Mon Dec 16 12:00:00 UTC 2024] Début du backup...
upload: /tmp/todo_backup_20241216_120000.db to s3://sirrdev-todo-backups/daily-backups/todo_backup_20241216_120000.db
[Mon Dec 16 12:00:01 UTC 2024] [OK] Backup réussi : todo_backup_20241216_120000.db
[Mon Dec 16 12:00:02 UTC 2024] Backups existants :
2024-12-16 12:00:00       8192 todo_backup_20241216_120000.db
[Mon Dec 16 12:00:02 UTC 2024] Backup terminé.
```

**[OK] Backup fonctionne !**

---

**Vérifier dans S3 :**

```bash
aws s3 ls s3://sirrdev-todo-backups/daily-backups/
```

**Ou dans la console S3 -> sirrdev-todo-backups -> daily-backups/**

---

#### Automatiser avec cron

**Éditer la crontab :**

```bash
crontab -e
```

**Sélectionner nano (option 1 si demandé)**

---

**Ajouter à la fin :**

```cron
# Backup de la base de données tous les jours à 2h du matin (UTC)
0 2 * * * /home/ubuntu/backup-db.sh >> /home/ubuntu/backup.log 2>&1
```

**Explication du format cron :**

```
┌───────────── Minute (0-59)
│ ┌───────────── Heure (0-23)
│ │ ┌───────────── Jour du mois (1-31)
│ │ │ ┌───────────── Mois (1-12)
│ │ │ │ ┌───────────── Jour de la semaine (0-7, 0 et 7 = dimanche)
│ │ │ │ │
│ │ │ │ │
0 2 * * *

Exemples :
0 2 * * *     -> Tous les jours à 2h
0 */6 * * *   -> Toutes les 6 heures
0 0 * * 0     -> Tous les dimanches à minuit
0 0 1 * *     -> Le 1er de chaque mois à minuit
```

**`>> /home/ubuntu/backup.log 2>&1` :**
- `>>` : Ajoute à la fin du fichier (append)
- `2>&1` : Redirige stderr vers stdout (toutes les erreurs dans le log)

---

**Sauvegarder et quitter (Ctrl+O, Entrée, Ctrl+X)**

---

**Vérifier que le cron est bien enregistré :**

```bash
crontab -l
```

**Résultat :**

```
0 2 * * * /home/ubuntu/backup-db.sh >> /home/ubuntu/backup.log 2>&1
```

---

**Tester le cron manuellement (sans attendre 2h) :**

```bash
# Exécuter la commande cron directement
/home/ubuntu/backup-db.sh >> /home/ubuntu/backup.log 2>&1

# Vérifier le log
cat /home/ubuntu/backup.log
```

**[OK] Backup automatique configuré !**

---

**Le backup s'exécutera automatiquement tous les jours à 2h UTC.**

**Avec la Lifecycle Policy créée précédemment :**
- Jour 7 : Transition vers Standard-IA
- Jour 30 : Transition vers Glacier Instant
- Jour 90 : Transition vers Glacier Deep Archive
- Après 5 ans : Suppression

---

### ÉTAPE 9 : Configurer les permissions S3 (Bucket Policies)

#### Comprendre les 3 types de permissions S3

**1. IAM Policies** (vu dans Exercice 1)
- Attachées à un utilisateur/rôle/groupe IAM
- Contrôle **qui** peut accéder à S3
- Exemple : "L'utilisateur Alice peut lire/écrire dans tout S3"

**2. Bucket Policies**
- Attachées à un bucket S3
- Contrôle l'accès au **bucket** depuis n'importe quelle identité
- Exemple : "Tout le monde peut lire les objets dans le dossier /public/"

**3. Access Control Lists (ACLs)** (ancien, déconseillé)
- Permissions granulaires par objet
- Complexe, éviter

---

**Quand utiliser quoi ?**

| Scénario | Solution |
|----------|----------|
| Donner accès S3 à un utilisateur IAM | IAM Policy |
| Rendre un dossier public | Bucket Policy |
| Partager avec un autre compte AWS | Bucket Policy |
| Logs CloudFront, ELB | Bucket Policy (service AWS) |
| Cross-account access | Bucket Policy + IAM |

---

#### Rendre un dossier public

**Scénario : On veut que les fichiers dans `public-files/` soient accessibles par tout le monde**

**1. Débloquer l'accès public sur le bucket**

**S3 -> sirrdev-todo-uploads -> Permissions -> Block public access -> Edit**

```
[x] Block all public access

  [ ] Block public access to buckets and objects granted through new public bucket or access point policies
  
  (Décocher seulement cette case)
```

**[ATTENTION] Confirmation requise :**

```
Type "confirm" to confirm
```

**Taper : `confirm`**

**Cliquer "Save changes"**

---

**2. Créer une Bucket Policy**

**Permissions -> Bucket policy -> Edit**

**Ajouter cette policy :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::sirrdev-todo-uploads/public-files/*"
    }
  ]
}
```

**[ATTENTION] Remplace `sirrdev-todo-uploads` par ton nom de bucket !**

---

**Explication ligne par ligne :**

**`"Version": "2012-10-17"` :**
- Version du langage de policy AWS
- Toujours utiliser "2012-10-17" (la plus récente)

**`"Statement": [...]` :**
- Liste de déclarations (règles)
- Chaque statement = une permission

**`"Sid": "PublicReadGetObject"` :**
- Statement ID (identifiant)
- Optionnel, pour lisibilité
- Peut être n'importe quelle chaîne

**`"Effect": "Allow"` :**
- Action autorisée (Allow) ou interdite (Deny)
- Deny > Allow (priorité)

**`"Principal": "*"` :**
- À qui s'applique la policy
- `"*"` = Tout le monde (anonyme inclus)
- Autres exemples :
  - `"AWS": "arn:aws:iam::123456789012:user/Alice"` (utilisateur spécifique)
  - `"Service": "cloudfront.amazonaws.com"` (service AWS)

**`"Action": "s3:GetObject"` :**
- Action autorisée
- `s3:GetObject` = Lire (télécharger) un objet
- Autres actions S3 :
  - `s3:PutObject` = Écrire
  - `s3:DeleteObject` = Supprimer
  - `s3:ListBucket` = Lister les objets
  - `s3:*` = Toutes les actions

**`"Resource": "arn:aws:s3:::sirrdev-todo-uploads/public-files/*"` :**
- Sur quelles ressources s'applique la policy
- ARN = Amazon Resource Name (identifiant unique)
- Format : `arn:aws:s3:::nom-bucket/chemin/*`
- `/*` = Tous les objets dans ce "dossier"

**Signification globale :**
"Autoriser tout le monde à télécharger les objets dans `public-files/`"

---

**Cliquer "Save changes"**

**[OK] Bucket Policy créée !**

---

**3. Tester l'accès public**

**Uploader un fichier dans `public-files/` :**

```bash
# Sur l'instance EC2
echo "Ceci est un fichier public" > public-test.txt

aws s3 cp public-test.txt s3://sirrdev-todo-uploads/public-files/public-test.txt
```

---

**Obtenir l'URL :**

```
https://sirrdev-todo-uploads.s3.eu-west-1.amazonaws.com/public-files/public-test.txt
```

**Ou via CLI :**

```bash
aws s3 presign s3://sirrdev-todo-uploads/public-files/public-test.txt --expires-in 0
```

**(Avec `--expires-in 0`, génère l'URL sans signature = URL publique)**

---

**Accéder dans le navigateur (sans authentification) :**

```
Ceci est un fichier public
```

**[OK] Fichier accessible publiquement !**

---

**Vérifier qu'un fichier hors de `public-files/` reste privé :**

```
https://sirrdev-todo-uploads.s3.eu-west-1.amazonaws.com/test-upload.txt

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

**[OK] Seul `public-files/` est public, le reste est protégé !**

---

#### Exemple de Bucket Policy : Restreindre par IP

**Scénario : Autoriser l'accès seulement depuis ton bureau (IP fixe)**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowFromOfficeIP",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::sirrdev-todo-backups/*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "203.0.113.0/24"
        }
      }
    }
  ]
}
```

**`"Condition"` :**
- Conditions supplémentaires pour appliquer la policy
- Ici : Seulement si l'IP source est dans `203.0.113.0/24`

**Remplace `203.0.113.0/24` par l'IP de ton bureau/VPN**

---

#### Exemple : Permettre l'accès cross-account

**Scénario : Le compte AWS `111122223333` doit pouvoir lire les backups**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCrossAccountRead",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:root"
      },
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::sirrdev-todo-backups",
        "arn:aws:s3:::sirrdev-todo-backups/*"
      ]
    }
  ]
}
```

**`"arn:aws:iam::111122223333:root"` :**
- Compte AWS complet (root)
- Tous les utilisateurs/rôles de ce compte peuvent accéder

**2 ressources :**
- `arn:aws:s3:::bucket` (sans /*) = Le bucket lui-même (pour ListBucket)
- `arn:aws:s3:::bucket/*` (avec /*) = Les objets dans le bucket

---

### ÉTAPE 10 : Monitorer l'utilisation de S3

#### CloudWatch Metrics pour S3

**S3 publie automatiquement des métriques dans CloudWatch :**

**1. CloudWatch -> Metrics -> S3**

**Métriques disponibles :**
- `BucketSizeBytes` : Taille totale du bucket
- `NumberOfObjects` : Nombre d'objets
- `AllRequests` : Nombre total de requêtes
- `GetRequests`, `PutRequests`, etc.

---

**2. Activer les métriques de requêtes (optionnel, coûte 0.01$/1000 requêtes)**

**S3 -> Bucket -> Metrics -> Request metrics -> Create filter**

```
Filter name: all-objects
Filter scope: This filter applies to all objects

[x] Enable request metrics
```

**Permet de voir :**
- Requêtes par seconde
- Latence (4xxErrors, 5xxErrors)
- Bytes downloaded/uploaded

---

#### S3 Storage Lens (recommandé)

**Dashboard centralisé pour toute l'utilisation S3**

**S3 -> Storage Lens -> Create dashboard**

```
Dashboard name: my-storage-overview
Scope: Account

[x] Free metrics (activated by default)
  - Storage metrics
  - Cost optimization
  - Data protection
  - Access management
```

**Cliquer "Create dashboard"**

---

**Après 24-48h, le dashboard affichera :**
- Taille par bucket
- Taille par classe de stockage
- Objets encrypted vs non-encrypted
- Versioning activé ou non
- Coûts estimés

**Très utile pour optimiser les coûts !**

---

#### Cost Explorer pour S3

**Billing -> Cost Explorer -> Launch Cost Explorer**

**Créer un rapport :**

```
Time range: Last 3 months
Granularity: Monthly
Group by: Service

Filter: S3
```

**Graphique montrant les coûts S3 par mois**

**Pour plus de détails :**

```
Group by: Usage type

Exemples de types :
- TimedStorage-ByteHrs -> Coût du stockage
- Requests-Tier1 -> GET requests
- Requests-Tier2 -> PUT requests
- DataTransfer-Out-Bytes -> Bande passante sortante
```

---

### [OK] TESTS DE VALIDATION

**Buckets S3 :**
- [ ] 2 buckets créés (uploads + backups)
- [ ] Versioning activé sur les 2 buckets
- [ ] Encryption SSE-S3 activée
- [ ] Block public access configuré correctement
- [ ] Tags appliqués

**Lifecycle Policies :**
- [ ] Policy sur uploads (Standard -> IA -> Glacier)
- [ ] Policy sur backups (accélérée vers Deep Archive)
- [ ] Suppression automatique des anciennes versions

**Intégration Flask :**
- [ ] boto3 installé
- [ ] IAM Role attaché à l'instance
- [ ] Upload de fichiers fonctionne
- [ ] Download via URL présignée fonctionne
- [ ] Fichiers stockés avec encryption

**Backups automatiques :**
- [ ] Script de backup fonctionnel
- [ ] Cron configuré (tous les jours à 2h)
- [ ] Logs de backup présents
- [ ] Backups visibles dans S3

**Permissions :**
- [ ] Bucket Policy pour `public-files/` fonctionne
- [ ] Autres fichiers restent privés
- [ ] IAM Role permet accès S3 depuis EC2

**Monitoring :**
- [ ] Métriques S3 visibles dans CloudWatch
- [ ] Storage Lens configuré (si activé)
- [ ] Alertes de facturation S3 configurées

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Access Denied lors de l'upload via Flask

**Symptôme :**

```
botocore.exceptions.ClientError: An error occurred (AccessDenied) when calling the PutObject operation
```

**Causes :**

**1. IAM Role pas attaché ou mal configuré**

**Vérifier :**

```bash
# Sur l'instance EC2
aws sts get-caller-identity
```

**Résultat attendu :**

```json
{
    "UserId": "AROAI...:i-0mno345...",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/EC2-S3-Full-Access/i-0mno345..."
}
```

**Si erreur "Unable to locate credentials" :**
- Le rôle n'est pas attaché
- Console EC2 -> Instance -> Actions -> Security -> Modify IAM role

---

**2. Policy IAM insuffisante**

**Le rôle doit avoir `s3:PutObject` au minimum**

**IAM -> Roles -> EC2-S3-Full-Access -> Permissions**

**Vérifier que `AmazonS3FullAccess` est attaché**

**Ou créer une policy custom :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::sirrdev-todo-uploads",
        "arn:aws:s3:::sirrdev-todo-uploads/*",
        "arn:aws:s3:::sirrdev-todo-backups",
        "arn:aws:s3:::sirrdev-todo-backups/*"
      ]
    }
  ]
}
```

---

#### Erreur 2 : Bucket name already exists

**Symptôme lors de la création :**

```
Bucket name already exists
```

**Cause : Les noms de buckets sont uniques GLOBALEMENT (dans tout AWS)**

**Solution : Choisir un nom plus unique**

```
[X] my-app
[OK] sirrdev-myapp-uploads-20241216-eu1
```

**Techniques pour garantir l'unicité :**
- Préfixe : nom d'entreprise/projet
- Suffixe : timestamp, région, environment
- UUID : `my-app-a1b2c3d4-e5f6`

---

#### Erreur 3 : Lifecycle transition impossible

**Symptôme :**

```
Lifecycle configuration rule transition is invalid: 
Storage class GLACIER is not supported because object storage class is GLACIER
```

**Cause : Tu essaies de transitionner un objet déjà en Glacier vers Glacier**

**Solution : Revoir l'ordre des transitions**

```
[OK] Standard -> Standard-IA -> Glacier
[X] Standard -> Glacier -> Standard-IA
```

**Les transitions doivent aller vers des classes "plus froides" :**

```
Ordre valide :
Standard -> Standard-IA -> Intelligent-Tiering -> Glacier -> Deep Archive

Invalide :
Glacier -> Standard (il faut "restore" puis re-upload)
```

---

#### Erreur 4 : URL présignée invalide/expirée

**Symptôme :**

```xml
<Error>
  <Code>AccessDenied</Code>
  <Message>Request has expired</Message>
  <Expires>2024-12-16T12:00:00Z</Expires>
</Error>
```

**Cause : L'URL présignée a expiré (après `ExpiresIn` secondes)**

**Solutions :**

**1. Augmenter la durée de validité**

```python
presigned_url = s3_client.generate_presigned_url(
    'get_object',
    Params={'Bucket': bucket, 'Key': key},
    ExpiresIn=86400  # 24 heures au lieu de 3600 (1h)
)
```

**Maximum : 7 jours (604800 secondes)**

---

**2. Générer l'URL à la demande (pas la stocker)**

```python
# [X] MAUVAIS : Stocker l'URL présignée en base de données
# Elle expirera et deviendra invalide

# [OK] BON : Stocker la clé S3, générer l'URL à chaque demande
```

---

#### Erreur 5 : Coûts inattendus (requests charges)

**Symptôme :**

```
Facture S3 :
Storage: $0.05
Requests: $2.50  <- Beaucoup !
```

**Cause : Trop de requêtes GET/PUT**

**Exemples de causes :**
- Sync toutes les minutes (au lieu de toutes les heures)
- Application qui liste le bucket en boucle
- Versioning + beaucoup de modifications = beaucoup de PUT

**Solutions :**

**1. Utiliser CloudFront devant S3 (cache les GET)**

**2. Réduire la fréquence de sync/backup**

**3. Utiliser S3 Inventory au lieu de ListBucket répétés**

**4. Activer S3 Request Metrics pour identifier les sources**

---

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

**S3 Fondamentaux :**
- Stockage objet, pas système de fichiers
- Durabilité 11 nines (99.999999999%)
- Buckets = Conteneurs, Objets = Fichiers
- Pas de hiérarchie (simulation avec préfixes)

**Classes de stockage :**
- Standard : Accès fréquent, le plus cher
- Standard-IA : Accès rare, moins cher, retrieval fees
- Glacier : Archivage, très peu cher, latence minutes-heures
- Deep Archive : Archivage long terme, ultra-cheap, latence 12h

**Versioning :**
- Garde toutes les versions des objets
- Protection contre suppression accidentelle
- Coûte plus cher (chaque version facturée)
- Combiné avec Lifecycle pour gérer les anciennes versions

**Encryption :**
- SSE-S3 : AWS gère tout, gratuit
- SSE-KMS : Plus de contrôle, audit, coûteux
- SSE-C : Tu fournis la clé, complexe
- Toujours activer (bonne pratique)

**Permissions :**
- IAM Policies : Qui peut accéder à S3 ?
- Bucket Policies : Accès au bucket depuis n'importe où
- Block Public Access : Protection contre exposition accidentelle
- Presigned URLs : Accès temporaire à objets privés

**Lifecycle Policies :**
- Automatise transitions et expirations
- Optimise les coûts (passe vers classes moins chères)
- Minimum 30 jours avant transition Standard -> IA
- Combiné avec versioning pour gérer l'historique

**Coûts :**
- Stockage facturé au GB-mois
- Requêtes facturées (PUT plus cher que GET)
- Data transfer OUT facturé (pas IN)
- Lifecycle transitions facturées
- Rester dans Free Tier : < 5 GB, < 20k GET, < 2k PUT

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. S3 Transfer Acceleration**

**Accélérer les uploads pour clients éloignés**

```
S3 -> Bucket -> Properties -> Transfer acceleration -> Enable
```

**Utilise le réseau global AWS (CloudFront edge locations)**

**URL d'upload change :**
```
Standard : https://bucket.s3.region.amazonaws.com
Accelerated : https://bucket.s3-accelerate.amazonaws.com
```

**Coût : +0.04$ à 0.08$/GB transféré**

**Quand utiliser ?**
- Uploads depuis loin (autre continent)
- Fichiers > 1 GB
- Sensibilité à la latence

---

**2. S3 Select (requêter des fichiers CSV/JSON sans les télécharger)**

```python
import boto3

s3 = boto3.client('s3')

response = s3.select_object_content(
    Bucket='my-bucket',
    Key='data/sales.csv',
    ExpressionType='SQL',
    Expression='SELECT * FROM s3object s WHERE s.amount > 1000',
    InputSerialization={'CSV': {'FileHeaderInfo': 'USE'}},
    OutputSerialization={'JSON': {}}
)

for event in response['Payload']:
    if 'Records' in event:
        print(event['Records']['Payload'].decode())
```

**Avantages :**
- Pas besoin de télécharger tout le fichier
- Économie bande passante
- Plus rapide

**Cas d'usage :**
- Analytics sur gros fichiers
- Log processing
- ETL (Extract Transform Load)

---

**3. S3 Batch Operations (opérations en masse)**

**Appliquer une action sur millions d'objets**

**Exemples :**
- Copier tous les objets vers autre bucket
- Changer encryption (SSE-S3 -> SSE-KMS)
- Invoquer Lambda pour chaque objet
- Restaurer depuis Glacier

**Console -> S3 -> Batch operations -> Create job**

```
Operation: Copy
Source: Inventory report ou S3 Select
Destination: autre bucket
```

---

**4. S3 Replication (CRR et SRR)**

**CRR = Cross-Region Replication**
**SRR = Same-Region Replication**

**Copier automatiquement les objets vers autre bucket**

**Configuration :**

```
S3 -> Bucket -> Management -> Replication rules -> Create rule

Source bucket: sirrdev-todo-uploads (eu-west-1)
Destination bucket: sirrdev-todo-uploads-replica (us-east-1)

[x] Replicate existing objects
[x] Replicate delete markers
```

**Cas d'usage :**
- Disaster recovery (CRR)
- Compliance (garder copie dans autre région)
- Latence réduite (SRR pour distribuer)
- Backup cross-region

---

**5. S3 Intelligent-Tiering**

**Classe de stockage qui optimise automatiquement**

**Comment ça marche ?**
- Surveille les patterns d'accès
- Objet non accédé 30 jours -> Infrequent Access tier
- Objet non accédé 90 jours -> Archive Instant Access tier
- Objet non accédé 180 jours -> Deep Archive tier (optionnel)

**Coût :**
- Même prix que Standard
- +0.0025$/1000 objets/mois (monitoring fee)

**Quand utiliser ?**
- Patterns d'accès imprévisibles
- Gros volumes (le monitoring fee devient négligeable)

**Quand éviter ?**
- Petits objets (< 128 KB, minimum billable)
- Peu d'objets (monitoring fee > économies)

---

**6. S3 Object Lock (WORM compliance)**

**Write Once Read Many = Immuabilité**

**2 modes :**

**Governance mode :**
- Empêche suppression/modification
- Sauf utilisateurs avec permission spéciale

**Compliance mode :**
- Personne ne peut supprimer (même root !)
- Jusqu'à la fin de la retention period

**Configuration :**

```
S3 -> Create bucket -> Object Lock -> Enable

Retention mode: Compliance
Retention period: 5 years
```

**Cas d'usage :**
- Conformité légale (HIPAA, SEC, FINRA)
- Protection contre ransomware
- Archives judiciaires

---

**7. CloudFront avec S3 (CDN)**

**Distribuer du contenu statique globalement**

```
Users (monde entier)
   v
CloudFront Edge Locations (cache)
   v
S3 Origin (bucket source)
```

**Configuration rapide :**

**CloudFront -> Create distribution**

```
Origin domain: sirrdev-todo-uploads.s3.eu-west-1.amazonaws.com
Origin access: Origin access control (OAC) - recommended

Default cache behavior:
  Viewer protocol policy: Redirect HTTP to HTTPS
  Allowed HTTP methods: GET, HEAD
  Cache policy: CachingOptimized
```

**Avantages :**
- Latence réduite (edge locations proches des users)
- Réduction coûts (moins de requêtes directes à S3)
- Protection DDoS (AWS Shield intégré)
- HTTPS custom domain facile

**Coût :**
- ~0.085$/GB (premier 10 TB)
- Moins cher que data transfer S3 direct

---

**8. S3 Event Notifications**

**Déclencher des actions automatiques sur événements S3**

**Événements supportés :**
- ObjectCreated:* (PUT, POST, Copy, CompleteMultipartUpload)
- ObjectRemoved:* (Delete, DeleteMarkerCreated)
- ObjectRestore:* (Post, Completed)

**Destinations :**
- Lambda (exécuter du code)
- SQS (queue)
- SNS (notifications)
- EventBridge (routing complexe)

**Exemple : Créer des thumbnails automatiquement**

```
S3 -> Properties -> Event notifications -> Create

Event name: create-thumbnail
Event types: ObjectCreated (All)
Prefix: uploads/
Suffix: .jpg

Destination: Lambda function
Lambda: thumbnail-generator
```

**Lambda reçoit l'événement et crée un thumbnail -> Re-upload vers S3**

---

**9. S3 Access Points**

**Simplifier la gestion des permissions pour gros buckets**

**Problème :**
```
1 bucket avec :
- /finance/ -> Équipe finance only
- /hr/ -> Équipe RH only
- /public/ -> Tout le monde

Bucket Policy devient très complexe...
```

**Solution : Access Points**

```
S3 -> Access Points -> Create access point

Name: finance-ap
Bucket: company-data
VPC: vpc-xxx (optionnel, pour limiter au VPC)

Access Point policy:
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"AWS": "arn:aws:iam::xxx:role/FinanceTeam"},
    "Action": "s3:*",
    "Resource": "arn:aws:s3:eu-west-1:xxx:accesspoint/finance-ap/object/finance/*"
  }]
}
```

**L'équipe finance accède via :**
```
arn:aws:s3:eu-west-1:xxx:accesspoint/finance-ap
```

**Au lieu de :**
```
s3://company-data/finance/
```

**Avantages :**
- Policies simplifiées
- Isolation réseau (VPC access points)
- Pas de modification du bucket policy central

---

**10. S3 Inventory (pour très gros buckets)**

**Problème : `aws s3 ls --recursive` est LENT sur gros buckets (millions d'objets)**

**Solution : S3 Inventory = Rapport quotidien/hebdomadaire de tous les objets**

**Configuration :**

```
S3 -> Bucket -> Management -> Inventory configurations -> Create

Inventory name: daily-inventory
Destination bucket: sirrdev-inventory-reports
Frequency: Daily
Output format: CSV
```

**Résultat : Fichier CSV avec tous les objets :**

```csv
Bucket,Key,VersionId,IsLatest,Size,StorageClass,LastModifiedDate
sirrdev-todo-uploads,file1.txt,v1,true,1024,STANDARD,2024-12-16T10:00:00Z
sirrdev-todo-uploads,file2.pdf,v1,true,5242880,GLACIER,2024-12-15T08:30:00Z
...
```

**Utilité :**
- Analytics (requêter avec Athena)
- Compliance (prouver que fichiers existent)
- Lifecycle policy planning
- S3 Batch Operations input

---

## [COURS] CONCLUSION DE L'EXERCICE 2

**[BRAVO] BRAVO ! Tu maîtrises maintenant S3 et l'archivage cloud ! [BRAVO]**

**Ce que tu as appris :**
- [OK] Architecture S3 (buckets, objets, versioning)
- [OK] Classes de stockage et optimisation des coûts
- [OK] Lifecycle policies (transitions automatiques)
- [OK] Encryption au repos (SSE-S3)
- [OK] Permissions granulaires (IAM, Bucket policies, presigned URLs)
- [OK] Intégration avec Flask (boto3)
- [OK] Sauvegardes automatiques (cron + S3)
- [OK] Monitoring et cost management

**Compétences acquises :**
- [OK] Stockage objet cloud (niveau avancé)
- [OK] Optimisation des coûts cloud
- [OK] Automatisation backups
- [OK] Sécurité des données (encryption, permissions)
- [OK] SDK AWS (boto3)

**Architecture déployée :**
```
[OK] 2 buckets S3 (uploads + backups)
[OK] Versioning activé
[OK] Encryption SSE-S3
[OK] Lifecycle policies (vers Glacier)
[OK] IAM Role pour EC2
[OK] Application Flask avec upload/download S3
[OK] Backups quotidiens automatisés (cron)
[OK] Bucket Policy pour accès public contrôlé
[OK] Monitoring CloudWatch + Storage Lens
```

**Coût total : 0$/mois (Free Tier) [OK]**

**Temps moyen de réalisation :** 4-5 heures

**Prochaine étape :** Exercice 3 - Base de données managée avec RDS (MySQL/PostgreSQL) ! [ARCHIVE]

---

**[ATTENTION] NETTOYAGE DES RESSOURCES (si tu veux arrêter)**

```bash
# 1. Vider les buckets (obligatoire avant suppression)
aws s3 rm s3://sirrdev-todo-uploads --recursive
aws s3 rm s3://sirrdev-todo-backups --recursive

# 2. Supprimer les buckets
aws s3 rb s3://sirrdev-todo-uploads
aws s3 rb s3://sirrdev-todo-backups

# 3. (Optionnel) Stopper l'instance EC2 si tu veux arrêter tout
# Console EC2 -> Instance -> Stop

# 4. (Optionnel) Supprimer le rôle IAM
# IAM -> Roles -> EC2-S3-Full-Access -> Delete
```

**[IDEE] Si tu continues avec l'Exercice 3 (RDS), garde tout !**

---

**FIN DE L'EXERCICE 2**

═══════════════════════════════════════════════════════════════

Veux-tu que je continue avec l'**Exercice 3 : Base de données managée avec RDS (PostgreSQL/MySQL)** ?

# [JAUNE] EXERCICE 3 : BASE DE DONNÉES MANAGÉE AVEC RDS (POSTGRESQL)

## [LISTE] ÉNONCÉ

### Contexte professionnel

La startup pour laquelle tu travailles continue de croître. L'application de gestion de tâches (Exercices 1 et 2) compte maintenant plusieurs centaines d'utilisateurs actifs quotidiennement.

Le CTO identifie plusieurs problèmes avec la base de données SQLite actuelle :

**Problèmes actuels :**
- [X] **Performance** : SQLite devient lent avec beaucoup de connexions simultanées
- [X] **Concurrence** : Verrouillage de la base entière lors des écritures
- [X] **Backups** : Processus manuel et peu fiable
- [X] **Scalabilité** : Impossible d'ajouter des read replicas
- [X] **Maintenance** : Patches de sécurité, monitoring, etc. à gérer manuellement
- [X] **Disaster Recovery** : Pas de Multi-AZ, risque de perte de données

**Décision : Migrer vers Amazon RDS (PostgreSQL)**

Le directeur technique demande :
1. Migration SQLite -> PostgreSQL sans downtime
2. Backups automatiques avec rétention 7 jours
3. Encryption des données au repos et en transit
4. Multi-AZ pour haute disponibilité
5. Read replica pour séparer lecture/écriture
6. Monitoring et alertes proactives
7. Optimisation des coûts (rester proche du Free Tier)

### Architecture cible

```
┌─────────────────────────────────────────────────────────────────┐
│                      UTILISATEURS WEB                            │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ HTTPS
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│              Instance EC2 (Flask Application)                    │
│  Public Subnet (10.0.1.0/24)                                    │
│                                                                  │
│  Application Logic:                                              │
│  ├─ Écritures (INSERT, UPDATE, DELETE) -> Master                 │
│  └─ Lectures (SELECT) -> Read Replica                            │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ Port 5432 (PostgreSQL)
                       │ Security Group: Seulement depuis EC2
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│                    RDS SUBNET GROUP                              │
│  Private Subnets (pas d'accès internet direct)                  │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  RDS MASTER (Primary)                                     │  │
│  │  ├─ Instance: db.t3.micro (Free Tier)                     │  │
│  │  ├─ Engine: PostgreSQL 15.x                               │  │
│  │  ├─ Storage: 20 GB gp3 SSD                                │  │
│  │  ├─ Subnet: Private 1 (eu-west-1a)                        │  │
│  │  ├─ Multi-AZ: Enabled                                     │  │
│  │  │   └─ Standby instance en eu-west-1b (sync replica)     │  │
│  │  ├─ Backups: Automated (7 days retention)                 │  │
│  │  ├─ Encryption: Enabled (AES-256)                         │  │
│  │  └─ Enhanced Monitoring: Enabled                          │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  RDS READ REPLICA (Optional - pour optimisation)         │  │
│  │  ├─ Instance: db.t3.micro                                 │  │
│  │  ├─ Subnet: Private 2 (eu-west-1b)                        │  │
│  │  ├─ Replication: Asynchrone depuis Master                 │  │
│  │  └─ Usage: Requêtes SELECT (analytics, rapports)          │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Backups & Snapshots:
├─ Automated Backups (quotidiens) -> S3 (AWS managed)
├─ Manual Snapshots (avant changements majeurs) -> S3
└─ Point-in-Time Recovery (jusqu'à 5 minutes dans le passé)

Monitoring:
├─ CloudWatch Metrics (CPU, RAM, IOPS, connections)
├─ CloudWatch Alarms (CPU > 80%, Storage < 10%)
└─ Enhanced Monitoring (processus OS-level)
```

### Contraintes techniques

- Région : `eu-west-1` (Irlande)
- Engine : PostgreSQL 15.x (compatible avec psycopg2)
- Instance class : `db.t3.micro` (Free Tier: 750h/mois)
- Storage : 20 GB gp3 (Free Tier: jusqu'à 20GB)
- Multi-AZ : Enabled (haute disponibilité)
- Backups : Automated, 7 jours rétention
- Encryption : At rest + In transit
- Budget : Rester dans Free Tier autant que possible
- Temps estimé : 5-6 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre RDS en profondeur (vs EC2 avec DB manuelle)
- [OK] Choisir le bon moteur de base de données
- [OK] Créer une DB subnet group (réseau)
- [OK] Lancer une instance RDS PostgreSQL
- [OK] Configurer les Security Groups pour RDS
- [OK] Migrer des données (SQLite -> PostgreSQL)
- [OK] Configurer Multi-AZ pour haute disponibilité
- [OK] Créer et gérer les backups/snapshots
- [OK] Mettre en place un read replica
- [OK] Optimiser les performances (Parameter Groups)
- [OK] Monitorer avec CloudWatch
- [OK] Sécuriser l'accès (encryption, SSL, IAM)
- [OK] Calculer et optimiser les coûts RDS

---

## [DOCS] PRÉREQUIS

- Exercices 1 et 2 terminés
- Instance EC2 + Flask app fonctionnelle
- VPC avec subnets publics et privés
- Connaissances SQL de base
- Compréhension des bases de données relationnelles

---

## [ARGENT] ESTIMATION DES COÛTS

**RDS Free Tier (12 premiers mois) :**

| Ressource | Quota gratuit | Coût après Free Tier |
|-----------|---------------|----------------------|
| Instance db.t3.micro | 750h/mois | 0.018$/h (~13$/mois) |
| Storage gp3 | 20 GB | 0.115$/GB-mois |
| Backup storage | 20 GB | 0.095$/GB-mois (au-delà de DB size) |
| I/O operations | Inclus (gp3) | Gratuit (SSD) |
| Data transfer OUT | 1 GB/mois | 0.09$/GB |

**Multi-AZ :**
- [X] **PAS dans Free Tier** (double le coût instance)
- Coût : +0.018$/h (même prix qu'instance primaire)
- **Total avec Multi-AZ : ~26$/mois**

**Read Replica :**
- [X] **PAS dans Free Tier**
- Coût : +0.018$/h (instance complète)

**Scénario de cet exercice :**

**Phase 1 (apprentissage - sans Multi-AZ) :**
```
Instance db.t3.micro : 750h/mois (Free Tier) [OK]
Storage 20 GB : Free Tier [OK]
Backups 20 GB : Free Tier [OK]
-> Coût : 0$/mois
```

**Phase 2 (production - avec Multi-AZ) :**
```
Instance db.t3.micro : 0.018$/h × 730h = 13.14$
Multi-AZ standby : 0.018$/h × 730h = 13.14$
Storage 20 GB : Free Tier [OK]
Backups : Free Tier (< 20 GB) [OK]
-> Coût : ~26$/mois
```

**[IDEE] Recommandation :**
- Apprendre sans Multi-AZ (Free Tier)
- Activer Multi-AZ seulement en production

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Comprendre Amazon RDS

#### Qu'est-ce que RDS ?

**RDS = Relational Database Service**

**Base de données relationnelle managée par AWS**

**Analogie :**
- **RDS** = Voiture de location (maintenance incluse)
- **EC2 + DB manuelle** = Voiture personnelle (tu fais tout toi-même)

---

#### RDS vs Base de données sur EC2

| Critère | RDS | DB sur EC2 (DIY) |
|---------|-----|------------------|
| **Installation** | Automatique | Manuelle |
| **Patches OS** | AWS gère | Tu gères |
| **Patches DB** | AWS gère | Tu gères |
| **Backups** | Automatiques | Scripts custom |
| **Haute dispo** | Multi-AZ (1 clic) | Tu configures tout |
| **Scaling** | Vertical: 1 clic<br>Horizontal: Read replicas | Complexe |
| **Monitoring** | CloudWatch intégré | Tu configures |
| **Coût** | ~20$/mois (petit) | Instance EC2 (~10$) + effort |
| **Flexibilité** | Limitée aux options RDS | Totale |
| **Expertise requise** | Faible | Élevée (DBA) |

**Quand utiliser RDS ?**
- [OK] Applications standard (web, mobile)
- [OK] Besoin de haute disponibilité
- [OK] Équipe sans DBA dédié
- [OK] Focus sur l'application, pas l'infra

**Quand utiliser EC2 + DB ?**
- [X] Besoin de configuration très spécifique
- [X] DB non supportée par RDS (Cassandra, Neo4j, etc.)
- [X] Accès root OS requis
- [X] Budget très serré (mais attention au coût caché du temps)

---

#### Moteurs de base de données RDS

**6 moteurs supportés :**

| Moteur | Version | Licence | Cas d'usage | Free Tier |
|--------|---------|---------|-------------|-----------|
| **MySQL** | 8.0 | Open Source | Apps web, e-commerce | [OK] |
| **PostgreSQL** | 15.x | Open Source | Apps complexes, analytics | [OK] |
| **MariaDB** | 10.11 | Open Source | Fork MySQL, plus de features | [OK] |
| **Oracle** | 19c, 21c | Commercial | Apps enterprise legacy | [X] |
| **SQL Server** | 2019, 2022 | Commercial | Apps .NET, Windows | [X] |
| **Aurora** | MySQL/PG compatible | AWS propriétaire | Haute performance, scale | [X] |

**Pour cet exercice : PostgreSQL 15 [OK]**

**Pourquoi PostgreSQL ?**
- [OK] Open source (pas de licence)
- [OK] Fonctionnalités avancées (JSONB, full-text search, GIS)
- [OK] Meilleure gestion de la concurrence que MySQL
- [OK] Types de données riches
- [OK] Compliance stricte avec SQL standard
- [OK] Excellent pour analytics et OLTP
- [OK] Support Python excellent (psycopg2)

**PostgreSQL vs MySQL :**

| Critère | PostgreSQL | MySQL |
|---------|------------|-------|
| **Concurrence** | MVCC (meilleure) | Locking |
| **Types données** | Très riche (JSON, arrays, etc.) | Basique |
| **Full-text search** | Natif (tsvector) | Basique |
| **Window functions** | Complet | Limité |
| **Transactions** | ACID strict | ACID (InnoDB) |
| **Replication** | Streaming (robuste) | Binlog |
| **Performance** | Lectures complexes | Lectures simples |
| **Popularité** | 4e mondiale | 2e mondiale |

---

#### Concepts clés RDS

**1. DB Instance**

Serveur de base de données dans le cloud

**Composants :**
- DB Engine (PostgreSQL, MySQL, etc.)
- Instance Class (CPU, RAM)
- Storage (type, taille)
- Networking (VPC, subnet, security group)

---

**2. DB Instance Classes**

Taille de l'instance (CPU, RAM)

**Familles :**

| Famille | Type | vCPU | RAM | Usage |
|---------|------|------|-----|-------|
| **t3** | Burstable | 2 | 1-8 GB | Dev/test, faible charge |
| **t4g** | Burstable (Graviton) | 2 | 1-8 GB | Comme t3, moins cher |
| **m5** | General purpose | 2-96 | 8-384 GB | Production standard |
| **r5** | Memory optimized | 2-96 | 16-768 GB | Apps gourmandes RAM |
| **x2g** | Memory optimized XL | 64 | 1024 GB | In-memory DB |

**Pour Free Tier : db.t3.micro**
- vCPU : 2
- RAM : 1 GB
- Network : Low to Moderate
- Burstable (comme EC2 t2.micro)

---

**3. Storage Types**

**3 types de stockage EBS pour RDS :**

| Type | IOPS | Débit | Prix | Usage |
|------|------|-------|------|-------|
| **gp3** | 3000-16000 | 125-1000 MB/s | 0.115$/GB | * Standard (recommandé) |
| **gp2** | 100-16000 | Variable | 0.115$/GB | Ancien (éviter) |
| **io1/io2** | Jusqu'à 64000 | Jusqu'à 1000 MB/s | 0.125$/GB + IOPS | Haute performance |

**Allocated Storage :**
- Minimum : 20 GB (la plupart des moteurs)
- Maximum : 64 TB
- Auto-scaling : Possible (AWS augmente auto si plein)

**Pour cet exercice : gp3, 20 GB [OK]**

---

**4. Multi-AZ (High Availability)**

**Standby instance dans une autre AZ**

```
Primary (eu-west-1a)  <-────sync────->  Standby (eu-west-1b)
     v                                      v
  Actif                                  Passif
  (reçoit trafic)                        (standby)

En cas de panne de Primary:
-> Failover automatique vers Standby (30-120 secondes)
-> Endpoint DNS reste identique
```

**Caractéristiques :**
- Réplication **synchrone** (pas de perte de données)
- Failover automatique
- Pas de lecture sur standby (seulement backup)
- Double le coût

**Multi-AZ vs Read Replica :**

| Critère | Multi-AZ | Read Replica |
|---------|----------|--------------|
| **But** | Haute disponibilité | Performance (lecture) |
| **Réplication** | Synchrone | Asynchrone |
| **Failover** | Automatique | Manuel |
| **Lecture** | [X] Non (standby passif) | [OK] Oui |
| **Coût** | Double | Prix d'une instance complète |

---

**5. Backups**

**2 types :**

**Automated Backups :**
- Quotidiens, pendant la maintenance window
- Rétention : 0-35 jours (0 = désactivé)
- Point-in-Time Recovery (PITR) jusqu'à 5 minutes
- Stockés dans S3 (géré par AWS)
- Gratuit jusqu'à taille de la DB

**Manual Snapshots :**
- Tu déclenches manuellement
- Conservés indéfiniment (jusqu'à suppression manuelle)
- Utiles avant changements majeurs
- Facturés au-delà de la taille de la DB

---

**6. Parameter Groups**

Configuration du moteur de base de données

**Exemples de paramètres PostgreSQL :**
- `max_connections` : Nombre max de connexions simultanées
- `shared_buffers` : Mémoire cache PostgreSQL
- `work_mem` : Mémoire par opération de tri
- `maintenance_work_mem` : Mémoire pour VACUUM, CREATE INDEX

**2 types :**
- **DB Parameter Group** : Config du moteur (PostgreSQL, MySQL, etc.)
- **DB Cluster Parameter Group** : Pour Aurora seulement

---

**7. Option Groups**

Features optionnelles du moteur

**Exemples :**
- Oracle : Advanced Security, Label Security
- SQL Server : Mirroring, SQL Server Audit
- MySQL : Audit Plugin

**PostgreSQL n'utilise pas d'Option Groups** (tout est dans Parameter Groups)

---

### ÉTAPE 2 : Préparer le réseau (DB Subnet Group)

#### Comprendre DB Subnet Group

**DB Subnet Group = Ensemble de subnets où RDS peut créer des instances**

**Pourquoi ?**
- RDS a besoin de **2+ subnets dans différentes AZs** (pour Multi-AZ)
- Tu choisis les subnets (généralement privés)
- RDS crée l'instance dans un de ces subnets

**Bonne pratique :**
- [OK] Subnets privés (pas d'accès internet direct)
- [OK] Minimum 2 AZs (haute disponibilité)
- [X] Jamais de subnet public (sécurité)

---

#### Vérifier les subnets disponibles

**Console VPC -> Subnets**

**Tu devrais avoir (créés dans Exercice 1) :**

```
sirrdev-vpc-subnet-private1-eu-west-1a (10.0.11.0/24)
sirrdev-vpc-subnet-private2-eu-west-1b (10.0.12.0/24)
```

**Si tu n'as pas de subnets privés, en créer :**

**VPC -> Subnets -> Create subnet**

```
VPC: sirrdev-vpc
Subnet name: sirrdev-vpc-subnet-private1-eu-west-1a
Availability Zone: eu-west-1a
IPv4 CIDR block: 10.0.11.0/24
```

**Répéter pour eu-west-1b :**

```
Subnet name: sirrdev-vpc-subnet-private2-eu-west-1b
Availability Zone: eu-west-1b
IPv4 CIDR block: 10.0.12.0/24
```

**[ATTENTION] Ces subnets doivent être dans la même route table (privée, sans IGW)**

---

#### Créer le DB Subnet Group

**Console RDS -> Subnet groups -> Create DB subnet group**

```
Name: sirrdev-db-subnet-group
Description: Subnet group for RDS instances
VPC: sirrdev-vpc
```

---

**Add subnets :**

```
Availability Zones:
[x] eu-west-1a
[x] eu-west-1b

Subnets:
[x] 10.0.11.0/24 (sirrdev-vpc-subnet-private1-eu-west-1a)
[x] 10.0.12.0/24 (sirrdev-vpc-subnet-private2-eu-west-1b)
```

**Explication :**
- RDS va choisir **1 subnet** pour créer l'instance primaire
- Si Multi-AZ, la standby sera dans l'**autre subnet/AZ**
- Minimum 2 subnets requis (même si pas Multi-AZ)

---

**Cliquer "Create"**

**[OK] DB Subnet Group créé !**

```
Name: sirrdev-db-subnet-group
VPC: sirrdev-vpc
Subnets: 2 subnets in 2 Availability Zones
```

---

### ÉTAPE 3 : Créer un Security Group pour RDS

**Console EC2 -> Security Groups -> Create security group**

```
Security group name: sirrdev-rds-sg
Description: Allow PostgreSQL access from Flask EC2 instance
VPC: sirrdev-vpc
```

---

**Inbound rules :**

**Règle 1 : PostgreSQL depuis EC2**

```
Type: PostgreSQL
Protocol: TCP
Port range: 5432
Source: Custom
  -> Sélectionner le Security Group de l'instance EC2 (sirrdev-web-sg)
Description: PostgreSQL from Flask app
```

**Explication :**

**Source = Security Group (pas une IP) :**
- Plus flexible (si EC2 change d'IP, toujours OK)
- Sécurisé (seulement les instances avec ce SG)
- Bonne pratique AWS

**Exemple :**
```
Source: sg-0abc123... (sirrdev-web-sg)
```

**Signifie : "Autoriser PostgreSQL depuis toute instance ayant le SG sirrdev-web-sg"**

---

**Outbound rules :**

**Par défaut : All traffic -> 0.0.0.0/0**

**Pour RDS, pas besoin de modifier (pas de connexions sortantes)**

---

**Cliquer "Create security group"**

**[OK] Security Group créé !**

```
Security group ID: sg-0def456...
Name: sirrdev-rds-sg
```

---

### ÉTAPE 4 : Lancer l'instance RDS PostgreSQL

**Console RDS -> Databases -> Create database**

---

#### Choose a database creation method

```
[x] Standard create
  (Plus de contrôle que "Easy create")
```

---

#### Engine options

```
Engine type: PostgreSQL

Edition: PostgreSQL (seule option)

Engine Version: PostgreSQL 15.x-R1 (latest)
  (Choisir la version 15 stable la plus récente)
```

**Pourquoi PostgreSQL 15 ?**
- Version stable et moderne
- Performance améliorée vs 14
- Nouvelles features (MERGE, JSON improvements)
- Support long terme

---

#### Templates

```
[x] Free tier
  (Configure automatiquement pour rester dans Free Tier)
```

**Si tu sélectionnes "Free tier" :**
- Instance : db.t3.micro forcé
- Storage : 20 GB max
- Multi-AZ : Désactivé (pas Free Tier)
- Backups : 7 jours (gratuit)

**Autres templates :**
- Production : Multi-AZ, io1 storage, grandes instances
- Dev/Test : Single-AZ, gp3, instances moyennes

**Pour cet exercice : Free tier [OK]**

---

#### Settings

```
DB instance identifier: sirrdev-todo-db
  (Nom unique dans la région, minuscules/tirets seulement)

Credentials Settings:
  Master username: postgres
    (Nom par défaut, peut être changé)
  
  [x] Auto generate a password
    OU
  [ ] Self managed
    Master password: PostgresAdminSecure2024!
    Confirm password: PostgresAdminSecure2024!
```

**Master username :**
- Super-utilisateur de la DB
- Pas modifiable après création
- Par défaut : `postgres` (PostgreSQL), `admin` (MySQL)

**[ATTENTION] IMPORTANT : Sauvegarder le mot de passe !**

**Si "Auto generate" :**
- AWS génère un mot de passe aléatoire
- Affiché **une seule fois** après création
- Stocké dans Secrets Manager (optionnel, payant)

---

#### Instance configuration

```
DB instance class:

  [x] Burstable classes (includes t classes)
  
  db.t3.micro
    2 vCPUs, 1 GiB RAM
    Free tier: 750 hours per month [OK]
```

**Si "Free tier" template sélectionné, c'est forcé à db.t3.micro**

---

#### Storage

```
Storage type: General Purpose SSD (gp3)
  (Plus récent et performant que gp2)

Allocated storage: 20 GiB
  (Minimum pour PostgreSQL)

[ ] Enable storage autoscaling
  (On désactive pour contrôler les coûts)
  
  Maximum storage threshold: 1000 GiB
  (Si activé, AWS peut auto-augmenter jusqu'à cette limite)
```

**Explication storage autoscaling :**

**Désactivé (recommandé pour Free Tier) :**
- Stockage fixe à 20 GB
- Si plein -> Erreurs
- Contrôle total des coûts

**Activé :**
- AWS augmente auto si > 90% plein
- Pratique, mais peut surprendre niveau facturation
- Utile en production (évite les pannes)

---

#### Availability & durability

```
[ ] Create a standby instance (Multi-AZ deployment)
  (Désactivé pour Free Tier)
```

**Si tu actives Multi-AZ :**
- Instance standby dans autre AZ
- Réplication synchrone
- Failover automatique
- [ATTENTION] Double le coût (~26$/mois au lieu de 0$)

**Pour apprentissage : Désactivé [OK]**

**En production : Activé [OK][OK][OK]**

---

#### Connectivity

```
Compute resource:
  [x] Don't connect to an EC2 compute resource
  (On configurera manuellement)

Virtual private cloud (VPC): sirrdev-vpc

DB subnet group: sirrdev-db-subnet-group

Public access: No [OK]
  (RDS ne sera PAS accessible depuis internet)

VPC security group:
  [x] Choose existing
  
  Existing VPC security groups:
    [x] sirrdev-rds-sg
    [ ] default (enlever)

Availability Zone: No preference
  (AWS choisit automatiquement)

RDS Proxy: Disable
  (Feature avancée pour pooling de connexions)
```

**Explication Public access :**

**No (recommandé) :**
- RDS n'a pas d'IP publique
- Accessible seulement depuis le VPC
- Sécurité maximale

**Yes (dangereux) :**
- RDS a une IP publique
- Accessible depuis internet (si SG autorise)
- [ATTENTION] Risque de sécurité (attaques brute-force)
- Utile seulement pour dev depuis ton PC

---

#### Database authentication

```
[x] Password authentication
  (Standard, avec username/password)

[ ] Password and IAM database authentication
  (Avancé, utilise IAM pour se connecter)

[ ] Password and Kerberos authentication
  (Enterprise, Active Directory)
```

**Password authentication suffit pour cet exercice**

**IAM authentication (avancé) :**
- Pas de mot de passe en dur
- Utilise des tokens temporaires (15 minutes)
- Intégration avec IAM roles
- On le verra dans "Pour aller plus loin"

---

#### Monitoring

```
[x] Enable Enhanced Monitoring

Granularity: 60 seconds
  (Fréquence des métriques OS-level)

Monitoring Role: Default
  (AWS crée un rôle IAM automatiquement)
```

**Enhanced Monitoring vs CloudWatch standard :**

| Métrique | CloudWatch standard | Enhanced Monitoring |
|----------|---------------------|---------------------|
| **CPU** | Hypervisor-level | [OK] OS-level (plus précis) |
| **RAM** | Non disponible | [OK] Oui |
| **Processus** | Non | [OK] Top processes |
| **File system** | Non | [OK] I/O stats |
| **Coût** | Gratuit | Gratuit (60s), 0.30$/mois (1s) |

**Pour Free Tier : 60 seconds suffit [OK]**

---

#### Additional configuration

**Expand pour voir toutes les options**

**Database options :**

```
Initial database name: todo_production
  (Nom de la base de données créée automatiquement)

[x] Automated backups
  Backup retention period: 7 days
  Backup window: No preference
    (AWS choisit une fenêtre de 30 min pendant la maintenance)
  
  [x] Copy tags to snapshots

[ ] Enable encryption
  (Désactivé en Free Tier pour simplifier, mais recommandé en prod)
```

**Explication backups :**

**Backup retention period :**
- 0 jours = Pas de backups automatiques
- 1-35 jours = Rétention (7 jours gratuit dans Free Tier)
- Recommandé : 7-14 jours (minimum viable)

**Backup window :**
- Fenêtre de 30 minutes où AWS prend un snapshot
- Peut causer une légère latence
- Choisir une heure de faible trafic (ex: 3h du matin)

**Point-in-Time Recovery (PITR) :**
- Restaurer à n'importe quel moment dans les X derniers jours
- Granularité : 5 minutes
- Exemple : Erreur à 10h45 -> Restaurer état de 10h40

---

**Maintenance :**

```
[x] Enable auto minor version upgrade
  (PostgreSQL 15.3 -> 15.4 automatiquement)

Maintenance window: No preference
  (AWS choisit automatiquement, généralement nuit)
```

**Auto minor version upgrade :**
- Patches de sécurité et bug fixes
- Exemple : PostgreSQL 15.3 -> 15.4 (minor)
- Pas de breaking changes
- Recommandé : Activé [OK]

**Major version upgrade (15 -> 16) :**
- Manuel seulement
- Peut avoir breaking changes
- Tester avant en dev

---

**Deletion protection :**

```
[x] Enable deletion protection
  (Empêche suppression accidentelle)
```

**Avec deletion protection :**
- Impossible de supprimer via console/CLI/API
- Il faut d'abord désactiver la protection
- Protection contre erreurs humaines

**Recommandé : Activé en production [OK][OK][OK]**

---

**Cliquer "Create database"**

**Résultat :**

```
Creating database sirrdev-todo-db...
This may take several minutes.
```

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

**[OK] RDS Instance en cours de création !**

---

**Pendant la création, AWS génère le mot de passe (si auto-generate) :**

**Cliquer "View credential details"**

```
Master username: postgres
Master password: aB3cD4eF5gH6iJ7kL8mN9oP0
  (Exemple, le tien sera différent)

Endpoint: (Pas encore disponible, en cours...)
Port: 5432
```

**[ATTENTION] SAUVEGARDER CE MOT DE PASSE ! Il ne sera plus jamais affiché.**

**Le stocker dans un gestionnaire de mots de passe sécurisé.**

---

**Surveiller la création :**

```
RDS -> Databases -> sirrdev-todo-db

Status: Creating...
  v (après 2-3 minutes)
Status: Backing-up...
  v (après 5 minutes)
Status: Available [OK]
```

**Une fois "Available" :**

```
Endpoint: sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com
Port: 5432
```

**[OK] RDS PostgreSQL créé et prêt !**

---

### ÉTAPE 5 : Se connecter à RDS depuis l'instance EC2

#### Installer le client PostgreSQL

**Se connecter en SSH à l'instance EC2 :**

```bash
ssh -i ~/.ssh/sirrdev-ec2-key.pem ubuntu@54.194.78.90
```

---

**Installer PostgreSQL client :**

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

**`postgresql-client-15` = Outils CLI PostgreSQL (psql, pg_dump, etc.)**

**Pas besoin d'installer le serveur PostgreSQL (déjà dans RDS) !**

---

**Vérifier l'installation :**

```bash
psql --version
```

**Résultat :**

```
psql (PostgreSQL) 15.5 (Ubuntu 15.5-1.pgdg22.04+1)
```

**[OK] Client installé !**

---

#### Tester la connexion

**Récupérer l'endpoint RDS :**

**Console RDS -> Databases -> sirrdev-todo-db -> Connectivity & security**

```
Endpoint: sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com
Port: 5432
```

---

**Se connecter avec psql :**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production
```

**(Remplace l'endpoint par le tien)**

---

**Saisir le mot de passe quand demandé :**

```
Password for user postgres: (taper le mot de passe)
```

---

**Si succès :**

```
psql (15.5 (Ubuntu 15.5-1.pgdg22.04+1), server 15.4)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

todo_production=>
```

**[OK] Connexion réussie !**

**Note : "SSL connection" -> La connexion est chiffrée (TLS 1.3) [OK]**

---

**Si erreur "could not connect to server" :**

**Vérifier :**

1. **Security Group RDS autorise l'accès depuis EC2 ?**
   
   RDS -> sirrdev-todo-db -> Connectivity & security -> VPC security groups
   
   Cliquer sur le SG -> Inbound rules
   
   Doit avoir : PostgreSQL (5432) depuis sg-xxx (sirrdev-web-sg)

2. **L'instance EC2 a bien le Security Group sirrdev-web-sg ?**
   
   EC2 -> Instances -> sirrdev-flask-server -> Security groups

3. **RDS et EC2 dans le même VPC ?**
   
   Vérifier que les deux sont dans `sirrdev-vpc`

---

**Tester quelques commandes PostgreSQL :**

```sql
-- Lister les bases de données
\l

-- Voir la base actuelle
SELECT current_database();

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

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

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

**Résultat :**

```
 id |    name     
----+-------------
  1 | Hello RDS!
```

---

**Supprimer la table de test :**

```sql
DROP TABLE test;
```

---

**Quitter psql :**

```sql
\q
```

**Ou Ctrl+D**

---

### ÉTAPE 6 : Migrer de SQLite vers PostgreSQL

#### Comprendre la migration

**Approches possibles :**

**1. Export/Import manuel (simple, petit volume)**
- Export SQLite -> CSV
- Import CSV -> PostgreSQL
- [OK] Pour cet exercice

**2. ORM migration (Flask-Migrate, Alembic)**
- Recréer le schéma avec SQLAlchemy
- Copier les données
- Professionnel

**3. Outils tiers (pgloader, AWS DMS)**
- Migration automatisée
- Pour gros volumes

**On va utiliser l'approche 1 (simple et pédagogique)**

---

#### Sauvegarder la base SQLite actuelle

```bash
cd ~/flask-todo-app

# Créer une sauvegarde
cp todo.db todo.db.backup

# Vérifier les données existantes
sqlite3 todo.db "SELECT COUNT(*) FROM tasks;"
```

**Résultat :**

```
5
```

**(Exemple : 5 tâches dans la base)**

---

#### Exporter les données en SQL

**Export complet avec sqlite3 :**

```bash
# Générer un dump SQL
sqlite3 todo.db .dump > sqlite_dump.sql

# Voir le contenu
head -30 sqlite_dump.sql
```

**Contenu (exemple) :**

```sql
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE tasks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    description TEXT,
    completed BOOLEAN NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    attachment_key TEXT,
    attachment_url TEXT
);
INSERT INTO tasks VALUES(1,'Apprendre AWS','Comprendre EC2, S3 et RDS',0,'2024-12-15 10:00:00',NULL,NULL);
INSERT INTO tasks VALUES(2,'Migrer vers PostgreSQL','Passer de SQLite à RDS',0,'2024-12-16 09:30:00',NULL,NULL);
...
COMMIT;
```

---

#### Adapter le schéma pour PostgreSQL

**Différences SQLite vs PostgreSQL :**

| Feature | SQLite | PostgreSQL |
|---------|--------|-----------|
| **AUTOINCREMENT** | AUTOINCREMENT | SERIAL ou IDENTITY |
| **BOOLEAN** | INTEGER (0/1) | BOOLEAN (true/false) |
| **TIMESTAMP** | TEXT | TIMESTAMP |
| **Constraints** | Permissif | Strict |

---

**Créer le schéma PostgreSQL :**

```bash
nano create_schema.sql
```

**Contenu :**

```sql
-- ═══════════════════════════════════════════════════════════════
-- SCHÉMA POSTGRESQL POUR TODO APP
-- ═══════════════════════════════════════════════════════════════

-- Supprimer la table si existe (pour réinitialisation)
DROP TABLE IF EXISTS tasks CASCADE;

-- Créer la table tasks
CREATE TABLE tasks (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    completed BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    attachment_key VARCHAR(512),
    attachment_url VARCHAR(1024)
);

-- Créer un index sur created_at (pour trier rapidement)
CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);

-- Créer un index sur completed (pour filtrer)
CREATE INDEX idx_tasks_completed ON tasks(completed);

-- Afficher confirmation
SELECT 'Schema créé avec succès!' AS status;
```

**Explication :**

**`SERIAL` :**
- Équivalent de AUTO_INCREMENT
- Crée une séquence automatiquement
- Type entier (INTEGER) avec incrémentation auto

**`VARCHAR(255)` vs `TEXT` :**
- VARCHAR(n) : Longueur max n caractères
- TEXT : Illimité
- En PostgreSQL, pas de différence de performance
- VARCHAR pour documentation (on sait la limite attendue)

**`BOOLEAN` :**
- Vrai type booléen (true/false)
- SQLite utilise INTEGER (0/1)

**`TIMESTAMP` :**
- Type date + heure
- DEFAULT CURRENT_TIMESTAMP = Heure actuelle à l'insertion

**Index :**
- Accélère les requêtes sur ces colonnes
- `idx_tasks_created_at` : Tri par date rapide
- `idx_tasks_completed` : Filtrer complété/non-complété

---

**Exécuter le schéma :**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production \
  --file=create_schema.sql
```

**Saisir le mot de passe**

**Résultat :**

```
DROP TABLE
CREATE TABLE
CREATE INDEX
CREATE INDEX
        status         
-----------------------
 Schema créé avec succès!
```

**[OK] Schéma créé dans PostgreSQL !**

---

#### Migrer les données

**Méthode 1 : Via CSV (simple)**

**Export SQLite -> CSV :**

```bash
sqlite3 todo.db <<EOF
.headers on
.mode csv
.output tasks.csv
SELECT * FROM tasks;
.quit
EOF
```

**Vérifier tasks.csv :**

```bash
head tasks.csv
```

**Résultat :**

```csv
id,title,description,completed,created_at,attachment_key,attachment_url
1,"Apprendre AWS","Comprendre EC2, S3 et RDS",0,"2024-12-15 10:00:00",,
2,"Migrer vers PostgreSQL","Passer de SQLite à RDS",0,"2024-12-16 09:30:00",,
```

---

**Import CSV -> PostgreSQL :**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production \
  --command="\COPY tasks(id,title,description,completed,created_at,attachment_key,attachment_url) FROM 'tasks.csv' CSV HEADER;"
```

**Explication :**

**`\COPY` (psql command) :**
- Lit le fichier depuis le client (EC2)
- Envoie les données vers RDS
- Différent de `COPY` (SQL command) qui lit depuis le serveur

**`FROM 'tasks.csv' CSV HEADER` :**
- Source : fichier tasks.csv
- Format : CSV
- HEADER : Première ligne = noms de colonnes (ignorer)

---

**Résultat :**

```
COPY 5
```

**(5 lignes copiées)**

---

**Vérifier les données :**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production \
  --command="SELECT COUNT(*), MAX(id) FROM tasks;"
```

**Résultat :**

```
 count | max 
-------+-----
     5 |   5
```

**[OK] Données migrées !**

---

**Réinitialiser la séquence (important !) :**

**Problème :**
- Les IDs ont été insérés manuellement (1, 2, 3, 4, 5)
- La séquence SERIAL est toujours à 1
- Prochaine insertion -> Conflit (ID 1 existe déjà)

**Solution : Réinitialiser la séquence au max(id) + 1**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production \
  --command="SELECT setval('tasks_id_seq', (SELECT MAX(id) FROM tasks));"
```

**Résultat :**

```
 setval 
--------
      5
```

**Maintenant, la prochaine insertion aura l'ID 6 [OK]**

---

### ÉTAPE 7 : Configurer Flask pour PostgreSQL

#### Installer psycopg2 (driver PostgreSQL)

**Sur l'instance EC2 :**

```bash
cd ~/flask-todo-app
source venv/bin/activate

# Installer les dépendances système (libpq-dev)
sudo apt install libpq-dev -y

# Installer psycopg2
pip install psycopg2-binary
```

**`psycopg2-binary` :**
- Driver PostgreSQL pour Python
- Version binary (wheels précompilés)
- Plus facile que `psycopg2` (pas besoin de compiler)

---

#### Créer un fichier de configuration DB

```bash
nano db_config.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# CONFIGURATION BASE DE DONNÉES
# ═══════════════════════════════════════════════════════════════

import os

# Type de base de données ('sqlite' ou 'postgresql')
DB_TYPE = os.getenv('DB_TYPE', 'postgresql')

# Configuration SQLite (ancien, pour fallback)
SQLITE_DB = 'todo.db'

# Configuration PostgreSQL (RDS)
POSTGRES_CONFIG = {
    'host': os.getenv('DB_HOST', 'sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com'),
    'port': int(os.getenv('DB_PORT', '5432')),
    'database': os.getenv('DB_NAME', 'todo_production'),
    'user': os.getenv('DB_USER', 'postgres'),
    'password': os.getenv('DB_PASSWORD', 'PostgresAdminSecure2024!'),
    'sslmode': 'require'  # Force SSL/TLS
}

# [ATTENTION] IMPORTANT : Ne JAMAIS commit ce fichier dans Git !
# Utiliser des variables d'environnement en production
```

**[ATTENTION] Remplace :**
- `host` : Ton endpoint RDS
- `password` : Ton mot de passe RDS

---

**Explication `sslmode` :**

| Mode | Comportement | Sécurité |
|------|--------------|----------|
| **disable** | Pas de SSL | [X] Non chiffré |
| **allow** | SSL si serveur supporte | [ATTENTION] Peut être non chiffré |
| **prefer** | SSL si possible, sinon non-SSL | [ATTENTION] Peut être non chiffré |
| **require** | SSL obligatoire | [OK] Chiffré (vérifie pas certificat) |
| **verify-ca** | SSL + vérifie certificat CA | [OK][OK] Très sécurisé |
| **verify-full** | SSL + vérifie hostname | [OK][OK][OK] Maximum sécurité |

**Pour RDS : `require` suffit (AWS gère les certificats)**

---

#### Modifier app.py pour supporter PostgreSQL

**Éditer app.py :**

```bash
nano app.py
```

**Remplacer la partie "CONFIGURATION" et "FONCTIONS DB" :**

```python
# ═══════════════════════════════════════════════════════════════
# IMPORTS
# ═══════════════════════════════════════════════════════════════

from flask import Flask, render_template, request, redirect, url_for
import boto3
from werkzeug.utils import secure_filename
import os
from datetime import datetime

# Nouveaux imports
import psycopg2
import psycopg2.extras
from db_config import DB_TYPE, SQLITE_DB, POSTGRES_CONFIG

app = Flask(__name__)

# ───────────────────────────────────────────────────────────────
# CONFIGURATION S3 (inchangé)
# ───────────────────────────────────────────────────────────────

S3_BUCKET = 'sirrdev-todo-uploads'
S3_REGION = 'eu-west-1'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx'}
s3_client = boto3.client('s3', region_name=S3_REGION)

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

# ───────────────────────────────────────────────────────────────
# FONCTIONS BASE DE DONNÉES (REFACTORISÉES)
# ───────────────────────────────────────────────────────────────

def get_db_connection():
    """
    Crée une connexion à la base de données (PostgreSQL ou SQLite)
    """
    if DB_TYPE == 'postgresql':
        # Connexion PostgreSQL
        conn = psycopg2.connect(**POSTGRES_CONFIG)
        # RealDictCursor = Retourne des dictionnaires au lieu de tuples
        conn.cursor_factory = psycopg2.extras.RealDictCursor
        return conn
    else:
        # Connexion SQLite (fallback)
        import sqlite3
        conn = sqlite3.connect(SQLITE_DB)
        conn.row_factory = sqlite3.Row
        return conn

def execute_query(query, params=None, fetch=False, fetchone=False):
    """
    Exécute une requête SQL de manière sécurisée
    
    Args:
        query: Requête SQL avec placeholders
        params: Paramètres à injecter
        fetch: Si True, retourne tous les résultats (fetchall)
        fetchone: Si True, retourne un seul résultat (fetchone)
    
    Returns:
        Résultats de la requête ou None
    """
    conn = get_db_connection()
    cursor = conn.cursor()
    
    try:
        if params:
            cursor.execute(query, params)
        else:
            cursor.execute(query)
        
        if fetch:
            results = cursor.fetchall()
            conn.close()
            return results
        elif fetchone:
            result = cursor.fetchone()
            conn.close()
            return result
        else:
            # INSERT/UPDATE/DELETE
            conn.commit()
            conn.close()
            return cursor.rowcount
    except Exception as e:
        print(f"Erreur DB: {e}")
        conn.rollback()
        conn.close()
        raise

# ───────────────────────────────────────────────────────────────
# ROUTES
# ───────────────────────────────────────────────────────────────

@app.route('/')
def index():
    """Page d'accueil - Liste des tâches"""
    
    # Adapter la requête selon le moteur
    if DB_TYPE == 'postgresql':
        # PostgreSQL : BOOLEAN true/false
        query = '''
            SELECT * FROM tasks 
            ORDER BY created_at DESC
        '''
    else:
        # SQLite : INTEGER 0/1
        query = '''
            SELECT * FROM tasks 
            ORDER BY created_at DESC
        '''
    
    tasks = execute_query(query, fetch=True)
    
    return render_template('index.html', tasks=tasks)

@app.route('/add', methods=['POST'])
def add_task():
    """Ajouter une nouvelle tâche"""
    title = request.form.get('title')
    description = request.form.get('description', '')
    
    if title:
        if DB_TYPE == 'postgresql':
            # PostgreSQL : RETURNING clause
            query = '''
                INSERT INTO tasks (title, description) 
                VALUES (%s, %s)
                RETURNING id
            '''
            execute_query(query, (title, description))
        else:
            # SQLite
            query = '''
                INSERT INTO tasks (title, description) 
                VALUES (?, ?)
            '''
            execute_query(query, (title, description))
    
    return redirect(url_for('index'))

@app.route('/complete/<int:task_id>')
def complete_task(task_id):
    """Marquer une tâche comme terminée"""
    
    if DB_TYPE == 'postgresql':
        query = 'UPDATE tasks SET completed = TRUE WHERE id = %s'
    else:
        query = 'UPDATE tasks SET completed = 1 WHERE id = ?'
    
    execute_query(query, (task_id,))
    
    return redirect(url_for('index'))

@app.route('/delete/<int:task_id>')
def delete_task(task_id):
    """Supprimer une tâche"""
    
    query = 'DELETE FROM tasks WHERE id = %s' if DB_TYPE == 'postgresql' else 'DELETE FROM tasks WHERE id = ?'
    execute_query(query, (task_id,))
    
    return redirect(url_for('index'))

@app.route('/upload/<int:task_id>', methods=['POST'])
def upload_file(task_id):
    """Uploader un fichier attaché à une tâche"""
    
    if 'file' not in request.files:
        return redirect(url_for('index'))
    
    file = request.files['file']
    
    if file.filename == '':
        return redirect(url_for('index'))
    
    if file and allowed_file(file.filename):
        filename = secure_filename(file.filename)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        s3_key = f"task-attachments/{task_id}/{timestamp}_{filename}"
        
        try:
            s3_client.upload_fileobj(
                file,
                S3_BUCKET,
                s3_key,
                ExtraArgs={
                    'ContentType': file.content_type,
                    'ServerSideEncryption': 'AES256'
                }
            )
            
            query = 'UPDATE tasks SET attachment_key = %s WHERE id = %s' if DB_TYPE == 'postgresql' else 'UPDATE tasks SET attachment_key = ? WHERE id = ?'
            execute_query(query, (s3_key, task_id))
            
        except Exception as e:
            print(f"Erreur S3: {e}")
    
    return redirect(url_for('index'))

@app.route('/download/<int:task_id>')
def download_file(task_id):
    """Générer une URL présignée pour télécharger le fichier"""
    
    query = 'SELECT attachment_key FROM tasks WHERE id = %s' if DB_TYPE == 'postgresql' else 'SELECT attachment_key FROM tasks WHERE id = ?'
    task = execute_query(query, (task_id,), fetchone=True)
    
    if task and task['attachment_key']:
        try:
            presigned_url = s3_client.generate_presigned_url(
                'get_object',
                Params={
                    'Bucket': S3_BUCKET,
                    'Key': task['attachment_key']
                },
                ExpiresIn=3600
            )
            
            return redirect(presigned_url)
            
        except Exception as e:
            print(f"Erreur génération URL: {e}")
            return "Erreur lors du téléchargement", 500
    
    return redirect(url_for('index'))

@app.route('/health')
def health():
    """Endpoint de santé pour monitoring"""
    try:
        # Test connexion DB
        if DB_TYPE == 'postgresql':
            execute_query('SELECT 1', fetch=True)
        else:
            execute_query('SELECT 1', fetch=True)
        
        db_status = 'OK'
    except:
        db_status = 'ERROR'
    
    return {
        'status': 'OK' if db_status == 'OK' else 'DEGRADED',
        'database': DB_TYPE,
        'db_status': db_status,
        'timestamp': datetime.now().isoformat()
    }

# ───────────────────────────────────────────────────────────────
# DÉMARRAGE
# ───────────────────────────────────────────────────────────────

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)
```

**Changements clés :**

**Placeholders :**
- PostgreSQL : `%s` (style psycopg2)
- SQLite : `?` (style sqlite3)

**BOOLEAN :**
- PostgreSQL : `TRUE` / `FALSE`
- SQLite : `1` / `0`

**Fonction `execute_query()` :**
- Abstraction pour gérer les deux moteurs
- Gestion d'erreurs avec rollback
- Support fetch/fetchone

---

**Sauvegarder et redémarrer Flask :**

```bash
# Arrêter Flask (si en screen)
screen -r flask-app
# Ctrl+C

# Relancer
python app.py

# Ctrl+A puis D pour détacher
```

---

**Tester dans le navigateur :**

```
http://54.194.78.90:5000
```

**Les tâches migrées depuis SQLite devraient s'afficher ! [OK]**

**Ajouter une nouvelle tâche pour tester l'insertion PostgreSQL [OK]**

---

**Vérifier dans PostgreSQL :**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production \
  --command="SELECT id, title, completed, created_at FROM tasks ORDER BY created_at DESC LIMIT 5;"
```

**Tu devrais voir les tâches, y compris la nouvelle [OK]**

---

### ÉTAPE 8 : Configurer les backups et snapshots

#### Vérifier les backups automatiques

**RDS -> Databases -> sirrdev-todo-db -> Maintenance & backups**

```
Automated backups:
  Backup retention period: 7 days [OK]
  Backup window: 03:00-03:30 UTC
  Latest restorable time: 2024-12-16 14:35 UTC (5 minutes ago)
```

**Point-in-Time Recovery activé automatiquement [OK]**

---

#### Créer un snapshot manuel

**Avant un changement majeur (mise à jour, migration, etc.) :**

**RDS -> Databases -> sirrdev-todo-db -> Actions -> Take snapshot**

```
Snapshot name: sirrdev-todo-db-before-migration-20241216
  (Convention: nom-instance-action-date)

Tags (optional):
  Purpose: Pre-migration backup
  Date: 2024-12-16
```

**Cliquer "Take snapshot"**

**Durée : 2-5 minutes (selon taille de la DB)**

---

**Vérifier le snapshot :**

**RDS -> Snapshots**

```
sirrdev-todo-db-before-migration-20241216
Status: Creating... -> Available [OK]
Size: ~200 MB (approximation)
Encrypted: No
```

**[OK] Snapshot créé !**

---

#### Restaurer depuis un snapshot (test)

**[ATTENTION] Restaurer crée une NOUVELLE instance (pas écrase l'ancienne)**

**RDS -> Snapshots -> Sélectionner snapshot -> Actions -> Restore snapshot**

```
DB instance identifier: sirrdev-todo-db-restored
DB instance class: db.t3.micro
VPC: sirrdev-vpc
DB subnet group: sirrdev-db-subnet-group
Public access: No
VPC security group: sirrdev-rds-sg
```

**Cliquer "Restore DB instance"**

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

**Résultat : Nouvelle instance `sirrdev-todo-db-restored` avec les données du snapshot [OK]**

**[ATTENTION] Penser à supprimer cette instance de test après (coûte de l'argent) :**

```
RDS -> Databases -> sirrdev-todo-db-restored -> Actions -> Delete
[ ] Create final snapshot (pour test, pas nécessaire)
[x] I acknowledge... (confirmer suppression)
```

---

#### Restaurer à un point dans le temps (PITR)

**Scénario : Erreur à 10h45, on veut revenir à 10h40**

**RDS -> Databases -> sirrdev-todo-db -> Actions -> Restore to point in time**

```
Restore mode:
  [x] Latest restorable time
  OU
  [x] Custom
    Date and time: 2024-12-16 10:40:00 UTC

DB instance identifier: sirrdev-todo-db-pitr-20241216-1040

(Mêmes paramètres que snapshot restore)
```

**Cliquer "Restore DB instance"**

**Résultat : Nouvelle instance avec l'état de la DB à 10h40 [OK]**

---

### ÉTAPE 9 : Activer Multi-AZ (Haute disponibilité)

**[ATTENTION] ATTENTION : Multi-AZ n'est PAS gratuit (~26$/mois)**

**Pour apprentissage, on peut sauter cette étape.**

**Mais voici comment l'activer :**

---

#### Activer Multi-AZ sur instance existante

**RDS -> Databases -> sirrdev-todo-db -> Modify**

```
Availability & durability:
  [x] Create a standby instance (Multi-AZ deployment)
```

**Scroll en bas :**

```
Scheduling of modifications:
  [x] Apply immediately
    (Modifie maintenant, pas pendant la maintenance window)
```

**Cliquer "Continue" -> "Modify DB instance"**

---

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

**Processus :**
1. AWS crée un snapshot
2. Restaure le snapshot en eu-west-1b (standby)
3. Configure la réplication synchrone
4. Met à jour le DNS endpoint

**Pendant ce temps :**
- L'instance reste accessible (pas de downtime)
- Performance peut être légèrement réduite

**Statut :**

```
RDS -> Databases -> sirrdev-todo-db

Status: Modifying... -> Available

Multi-AZ: Yes [OK]
Secondary AZ: eu-west-1b
```

**[OK] Multi-AZ activé !**

---

#### Tester le failover (optionnel, avancé)

**RDS -> Databases -> sirrdev-todo-db -> Actions -> Reboot**

```
[x] Reboot with failover
```

**Ce qui se passe :**
1. Primary eu-west-1a devient indisponible (simulé)
2. Standby eu-west-1b est promue en Primary
3. Endpoint DNS pointe vers new Primary
4. Ancien Primary redémarre et devient Standby

**Durée downtime : 30-120 secondes**

**Application Flask : Reconnexion automatique après ~1 minute [OK]**

---

### ÉTAPE 10 : Créer un Read Replica (optimisation)

**Read Replica = Copie en lecture seule de la DB**

**Cas d'usage :**
- Séparer lecture (SELECT) / écriture (INSERT/UPDATE/DELETE)
- Analytics, reporting (pas impact sur DB principale)
- Geo-distribution (replica dans autre région)

**[ATTENTION] PAS gratuit : +0.018$/h (~13$/mois) par replica**

---

#### Créer un Read Replica

**RDS -> Databases -> sirrdev-todo-db -> Actions -> Create read replica**

```
DB instance identifier: sirrdev-todo-db-replica

Region: Same as source (eu-west-1)
  (Ou autre région pour multi-region)

DB instance class: db.t3.micro
  (Peut être différent de la source)

Public access: No

VPC security group: sirrdev-rds-sg

Availability Zone: eu-west-1b
  (Différente de la source pour redondance)

Enhanced monitoring: Enable
```

**Cliquer "Create read replica"**

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

---

**Résultat :**

```
RDS -> Databases

sirrdev-todo-db (Primary)
  Role: Source
  Replicas: 1

sirrdev-todo-db-replica (Replica)
  Role: Replica
  Source: sirrdev-todo-db
  Replication state: Replicating [OK]
```

---

#### Configurer Flask pour utiliser le Read Replica

**Modifier db_config.py :**

```python
# Configuration PostgreSQL (RDS)
POSTGRES_CONFIG_WRITE = {
    'host': 'sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com',
    'port': 5432,
    'database': 'todo_production',
    'user': 'postgres',
    'password': 'PostgresAdminSecure2024!',
    'sslmode': 'require'
}

POSTGRES_CONFIG_READ = {
    'host': 'sirrdev-todo-db-replica.c1abc2defgh.eu-west-1.rds.amazonaws.com',  # Replica endpoint
    'port': 5432,
    'database': 'todo_production',
    'user': 'postgres',
    'password': 'PostgresAdminSecure2024!',
    'sslmode': 'require'
}
```

---

**Modifier app.py pour router les lectures vers replica :**

```python
def get_db_connection(readonly=False):
    """
    Crée une connexion à la base de données
    
    Args:
        readonly: Si True, utilise le read replica (si disponible)
    """
    if DB_TYPE == 'postgresql':
        config = POSTGRES_CONFIG_READ if readonly and 'POSTGRES_CONFIG_READ' in globals() else POSTGRES_CONFIG_WRITE
        conn = psycopg2.connect(**config)
        conn.cursor_factory = psycopg2.extras.RealDictCursor
        return conn
    else:
        import sqlite3
        conn = sqlite3.connect(SQLITE_DB)
        conn.row_factory = sqlite3.Row
        return conn
```

---

**Modifier les routes pour utiliser readonly :**

```python
@app.route('/')
def index():
    """Page d'accueil - Liste des tâches (LECTURE)"""
    query = 'SELECT * FROM tasks ORDER BY created_at DESC'
    tasks = execute_query(query, fetch=True, readonly=True)  # <- readonly=True
    return render_template('index.html', tasks=tasks)

@app.route('/add', methods=['POST'])
def add_task():
    """Ajouter une nouvelle tâche (ÉCRITURE)"""
    title = request.form.get('title')
    description = request.form.get('description', '')
    
    if title:
        query = 'INSERT INTO tasks (title, description) VALUES (%s, %s)'
        execute_query(query, (title, description), readonly=False)  # <- readonly=False
    
    return redirect(url_for('index'))
```

**Avantages :**
- Lectures (affichage liste) -> Read replica (pas impact DB principale)
- Écritures (ajout/modif) -> DB principale
- Meilleure performance globale [OK]

---

**Je vais continuer avec le reste de l'exercice dans le prochain message...**

Veux-tu que je continue avec la suite (Monitoring, Optimisation, Sécurité, Coûts, etc.) ?

### ÉTAPE 11 : Monitoring et alertes CloudWatch

#### Métriques RDS dans CloudWatch

**CloudWatch -> Metrics -> RDS**

**Métriques automatiques (gratuites) :**

| Métrique | Description | Seuil d'alerte typique |
|----------|-------------|------------------------|
| **CPUUtilization** | % CPU utilisé | > 80% |
| **DatabaseConnections** | Connexions actives | > 80% de max_connections |
| **FreeableMemory** | RAM disponible | < 256 MB |
| **FreeStorageSpace** | Espace disque libre | < 2 GB (10%) |
| **ReadLatency** | Latence lecture (ms) | > 50ms |
| **WriteLatency** | Latence écriture (ms) | > 100ms |
| **ReadIOPS** | Opérations lecture/sec | Dépend du workload |
| **WriteIOPS** | Opérations écriture/sec | Dépend du workload |
| **NetworkReceiveThroughput** | Bande passante entrante | - |
| **NetworkTransmitThroughput** | Bande passante sortante | - |

---

**Enhanced Monitoring (activé dans ÉTAPE 4) :**

**Métriques OS-level (plus précises) :**
- CPU par processus (postgres, wal writer, etc.)
- RAM utilisée par processus
- Swap usage
- I/O operations par device
- File system stats

**Voir dans CloudWatch Logs -> Log groups :**
```
/aws/rds/instance/sirrdev-todo-db/enhanced-monitoring
```

---

#### Créer une alarme CloudWatch

**CloudWatch -> Alarms -> Create alarm**

**Alarme 1 : CPU trop élevé**

```
Select metric -> RDS -> Per-Database Metrics

DB instance identifier: sirrdev-todo-db
Metric name: CPUUtilization
```

**Cliquer "Select metric"**

---

**Specify metric and conditions :**

```
Metric name: CPUUtilization
Statistic: Average
Period: 5 minutes

Conditions:
  Threshold type: Static
  Whenever CPUUtilization is: Greater than 80
  
  (Alerte si CPU > 80% pendant 5 minutes)
```

---

**Configure actions :**

```
Alarm state trigger: In alarm

Send a notification to:
  [x] Create new topic
  
  Topic name: rds-high-cpu-alert
  Email endpoints: ton-email@example.com
```

**Cliquer "Create topic"**

**[ATTENTION] Tu recevras un email de confirmation à valider !**

---

**Name and description :**

```
Alarm name: sirrdev-todo-db-high-cpu
Alarm description: Alert when RDS CPU exceeds 80% for 5 minutes
```

**Cliquer "Create alarm"**

**[OK] Alarme créée !**

---

**Vérifier ton email et confirmer la souscription SNS**

**Email reçu :**
```
Subject: AWS Notification - Subscription Confirmation

You have chosen to subscribe to the topic:
arn:aws:sns:eu-west-1:123456789012:rds-high-cpu-alert

To confirm this subscription, click here: [Lien]
```

**Cliquer sur le lien de confirmation**

**[OK] Souscription confirmée ! Tu recevras des alertes par email.**

---

**Créer d'autres alarmes importantes :**

**Alarme 2 : Espace disque faible**

```
Metric: FreeStorageSpace
Condition: Less than 2147483648 (2 GB en bytes)
Period: 5 minutes
Action: Même SNS topic (rds-high-cpu-alert)
Name: sirrdev-todo-db-low-storage
```

---

**Alarme 3 : Trop de connexions**

```
Metric: DatabaseConnections
Condition: Greater than 80
Period: 5 minutes
  (db.t3.micro : max_connections = 87 par défaut)
Action: Même SNS topic
Name: sirrdev-todo-db-high-connections
```

---

**Alarme 4 : RAM faible**

```
Metric: FreeableMemory
Condition: Less than 268435456 (256 MB en bytes)
Period: 5 minutes
Action: Même SNS topic
Name: sirrdev-todo-db-low-memory
```

---

#### Dashboard CloudWatch personnalisé

**CloudWatch -> Dashboards -> Create dashboard**

```
Dashboard name: RDS-Todo-Monitoring
```

**Cliquer "Create dashboard"**

---

**Add widget -> Line**

```
Data source: Metrics

RDS -> Per-Database Metrics
[x] CPUUtilization (sirrdev-todo-db)
[x] DatabaseConnections (sirrdev-todo-db)
[x] FreeableMemory (sirrdev-todo-db)
```

**Cliquer "Create widget"**

---

**Ajouter d'autres widgets :**

**Widget 2 : Latence I/O**
```
Metrics:
  [x] ReadLatency
  [x] WriteLatency
```

**Widget 3 : IOPS**
```
Metrics:
  [x] ReadIOPS
  [x] WriteIOPS
```

**Widget 4 : Storage**
```
Metrics:
  [x] FreeStorageSpace
```

---

**Résultat : Dashboard complet avec tous les KPIs RDS [OK]**

**Accessible via CloudWatch -> Dashboards -> RDS-Todo-Monitoring**

---

### ÉTAPE 12 : Optimiser les performances (Parameter Groups)

#### Comprendre les Parameter Groups

**Parameter Group = Configuration du moteur PostgreSQL**

**Par défaut, RDS utilise `default.postgres15`**

**On ne peut PAS modifier les parameter groups par défaut -> Créer un custom**

---

#### Créer un custom parameter group

**RDS -> Parameter groups -> Create parameter group**

```
Parameter group family: postgres15

Type: DB Parameter Group
  (Pas "DB Cluster" - c'est pour Aurora)

Group name: sirrdev-postgres15-optimized

Description: Optimized PostgreSQL 15 parameters for t3.micro
```

**Cliquer "Create"**

**[OK] Parameter group créé !**

---

#### Modifier les paramètres

**Parameter groups -> sirrdev-postgres15-optimized -> Edit parameters**

**Paramètres à optimiser pour t3.micro (1 GB RAM) :**

---

**1. shared_buffers**

```
Valeur par défaut : {DBInstanceClassMemory/10922} (≈ 100 MB pour t3.micro)
Nouvelle valeur : {DBInstanceClassMemory/4} (≈ 256 MB)
```

**Explication :**
- Cache PostgreSQL en RAM
- Recommandation : 25% de la RAM totale
- Plus = moins de lectures disque

**[ATTENTION] Attention : Ne pas dépasser 40% RAM (risque OOM)**

---

**2. effective_cache_size**

```
Valeur par défaut : {DBInstanceClassMemory*3/4} (≈ 768 MB)
Nouvelle valeur : {DBInstanceClassMemory*3/4} (garder)
```

**Explication :**
- Indication au query planner de la mémoire disponible
- Utilisé pour planifier les requêtes (pas allocation réelle)
- Recommandation : 50-75% de la RAM totale

---

**3. work_mem**

```
Valeur par défaut : 4096 (4 MB)
Nouvelle valeur : 16384 (16 MB)
```

**Explication :**
- Mémoire allouée par opération de tri/hash
- Si trop petit -> Tris sur disque (lent)
- Si trop grand × connexions -> OOM

**Calcul :**
```
work_mem × max_connections < RAM disponible
16 MB × 87 connexions = 1.4 GB

[ATTENTION] Dangereux si toutes les connexions trient en même temps !
Solution : Limiter max_connections ou réduire work_mem
```

---

**4. maintenance_work_mem**

```
Valeur par défaut : {DBInstanceClassMemory/16384} (≈ 64 MB)
Nouvelle valeur : 131072 (128 MB)
```

**Explication :**
- Mémoire pour VACUUM, CREATE INDEX, ALTER TABLE
- Opérations de maintenance
- Pas utilisé par connexions normales
- Plus = maintenance plus rapide

---

**5. max_connections**

```
Valeur par défaut : LEAST({DBInstanceClassMemory/9531392},5000) (≈ 87 pour t3.micro)
Nouvelle valeur : 50
```

**Explication :**
- Nombre max de connexions simultanées
- Pour t3.micro : 87 par défaut (trop pour 1 GB RAM)
- Réduire = plus de RAM par connexion
- Utiliser un connection pooler (PgBouncer) si besoin de plus

---

**6. random_page_cost**

```
Valeur par défaut : 4
Nouvelle valeur : 1.1
```

**Explication :**
- Coût estimé d'une lecture aléatoire sur disque
- 4 = HDD traditionnel
- 1.1 = SSD (gp3 RDS)
- Influence le query planner (index vs seq scan)

---

**7. effective_io_concurrency**

```
Valeur par défaut : 1
Nouvelle valeur : 200
```

**Explication :**
- Nombre d'opérations I/O simultanées pour SSD
- 1 = HDD (une tête de lecture)
- 200 = SSD (multiples opérations parallèles)
- Améliore performance sur gp3

---

**8. log_min_duration_statement**

```
Valeur par défaut : -1 (désactivé)
Nouvelle valeur : 1000 (1 seconde)
```

**Explication :**
- Log toutes les requêtes > 1 seconde
- Identifier les requêtes lentes
- Utile pour optimisation
- [ATTENTION] Peut générer beaucoup de logs (coût CloudWatch Logs)

---

**9. log_connections et log_disconnections**

```
Valeur par défaut : 0 (désactivé)
Nouvelle valeur : 1 (activé)
```

**Explication :**
- Log chaque connexion/déconnexion
- Audit et debugging
- Détecte les connection leaks

---

**Cliquer "Save changes"**

**[OK] Paramètres modifiés !**

---

#### Appliquer le parameter group à l'instance

**RDS -> Databases -> sirrdev-todo-db -> Modify**

```
Database options:
  DB parameter group: sirrdev-postgres15-optimized
```

**Scroll en bas :**

```
Scheduling of modifications:
  [x] Apply during the next scheduled maintenance window
    OU
  [x] Apply immediately
    (Nécessite un reboot - choisir si urgent)
```

**Cliquer "Continue" -> "Modify DB instance"**

---

**Si "Apply immediately" :**

**RDS -> Databases -> sirrdev-todo-db**

```
Status: Modifying... -> Rebooting... -> Available

Parameter group: sirrdev-postgres15-optimized (in-sync) [OK]
```

**Downtime : ~3-5 minutes pendant le reboot**

---

**Vérifier les paramètres appliqués :**

**Depuis l'instance EC2, se connecter en psql :**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production
```

---

**Requêtes pour vérifier :**

```sql
-- Voir un paramètre spécifique
SHOW shared_buffers;
```

**Résultat :**

```
 shared_buffers 
----------------
 256MB
```

**[OK] Paramètre appliqué !**

---

**Voir tous les paramètres modifiés :**

```sql
SELECT 
    name, 
    setting, 
    unit, 
    source,
    short_desc
FROM pg_settings 
WHERE source != 'default'
ORDER BY name;
```

---

**Tester la performance :**

```sql
-- Créer une table de test avec beaucoup de données
CREATE TABLE perf_test AS
SELECT 
    generate_series(1, 100000) AS id,
    md5(random()::text) AS data,
    now() - (random() * interval '365 days') AS created_at;

-- Index sur created_at
CREATE INDEX idx_perf_test_created_at ON perf_test(created_at);

-- Requête avec tri (utilise work_mem)
EXPLAIN ANALYZE
SELECT * FROM perf_test 
WHERE created_at > now() - interval '30 days'
ORDER BY created_at DESC;
```

**Résultat (exemple) :**

```
Sort  (cost=2345.67..2456.78 rows=44444 width=49) (actual time=25.123..28.456 rows=8333 loops=1)
  Sort Key: created_at DESC
  Sort Method: quicksort  Memory: 1234kB  <- Utilise RAM (work_mem), pas disque [OK]
  ->  Seq Scan on perf_test  (cost=0.00..1789.00 rows=44444 width=49) (actual time=0.015..12.345 rows=8333 loops=1)
        Filter: (created_at > (now() - '30 days'::interval))
Planning Time: 0.234 ms
Execution Time: 30.123 ms
```

**Si `Sort Method: external merge Disk` -> work_mem trop petit**

---

**Nettoyer la table de test :**

```sql
DROP TABLE perf_test;
```

---

### ÉTAPE 13 : Sécurité avancée

#### Activer SSL/TLS obligatoire

**Par défaut, RDS accepte connexions SSL et non-SSL**

**Forcer SSL uniquement :**

**Parameter group -> sirrdev-postgres15-optimized -> Edit**

```
Paramètre: rds.force_ssl
Nouvelle valeur: 1
```

**Save changes -> Reboot instance**

---

**Vérifier depuis psql :**

```bash
# Essayer de se connecter SANS SSL
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production \
  --set=sslmode=disable
```

**Résultat :**

```
psql: error: connection to server at "..." failed: FATAL: no pg_hba.conf entry for host "...", user "postgres", database "todo_production", no encryption
```

**[X] Connexion refusée (pas de SSL) [OK] (c'est ce qu'on veut)**

---

**Connexion AVEC SSL (fonctionne) :**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production
```

**Résultat :**

```
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

todo_production=> 
```

**[OK] SSL forcé !**

---

#### IAM Database Authentication (avancé)

**Se connecter à RDS avec IAM (sans mot de passe) :**

**Avantages :**
- Pas de mot de passe en dur
- Tokens temporaires (15 minutes)
- Audit via CloudTrail
- Rotation automatique

---

**Activer IAM authentication sur RDS :**

**RDS -> Databases -> sirrdev-todo-db -> Modify**

```
Database authentication:
  [x] Password and IAM database authentication
```

**Apply immediately -> Modify**

---

**Créer un utilisateur PostgreSQL pour IAM :**

```sql
-- Se connecter en tant que postgres (master user)
psql ...

-- Créer un utilisateur IAM
CREATE USER iam_user;

-- Donner le rôle rds_iam
GRANT rds_iam TO iam_user;

-- Donner des permissions
GRANT CONNECT ON DATABASE todo_production TO iam_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO iam_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO iam_user;
```

---

**Modifier le rôle IAM de l'instance EC2 :**

**IAM -> Roles -> EC2-S3-Full-Access -> Add permissions -> Attach policies**

**Créer une policy custom :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "rds-db:connect",
      "Resource": "arn:aws:rds-db:eu-west-1:123456789012:dbuser:db-XXXXXXXXXXXXX/iam_user"
    }
  ]
}
```

**[ATTENTION] Remplacer :**
- `123456789012` : Ton AWS Account ID
- `db-XXXXXXXXXXXXX` : Resource ID de l'instance RDS

**Pour trouver le Resource ID :**

**RDS -> Databases -> sirrdev-todo-db -> Configuration**

```
Resource ID: db-ABC123DEF456GHI789
```

**ARN complet :**

```
arn:aws:rds-db:eu-west-1:123456789012:dbuser:db-ABC123DEF456GHI789/iam_user
```

---

**Se connecter avec IAM (depuis EC2) :**

```bash
# Installer awscli (si pas déjà fait)
pip install awscli

# Générer un token IAM (valide 15 minutes)
export PGPASSWORD=$(aws rds generate-db-auth-token \
    --hostname sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
    --port 5432 \
    --region eu-west-1 \
    --username iam_user)

# Se connecter avec le token
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=iam_user \
  --dbname=todo_production
```

**Résultat :**

```
SSL connection (protocol: TLSv1.3, ...)
Type "help" for help.

todo_production=> 
```

**[OK] Connexion via IAM (sans mot de passe) !**

---

#### Rotation automatique des credentials (Secrets Manager)

**Au lieu de stocker le mot de passe en dur dans `db_config.py` :**

**AWS Secrets Manager -> Store a new secret**

```
Secret type: Credentials for Amazon RDS database

Username: postgres
Password: (ton mot de passe RDS)

Database: sirrdev-todo-db
```

**Next**

```
Secret name: rds/todo-production/master

Automatic rotation: Enable
  Rotation schedule: 30 days
  Lambda function: (Create new - AWS gère)
```

**Store**

---

**Récupérer le secret depuis Flask :**

```python
import boto3
import json

def get_db_credentials():
    """Récupère les credentials depuis Secrets Manager"""
    secret_name = "rds/todo-production/master"
    region_name = "eu-west-1"
    
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )
    
    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except Exception as e:
        raise e
    
    secret = json.loads(get_secret_value_response['SecretString'])
    return secret

# Utilisation
credentials = get_db_credentials()

POSTGRES_CONFIG = {
    'host': credentials['host'],
    'port': credentials['port'],
    'database': credentials['dbname'],
    'user': credentials['username'],
    'password': credentials['password'],
    'sslmode': 'require'
}
```

**Avantages :**
- Pas de mot de passe en dur
- Rotation automatique tous les 30 jours
- Audit CloudTrail
- Chiffrement au repos

**Coût :**
- 0.40$/secret/mois
- 0.05$/10000 appels API

---

### ÉTAPE 14 : Optimisation des coûts

#### Analyser les coûts RDS

**Cost Explorer -> Filters**

```
Service: Amazon Relational Database Service
Time: Last 3 months
Group by: Usage type
```

**Breakdown typique :**

```
InstanceUsage:db.t3.micro        13.14$/mois  (instance)
StorageUsage                      2.30$/mois  (20 GB gp3)
BackupUsage                       0.00$/mois  (< 20 GB gratuit)
Multi-AZ:InstanceUsage           13.14$/mois  (si activé)
```

---

#### Optimisations pour réduire les coûts

**1. Utiliser Reserved Instances (production)**

**Engagement 1 an ou 3 ans -> Jusqu'à 69% de réduction**

**RDS -> Reserved instances -> Purchase reserved DB instance**

```
DB instance class: db.t3.micro
Deployment option: Single-AZ (ou Multi-AZ)
Term: 1 Year
Payment option: All Upfront (le plus économique)

Coût:
  On-Demand: 13.14$/mois × 12 = 157.68$/an
  Reserved (1 yr, all upfront): 78$/an (50% économie) [OK]
```

**[ATTENTION] Réservation = Engagement (pas de remboursement)**

---

**2. Stopper l'instance en dehors des heures de travail**

**Pour dev/test uniquement (pas production) :**

**RDS -> Databases -> sirrdev-todo-db -> Actions -> Stop temporarily**

**Limites :**
- Max 7 jours (AWS redémarre auto après)
- Backups continuent (coût storage)
- Snapshots préservés

**Automatiser avec Lambda + EventBridge :**

```python
# Lambda function: stop-rds-nights.py
import boto3

rds = boto3.client('rds')

def lambda_handler(event, context):
    # Arrêter l'instance RDS
    rds.stop_db_instance(DBInstanceIdentifier='sirrdev-todo-db')
    return {'status': 'stopped'}
```

**EventBridge rule :**
```
Schedule: cron(0 20 * * ? *) # Tous les jours à 20h UTC
Target: Lambda function (stop-rds-nights)
```

**Économie :** ~60% si arrêt 12h/jour (hors prod uniquement)

---

**3. Utiliser Graviton (db.t4g au lieu de db.t3)**

**db.t4g = Processeurs ARM Graviton (plus efficaces) :**

```
db.t3.micro: 0.018$/h
db.t4g.micro: 0.016$/h (11% moins cher) [OK]
```

**[ATTENTION] Vérifier compatibilité application (ARM vs x86)**

---

**4. Optimiser le stockage**

**Supprimer les snapshots manuels anciens :**

```
RDS -> Snapshots
  -> Sélectionner snapshots > 30 jours
  -> Delete
```

**Activer Storage Autoscaling modéré :**

```
Allocated storage: 20 GB
Max storage threshold: 30 GB (pas 1000 GB)
```

**Évite de payer pour du stockage inutile**

---

**5. Utiliser Aurora Serverless v2 (si workload variable)**

**Pour des workloads très variables :**

**Aurora Serverless v2 :**
- Scale automatiquement (0.5 ACU -> 128 ACU)
- Paye seulement ce que tu utilises
- Compatible PostgreSQL/MySQL

**Coût :**
```
0.12$/ACU-hour

1 ACU = 2 GB RAM + CPU proportionnel

Exemple:
  App inactive: 0.5 ACU × 8h = 0.48$/jour
  App active: 2 ACU × 16h = 3.84$/jour
  Moyenne: ~2.16$/jour (~65$/mois)
```

**Quand utiliser Aurora Serverless :**
- [OK] Workload imprévisible
- [OK] Dev/test
- [OK] Apps occasionnelles

**Quand rester sur RDS standard :**
- [OK] Workload stable 24/7
- [OK] Très petit (t3.micro moins cher qu'Aurora)

---

### ÉTAPE 15 : Restauration et Disaster Recovery

#### Scénarios de disaster recovery

**Scénario 1 : Suppression accidentelle de données**

```sql
-- Oops ! Suppression de toutes les tâches
DELETE FROM tasks;

-- 1000 tâches supprimées !
```

**Solution : Point-in-Time Recovery (PITR)**

**RDS -> Databases -> sirrdev-todo-db -> Actions -> Restore to point in time**

```
Restore time: 5 minutes avant la suppression
DB instance identifier: sirrdev-todo-db-recovered
```

**Vérifier les données -> Copier les tâches vers DB principale**

---

**Scénario 2 : Corruption de l'instance**

**Problème : L'instance RDS ne démarre plus**

**Solution : Restaurer depuis snapshot**

**RDS -> Snapshots -> Dernier snapshot automatique -> Restore**

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

---

**Scénario 3 : Catastrophe régionale (toute eu-west-1 down)**

**Solution : Cross-Region Replica + Promotion**

**Créer un Read Replica dans autre région :**

```
RDS -> Create read replica
Region: US East (N. Virginia) us-east-1
```

**En cas de catastrophe :**

```
RDS -> Databases -> replica-us-east-1 -> Actions -> Promote
```

**Le replica devient instance standalone**

**RTO (Recovery Time Objective) : 15-30 minutes**

**RPO (Recovery Point Objective) : < 5 minutes (replication lag)**

---

#### Plan de disaster recovery complet

**1. Backups automatiques activés (7 jours minimum) [OK]**

**2. Snapshots manuels avant changements majeurs [OK]**

**3. Multi-AZ activé (production) [OK]**

**4. Cross-region replica (haute criticité seulement)**

**5. Tests de restauration réguliers (1× par trimestre)**

**6. Documentation du processus (runbook)**

**7. Alertes CloudWatch configurées [OK]**

**8. Monitoring 24/7 (PagerDuty, OpsGenie, etc.)**

---

### [OK] TESTS DE VALIDATION

**Infrastructure RDS :**
- [ ] Instance RDS PostgreSQL 15 créée et disponible
- [ ] DB Subnet Group avec 2+ subnets privés dans 2 AZs
- [ ] Security Group autorise accès depuis EC2 uniquement
- [ ] Endpoint RDS accessible depuis EC2
- [ ] SSL/TLS activé et forcé
- [ ] Multi-AZ configuré (optionnel, production)

**Migration de données :**
- [ ] Schéma PostgreSQL créé (table tasks)
- [ ] Données migrées depuis SQLite
- [ ] Séquence réinitialisée correctement
- [ ] Indices créés (performance)

**Application Flask :**
- [ ] psycopg2 installé
- [ ] Configuration DB centralisée (db_config.py)
- [ ] Flask se connecte à RDS (pas SQLite)
- [ ] CRUD fonctionne (Create, Read, Update, Delete)
- [ ] Upload S3 toujours fonctionnel
- [ ] Read replica configuré et utilisé (optionnel)

**Backups :**
- [ ] Automated backups activés (7 jours)
- [ ] Point-in-Time Recovery disponible
- [ ] Snapshot manuel créé
- [ ] Restauration testée (nouvelle instance)

**Performance :**
- [ ] Custom parameter group créé
- [ ] Paramètres optimisés (shared_buffers, work_mem, etc.)
- [ ] Parameter group appliqué à l'instance
- [ ] Performance vérifiée (requêtes < 50ms)

**Monitoring :**
- [ ] CloudWatch métriques visibles
- [ ] Enhanced Monitoring activé (60s)
- [ ] 4 alarmes CloudWatch créées et actives
- [ ] SNS topic configuré avec email confirmé
- [ ] Dashboard CloudWatch créé

**Sécurité :**
- [ ] RDS dans subnets privés (pas d'IP publique)
- [ ] Security Group restrictif (seulement port 5432 depuis EC2)
- [ ] SSL/TLS forcé (rds.force_ssl=1)
- [ ] Credentials sécurisés (pas en clair dans code)
- [ ] IAM authentication testée (avancé, optionnel)
- [ ] Secrets Manager configuré (optionnel)

**Coûts :**
- [ ] Instance dans Free Tier (db.t3.micro)
- [ ] Storage ≤ 20 GB
- [ ] Backup storage ≤ DB size (gratuit)
- [ ] Multi-AZ désactivé (pour Free Tier) OU budget approuvé
- [ ] Read replica désactivé (pour Free Tier) OU budget approuvé

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : "could not connect to server"

**Symptôme :**

```bash
psql: error: connection to server at "..." port 5432 failed: Connection timed out
```

**Causes et solutions :**

**1. Security Group incorrect**

**Vérifier :**
```
RDS SG inbound rules doit avoir:
PostgreSQL (5432) depuis Security Group de EC2
```

**Pas depuis IP (change si instance EC2 redémarre)**

---

**2. RDS dans subnet public (mauvaise pratique)**

**Si vraiment nécessaire (dev seulement) :**
```
RDS -> Modify
Public access: Yes
Security Group: Ajouter ton IP sur port 5432
```

**[ATTENTION] Dangereux en production !**

---

**3. Route table mal configurée**

**DB subnet doit être dans route table privée (pas de IGW direct)**

**Mais EC2 doit être dans subnet avec NAT ou IGW pour installer packages**

---

#### Erreur 2 : "FATAL: password authentication failed"

**Symptôme :**

```
psql: FATAL: password authentication failed for user "postgres"
```

**Solutions :**

**1. Mot de passe incorrect**

**Vérifier le mot de passe (attention casse, caractères spéciaux)**

**Réinitialiser le mot de passe :**

```
RDS -> Databases -> sirrdev-todo-db -> Modify
New master password: NouveauMotDePasse123!
Apply immediately
```

---

**2. Utilisateur inexistant**

```sql
-- Se connecter en tant que master user
-- Créer l'utilisateur
CREATE USER monuser WITH PASSWORD 'password';
GRANT ALL ON DATABASE todo_production TO monuser;
```

---

#### Erreur 3 : "too many connections"

**Symptôme :**

```
FATAL: remaining connection slots are reserved for non-replication superuser connections
```

**Cause : max_connections atteint**

**Solutions :**

**1. Fermer les connexions inactives**

```sql
-- Voir les connexions actives
SELECT 
    pid,
    usename,
    application_name,
    state,
    state_change
FROM pg_stat_activity
WHERE datname = 'todo_production';

-- Tuer une connexion inactive
SELECT pg_terminate_backend(12345); -- Remplacer par le PID
```

---

**2. Augmenter max_connections (parameter group)**

```
max_connections: 87 -> 100

[ATTENTION] Plus de connexions = plus de RAM nécessaire
```

---

**3. Utiliser un connection pooler (PgBouncer)**

**Installer sur EC2 :**

```bash
sudo apt install pgbouncer -y
```

**Configurer `/etc/pgbouncer/pgbouncer.ini` :**

```ini
[databases]
todo_production = host=sirrdev-todo-db.xxx.rds.amazonaws.com port=5432 dbname=todo_production

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
```

**Flask se connecte à PgBouncer (localhost:6432) au lieu de RDS direct**

**Résultat : 1000 connexions app -> 20 connexions RDS [OK]**

---

#### Erreur 4 : "disk full" / "out of space"

**Symptôme :**

```
ERROR: could not extend file "base/12345/67890": No space left on device
```

**Solutions :**

**1. Augmenter le stockage**

```
RDS -> Modify
Allocated storage: 20 GB -> 50 GB
Apply immediately
```

**Durée : Quelques minutes, pas de downtime**

---

**2. Nettoyer les données inutiles**

```sql
-- Vacuum full (récupère l'espace)
VACUUM FULL;

-- Supprimer anciennes données
DELETE FROM tasks WHERE created_at < now() - interval '1 year';
```

---

**3. Activer storage autoscaling**

```
RDS -> Modify
[x] Enable storage autoscaling
Maximum storage threshold: 100 GB
```

**AWS augmente automatiquement si > 90% plein**

---

#### Erreur 5 : Performance dégradée après migration

**Symptôme : Requêtes lentes (> 1 seconde)**

**Causes et solutions :**

**1. Indices manquants**

```sql
-- Identifier les scans séquentiels (lents)
SELECT 
    schemaname,
    tablename,
    seq_scan,
    seq_tup_read,
    idx_scan,
    seq_tup_read / seq_scan AS avg_seq_read
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC;

-- Créer des indices sur colonnes fréquemment filtrées
CREATE INDEX idx_tasks_completed ON tasks(completed);
CREATE INDEX idx_tasks_created_at ON tasks(created_at);
```

---

**2. VACUUM pas fait**

```sql
-- Analyser les tables (stats pour query planner)
ANALYZE;

-- Vacuum (récupère espace, met à jour stats)
VACUUM ANALYZE;
```

**En production : Autovacuum activé par défaut (RDS)**

---

**3. Paramètres mal configurés**

**Vérifier les paramètres clés :**

```sql
SHOW shared_buffers;  -- Doit être ~256 MB (25% RAM)
SHOW work_mem;        -- Doit être 8-16 MB
SHOW effective_cache_size;  -- Doit être ~768 MB (75% RAM)
```

---

**4. Connection pooling manquant**

**Chaque requête Flask ouvre/ferme une connexion -> Lent**

**Solution : Réutiliser les connexions (pool)**

```python
import psycopg2.pool

# Créer un pool de connexions (au démarrage de Flask)
connection_pool = psycopg2.pool.SimpleConnectionPool(
    1,  # minconn
    10, # maxconn
    **POSTGRES_CONFIG
)

def get_db_connection():
    return connection_pool.getconn()

def release_connection(conn):
    connection_pool.putconn(conn)
```

---

#### Erreur 6 : Coûts inattendus

**Symptôme : Facture RDS = 50$/mois au lieu de 0$**

**Causes :**

**1. Multi-AZ activé par erreur**

```
Coût: Instance × 2 = 13.14$ × 2 = 26.28$/mois
```

**Désactiver :**

```
RDS -> Modify
[ ] Multi-AZ deployment
Apply immediately
```

---

**2. Read replicas oubliés**

```
Coût: +13.14$/mois par replica
```

**Supprimer :**

```
RDS -> Databases -> replica -> Delete
```

---

**3. Snapshots manuels accumulés**

```
Coût: 0.095$/GB-mois (au-delà de DB size)
```

**Nettoyer :**

```
RDS -> Snapshots
  -> Supprimer snapshots > 90 jours
```

---

**4. Storage qui grandit (autoscaling actif)**

```
20 GB -> 50 GB -> 100 GB (autoscaling)
Coût: 0.115$/GB × 100 = 11.50$/mois
```

**Désactiver autoscaling ou limiter le max**

---

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

**RDS vs DB sur EC2 :**
- RDS = Managé (backups auto, patches, Multi-AZ facile)
- EC2 = Contrôle total, plus complexe, plus d'effort
- Production = RDS recommandé (sauf cas très spécifiques)

**Choix du moteur :**
- PostgreSQL = Fonctionnalités riches, ACID strict, analytics
- MySQL = Simplicité, popularité, compatibilité
- Aurora = Performance maximale, haute dispo, plus cher

**Networking :**
- Toujours dans subnets privés (sécurité)
- Security Groups = Pare-feu (port 5432 depuis EC2 seulement)
- Public access = [X] Dangereux (sauf dev temporaire)

**Backups :**
- Automated backups = Quotidiens, PITR jusqu'à 35 jours
- Manual snapshots = Avant changements majeurs
- Multi-AZ = Haute dispo (pas backup, réplication sync)
- Read replica = Performance (pas backup, réplication async)

**Performance :**
- Parameter groups = Config moteur (shared_buffers, work_mem, etc.)
- Indices = Essentiels pour requêtes rapides
- Connection pooling = Réutiliser connexions
- VACUUM = Maintenance régulière (auto par défaut)

**Sécurité :**
- SSL/TLS obligatoire (rds.force_ssl=1)
- Credentials dans Secrets Manager (pas en dur)
- IAM authentication = Tokens temporaires (pas mot de passe)
- Security Groups restrictifs
- Audit avec CloudTrail + Database logs

**Monitoring :**
- CloudWatch métriques = CPU, RAM, IOPS, latence
- Enhanced Monitoring = OS-level (processus, I/O)
- Alarmes = Proactif (alerte avant problème)
- Slow query logs = Identifier requêtes à optimiser

**Coûts :**
- Free Tier = db.t3.micro, 20 GB, 750h/mois (12 mois)
- Multi-AZ = Double le coût instance
- Reserved instances = 50%+ économie (engagement 1-3 ans)
- Arrêter en dev/test = Économie significative
- Storage autoscaling = Pratique mais surveiller

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Amazon Aurora (PostgreSQL compatible)**

**RDS Aurora = Version AWS-optimisée de PostgreSQL/MySQL**

**Avantages vs RDS PostgreSQL :**
- Performance : 3-5× plus rapide (architecture distribuée)
- Stockage : Auto-scaling jusqu'à 128 TB
- Réplication : Jusqu'à 15 read replicas (vs 5 RDS standard)
- Failover : < 30 secondes (vs 60-120s RDS Multi-AZ)
- Backup : Continu vers S3 (pas impact performance)
- Global Database : Réplication cross-region < 1 seconde

**Inconvénients :**
- Coût : 2-3× plus cher que RDS standard
- Pas de Free Tier

**Quand utiliser Aurora :**
- [OK] Apps critiques (haute dispo cruciale)
- [OK] Gros volume de données (> 100 GB)
- [OK] Besoin de performance extrême
- [OK] Multi-region / Global apps

**Migration RDS -> Aurora :**

```
RDS -> Databases -> sirrdev-todo-db -> Actions -> Create Aurora read replica

Aurora cluster créé
  -> Promotion possible en cluster standalone
```

---

**2. Aurora Serverless v2**

**Database serverless = Auto-scaling complet**

**Fonctionnement :**
- Scale de 0.5 ACU à 128 ACU (Aurora Capacity Units)
- Ajustement en secondes (pas minutes)
- Paye seulement ce qui est utilisé

**Cas d'usage :**
- Apps avec trafic très variable
- Dev/test (scale down la nuit)
- Apps occasionnelles (SaaS multi-tenant)

**Coût :**
```
1 ACU = 2 GB RAM + CPU
Prix: 0.12$/ACU-hour (eu-west-1)

Exemple dev/test:
  Nuit (8h): 0.5 ACU × 8 = 0.48$/jour
  Jour (16h): 2 ACU × 16 = 3.84$/jour
  Total: ~4.32$/jour × 30 = ~130$/mois

vs RDS standard t3.micro: 13$/mois (moins cher pour constant load)
```

---

**3. RDS Proxy (Connection pooling managé)**

**Problème : Connection storms, too many connections**

**RDS Proxy = Connection pooler managé par AWS**

**Avantages :**
- Pool automatique de connexions
- Failover transparent (masque changement IP)
- IAM authentication intégré
- Supporte Lambda (connections éphémères)

**Architecture :**

```
Flask App -> RDS Proxy (pool) -> RDS Database
  1000 connexions app -> 20 connexions DB
```

**Configuration :**

```
RDS -> Proxies -> Create proxy

Proxy identifier: todo-db-proxy
Target group: sirrdev-todo-db
Authentication: IAM ou Secrets Manager

Connectivity:
  Subnets: Privés (même que RDS)
  Security group: Créer nouveau
```

**Flask se connecte au proxy endpoint :**

```python
POSTGRES_CONFIG = {
    'host': 'todo-db-proxy.proxy-xxx.eu-west-1.rds.amazonaws.com',
    ...
}
```

**Coût : 0.015$/vCPU-hour (≈ 10$/mois pour 2 vCPU)**

---

**4. Database Migration Service (DMS)**

**Migration complexe avec downtime minimal**

**Scénario : Migration MySQL on-premise -> RDS PostgreSQL**

**DMS :**
- Réplication continue (Change Data Capture)
- Conversion de schéma (AWS SCT - Schema Conversion Tool)
- Downtime minimal (< 5 minutes)

**Processus :**

```
1. Full load: Copie initiale des données
2. CDC (Change Data Capture): Réplication continue des changements
3. Cutover: Basculer l'app vers nouvelle DB
```

**Coût : 0.14$/h (instance c4.large)**

---

**5. Performance Insights (analyse approfondie)**

**RDS Performance Insights = Profiling SQL avancé**

**Activer :**

```
RDS -> Modify
[x] Turn on Performance Insights
Retention: 7 days (gratuit) ou 731 days (0.10$/vCPU/jour)
```

**Fonctionnalités :**
- Top SQL par temps CPU, I/O, locks
- Wait events (sur quoi la DB attend)
- Graphiques interactifs (drill-down)
- Identification des requêtes problématiques

**Console RDS -> Performance Insights -> sirrdev-todo-db**

**Dashboard montre :**
- Top 10 requêtes par charge (DB Load)
- Wait events (I/O, CPU, locks)
- Corrélation avec métriques CloudWatch

**Exemple d'analyse :**

```
Top query (50% du temps):
  SELECT * FROM tasks WHERE title LIKE '%AWS%'
  
Wait event: io/DataFileRead (lecture disque)

Action: Créer index full-text
  CREATE INDEX idx_tasks_title_fulltext ON tasks USING gin(to_tsvector('english', title));
```

---

**6. Blue/Green Deployments**

**Déployer changements sans downtime**

**Blue/Green :**
- Blue = Environnement actuel (production)
- Green = Nouvel environnement (avec changements)
- Cutover = Basculer trafic Blue -> Green

**RDS Blue/Green Deployment :**

```
RDS -> Databases -> sirrdev-todo-db -> Actions -> Create Blue/Green Deployment

Green environment configuration:
  DB parameter group: nouveau-parameter-group
  DB engine version: PostgreSQL 16 (upgrade)
```

**AWS crée :**
1. Clone exact de la DB (Green)
2. Réplication continue Blue -> Green
3. Quand prêt : Switch DNS endpoint (30-60 secondes downtime)

**Use cases :**
- Upgrade PostgreSQL 15 -> 16
- Tester parameter group en prod-like
- Changements de schéma (Blue = old, Green = new)

---

**7. Cross-Region Automated Backups**

**Backups dans autre région (Disaster Recovery)**

```
RDS -> Automated backups -> Manage cross-Region automated backups

Source region: eu-west-1
Destination region: us-east-1
Backup retention: 7 days
```

**Coût :**
- Storage: 0.095$/GB-mois (région destination)
- Data transfer: 0.02$/GB (entre régions)

**En cas de catastrophe eu-west-1 :**

```
us-east-1 -> RDS -> Automated backups
  -> Restore backup -> Nouvelle instance dans us-east-1
```

**RTO : 30-60 minutes**

---

**8. Database Activity Streams**

**Audit complet de toutes les activités DB**

**Fonctionnalités :**
- Log TOUTES les connexions et requêtes
- Stockage dans Kinesis Data Streams
- Chiffrement de bout en bout
- Compliance (SOC, PCI-DSS, etc.)

**Activer :**

```
RDS -> Databases -> sirrdev-todo-db -> Modify
Database activity stream: Enabled
KMS key: (AWS managed ou custom)
```

**Exemples d'events loggés :**

```json
{
  "type": "record",
  "clusterId": "cluster-xxx",
  "instanceId": "instance-xxx",
  "databaseActivityEventList": [
    {
      "logTime": "2024-12-16 10:30:45.123",
      "type": "connect",
      "clientApplication": "psql",
      "databaseName": "todo_production",
      "dbUserName": "postgres",
      "remoteHost": "10.0.1.234",
      "commandText": "",
      "exitCode": "0"
    },
    {
      "logTime": "2024-12-16 10:30:50.456",
      "type": "query",
      "databaseName": "todo_production",
      "dbUserName": "postgres",
      "remoteHost": "10.0.1.234",
      "commandText": "SELECT * FROM tasks WHERE id = 42",
      "exitCode": "0"
    }
  ]
}
```

**Coût : 0.18$/100k events + Kinesis (0.015$/shard-hour)**

**Use case :** Compliance, forensics, détection intrusions

---

**9. Custom Engines (Babelfish pour SQL Server)**

**Babelfish = Émulateur SQL Server dans Aurora PostgreSQL**

**Problème :** Migration SQL Server -> PostgreSQL = Réécrire app

**Solution Babelfish :**
- Aurora PostgreSQL avec extension Babelfish
- Comprend le protocole TDS (SQL Server)
- Compatible T-SQL (dialecte SQL Server)

**App écrite pour SQL Server -> Fonctionne sans changement sur Aurora PostgreSQL [OK]**

**Use case :** Migration SQL Server sans réécriture

---

**10. RDS on Outposts**

**RDS dans ton datacenter on-premise**

**AWS Outposts = Rack AWS dans ton datacenter**

**RDS on Outposts :**
- Instances RDS tournent physiquement chez toi
- Managées par AWS (même expérience que cloud)
- Latence ultra-faible (local)
- Compliance (données ne quittent pas le pays)

**Use case :**
- Latence critique (< 5ms)
- Compliance stricte (données locales)
- Hybrid cloud

---

## [COURS] CONCLUSION DE L'EXERCICE 3

**[BRAVO] FÉLICITATIONS ! Tu maîtrises maintenant RDS et les bases de données cloud ! [BRAVO]**

**Ce que tu as appris :**
- [OK] Concepts RDS (vs DB sur EC2)
- [OK] Choix du moteur (PostgreSQL, MySQL, Aurora)
- [OK] Création et configuration d'instance RDS
- [OK] Networking (DB Subnet Groups, Security Groups)
- [OK] Migration de données (SQLite -> PostgreSQL)
- [OK] Backups et disaster recovery (snapshots, PITR, Multi-AZ)
- [OK] Performance tuning (Parameter Groups, indices, pooling)
- [OK] Monitoring (CloudWatch, Enhanced Monitoring, alarmes)
- [OK] Sécurité (SSL, IAM auth, Secrets Manager)
- [OK] Optimisation des coûts (Reserved Instances, autoscaling)
- [OK] High availability (Multi-AZ, Read Replicas)

**Compétences acquises :**
- [OK] Administration bases de données cloud
- [OK] Migration et modernisation DB
- [OK] Optimisation performance SQL
- [OK] Disaster recovery planning
- [OK] Database security best practices
- [OK] Cloud cost optimization

**Architecture déployée :**
```
[OK] 1 Instance RDS PostgreSQL 15.x
[OK] DB dans subnets privés (2 AZs)
[OK] Security Groups configurés
[OK] Application Flask migrée vers RDS
[OK] Backups automatiques (7 jours)
[OK] Snapshots manuels
[OK] Parameter Group optimisé
[OK] CloudWatch monitoring + 4 alarmes
[OK] SSL/TLS forcé
[OK] Multi-AZ configuré (optionnel, production)
[OK] Read Replica déployé (optionnel)
```

**Coût estimé :**
- **Free Tier (Single-AZ) : 0$/mois** [OK]
- **Après Free Tier (Single-AZ) : ~13$/mois**
- **Production (Multi-AZ) : ~26$/mois**
- **Avec Read Replica : ~39$/mois**

**Temps moyen de réalisation :** 5-6 heures

**Prochaine étape :** Exercice 4 - Architecture serverless avec Lambda + API Gateway + DynamoDB ! [RAPIDE]

---

**[ATTENTION] NETTOYAGE DES RESSOURCES (si tu veux arrêter)**

```bash
# 1. Supprimer les Read Replicas (si créés)
RDS -> Databases -> sirrdev-todo-db-replica -> Delete
[ ] Create final snapshot
[x] I acknowledge...

# 2. Désactiver deletion protection sur instance principale
RDS -> sirrdev-todo-db -> Modify
[ ] Enable deletion protection
Apply immediately

# 3. Supprimer l'instance RDS
RDS -> sirrdev-todo-db -> Delete
[x] Create final snapshot: sirrdev-todo-db-final-backup-20241216
[x] I acknowledge...

# 4. (Optionnel après 30 jours) Supprimer les snapshots
RDS -> Snapshots -> Sélectionner -> Delete

# 5. Supprimer le parameter group custom
RDS -> Parameter groups -> sirrdev-postgres15-optimized -> Delete

# 6. Supprimer le DB subnet group
RDS -> Subnet groups -> sirrdev-db-subnet-group -> Delete

# 7. Supprimer le security group RDS
EC2 -> Security Groups -> sirrdev-rds-sg -> Delete

# 8. (Si créé) Supprimer le secret Secrets Manager
Secrets Manager -> rds/todo-production/master -> Delete
```

**[IDEE] Si tu continues avec l'Exercice 4 (Lambda), tu n'as pas besoin de tout supprimer !**

**Tu peux simplement arrêter l'instance RDS pour économiser :**

```
RDS -> sirrdev-todo-db -> Stop temporarily
  (Max 7 jours, puis redémarre auto)
```

---

**FIN DE L'EXERCICE 3**

═══════════════════════════════════════════════════════════════

Veux-tu que je continue avec l'**Exercice 4 : Architecture Serverless (Lambda + API Gateway + DynamoDB)** ?

# [JAUNE] EXERCICE 4 : ARCHITECTURE SERVERLESS (LAMBDA + API GATEWAY + DYNAMODB)

## [LISTE] ÉNONCÉ

### Contexte professionnel

L'application de gestion de tâches connaît un grand succès. Le CTO souhaite maintenant ajouter de nouvelles fonctionnalités tout en optimisant les coûts et la scalabilité :

**Nouvelles exigences :**
1. **API REST publique** pour permettre aux clients mobiles d'accéder aux tâches
2. **Notifications en temps réel** quand une tâche est créée/modifiée
3. **Analytics** : Compter les tâches créées par jour/mois/année
4. **Traitement asynchrone** : Génération de rapports PDF quotidiens
5. **Scalabilité automatique** : Gérer 10 utilisateurs ou 10,000 sans changement d'infrastructure
6. **Optimisation des coûts** : Payer seulement pour l'utilisation réelle (pas de serveurs idle)

**Décision : Architecture serverless avec AWS Lambda**

Le directeur technique impose :
- [X] **Pas de serveurs à gérer** (EC2, RDS, etc. pour cette partie)
- [OK] **Lambda** pour la logique métier (fonctions event-driven)
- [OK] **API Gateway** pour exposer une API REST
- [OK] **DynamoDB** pour le stockage NoSQL (haute performance, scalable)
- [OK] **EventBridge** pour les événements planifiés
- [OK] **SNS** pour les notifications
- [OK] **CloudWatch** pour logs et monitoring
- [OK] **Coût minimal** : Rester dans le Free Tier autant que possible

### Architecture cible

```
┌─────────────────────────────────────────────────────────────────┐
│                   CLIENTS (Web, Mobile, API)                     │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ HTTPS
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│                    API GATEWAY (REST API)                        │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Routes:                                                  │   │
│  │  GET    /tasks           -> Lambda: list-tasks            │   │
│  │  POST   /tasks           -> Lambda: create-task           │   │
│  │  GET    /tasks/{id}      -> Lambda: get-task              │   │
│  │  PUT    /tasks/{id}      -> Lambda: update-task           │   │
│  │  DELETE /tasks/{id}      -> Lambda: delete-task           │   │
│  │  GET    /analytics/stats -> Lambda: get-stats             │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  Authorization: API Key (ou IAM, Cognito)                        │
│  Throttling: 1000 req/sec                                        │
│  Caching: 5 minutes TTL                                          │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ Invoke
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│                    AWS LAMBDA FUNCTIONS                          │
│                                                                  │
│  ┌────────────────────────────────────────────────────────┐    │
│  │  create-task (Python 3.12)                             │    │
│  │  - Valide input                                         │    │
│  │  - Crée UUID                                            │    │
│  │  - Écrit dans DynamoDB                                  │    │
│  │  - Publie event SNS (notification)                      │    │
│  │  - Retourne 201 Created                                 │    │
│  └────────────────────────────────────────────────────────┘    │
│                                                                  │
│  ┌────────────────────────────────────────────────────────┐    │
│  │  list-tasks (Python 3.12)                              │    │
│  │  - Scan/Query DynamoDB                                  │    │
│  │  - Pagination (limit + lastEvaluatedKey)               │    │
│  │  - Filtre completed=true/false                          │    │
│  │  - Retourne JSON                                        │    │
│  └────────────────────────────────────────────────────────┘    │
│                                                                  │
│  [get-task, update-task, delete-task, get-stats...]            │
│                                                                  │
│  Runtime: Python 3.12                                            │
│  Memory: 256 MB (configurable)                                   │
│  Timeout: 10 seconds                                             │
│  IAM Role: lambda-dynamodb-role (read/write DynamoDB)           │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ Read/Write
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│                      DYNAMODB TABLE                              │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  Table: Tasks                                             │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  Partition Key: task_id (String, UUID)             │  │  │
│  │  │  Attributes:                                        │  │  │
│  │  │    - title (String)                                 │  │  │
│  │  │    - description (String)                           │  │  │
│  │  │    - completed (Boolean)                            │  │  │
│  │  │    - created_at (Number, timestamp)                 │  │  │
│  │  │    - updated_at (Number, timestamp)                 │  │  │
│  │  │    - user_id (String) - pour multi-tenant          │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │                                                           │  │
│  │  Global Secondary Index (GSI):                           │  │
│  │  - GSI-UserCreatedAt                                     │  │
│  │    PK: user_id, SK: created_at                           │  │
│  │    (Pour lister tâches par user, triées par date)       │  │
│  │                                                           │  │
│  │  Capacity: On-Demand (auto-scaling)                      │  │
│  │  Encryption: AWS managed key                             │  │
│  │  Point-in-time recovery: Enabled                         │  │
│  └──────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Event-Driven Architecture:
┌─────────────────────────────────────────────────────────────────┐
│  EventBridge Rule (Scheduled)                                    │
│  - Cron: cron(0 8 * * ? *) -> Tous les jours 8h UTC             │
│  - Target: Lambda (daily-report)                                │
│                                                                  │
│  Lambda: daily-report                                            │
│  - Scan toutes les tâches créées hier                           │
│  - Génère PDF avec reportlab                                    │
│  - Upload vers S3                                               │
│  - Envoie email SNS                                             │
└─────────────────────────────────────────────────────────────────┘

Notifications:
┌─────────────────────────────────────────────────────────────────┐
│  SNS Topic: task-notifications                                  │
│  - Subscribers: Email, SMS, Lambda                               │
│  - Publié par: create-task, update-task Lambda                  │
│  - Message: JSON avec détails de la tâche                       │
└─────────────────────────────────────────────────────────────────┘
```

### Contraintes techniques

- Région : `eu-west-1` (Irlande)
- Runtime Lambda : Python 3.12
- API Gateway : REST API (pas HTTP API pour cet exercice)
- DynamoDB : On-Demand capacity (pas Provisioned)
- Authentification : API Key (simple, pour commencer)
- Logs : CloudWatch Logs (rétention 7 jours)
- Budget : Rester dans Free Tier autant que possible
- Temps estimé : 5-6 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre l'architecture serverless (avantages/inconvénients)
- [OK] Créer et déployer des fonctions Lambda
- [OK] Configurer API Gateway (routes, méthodes, intégrations)
- [OK] Créer et gérer des tables DynamoDB
- [OK] Comprendre les indices DynamoDB (GSI, LSI)
- [OK] Gérer les permissions IAM pour Lambda
- [OK] Implémenter la pagination et le filtering
- [OK] Configurer EventBridge pour tâches planifiées
- [OK] Utiliser SNS pour notifications
- [OK] Monitorer avec CloudWatch Logs et Metrics
- [OK] Optimiser les cold starts
- [OK] Gérer les erreurs et retries
- [OK] Calculer et optimiser les coûts serverless

---

## [DOCS] PRÉREQUIS

- Exercices 1, 2 et 3 terminés (concepts AWS de base)
- Connaissances Python intermédiaires
- Compréhension des API REST
- Notions de NoSQL (vs SQL)
- Compte AWS avec Free Tier actif

---

## [ARGENT] ESTIMATION DES COÛTS

**Lambda Free Tier (permanent, pas limité à 12 mois) :**

| Ressource | Quota gratuit | Coût après Free Tier |
|-----------|---------------|----------------------|
| Requêtes | 1M/mois | 0.20$/1M requêtes |
| Durée compute | 400,000 GB-secondes/mois | 0.0000166667$/GB-seconde |
| Durée (128 MB) | ~3.2M secondes | - |

**API Gateway Free Tier (12 premiers mois) :**

| Ressource | Quota gratuit | Coût après Free Tier |
|-----------|---------------|----------------------|
| Requêtes REST API | 1M/mois | 3.50$/1M requêtes |
| Caching | N/A | 0.02$/h (0.5 GB cache) |

**DynamoDB Free Tier (permanent) :**

| Ressource | Quota gratuit | Coût après Free Tier |
|-----------|---------------|----------------------|
| Stockage | 25 GB | 0.25$/GB-mois |
| Write Request Units | 25 WRU/sec (2.5M/mois) | 1.25$/1M WRU |
| Read Request Units | 25 RRU/sec (2.5M/mois) | 0.25$/1M RRU |

**SNS Free Tier :**

| Ressource | Quota gratuit | Coût après Free Tier |
|-----------|---------------|----------------------|
| Notifications (email/HTTP) | 1,000/mois | 0.50$/1M |
| Notifications (SMS) | N/A | Variable (0.60$/message France) |

**Scénario de cet exercice :**

```
API: 10,000 requêtes/mois
Lambda: 10,000 invocations × 1 sec × 256 MB = 2,500 GB-sec
DynamoDB: 1 GB stockage, 10k reads, 5k writes
SNS: 100 notifications email

Coût Free Tier (12 premiers mois): 0$/mois [OK]

Après Free Tier:
  API Gateway: 10k × 0.0000035 = 0.035$
  Lambda: 0$ (dans quota 1M requêtes)
  DynamoDB: 0$ (dans quota 25 GB)
  SNS: 0$ (dans quota 1k)
  
Total après Free Tier: ~0.04$/mois [OK]
```

**Comparaison avec EC2 :**

```
Serverless (Lambda + API Gateway + DynamoDB):
  Dev/test (faible trafic): ~0$/mois
  Production (trafic moyen): ~5-10$/mois

EC2 + RDS (toujours actif):
  t3.micro EC2: ~10$/mois
  db.t3.micro RDS: ~13$/mois
  Total: ~23$/mois

Économie serverless: ~50-100% selon usage [OK]
```

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Comprendre l'architecture serverless

#### Qu'est-ce que le serverless ?

**Serverless = "Sans serveur" (mais il y a quand même des serveurs !)**

**Définition :**
- Tu n'installes/gères AUCUN serveur
- AWS exécute ton code à la demande
- Tu payes seulement quand le code s'exécute
- Scaling automatique (0 -> ∞)

**Analogie :**
- **Serveur traditionnel (EC2)** = Louer un appartement
  - Tu payes même si tu n'y es pas
  - Tu gères l'entretien, réparations, etc.
  
- **Serverless (Lambda)** = Hôtel
  - Tu payes seulement les nuits que tu dors
  - Pas d'entretien à gérer
  - Chambre prête instantanément

---

#### Serverless vs Traditional (comparaison approfondie)

| Critère | Serverless (Lambda) | Traditional (EC2) |
|---------|---------------------|-------------------|
| **Gestion serveur** | [X] Aucune | [OK] Installation, patches, scaling |
| **Scaling** | [RAPIDE] Automatique (0-∞) | [OUTIL] Manuel (Auto Scaling Groups) |
| **Coût idle** | [ARGENT] 0$ (pas d'exec = pas de coût) | [ARGENT] Toujours facturé |
| **Cold start** | [TEMPS] 100-1000ms première requête | [OK] Toujours chaud |
| **Durée max** | [ALARM_CLOCK] 15 minutes max | ∞ Illimité |
| **État** | [NOTE] Stateless (pas de persistance) | [SAUVEGARDE] Stateful possible |
| **Déploiement** | [RAPIDE] Zip upload (secondes) | [LENT] AMI, provisioning (minutes) |
| **Monitoring** | [GRAPHIQUE] CloudWatch intégré | [OUTIL] À configurer |
| **Vendor lock-in** | [ATTENTION] Fort (AWS Lambda spécifique) | [OK] Portable (Linux standard) |

---

#### Quand utiliser serverless ?

**[OK] Cas d'usage idéaux :**

**1. API Backend (ce qu'on va faire)**
```
Client mobile -> API Gateway -> Lambda -> DynamoDB
```
- Trafic variable
- Pas de state
- Requêtes courtes (< 15 min)

**2. Traitement événementiel**
```
S3 Upload -> Trigger Lambda -> Resize image -> Upload S3
```
- Event-driven
- Traitement asynchrone
- Scaling automatique

**3. Scheduled jobs (cron)**
```
EventBridge (cron) -> Lambda -> Backup DB -> S3
```
- Tâches planifiées
- Pas besoin de serveur 24/7

**4. Data processing (ETL)**
```
Kinesis Stream -> Lambda -> Transform -> DynamoDB
```
- Streaming data
- Transform/Aggregate
- Scalable

---

**[X] Cas où éviter serverless :**

**1. Applications long-running**
```
Video encoding (> 15 min) -> EC2 + Queue
Machine learning training -> SageMaker
```
- Limite 15 minutes Lambda
- Batch processing lourd -> EC2 Spot

**2. Applications stateful**
```
WebSocket server avec state -> EC2 + Load Balancer
Game server -> EC2 avec persistent connections
```
- Lambda = stateless
- State doit être externalisé (DynamoDB, Redis)

**3. Workload constant 24/7**
```
Si utilisation constante -> EC2 peut être moins cher
  Lambda: 1M req/mois × 1 sec × 256 MB = 50$
  EC2 t3.micro: 10$/mois
```
- Si > 80% utilisation -> EC2 plus économique

**4. Cold start critique**
```
Latency < 50ms required -> EC2 ou Lambda Provisioned Concurrency
```
- Cold start Lambda = 100-1000ms
- Pas acceptable pour ultra low-latency

---

#### Composants de l'architecture serverless AWS

**1. Lambda**
- Exécution du code (compute)
- Event-driven
- Stateless

**2. API Gateway**
- Exposition HTTP/REST
- Routing, autorisation
- Throttling, caching

**3. DynamoDB**
- Base NoSQL managed
- Haute performance (ms latency)
- Auto-scaling

**4. EventBridge**
- Event bus
- Cron jobs
- Routing événements

**5. SNS (Simple Notification Service)**
- Pub/Sub messaging
- Email, SMS, push
- Fan-out (1 -> many)

**6. S3**
- Stockage objets
- Trigger Lambda
- Durable, cheap

**7. CloudWatch**
- Logs centralisés
- Métriques
- Alarmes

---

### ÉTAPE 2 : Créer une table DynamoDB

#### Comprendre DynamoDB

**DynamoDB = Base de données NoSQL fully managed**

**Différences DynamoDB vs PostgreSQL (RDS) :**

| Critère | DynamoDB (NoSQL) | PostgreSQL (SQL) |
|---------|------------------|------------------|
| **Schéma** | Sans schéma (flexible) | Schéma strict |
| **Requêtes** | Key-value, scan, query | SQL complet |
| **Joins** | [X] Pas de joins natifs | [OK] Joins complexes |
| **Transactions** | [OK] ACID (limitées) | [OK] ACID complet |
| **Scaling** | [RAPIDE] Horizontal automatique | [OUTIL] Vertical (Read replicas) |
| **Performance** | [RAPIDE] Single-digit ms | ~10-50ms (selon load) |
| **Coût idle** | [ARGENT] Stockage + quelques req | [ARGENT] Instance 24/7 |
| **Cas d'usage** | High throughput, key-value | Analytique, relations complexes |

---

#### Concepts clés DynamoDB

**1. Table**

Collection d'items (= lignes)

**Exemple table Tasks :**
```
task_id (PK)      | title           | completed | created_at
------------------|-----------------|-----------|-------------------
uuid-1234         | Apprendre AWS   | false     | 1702732800
uuid-5678         | Migrer vers RDS | true      | 1702819200
```

---

**2. Primary Key**

Identifiant unique de chaque item

**2 types :**

**Simple Primary Key (Partition Key seulement) :**
```
Partition Key: task_id (UUID)

Exemple:
  task_id = "abc-123" -> Item unique
```

**Composite Primary Key (Partition Key + Sort Key) :**
```
Partition Key: user_id
Sort Key: created_at

Exemple:
  user_id = "user-123", created_at = 1702732800 -> Item unique
  
Permet: 
  - Requêtes sur user_id (toutes les tâches d'un user)
  - Tri par created_at
```

---

**3. Partition Key (clé de hachage)**

**Détermine dans quelle partition physique l'item est stocké**

```
Hash(partition_key) -> Partition number

Exemple:
  Hash("abc-123") -> Partition 42
  Hash("xyz-789") -> Partition 17
```

**[ATTENTION] Important pour performance :**
- Bien distribuer les données (éviter hot partitions)
- Partition Key doit avoir haute cardinalité

**Exemples :**

```
[OK] BON: user_id (beaucoup d'utilisateurs différents)
[OK] BON: task_id (UUID unique)
[X] MAUVAIS: status ("pending", "done" - seulement 2 valeurs)
[X] MAUVAIS: date (beaucoup d'items sur même date)
```

---

**4. Sort Key (clé de tri)**

**Permet de trier les items d'une même partition**

```
Partition Key: user_id = "user-123"
Sort Key: created_at

Items:
  user-123, 1702732800, "Task A"
  user-123, 1702819200, "Task B"
  user-123, 1702905600, "Task C"

Query:
  user_id = "user-123" AND created_at > 1702800000
  -> Retourne Task B et Task C (triés par date)
```

---

**5. Attributes (attributs)**

Colonnes supplémentaires (pas dans la clé)

```
{
  "task_id": "abc-123",              # Partition Key
  "title": "Apprendre DynamoDB",     # Attribut
  "description": "NoSQL avec AWS",   # Attribut
  "completed": false,                # Attribut
  "created_at": 1702732800,          # Attribut
  "tags": ["aws", "database"]        # Attribut (liste)
}
```

**DynamoDB est schemaless :**
- Chaque item peut avoir attributs différents
- Pas besoin de déclarer colonnes à l'avance
- Flexible mais moins de contraintes

---

**6. Secondary Indices**

**Permettre des requêtes sur d'autres attributs que la PK**

**2 types :**

**Global Secondary Index (GSI) :**
- Nouvelle Partition Key + Sort Key
- Index distribué (différentes partitions)
- Eventual consistency
- Peut être ajouté après création table

**Exemple GSI :**
```
Table PK: task_id
GSI PK: user_id
GSI SK: created_at

-> Permet: Query toutes les tâches d'un user, triées par date
```

**Local Secondary Index (LSI) :**
- Même Partition Key que table
- Sort Key différente
- Strong consistency possible
- [ATTENTION] DOIT être créé à la création de table (pas après)

**Exemple LSI :**
```
Table PK: user_id, SK: created_at
LSI PK: user_id, SK: title

-> Permet: Trier les tâches d'un user par titre alphabétique
```

---

**7. Capacity Modes**

**On-Demand (recommandé pour commencer) :**
- Pas de planification
- Scaling automatique
- Paye par requête
- Coût : 1.25$/1M writes, 0.25$/1M reads

**Provisioned (pour workload prévisible) :**
- Tu définis RCU/WCU (Read/Write Capacity Units)
- Moins cher si utilisation constante
- Auto-scaling disponible
- Coût : 0.47$/WCU-mois, 0.09$/RCU-mois

**Pour cet exercice : On-Demand [OK]**

---

#### Créer la table Tasks

**Console DynamoDB -> Tables -> Create table**

```
Table name: Tasks

Partition key: task_id (String)

Sort key: (Laisser vide pour partition key simple)
  (On pourrait mettre created_at, mais task_id suffit pour identifier uniquement)
```

---

**Table settings :**

```
[x] Customize settings
```

---

**Table class :**

```
[x] DynamoDB Standard
  (Meilleure performance, coût normal)

[ ] DynamoDB Standard-IA (Infrequent Access)
  (50% moins cher storage, mais requêtes plus chères)
  (Utile si accès < 1×/mois)
```

---

**Read/write capacity settings :**

```
Capacity mode: On-demand
  (Scaling automatique, pas de planning)

[ ] Provisioned
```

**Explication On-Demand :**
- AWS ajuste automatiquement la capacité
- Pas de throttling (sauf burst limits)
- Paye uniquement les requêtes effectuées
- Parfait pour trafic imprévisible

---

**Secondary indexes (optionnel pour l'instant) :**

```
(On créera un GSI après, c'est possible avec On-Demand)
```

---

**Encryption at rest :**

```
Encryption type:
  [x] Owned by Amazon DynamoDB
    (Gratuit, AWS gère les clés)

  [ ] AWS managed key (KMS)
    (Coûte 1$/clé/mois + 0.03$/10k requêtes)

  [ ] Stored in your AWS account (KMS)
    (Contrôle total, coûteux)
```

**Pour cet exercice : Owned by Amazon DynamoDB [OK]**

---

**Point-in-time recovery (PITR) :**

```
[x] Turn on point-in-time recovery
  (Backup continu, restore à n'importe quel moment)

Coût: 0.20$/GB-mois (20% de la taille table)
```

**PITR = Équivalent des automated backups RDS**

**Pour production : [OK] Activer**

---

**Tags (optionnel) :**

```
Key: Project | Value: TodoApp
Key: Environment | Value: Development
```

---

**Cliquer "Create table"**

**Durée : ~10-30 secondes**

**Résultat :**

```
Table name: Tasks
Status: Active [OK]
Partition key: task_id (String)
Table ARN: arn:aws:dynamodb:eu-west-1:123456789012:table/Tasks
Item count: 0
Table size: 0 Bytes
```

**[OK] Table DynamoDB créée !**

---

#### Ajouter un Global Secondary Index (GSI)

**Scénario : On veut lister toutes les tâches d'un utilisateur, triées par date**

**Problème : La table actuelle a seulement `task_id` comme clé**

```
Query "toutes les tâches de user-123" -> [X] Impossible

Il faudrait Scan toute la table et filtrer -> Lent et cher
```

**Solution : GSI avec user_id comme Partition Key**

---

**DynamoDB -> Tables -> Tasks -> Indexes -> Create index**

```
Partition key: user_id (String)

Sort key: created_at (Number)
  (Timestamp Unix en secondes)

Index name: GSI-UserCreatedAt
  (Auto-généré, peut être changé)

Attribute projections:
  [x] All
    (Tous les attributs projetés dans l'index)
  
  [ ] Keys only (seulement les clés)
  [ ] Include (choisir attributs spécifiques)
```

**Explication "Attribute projections" :**

**All :**
- Index contient TOUS les attributs de l'item
- Requête sur GSI = Pas besoin de fetch table principale
- Plus de storage (coût)
- Meilleure performance

**Keys only :**
- Index contient seulement PK table + PK/SK index
- Requête sur GSI = Fetch supplémentaire table principale si besoin d'autres attributs
- Moins de storage
- Peut être plus lent

**Include :**
- Index contient clés + attributs spécifiés
- Compromis entre All et Keys only

**Pour cet exercice : All [OK] (simplicité)**

---

**Cliquer "Create index"**

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

**Résultat :**

```
Index name: GSI-UserCreatedAt
Status: Active [OK]
Partition key: user_id (String)
Sort key: created_at (Number)
Projection type: All
```

**Maintenant on peut :**

```python
# Query toutes les tâches d'un user, triées par date
response = dynamodb.query(
    TableName='Tasks',
    IndexName='GSI-UserCreatedAt',
    KeyConditionExpression='user_id = :uid',
    ExpressionAttributeValues={':uid': 'user-123'},
    ScanIndexForward=False  # Tri DESC (plus récent d'abord)
)
```

**[OK] GSI créé !**

---

### ÉTAPE 3 : Créer la première fonction Lambda

#### Installer AWS SAM CLI (optionnel mais recommandé)

**SAM = Serverless Application Model**

**Outil pour développer/tester/déployer serverless localement**

**Sur l'instance EC2 (ou ton PC) :**

```bash
# Installer AWS SAM CLI
wget https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip aws-sam-cli-linux-x86_64.zip -d sam-installation
sudo ./sam-installation/install

# Vérifier
sam --version
```

**Résultat :**

```
SAM CLI, version 1.106.0
```

**[OK] SAM installé !**

---

#### Créer le projet Lambda localement

**Créer un dossier pour le projet :**

```bash
mkdir ~/serverless-todo-api
cd ~/serverless-todo-api
```

---

**Structure du projet :**

```bash
mkdir -p lambda/create-task
mkdir -p lambda/list-tasks
mkdir -p lambda/get-task
mkdir -p lambda/update-task
mkdir -p lambda/delete-task
mkdir -p lambda/layers/python
```

**Explication :**
- `lambda/create-task/` : Une fonction Lambda
- `lambda/layers/` : Dépendances partagées (boto3, etc.)

---

#### Créer la fonction create-task

**Créer le fichier handler :**

```bash
nano lambda/create-task/lambda_function.py
```

**Contenu :**

```python
# ═══════════════════════════════════════════════════════════════
# LAMBDA FUNCTION: CREATE TASK
# ═══════════════════════════════════════════════════════════════
# Description: Crée une nouvelle tâche dans DynamoDB
# Trigger: API Gateway POST /tasks
# ═══════════════════════════════════════════════════════════════

import json
import boto3
import uuid
import time
from datetime import datetime

# Client DynamoDB
dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
table = dynamodb.Table('Tasks')

# Client SNS (pour notifications)
sns = boto3.client('sns', region_name='eu-west-1')
SNS_TOPIC_ARN = 'arn:aws:sns:eu-west-1:123456789012:task-notifications'  # À remplacer


def lambda_handler(event, context):
    """
    Handler principal de la fonction Lambda
    
    Args:
        event: Événement API Gateway
        context: Contexte d'exécution Lambda
    
    Returns:
        Response API Gateway (statusCode, body, headers)
    """
    
    print(f"Event reçu: {json.dumps(event)}")
    
    try:
        # ───────────────────────────────────────────────────────
        # 1. PARSER LE BODY (JSON)
        # ───────────────────────────────────────────────────────
        
        # event['body'] est une string JSON (API Gateway)
        if isinstance(event.get('body'), str):
            body = json.loads(event['body'])
        else:
            body = event.get('body', {})
        
        # ───────────────────────────────────────────────────────
        # 2. VALIDER L'INPUT
        # ───────────────────────────────────────────────────────
        
        title = body.get('title', '').strip()
        
        if not title:
            return {
                'statusCode': 400,
                'headers': {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'  # CORS
                },
                'body': json.dumps({
                    'error': 'BadRequest',
                    'message': 'Le champ "title" est requis'
                })
            }
        
        # ───────────────────────────────────────────────────────
        # 3. CONSTRUIRE L'ITEM DYNAMODB
        # ───────────────────────────────────────────────────────
        
        task_id = str(uuid.uuid4())
        current_timestamp = int(time.time())
        
        item = {
            'task_id': task_id,
            'title': title,
            'description': body.get('description', ''),
            'completed': False,
            'created_at': current_timestamp,
            'updated_at': current_timestamp,
            'user_id': body.get('user_id', 'anonymous')  # Multi-tenant
        }
        
        # ───────────────────────────────────────────────────────
        # 4. ÉCRIRE DANS DYNAMODB
        # ───────────────────────────────────────────────────────
        
        table.put_item(Item=item)
        
        print(f"[OK] Tâche créée: {task_id}")
        
        # ───────────────────────────────────────────────────────
        # 5. PUBLIER NOTIFICATION SNS
        # ───────────────────────────────────────────────────────
        
        try:
            sns.publish(
                TopicArn=SNS_TOPIC_ARN,
                Subject='Nouvelle tâche créée',
                Message=json.dumps({
                    'event': 'task.created',
                    'task_id': task_id,
                    'title': title,
                    'created_at': datetime.fromtimestamp(current_timestamp).isoformat()
                }, indent=2)
            )
            print("[EMAIL] Notification SNS envoyée")
        except Exception as e:
            # Notification non-critique, on log juste l'erreur
            print(f"[ATTENTION] Erreur SNS (non-bloquant): {str(e)}")
        
        # ───────────────────────────────────────────────────────
        # 6. RETOURNER LA RÉPONSE
        # ───────────────────────────────────────────────────────
        
        return {
            'statusCode': 201,  # Created
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'message': 'Tâche créée avec succès',
                'task': item
            })
        }
    
    except json.JSONDecodeError as e:
        print(f"[X] Erreur parsing JSON: {str(e)}")
        return {
            'statusCode': 400,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({
                'error': 'InvalidJSON',
                'message': 'Le body n\'est pas un JSON valide'
            })
        }
    
    except Exception as e:
        print(f"[X] Erreur interne: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({
                'error': 'InternalServerError',
                'message': 'Une erreur est survenue'
            })
        }
```

**Explications détaillées :**

---

**`event` :**

Format API Gateway :

```python
{
    "httpMethod": "POST",
    "path": "/tasks",
    "headers": {...},
    "body": "{\"title\": \"Ma tâche\"}",  # String JSON !
    "queryStringParameters": {...},
    "pathParameters": {...},
    "requestContext": {...}
}
```

**[ATTENTION] `event['body']` est une STRING, pas un dict !**

**Il faut parser avec `json.loads()`**

---

**`context` :**

Métadonnées d'exécution Lambda :

```python
context.function_name        # "create-task"
context.function_version     # "$LATEST" ou version
context.invoked_function_arn # ARN complet
context.memory_limit_in_mb   # 256
context.request_id           # UUID unique de l'invocation
context.log_group_name       # "/aws/lambda/create-task"
context.log_stream_name      # "2024/12/16/[$LATEST]abc123..."

# Temps restant avant timeout
context.get_remaining_time_in_millis()  # 9500 (si timeout 10s)
```

---

**Response format API Gateway :**

```python
{
    "statusCode": 201,           # HTTP status code
    "headers": {...},            # Response headers
    "body": "{\"key\": \"val\"}" # String JSON (pas dict!)
}
```

**[ATTENTION] `body` doit être une STRING JSON, pas un dict !**

**Utiliser `json.dumps()` pour convertir**

---

**CORS (Cross-Origin Resource Sharing) :**

```python
'Access-Control-Allow-Origin': '*'
```

**Permet les requêtes depuis n'importe quel domaine (frontend web)**

**Production : Restreindre à ton domaine**

```python
'Access-Control-Allow-Origin': 'https://mon-app.com'
```

---

**UUID pour task_id :**

```python
import uuid
task_id = str(uuid.uuid4())
# "f47ac10b-58cc-4372-a567-0e02b2c3d479"
```

**UUID v4 = Aléatoire, collision quasi-impossible**

**Alternative : ULID (Universally Unique Lexicographically Sortable ID)**

---

**Timestamp Unix :**

```python
import time
current_timestamp = int(time.time())
# 1702819200 (secondes depuis 1970-01-01 00:00:00 UTC)
```

**Pourquoi Number et pas String en DynamoDB ?**
- Tri numérique (pas lexicographique)
- Comparaisons (>, <, BETWEEN)
- Moins de storage

---

**DynamoDB put_item :**

```python
table.put_item(Item={...})
```

**Comportement :**
- Si `task_id` n'existe pas -> Insert
- Si `task_id` existe -> Overwrite ([ATTENTION] écrase)

**Pour update conditionnel (seulement si pas existe) :**

```python
table.put_item(
    Item=item,
    ConditionExpression='attribute_not_exists(task_id)'
)
```

**Si existe -> Exception `ConditionalCheckFailedException`**

---

**Sauvegarde du fichier**

---

#### Créer les autres fonctions Lambda

**Je vais créer les autres fonctions de manière plus concise.**

**list-tasks :**

```bash
nano lambda/list-tasks/lambda_function.py
```

```python
import json
import boto3
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """Liste toutes les tâches ou filtre par user_id"""
    
    try:
        # Query parameters
        params = event.get('queryStringParameters') or {}
        user_id = params.get('user_id')
        limit = int(params.get('limit', 50))
        
        if user_id:
            # Query avec GSI
            response = table.query(
                IndexName='GSI-UserCreatedAt',
                KeyConditionExpression=Key('user_id').eq(user_id),
                Limit=limit,
                ScanIndexForward=False  # DESC order
            )
        else:
            # Scan toute la table ([ATTENTION] coûteux si table grande)
            response = table.scan(Limit=limit)
        
        items = response.get('Items', [])
        
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'tasks': items,
                'count': len(items),
                'scannedCount': response.get('ScannedCount', 0)
            })
        }
    
    except Exception as e:
        print(f"[X] Erreur: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({'error': str(e)})
        }
```

---

**get-task :**

```bash
nano lambda/get-task/lambda_function.py
```

```python
import json
import boto3

dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """Récupère une tâche par ID"""
    
    try:
        # Path parameter: /tasks/{task_id}
        task_id = event['pathParameters']['id']
        
        response = table.get_item(Key={'task_id': task_id})
        
        item = response.get('Item')
        
        if not item:
            return {
                'statusCode': 404,
                'headers': {'Content-Type': 'application/json'},
                'body': json.dumps({'error': 'Task not found'})
            }
        
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'task': item})
        }
    
    except Exception as e:
        print(f"[X] Erreur: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({'error': str(e)})
        }
```

---

**update-task :**

```bash
nano lambda/update-task/lambda_function.py
```

```python
import json
import boto3
import time

dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """Met à jour une tâche"""
    
    try:
        task_id = event['pathParameters']['id']
        body = json.loads(event['body']) if isinstance(event.get('body'), str) else event.get('body', {})
        
        # Construire UpdateExpression dynamiquement
        update_expr_parts = []
        expr_attr_values = {}
        expr_attr_names = {}
        
        if 'title' in body:
            update_expr_parts.append('#title = :title')
            expr_attr_names['#title'] = 'title'
            expr_attr_values[':title'] = body['title']
        
        if 'description' in body:
            update_expr_parts.append('description = :desc')
            expr_attr_values[':desc'] = body['description']
        
        if 'completed' in body:
            update_expr_parts.append('completed = :comp')
            expr_attr_values[':comp'] = body['completed']
        
        # Toujours mettre à jour updated_at
        update_expr_parts.append('updated_at = :upd')
        expr_attr_values[':upd'] = int(time.time())
        
        if not update_expr_parts:
            return {
                'statusCode': 400,
                'headers': {'Content-Type': 'application/json'},
                'body': json.dumps({'error': 'No fields to update'})
            }
        
        update_expression = 'SET ' + ', '.join(update_expr_parts)
        
        response = table.update_item(
            Key={'task_id': task_id},
            UpdateExpression=update_expression,
            ExpressionAttributeValues=expr_attr_values,
            ExpressionAttributeNames=expr_attr_names if expr_attr_names else None,
            ReturnValues='ALL_NEW'
        )
        
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({
                'message': 'Task updated',
                'task': response['Attributes']
            })
        }
    
    except Exception as e:
        print(f"[X] Erreur: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({'error': str(e)})
        }
```

---

**delete-task :**

```bash
nano lambda/delete-task/lambda_function.py
```

```python
import json
import boto3

dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    """Supprime une tâche"""
    
    try:
        task_id = event['pathParameters']['id']
        
        table.delete_item(Key={'task_id': task_id})
        
        return {
            'statusCode': 204,  # No Content
            'headers': {'Access-Control-Allow-Origin': '*'},
            'body': ''
        }
    
    except Exception as e:
        print(f"[X] Erreur: {str(e)}")
        return {
            'statusCode': 500,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({'error': str(e)})
        }
```

---

### ÉTAPE 4 : Créer les rôles IAM pour Lambda

**Lambda a besoin de permissions pour :**
- Écrire logs dans CloudWatch
- Lire/écrire DynamoDB
- Publier sur SNS

---

#### Créer le rôle IAM

**IAM -> Roles -> Create role**

```
Trusted entity type: AWS service
Use case: Lambda
```

**Next**

---

**Add permissions :**

**Rechercher et sélectionner :**

```
[x] AWSLambdaBasicExecutionRole
  (CloudWatch Logs: CreateLogGroup, CreateLogStream, PutLogEvents)
```

**Next**

---

**Role name :**

```
lambda-todo-api-role
```

**Create role**

---

**Ajouter permissions DynamoDB :**

**Roles -> lambda-todo-api-role -> Add permissions -> Create inline policy**

**JSON :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:PutItem",
        "dynamodb:GetItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:Query",
        "dynamodb:Scan"
      ],
      "Resource": [
        "arn:aws:dynamodb:eu-west-1:123456789012:table/Tasks",
        "arn:aws:dynamodb:eu-west-1:123456789012:table/Tasks/index/*"
      ]
    }
  ]
}
```

**[ATTENTION] Remplacer `123456789012` par ton AWS Account ID**

**Name : DynamoDBAccessPolicy**

**Create policy**

---

**Ajouter permissions SNS :**

**Create inline policy**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": "arn:aws:sns:eu-west-1:123456789012:task-notifications"
    }
  ]
}
```

**Name : SNSPublishPolicy**

---

**[OK] Rôle IAM complet !**

**Permissions finales :**
- [OK] CloudWatch Logs (écriture)
- [OK] DynamoDB (CRUD + Query/Scan)
- [OK] SNS (Publish)

---

Je vais continuer dans le prochain message avec le déploiement des Lambdas et la création de l'API Gateway. Veux-tu que je continue ?

### ÉTAPE 5 : Déployer les fonctions Lambda

#### Packager la fonction create-task

**Créer un fichier ZIP avec le code :**

```bash
cd ~/serverless-todo-api/lambda/create-task

# Créer le ZIP
zip -r create-task.zip lambda_function.py

# Vérifier le contenu
unzip -l create-task.zip
```

**Résultat :**

```
Archive:  create-task.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     4256  2024-12-16 14:30   lambda_function.py
---------                     -------
     4256                     1 file
```

---

#### Déployer via la console AWS

**Lambda -> Functions -> Create function**

```
[x] Author from scratch

Function name: create-task

Runtime: Python 3.12

Architecture: x86_64
  (Ou arm64/Graviton2 - 20% moins cher, même perf)

Permissions:
  [x] Use an existing role
  Role: lambda-todo-api-role
```

**Create function**

---

**Upload du code :**

**Code source -> Upload from -> .zip file**

**Sélectionner `create-task.zip`**

**Save**

---

**Configuration :**

**Configuration -> General configuration -> Edit**

```
Memory: 256 MB
  (128 MB = Minimum, 256 MB = Bon compromis perf/coût)

Timeout: 10 seconds
  (3 sec par défaut, 10 sec pour marge)

Ephemeral storage: 512 MB
  (Espace /tmp, default OK)
```

**Save**

---

**Variables d'environnement (optionnel) :**

**Configuration -> Environment variables -> Edit**

```
Key: SNS_TOPIC_ARN
Value: arn:aws:sns:eu-west-1:123456789012:task-notifications

Key: TABLE_NAME
Value: Tasks
```

**Modifier le code pour utiliser ces variables :**

```python
import os

SNS_TOPIC_ARN = os.environ.get('SNS_TOPIC_ARN')
TABLE_NAME = os.environ.get('TABLE_NAME', 'Tasks')

table = dynamodb.Table(TABLE_NAME)
```

**Avantage : Code réutilisable (dev, staging, prod)**

---

**[OK] Fonction create-task déployée !**

---

#### Tester la fonction Lambda (avant API Gateway)

**Lambda -> create-task -> Test**

**Configure test event :**

```
Event name: test-create-task

Template: API Gateway AWS Proxy

Event JSON:
```

```json
{
  "httpMethod": "POST",
  "path": "/tasks",
  "headers": {
    "Content-Type": "application/json"
  },
  "body": "{\"title\": \"Apprendre Lambda\", \"description\": \"Comprendre AWS Lambda en profondeur\", \"user_id\": \"user-123\"}",
  "queryStringParameters": null,
  "pathParameters": null,
  "requestContext": {
    "requestId": "test-request-id"
  }
}
```

**Save**

---

**Cliquer "Test"**

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

```
Execution result: succeeded

Response:
{
  "statusCode": 201,
  "headers": {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*"
  },
  "body": "{\"message\": \"Tâche créée avec succès\", \"task\": {\"task_id\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", ...}}"
}

Function Logs:
START RequestId: abc-123 Version: $LATEST
Event reçu: {"httpMethod": "POST", ...}
[OK] Tâche créée: f47ac10b-58cc-4372-a567-0e02b2c3d479
[EMAIL] Notification SNS envoyée
END RequestId: abc-123
REPORT RequestId: abc-123  Duration: 234.56 ms  Billed Duration: 235 ms  Memory Size: 256 MB  Max Memory Used: 67 MB
```

**Analyse du rapport :**
- **Duration** : 234.56 ms (temps réel d'exécution)
- **Billed Duration** : 235 ms (arrondi au ms supérieur)
- **Memory Size** : 256 MB (alloué)
- **Max Memory Used** : 67 MB (utilisé réellement - on pourrait réduire)

---

**Vérifier dans DynamoDB :**

**DynamoDB -> Tables -> Tasks -> Explore table items**

```
task_id: f47ac10b-58cc-4372-a567-0e02b2c3d479
title: Apprendre Lambda
description: Comprendre AWS Lambda en profondeur
completed: false
created_at: 1702819200
updated_at: 1702819200
user_id: user-123
```

**[OK] Tâche créée dans DynamoDB !**

---

#### Déployer les autres fonctions

**Répéter le processus pour :**

**list-tasks :**
```bash
cd ~/serverless-todo-api/lambda/list-tasks
zip -r list-tasks.zip lambda_function.py
```

**Lambda -> Create function -> list-tasks -> Upload list-tasks.zip**

---

**get-task :**
```bash
cd ~/serverless-todo-api/lambda/get-task
zip -r get-task.zip lambda_function.py
```

**Lambda -> Create function -> get-task -> Upload get-task.zip**

---

**update-task :**
```bash
cd ~/serverless-todo-api/lambda/update-task
zip -r update-task.zip lambda_function.py
```

**Lambda -> Create function -> update-task -> Upload update-task.zip**

---

**delete-task :**
```bash
cd ~/serverless-todo-api/lambda/delete-task
zip -r delete-task.zip lambda_function.py
```

**Lambda -> Create function -> delete-task -> Upload delete-task.zip**

---

**Toutes les fonctions utilisent le même rôle IAM : `lambda-todo-api-role`**

---

### ÉTAPE 6 : Créer l'API Gateway

#### Comprendre API Gateway

**API Gateway = Service AWS pour créer/publier/gérer des API**

**3 types d'API :**

| Type | Protocole | Use case | Prix |
|------|-----------|----------|------|
| **REST API** | HTTP/HTTPS | Standard, features complètes | 3.50$/1M req |
| **HTTP API** | HTTP/HTTPS | Simplifié, moins cher | 1.00$/1M req |
| **WebSocket API** | WebSocket | Bidirectionnel, real-time | 1.00$/1M msg |

**Pour cet exercice : REST API** (plus de features, pédagogique)

---

#### Créer l'API REST

**API Gateway -> APIs -> Create API**

**Choisir "REST API" (pas "REST API Private") :**

```
[x] REST API
  Build

Protocol: REST
Create new API: New API
```

---

**Settings :**

```
API name: todo-api

Description: API REST pour gestion de tâches

Endpoint Type: Regional
  (Edge-optimized = CloudFront intégré, plus cher)
  (Private = VPC seulement)
```

**Create API**

---

**[OK] API créée !**

**API ID : abc123xyz**

**Invoke URL : https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod**

---

#### Créer les ressources et méthodes

**API Gateway utilise une structure hiérarchique :**

```
/
├── /tasks (resource)
│   ├── GET (method) -> Lambda: list-tasks
│   ├── POST (method) -> Lambda: create-task
│   └── /{id} (resource)
│       ├── GET -> Lambda: get-task
│       ├── PUT -> Lambda: update-task
│       └── DELETE -> Lambda: delete-task
└── /analytics
    └── /stats
        └── GET -> Lambda: get-stats (plus tard)
```

---

**Créer la ressource `/tasks` :**

**Actions -> Create Resource**

```
Resource Name: tasks
Resource Path: /tasks

[x] Enable API Gateway CORS
  (Ajoute automatiquement OPTIONS method)
```

**Create Resource**

---

**Créer la méthode POST sur `/tasks` :**

**Sélectionner `/tasks` -> Actions -> Create Method**

**Dropdown : POST -> [OK] (checkmark)**

---

**Setup :**

```
Integration type: Lambda Function

[x] Use Lambda Proxy integration
  (Envoie tout l'event à Lambda, Lambda gère le response)

Lambda Region: eu-west-1

Lambda Function: create-task
  (Auto-complete en tapant)

[ ] Use Default Timeout
```

**Save**

---

**Confirmation :**

```
Add Permission to Lambda Function

You are about to give API Gateway permission to invoke your Lambda function:
arn:aws:lambda:eu-west-1:123456789012:function:create-task

OK
```

**[OK] Méthode POST créée !**

---

**Tester la méthode :**

**POST -> Test**

**Request Body :**

```json
{
  "title": "Test API Gateway",
  "description": "Tester l'intégration Lambda",
  "user_id": "user-456"
}
```

**Test**

---

**Résultat :**

```
Response Body:
{
  "message": "Tâche créée avec succès",
  "task": {
    "task_id": "xyz-789",
    "title": "Test API Gateway",
    ...
  }
}

Response Headers:
Content-Type: application/json
Access-Control-Allow-Origin: *

Logs:
Execution log for request abc-123
...
Endpoint response body before transformations: {"statusCode": 201, ...}
Method completed with status: 201
```

**[OK] Intégration fonctionnelle !**

---

**Créer la méthode GET sur `/tasks` :**

**Sélectionner `/tasks` -> Actions -> Create Method -> GET**

```
Integration type: Lambda Function
[x] Use Lambda Proxy integration
Lambda Function: list-tasks
```

**Save -> OK**

---

**Tester GET `/tasks` :**

**GET -> Test**

**Query Strings : `user_id=user-123`**

**Test**

**Résultat : Liste des tâches de user-123 [OK]**

---

**Créer la ressource `/{id}` sous `/tasks` :**

**Sélectionner `/tasks` -> Actions -> Create Resource**

```
Resource Name: task-id
Resource Path: {id}
  (Les accolades = path parameter)

[x] Enable API Gateway CORS
```

**Create Resource**

---

**Créer les méthodes sur `/{id}` :**

**GET :**
```
Lambda: get-task
```

**PUT :**
```
Lambda: update-task
```

**DELETE :**
```
Lambda: delete-task
```

---

**Résultat final :**

```
/
└── /tasks
    ├── GET -> list-tasks
    ├── POST -> create-task
    └── /{id}
        ├── GET -> get-task
        ├── PUT -> update-task
        └── DELETE -> delete-task
```

**[OK] Toutes les routes créées !**

---

### ÉTAPE 7 : Déployer l'API

**Créer un stage (environnement) :**

**Actions -> Deploy API**

```
Deployment stage: [New Stage]

Stage name: prod

Stage description: Production environment

Deployment description: Initial deployment
```

**Deploy**

---

**[OK] API déployée !**

**Invoke URL :**

```
https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod
```

**Routes disponibles :**

```
POST   https://abc123xyz.../prod/tasks
GET    https://abc123xyz.../prod/tasks
GET    https://abc123xyz.../prod/tasks/{id}
PUT    https://abc123xyz.../prod/tasks/{id}
DELETE https://abc123xyz.../prod/tasks/{id}
```

---

**Tester avec curl (depuis l'instance EC2 ou ton PC) :**

```bash
# Créer une tâche
curl -X POST https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Tester API Gateway depuis curl",
    "description": "Vérifier que tout fonctionne",
    "user_id": "user-789"
  }'
```

**Résultat :**

```json
{
  "message": "Tâche créée avec succès",
  "task": {
    "task_id": "def-456",
    "title": "Tester API Gateway depuis curl",
    "description": "Vérifier que tout fonctionne",
    "completed": false,
    "created_at": 1702820000,
    "updated_at": 1702820000,
    "user_id": "user-789"
  }
}
```

**[OK] API publique fonctionnelle !**

---

**Lister toutes les tâches :**

```bash
curl https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod/tasks
```

**Résultat :**

```json
{
  "tasks": [
    {
      "task_id": "def-456",
      "title": "Tester API Gateway depuis curl",
      ...
    },
    {
      "task_id": "xyz-789",
      "title": "Test API Gateway",
      ...
    }
  ],
  "count": 2,
  "scannedCount": 2
}
```

---

**Récupérer une tâche spécifique :**

```bash
curl https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod/tasks/def-456
```

---

**Mettre à jour une tâche :**

```bash
curl -X PUT https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod/tasks/def-456 \
  -H "Content-Type: application/json" \
  -d '{
    "completed": true
  }'
```

---

**Supprimer une tâche :**

```bash
curl -X DELETE https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod/tasks/def-456
```

**Résultat : HTTP 204 (No Content) [OK]**

---

### ÉTAPE 8 : Sécuriser l'API avec API Keys

**Par défaut, l'API est publique (n'importe qui peut appeler)**

**Ajouter une authentification simple avec API Keys :**

---

#### Créer un usage plan et une API key

**API Gateway -> Usage Plans -> Create**

```
Name: free-tier-plan
Description: Plan gratuit avec limite

Throttling:
  Rate: 100 requests per second
  Burst: 200 requests
  
Quota:
  10000 requests per month

Associated API Stages:
  API: todo-api
  Stage: prod
```

**Next -> Next -> Done**

---

**Créer une API Key :**

**API Keys -> Actions -> Create API Key**

```
Name: mobile-app-key
Description: Clé pour app mobile

Auto Generate
  (AWS génère une clé aléatoire)
```

**Save**

---

**Résultat :**

```
API key: AbCdEfGh123456789
```

**[ATTENTION] SAUVEGARDER cette clé ! Pas ré-affichable**

---

**Associer la clé au usage plan :**

**Usage Plans -> free-tier-plan -> API Keys -> Add API Key to Usage Plan**

```
[x] mobile-app-key
```

**Add**

---

#### Activer l'authentification sur les méthodes

**API Gateway -> todo-api -> Resources**

**Sélectionner POST /tasks -> Method Request**

```
Authorization: NONE -> AWS_IAM (Ou laisser NONE pour l'instant)

API Key Required: false -> true
```

**Cliquer sur la [OK] checkmark pour sauvegarder**

---

**Répéter pour toutes les méthodes (GET, PUT, DELETE)**

---

**Re-déployer l'API :**

**Actions -> Deploy API**

```
Deployment stage: prod
```

**Deploy**

---

**Tester sans API Key (doit échouer) :**

```bash
curl https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod/tasks
```

**Résultat :**

```json
{
  "message": "Forbidden"
}
```

**HTTP 403 Forbidden [OK] (c'est ce qu'on veut)**

---

**Tester avec API Key :**

```bash
curl https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod/tasks \
  -H "x-api-key: AbCdEfGh123456789"
```

**Résultat : Liste des tâches [OK]**

---

**Maintenant, toutes les requêtes doivent inclure le header `x-api-key` !**

---

### ÉTAPE 9 : Créer un topic SNS pour notifications

**SNS = Simple Notification Service**

**Pub/Sub : Publier un message -> Plusieurs subscribers reçoivent**

---

#### Créer le topic SNS

**SNS -> Topics -> Create topic**

```
Type: Standard
  (FIFO = Ordre garanti, plus cher, pas nécessaire ici)

Name: task-notifications

Display name: Task Notifications
  (Pour SMS, max 10 caractères)

Encryption: Disabled
  (Optionnel, pas de données sensibles)
```

**Create topic**

---

**Topic ARN :**

```
arn:aws:sns:eu-west-1:123456789012:task-notifications
```

**[OK] Topic créé !**

---

#### Souscrire par email

**Topics -> task-notifications -> Subscriptions -> Create subscription**

```
Protocol: Email

Endpoint: ton-email@example.com
```

**Create subscription**

---

**Vérifier ton email :**

```
Subject: AWS Notification - Subscription Confirmation

You have chosen to subscribe to the topic:
arn:aws:sns:eu-west-1:123456789012:task-notifications

To confirm this subscription, click here: [Lien]
```

**Cliquer sur le lien de confirmation**

---

**Status change :**

```
Pending confirmation -> Confirmed [OK]
```

---

**Tester en publiant un message manuellement :**

**Topics -> task-notifications -> Publish message**

```
Subject: Test SNS

Message body:
Ceci est un test de notification SNS.
```

**Publish message**

---

**Email reçu :**

```
Subject: Test SNS

Ceci est un test de notification SNS.
```

**[OK] SNS fonctionne !**

---

**Maintenant, quand une tâche est créée via Lambda, tu recevras un email ! [EMAIL]**

**Tester :**

```bash
curl -X POST https://abc123xyz.execute-api.eu-west-1.amazonaws.com/prod/tasks \
  -H "x-api-key: AbCdEfGh123456789" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Test notification SNS",
    "user_id": "user-123"
  }'
```

**Email reçu quelques secondes après :**

```
Subject: Nouvelle tâche créée

{
  "event": "task.created",
  "task_id": "ghi-789",
  "title": "Test notification SNS",
  "created_at": "2024-12-16T15:30:00"
}
```

**[OK] Notification automatique fonctionnelle !**

---

### ÉTAPE 10 : Créer une tâche planifiée avec EventBridge

**Scénario : Générer un rapport quotidien des tâches créées**

---

#### Créer la fonction Lambda pour le rapport

```bash
mkdir ~/serverless-todo-api/lambda/daily-report
nano ~/serverless-todo-api/lambda/daily-report/lambda_function.py
```

**Contenu :**

```python
import json
import boto3
import time
from datetime import datetime, timedelta

dynamodb = boto3.resource('dynamodb', region_name='eu-west-1')
table = dynamodb.Table('Tasks')

sns = boto3.client('sns', region_name='eu-west-1')
SNS_TOPIC_ARN = 'arn:aws:sns:eu-west-1:123456789012:task-notifications'

def lambda_handler(event, context):
    """
    Génère un rapport quotidien des tâches créées hier
    Déclenché par EventBridge tous les jours à 8h UTC
    """
    
    print(f"[NOTIF] Début du rapport quotidien - {datetime.now().isoformat()}")
    
    # ───────────────────────────────────────────────────────────
    # 1. CALCULER LA PÉRIODE (hier)
    # ───────────────────────────────────────────────────────────
    
    today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
    yesterday_start = today_start - timedelta(days=1)
    
    yesterday_start_ts = int(yesterday_start.timestamp())
    today_start_ts = int(today_start.timestamp())
    
    print(f"Période : {yesterday_start.isoformat()} -> {today_start.isoformat()}")
    
    # ───────────────────────────────────────────────────────────
    # 2. SCANNER LES TÂCHES CRÉÉES HIER
    # ───────────────────────────────────────────────────────────
    
    response = table.scan(
        FilterExpression='created_at >= :start AND created_at < :end',
        ExpressionAttributeValues={
            ':start': yesterday_start_ts,
            ':end': today_start_ts
        }
    )
    
    tasks = response.get('Items', [])
    
    print(f"[GRAPHIQUE] {len(tasks)} tâches créées hier")
    
    # ───────────────────────────────────────────────────────────
    # 3. GÉNÉRER LE RAPPORT
    # ───────────────────────────────────────────────────────────
    
    completed_count = sum(1 for t in tasks if t.get('completed', False))
    pending_count = len(tasks) - completed_count
    
    users = {}
    for task in tasks:
        user_id = task.get('user_id', 'anonymous')
        users[user_id] = users.get(user_id, 0) + 1
    
    report = f"""
[GRAPHIQUE] RAPPORT QUOTIDIEN - {yesterday_start.strftime('%d/%m/%Y')}

Résumé :
  • Total tâches créées : {len(tasks)}
  • Terminées : {completed_count}
  • En attente : {pending_count}

Par utilisateur :
"""
    
    for user_id, count in sorted(users.items(), key=lambda x: x[1], reverse=True):
        report += f"  • {user_id}: {count} tâches\n"
    
    if tasks:
        report += "\nDernières tâches créées :\n"
        for task in sorted(tasks, key=lambda t: t.get('created_at', 0), reverse=True)[:5]:
            created = datetime.fromtimestamp(task['created_at'])
            report += f"  • [{created.strftime('%H:%M')}] {task['title']}\n"
    
    print(report)
    
    # ───────────────────────────────────────────────────────────
    # 4. ENVOYER PAR EMAIL (SNS)
    # ───────────────────────────────────────────────────────────
    
    try:
        sns.publish(
            TopicArn=SNS_TOPIC_ARN,
            Subject=f"[GRAPHIQUE] Rapport quotidien - {yesterday_start.strftime('%d/%m/%Y')}",
            Message=report
        )
        print("[OK] Rapport envoyé par email")
    except Exception as e:
        print(f"[X] Erreur envoi SNS: {str(e)}")
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': 'Rapport généré avec succès',
            'tasks_count': len(tasks),
            'completed': completed_count,
            'pending': pending_count
        })
    }
```

---

**Packager et déployer :**

```bash
cd ~/serverless-todo-api/lambda/daily-report
zip -r daily-report.zip lambda_function.py
```

**Lambda -> Create function -> daily-report**

```
Runtime: Python 3.12
Role: lambda-todo-api-role
Upload: daily-report.zip
```

---

#### Créer la règle EventBridge

**EventBridge -> Rules -> Create rule**

```
Name: daily-report-trigger

Description: Déclenche le rapport quotidien tous les jours à 8h UTC

Event bus: default

Rule type: Schedule
```

**Next**

---

**Schedule pattern :**

```
[x] A schedule that runs at a regular rate, such as every 10 minutes

Rate expression:
  [ ] rate(...)
  
[x] A fine-grained schedule that runs at a specific time

Cron expression:
  0 8 * * ? *
  
  Explication:
  0      -> Minute 0
  8      -> Heure 8 (UTC)
  *      -> Tous les jours du mois
  *      -> Tous les mois
  ?      -> Pas de jour de semaine spécifique
  *      -> Toutes les années
  
  = Tous les jours à 8h00 UTC (9h Paris hiver, 10h Paris été)
```

**Next**

---

**Select target(s) :**

```
Target types: AWS service

Select a target: Lambda function

Function: daily-report
```

**Next -> Next -> Create rule**

---

**[OK] Règle EventBridge créée !**

**La fonction Lambda sera exécutée automatiquement tous les jours à 8h UTC [OK]**

---

**Tester manuellement (sans attendre 8h) :**

**Lambda -> daily-report -> Test**

```
Event name: test-daily-report

Event JSON: {}
  (EventBridge envoie un événement vide)
```

**Test**

---

**Résultat :**

```
[GRAPHIQUE] RAPPORT QUOTIDIEN - 16/12/2024

Résumé :
  • Total tâches créées : 3
  • Terminées : 1
  • En attente : 2

Par utilisateur :
  • user-123: 2 tâches
  • user-789: 1 tâche

Dernières tâches créées :
  • [15:30] Test notification SNS
  • [14:45] Tester API Gateway depuis curl
  • [10:20] Apprendre Lambda
```

**Email reçu avec ce rapport [OK]**

---

### ÉTAPE 11 : Monitoring et logs CloudWatch

#### Voir les logs Lambda

**CloudWatch -> Logs -> Log groups**

**Rechercher : `/aws/lambda/create-task`**

---

**Cliquer sur le log group -> Voir les log streams**

**Chaque invocation = Un log stream avec timestamp**

---

**Cliquer sur un log stream récent :**

```
START RequestId: abc-123 Version: $LATEST
Event reçu: {"httpMethod": "POST", "body": "{\"title\": \"Test\"}", ...}
[OK] Tâche créée: f47ac10b-58cc-4372-a567-0e02b2c3d479
[EMAIL] Notification SNS envoyée
END RequestId: abc-123
REPORT RequestId: abc-123  Duration: 234.56 ms  Billed Duration: 235 ms  Memory Size: 256 MB  Max Memory Used: 67 MB  Init Duration: 512.34 ms
```

---

**Explication du REPORT :**

**Duration : 234.56 ms**
- Temps réel d'exécution du code

**Billed Duration : 235 ms**
- Temps facturé (arrondi au ms supérieur)

**Memory Size : 256 MB**
- Mémoire allouée (configuration)

**Max Memory Used : 67 MB**
- Mémoire réellement utilisée
- [ATTENTION] Si proche de Memory Size -> Augmenter
- [OK] Si très en dessous (ici 67/256 = 26%) -> Peut réduire à 128 MB

**Init Duration : 512.34 ms**
- Cold start (première invocation)
- Temps de chargement du runtime + code
- Pas facturé dans Free Tier (facturé au-delà)

---

#### Insights CloudWatch

**CloudWatch -> Insights**

**Requêter les logs Lambda avec queries SQL-like**

---

**Exemple : Durée moyenne des invocations**

```
fields @timestamp, @duration
| filter @type = "REPORT"
| stats avg(@duration) as avg_duration, max(@duration) as max_duration, min(@duration) as min_duration
| sort @timestamp desc
```

**Run query**

**Résultat :**

```
avg_duration: 245.32 ms
max_duration: 523.12 ms
min_duration: 187.45 ms
```

---

**Exemple : Compter les erreurs**

```
fields @timestamp, @message
| filter @message like /[X]/
| stats count() as error_count by bin(5m)
```

**Graphique montrant les erreurs par tranche de 5 minutes**

---

#### Métriques Lambda

**CloudWatch -> Metrics -> Lambda**

**Métriques par défaut (gratuites) :**

**Invocations :** Nombre d'exécutions
**Duration :** Temps d'exécution
**Errors :** Nombre d'erreurs
**Throttles :** Requêtes throttlées (limite de concurrence)
**ConcurrentExecutions :** Exécutions simultanées
**UnreservedConcurrentExecutions :** Concurrence non-réservée

---

**Créer un dashboard :**

**CloudWatch -> Dashboards -> Create dashboard**

```
Dashboard name: Serverless-Todo-API
```

**Add widget -> Line**

---

**Metrics :**

```
Lambda -> By Function Name
[x] create-task : Invocations
[x] create-task : Duration
[x] create-task : Errors
```

**Create widget**

---

**Ajouter d'autres widgets :**

**Widget 2 : DynamoDB metrics**
```
DynamoDB -> Table Metrics
[x] Tasks : ConsumedReadCapacityUnits
[x] Tasks : ConsumedWriteCapacityUnits
```

**Widget 3 : API Gateway metrics**
```
API Gateway -> By API Name
[x] todo-api : Count (nombre de requêtes)
[x] todo-api : Latency
[x] todo-api : 4XXError
[x] todo-api : 5XXError
```

---

**Résultat : Dashboard complet avec toutes les métriques [OK]**

---

### ÉTAPE 12 : Optimiser les performances

#### Réduire les cold starts

**Cold start = Première invocation (ou après inactivité)**

**Causes :**
- Chargement du runtime Python
- Import des bibliothèques (boto3, etc.)
- Initialisation des connexions

**Temps : 500-2000ms (vs 50-200ms warm start)**

---

**Solutions :**

**1. Réduire la taille du package**

```bash
# Supprimer fichiers inutiles
zip -r function.zip lambda_function.py
# PAS : zip -r function.zip . (tout le dossier)
```

**Éviter les dépendances lourdes (numpy, pandas, etc.)**

---

**2. Utiliser Lambda Layers pour dépendances**

**Layer = Package de dépendances réutilisable par plusieurs fonctions**

**Créer un layer pour boto3 (exemple) :**

```bash
mkdir -p lambda/layers/python
pip install boto3 -t lambda/layers/python/

cd lambda/layers
zip -r dependencies-layer.zip python
```

**Lambda -> Layers -> Create layer**

```
Name: dependencies-layer
Upload: dependencies-layer.zip
Compatible runtimes: Python 3.12
```

**Ajouter le layer aux fonctions :**

**Lambda -> create-task -> Layers -> Add a layer**

```
Custom layers: dependencies-layer
Version: 1
```

**Avantage : Code plus léger = Cold start plus rapide**

---

**3. Provisioned Concurrency (payant)**

**Garde des instances "chaudes" en permanence**

**Lambda -> create-task -> Configuration -> Provisioned concurrency**

```
Provisioned concurrency: 2
  (2 instances toujours prêtes)
```

**Coût : 0.0000041667$/GB-seconde (en plus de l'exécution)**

**Exemple :**
```
256 MB × 2 instances × 30 jours × 24h × 3600s
= 0.25 GB × 2 × 2,592,000 s
= 1,296,000 GB-seconds
× 0.0000041667 = 5.40$/mois
```

**Utiliser seulement si cold starts critiques**

---

**4. Optimiser les imports**

```python
# [X] MAUVAIS : Import global (exécuté à chaque cold start)
import heavy_library  # 500ms de chargement

def lambda_handler(event, context):
    ...

# [OK] BON : Import local (seulement si utilisé)
def lambda_handler(event, context):
    if need_heavy_feature:
        import heavy_library
    ...
```

---

**5. Connexions réutilisables (hors handler)**

```python
# [OK] BON : Connexion globale (réutilisée entre invocations)
import boto3

dynamodb = boto3.resource('dynamodb')  # Créé au cold start
table = dynamodb.Table('Tasks')

def lambda_handler(event, context):
    table.put_item(Item={...})  # Réutilise connexion existante
```

**Éviter :**

```python
# [X] MAUVAIS : Connexion dans handler (créée à chaque invocation)
def lambda_handler(event, context):
    dynamodb = boto3.resource('dynamodb')  # Reconnexion à chaque fois
    table = dynamodb.Table('Tasks')
    table.put_item(Item={...})
```

---

#### Optimiser la mémoire Lambda

**Plus de mémoire = Plus de CPU (proportionnel)**

**Test : Réduire de 256 MB à 128 MB**

**Lambda -> create-task -> Configuration -> General -> Edit**

```
Memory: 128 MB
```

**Save**

---

**Tester et comparer :**

**Test avec 256 MB :**
```
Duration: 234 ms
Max Memory Used: 67 MB
Cost: 234 ms × 256 MB = 59,904 MB-ms
```

**Test avec 128 MB :**
```
Duration: 298 ms (un peu plus lent)
Max Memory Used: 67 MB (même)
Cost: 298 ms × 128 MB = 38,144 MB-ms
```

**Économie : 36% [OK]**

**[ATTENTION] Mais attention : Si durée augmente trop, le coût total peut augmenter**

**Trouver le sweet spot (test avec différentes tailles)**

---

#### Optimiser DynamoDB

**1. Utiliser BatchWriteItem pour insertions multiples**

```python
# [X] MAUVAIS : 100 PutItem (100 requêtes)
for i in range(100):
    table.put_item(Item={...})

# [OK] BON : 4 BatchWriteItem (max 25 items par batch)
with table.batch_writer() as batch:
    for i in range(100):
        batch.put_item(Item={...})
```

**Économie : ~75% de requêtes**

---

**2. Projection expressions (récupérer seulement certains attributs)**

```python
# [X] MAUVAIS : Récupère tous les attributs
response = table.get_item(Key={'task_id': 'abc'})

# [OK] BON : Récupère seulement title et completed
response = table.get_item(
    Key={'task_id': 'abc'},
    ProjectionExpression='title, completed'
)
```

**Économie : Moins de data transfer, plus rapide**

---

**3. Utiliser Query au lieu de Scan**

```python
# [X] MAUVAIS : Scan (lit TOUTE la table)
response = table.scan(
    FilterExpression='user_id = :uid',
    ExpressionAttributeValues={':uid': 'user-123'}
)
# RCU consommés : Toute la table (même items non matchés)

# [OK] BON : Query avec GSI
response = table.query(
    IndexName='GSI-UserCreatedAt',
    KeyConditionExpression=Key('user_id').eq('user-123')
)
# RCU consommés : Seulement items matchés
```

**Économie : 90%+ de RCU**

---

**4. Conditional writes (éviter écritures inutiles)**

```python
# Mettre à jour seulement si completed a changé
table.update_item(
    Key={'task_id': 'abc'},
    UpdateExpression='SET completed = :val',
    ConditionExpression='completed <> :val',  # Seulement si différent
    ExpressionAttributeValues={':val': True}
)
```

**Si condition non remplie -> Exception (pas de WCU consommé) [OK]**

---

### ÉTAPE 13 : Gérer les erreurs et retries

#### Retry automatique Lambda

**Lambda retry automatiquement en cas d'erreur :**

**Synchrone (API Gateway) :**
- Pas de retry automatique
- API Gateway retourne l'erreur au client
- Client décide de retry ou non

**Asynchrone (SNS, EventBridge, S3) :**
- 2 retries automatiques
- Délai exponentiel : 1 min, 2 min
- Après 2 échecs -> DLQ (Dead Letter Queue) si configuré

---

#### Configurer une Dead Letter Queue (DLQ)

**DLQ = File d'attente pour événements en échec**

**Créer une queue SQS :**

**SQS -> Queues -> Create queue**

```
Type: Standard

Name: lambda-dlq

Configuration: (default OK)
```

**Create queue**

---

**Configurer Lambda pour utiliser la DLQ :**

**Lambda -> daily-report -> Configuration -> Asynchronous invocation**

```
Retry attempts: 2

Dead-letter queue service: SQS

Dead-letter queue: lambda-dlq
```

**Save**

---

**Maintenant, si la fonction échoue 3 fois (1 initial + 2 retries) :**
- L'événement est envoyé dans `lambda-dlq`
- Tu peux créer une alarme CloudWatch sur la taille de la queue
- Ou une Lambda qui traite les DLQ messages

---

#### Gestion d'erreurs dans le code

**Bonnes pratiques :**

```python
def lambda_handler(event, context):
    try:
        # Code métier
        ...
        
        return {
            'statusCode': 200,
            'body': json.dumps({'message': 'Success'})
        }
    
    except KeyError as e:
        # Erreur validation (input manquant)
        print(f"[X] Validation error: {str(e)}")
        return {
            'statusCode': 400,
            'body': json.dumps({
                'error': 'BadRequest',
                'message': f'Missing field: {str(e)}'
            })
        }
    
    except boto3.exceptions.Boto3Error as e:
        # Erreur AWS (DynamoDB, SNS, etc.)
        print(f"[X] AWS error: {str(e)}")
        return {
            'statusCode': 502,
            'body': json.dumps({
                'error': 'BadGateway',
                'message': 'Service unavailable'
            })
        }
    
    except Exception as e:
        # Erreur inconnue
        print(f"[X] Unexpected error: {str(e)}")
        import traceback
        traceback.print_exc()
        
        return {
            'statusCode': 500,
            'body': json.dumps({
                'error': 'InternalServerError',
                'message': 'An error occurred'
            })
        }
```

**[ATTENTION] Ne jamais laisser une exception non catchée (Lambda retry inutile)**

---

### ÉTAPE 14 : Calculer les coûts réels

#### Utiliser AWS Cost Explorer

**Billing -> Cost Explorer -> Launch Cost Explorer**

**Créer un rapport serverless :**

```
Time: Last month
Granularity: Daily
Group by: Service

Filters:
  Service: Lambda, API Gateway, DynamoDB, SNS
```

**Graphique montrant coûts par service**

---

**Exemple de breakdown :**

```
Lambda:
  Invocations: 50,000 (dans Free Tier 1M) = 0$
  Duration: 150,000 GB-seconds (dans Free Tier 400k) = 0$
  Total Lambda: 0$

API Gateway:
  Requests: 50,000 (dans Free Tier 1M) = 0$
  Total API Gateway: 0$

DynamoDB:
  Storage: 0.5 GB (dans Free Tier 25 GB) = 0$
  WRU: 500,000 (dans Free Tier 2.5M/mois) = 0$
  RRU: 1,000,000 (dans Free Tier 2.5M/mois) = 0$
  Total DynamoDB: 0$

SNS:
  Notifications: 500 (dans Free Tier 1,000) = 0$
  Total SNS: 0$

TOTAL: 0$/mois [OK]
```

---

**Après Free Tier (simulation) :**

```
Lambda: 1,000,000 invocations × 0.20$/1M = 0.20$
API Gateway: 1,000,000 requests × 3.50$/1M = 3.50$
DynamoDB: 30 GB × 0.25$ = 7.50$ + requêtes ~1$
SNS: 10,000 emails × 0.50$/1M = 0.005$

TOTAL: ~12$/mois
```

**vs EC2 + RDS : ~30-50$/mois**

**Économie : 60-75% [OK]**

---

### [OK] TESTS DE VALIDATION

**DynamoDB :**
- [ ] Table Tasks créée avec task_id comme PK
- [ ] GSI créé (user_id + created_at)
- [ ] Point-in-time recovery activé
- [ ] Items insérés et récupérés avec succès

**Lambda Functions :**
- [ ] 5 fonctions déployées (create, list, get, update, delete)
- [ ] Rôle IAM avec permissions DynamoDB + SNS
- [ ] Variables d'environnement configurées
- [ ] Tests unitaires réussis
- [ ] Logs visibles dans CloudWatch

**API Gateway :**
- [ ] REST API créée et déployée (stage: prod)
- [ ] 5 routes configurées (POST/GET /tasks, GET/PUT/DELETE /tasks/{id})
- [ ] Intégrations Lambda proxy fonctionnelles
- [ ] CORS activé
- [ ] API Keys configurées et testées
- [ ] Throttling configuré (usage plan)

**SNS :**
- [ ] Topic créé (task-notifications)
- [ ] Subscription email confirmée
- [ ] Notifications reçues lors création de tâches

**EventBridge :**
- [ ] Règle cron créée (daily-report)
- [ ] Lambda daily-report exécutée avec succès
- [ ] Rapport quotidien reçu par email

**Monitoring :**
- [ ] CloudWatch Logs pour toutes les Lambdas
- [ ] Dashboard créé avec métriques clés
- [ ] Alarmes configurées (optionnel)

**Performance :**
- [ ] Cold starts < 1 seconde
- [ ] Warm starts < 200ms
- [ ] Mémoire Lambda optimisée

**Sécurité :**
- [ ] API protégée par API Key
- [ ] IAM rôle avec principe du moindre privilège
- [ ] HTTPS uniquement (API Gateway)
- [ ] Pas de credentials en dur dans le code

**Coûts :**
- [ ] Utilisation dans Free Tier vérifiée
- [ ] Cost Explorer configuré
- [ ] Budget alerts activées

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : "errorMessage": "Unable to import module 'lambda_function'"

**Symptôme :**

```
Unable to import module 'lambda_function': No module named 'lambda_function'
```

**Cause : Structure ZIP incorrecte**

**[X] MAUVAIS :**
```
function.zip
  └── my-function/
      └── lambda_function.py
```

**[OK] BON :**
```
function.zip
  └── lambda_function.py
```

**Solution :**

```bash
cd lambda/create-task
zip -r create-task.zip lambda_function.py
# PAS : zip -r create-task.zip .
```

---

#### Erreur 2 : AccessDeniedException DynamoDB

**Symptôme :**

```
botocore.exceptions.ClientError: An error occurred (AccessDeniedException) when calling the PutItem operation
```

**Cause : Rôle IAM manquant ou mal configuré**

**Vérifier :**

**Lambda -> create-task -> Configuration -> Permissions**

**Execution role : lambda-todo-api-role**

**Cliquer sur le rôle -> Vérifier policies :**

```json
{
  "Effect": "Allow",
  "Action": "dynamodb:PutItem",
  "Resource": "arn:aws:dynamodb:eu-west-1:123456789012:table/Tasks"
}
```

**[ATTENTION] ARN correct ? Région ? Account ID ?**

---

#### Erreur 3 : API Gateway renvoie 403 Forbidden

**Symptôme :**

```
{"message": "Forbidden"}
```

**Causes possibles :**

**1. API Key manquante**

```bash
# [X] Sans API Key
curl https://abc.execute-api.eu-west-1.amazonaws.com/prod/tasks

# [OK] Avec API Key
curl -H "x-api-key: AbCdEf..." https://...
```

---

**2. API pas re-déployée après modification**

**Après TOUT changement (méthode, integration, etc.) :**

```
Actions -> Deploy API -> Stage: prod
```

---

**3. Permissions Lambda manquantes**

**API Gateway -> todo-api -> Resources -> POST /tasks -> Integration Request**

**Vérifier : Lambda Function = `create-task`**

**Si pas de permission :**

```bash
aws lambda add-permission \
  --function-name create-task \
  --statement-id apigateway-access \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:eu-west-1:123:abc/*/POST/tasks"
```

---

#### Erreur 4 : Cold start très lent (> 5 secondes)

**Causes :**

**1. Package trop gros**

```bash
# Vérifier taille
ls -lh function.zip
# > 50 MB -> Problème

# Solution : Supprimer dépendances inutiles, utiliser Layers
```

---

**2. VPC configuration (si Lambda dans VPC)**

**Lambda dans VPC = +10-30s cold start (ENI setup)**

**Solution :**
- Sortir Lambda du VPC (pas nécessaire pour DynamoDB/S3)
- Ou utiliser VPC endpoints pour DynamoDB

---

**3. Provisioned Concurrency pas configuré**

**Si latence critique -> Activer Provisioned Concurrency (payant)**

---

#### Erreur 5 : DynamoDB Scan trop lent/cher

**Symptôme :**

```
Scan: 10,000 items scannés, 50 RCU consommés
Query: 10 items retournés
```

**Cause : Scan lit TOUTE la table, même les items non matchés**

**Solution : Utiliser Query avec index approprié**

```python
# [X] MAUVAIS : Scan
response = table.scan(
    FilterExpression='user_id = :uid',
    ExpressionAttributeValues={':uid': 'user-123'}
)

# [OK] BON : Query avec GSI
response = table.query(
    IndexName='GSI-UserCreatedAt',
    KeyConditionExpression=Key('user_id').eq('user-123')
)
```

---

#### Erreur 6 : Lambda timeout

**Symptôme :**

```
Task timed out after 3.00 seconds
```

**Solutions :**

**1. Augmenter le timeout**

```
Configuration -> General -> Timeout: 10 seconds
```

**Max Lambda timeout : 15 minutes**

---

**2. Optimiser le code**

```python
# [X] MAUVAIS : Traitement synchrone lourd
for i in range(10000):
    process_item(i)  # 15 minutes total

# [OK] BON : Split en plusieurs invocations
# Ou utiliser Step Functions pour orchestration
```

---

#### Erreur 7 : CORS errors dans le navigateur

**Symptôme (console navigateur) :**

```
Access to fetch at '...' from origin '...' has been blocked by CORS policy
```

**Solution :**

**1. Activer CORS sur API Gateway**

**Resources -> /tasks -> Actions -> Enable CORS**

```
[x] Default 4XX
[x] Default 5XX

Access-Control-Allow-Headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key
Access-Control-Allow-Origin: * (ou ton domaine)
```

**Enable**

---

**2. Headers CORS dans Lambda response**

```python
return {
    'statusCode': 200,
    'headers': {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Content-Type,X-Api-Key',
        'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS'
    },
    'body': json.dumps({...})
}
```

---

**3. Re-déployer l'API**

```
Actions -> Deploy API -> prod
```

---

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

**Serverless :**
- Pas de serveurs à gérer (AWS s'occupe de tout)
- Paye seulement l'utilisation réelle (pas de idle)
- Scaling automatique (0 -> ∞)
- Stateless (état externalisé dans DynamoDB, S3, etc.)
- Cold starts (100-1000ms première invocation)

**Lambda :**
- Fonction = Code + Runtime + Config
- Timeout max : 15 minutes
- Mémoire : 128 MB - 10 GB (CPU proportionnel)
- Connexions réutilisables (hors handler)
- Logs automatiques dans CloudWatch

**API Gateway :**
- Exposition HTTP/REST de fonctions Lambda
- Throttling, caching, API keys intégrés
- CORS à configurer pour apps web
- Stages (dev, staging, prod)
- Toujours re-déployer après modification

**DynamoDB :**
- Base NoSQL key-value (pas de SQL)
- Performance : single-digit milliseconds
- Scaling automatique (On-Demand mode)
- Query > Scan (toujours)
- GSI pour requêtes sur autres attributs
- Pas de joins natifs (dénormaliser)

**EventBridge :**
- Event bus serverless
- Cron jobs (expressions standard)
- Routing d'événements
- Intégration native avec Lambda

**SNS :**
- Pub/Sub messaging
- Fan-out (1 publish -> N subscribers)
- Email, SMS, HTTP, Lambda, SQS
- Gratuit jusqu'à 1000 notifications/mois

**Coûts :**
- Lambda : 1M requêtes + 400k GB-sec gratuit
- API Gateway : 1M requêtes gratuit (12 mois)
- DynamoDB : 25 GB + 25 RCU/WCU gratuit
- SNS : 1000 notifications gratuites
- Serverless << EC2 (si trafic variable)

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. AWS Step Functions (Orchestration)**

**Scénario : Workflow complexe multi-étapes**

```
Nouvelle commande
  v
1. Valider paiement (Lambda)
  v Success
2. Créer facture (Lambda)
  v
3. Envoyer email (SNS)
  v
4. Mettre à jour inventaire (Lambda + DynamoDB)
  v
5. Notifier expédition (SQS)
```

**Step Functions = State machine serverless**

**Définition JSON (Amazon States Language) :**

```json
{
  "StartAt": "ValidatePayment",
  "States": {
    "ValidatePayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:...:function:validate-payment",
      "Next": "CreateInvoice",
      "Catch": [{
        "ErrorEquals": ["PaymentFailed"],
        "Next": "SendFailureEmail"
      }]
    },
    "CreateInvoice": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:...:function:create-invoice",
      "Next": "SendEmail"
    },
    ...
  }
}
```

**Avantages :**
- Orchestration visuelle (state machine graph)
- Retry/error handling automatique
- Historique d'exécution
- Jusqu'à 1 an de durée (vs 15 min Lambda)

**Coût : 0.025$/1000 transitions d'état**

---

**2. Lambda@Edge (CloudFront)**

**Exécuter Lambda aux edge locations CloudFront**

**Use cases :**
- Manipulation headers HTTP
- Authentification/autorisation
- A/B testing
- Redirection basée sur géolocalisation
- SEO (user-agent based rendering)

**Exemple : Redirection mobile :**

```python
def lambda_handler(event, context):
    request = event['Records'][0]['cf']['request']
    headers = request['headers']
    
    user_agent = headers.get('user-agent', [{}])[0].get('value', '')
    
    if 'Mobile' in user_agent:
        return {
            'status': '302',
            'headers': {
                'location': [{'value': 'https://m.example.com'}]
            }
        }
    
    return request
```

**Latence ultra-faible (exécuté au edge, proche de l'utilisateur)**

---

**3. Lambda Layers avancés**

**Partager code/dépendances entre fonctions**

**Structure :**

```
layer.zip
  └── python/
      ├── lib/
      │   └── my_shared_module.py
      └── requirements.txt (dépendances)
```

**Layer pour utilities communes :**

```python
# python/lib/validators.py
def validate_email(email):
    import re
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return re.match(pattern, email) is not None

def validate_uuid(uuid_str):
    import uuid
    try:
        uuid.UUID(uuid_str)
        return True
    except ValueError:
        return False
```

**Dans Lambda :**

```python
from validators import validate_email, validate_uuid

def lambda_handler(event, context):
    email = event['body']['email']
    if not validate_email(email):
        return {'statusCode': 400, 'body': 'Invalid email'}
    ...
```

**Avantage : DRY (Don't Repeat Yourself), maintenance centralisée**

---

**4. DynamoDB Streams + Lambda Triggers**

**Stream = Flux de changements dans DynamoDB**

**Scénario : Synchronisation Elasticsearch**

```
DynamoDB (Tasks)
  -> DynamoDB Stream
    -> Lambda (sync-to-elasticsearch)
      -> Elasticsearch
```

**Activer Streams :**

**DynamoDB -> Tasks -> Exports and streams -> DynamoDB stream details**

```
[x] Enable

View type: New and old images
  (Contient l'item avant et après modification)
```

**Créer Lambda trigger :**

```python
def lambda_handler(event, context):
    for record in event['Records']:
        if record['eventName'] == 'INSERT':
            new_image = record['dynamodb']['NewImage']
            task_id = new_image['task_id']['S']
            title = new_image['title']['S']
            
            # Index dans Elasticsearch
            es_client.index(
                index='tasks',
                id=task_id,
                body={'title': title, ...}
            )
        
        elif record['eventName'] == 'REMOVE':
            old_image = record['dynamodb']['OldImage']
            task_id = old_image['task_id']['S']
            
            # Supprimer d'Elasticsearch
            es_client.delete(index='tasks', id=task_id)
```

**Use cases :**
- Synchronisation vers autre système
- Aggregation en temps réel
- Audit trail
- Notifications complexes

---

**5. API Gateway Custom Authorizers (Lambda Authorizer)**

**Authentification custom (JWT, OAuth, etc.)**

**Créer Lambda authorizer :**

```python
import jwt

def lambda_handler(event, context):
    token = event['authorizationToken']  # Header: Authorization: Bearer <token>
    
    try:
        # Vérifier JWT
        payload = jwt.decode(token, 'secret-key', algorithms=['HS256'])
        
        # Générer IAM policy
        return {
            'principalId': payload['user_id'],
            'policyDocument': {
                'Version': '2012-10-17',
                'Statement': [{
                    'Action': 'execute-api:Invoke',
                    'Effect': 'Allow',
                    'Resource': event['methodArn']
                }]
            },
            'context': {
                'user_id': payload['user_id'],
                'email': payload['email']
            }
        }
    
    except jwt.InvalidTokenError:
        raise Exception('Unauthorized')
```

**API Gateway -> Authorizers -> Create authorizer**

```
Name: jwt-authorizer
Type: Lambda
Lambda Function: jwt-authorizer
Token Source: Authorization
```

**Attacher aux méthodes :**

**POST /tasks -> Method Request -> Authorization: jwt-authorizer**

**Résultat : JWT vérifié avant d'invoquer Lambda [OK]**

---

**6. DynamoDB Global Tables (Multi-Region)**

**Réplication multi-région automatique**

**Scénario : App globale avec faible latence partout**

```
Europe (eu-west-1)     US East (us-east-1)     Asia (ap-southeast-1)
   DynamoDB                 DynamoDB                 DynamoDB
      ^v                        ^v                        ^v
   (Réplication bidirectionnelle automatique)
```

**Activer Global Tables :**

**DynamoDB -> Tasks -> Global Tables -> Create replica**

```
Regions:
  [x] US East (N. Virginia) us-east-1
  [x] Asia Pacific (Singapore) ap-southeast-1
```

**Caractéristiques :**
- Réplication < 1 seconde
- Active-active (écriture dans toutes les régions)
- Résolution de conflits automatique (last-writer-wins)

**Use case : Application mondiale avec latence < 50ms partout**

---

**7. Lambda Container Images**

**Déployer Lambda avec Docker**

**Avantages :**
- Image jusqu'à 10 GB (vs 250 MB ZIP)
- Dépendances complexes (ML models, etc.)
- Outils custom

**Dockerfile :**

```dockerfile
FROM public.ecr.aws/lambda/python:3.12

# Copier code
COPY lambda_function.py ${LAMBDA_TASK_ROOT}

# Installer dépendances lourdes
RUN pip install tensorflow numpy pandas

# Handler
CMD ["lambda_function.lambda_handler"]
```

**Build et push :**

```bash
docker build -t my-lambda .

aws ecr create-repository --repository-name my-lambda

docker tag my-lambda:latest 123.dkr.ecr.eu-west-1.amazonaws.com/my-lambda:latest

docker push 123.dkr.ecr.eu-west-1.amazonaws.com/my-lambda:latest
```

**Créer Lambda :**

```
Image URI: 123.dkr.ecr.eu-west-1.amazonaws.com/my-lambda:latest
```

---

**8. AWS SAM (Serverless Application Model)**

**Infrastructure as Code pour serverless**

**template.yaml :**

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  TasksTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Tasks
      AttributeDefinitions:
        - AttributeName: task_id
          AttributeType: S
      KeySchema:
        - AttributeName: task_id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

  CreateTaskFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: create-task
      Runtime: python3.12
      Handler: lambda_function.lambda_handler
      CodeUri: lambda/create-task/
      Environment:
        Variables:
          TABLE_NAME: !Ref TasksTable
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref TasksTable
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /tasks
            Method: POST

Outputs:
  ApiUrl:
    Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod'
```

**Déployer :**

```bash
sam build
sam deploy --guided
```

**Avantages :**
- Infrastructure versionée (Git)
- Déploiement reproductible
- Rollback facile
- CI/CD intégrable

---

**9. X-Ray (Tracing distribué)**

**Tracer les requêtes à travers Lambda, API Gateway, DynamoDB**

**Activer sur Lambda :**

```
Configuration -> Monitoring -> Active tracing: Enabled
```

**Instrumenter le code :**

```python
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.core import patch_all

patch_all()  # Instrumente automatiquement boto3

@xray_recorder.capture('create_task')
def lambda_handler(event, context):
    # Sous-segment custom
    with xray_recorder.capture('validate_input'):
        validate(event['body'])
    
    with xray_recorder.capture('write_to_dynamodb'):
        table.put_item(Item=item)
    
    return response
```

**Visualiser dans X-Ray :**

```
Client -> API Gateway (23ms)
  -> Lambda cold start (512ms)
    -> Validate input (5ms)
    -> DynamoDB PutItem (12ms)
  -> SNS Publish (34ms)
Total: 586ms
```

**Graphique visuel montrant chaque composant + latence**

---

**10. Reserved Concurrency (Limite de concurrence)**

**Limiter le nombre d'exécutions simultanées**

**Use case : Éviter de surcharger une DB legacy**

```
Lambda -> Configuration -> Concurrency

Reserved concurrency: 10
  (Max 10 exécutions simultanées)
```

**Si 11e requête arrive -> Throttled (429)**

**Utiliser avec SQS pour queue les requêtes :**

```
API -> SQS -> Lambda (10 concurrence) -> Legacy DB
```

**Requêtes en surplus = Dans la queue, traitées progressivement**

---

## [COURS] CONCLUSION DE L'EXERCICE 4

**[BRAVO] BRAVO ! Tu maîtrises maintenant l'architecture serverless AWS ! [BRAVO]**

**Ce que tu as appris :**
- [OK] Architecture serverless (concepts, avantages, limites)
- [OK] AWS Lambda (création, déploiement, configuration)
- [OK] API Gateway (REST API, routes, intégrations)
- [OK] DynamoDB (NoSQL, Query vs Scan, GSI)
- [OK] EventBridge (cron jobs, event-driven)
- [OK] SNS (notifications pub/sub)
- [OK] CloudWatch (logs, métriques, alarmes)
- [OK] IAM (rôles et permissions granulaires)
- [OK] Optimisation performance (cold starts, mémoire)
- [OK] Gestion d'erreurs et retries
- [OK] Calcul et optimisation des coûts

**Compétences acquises :**
- [OK] Développement serverless
- [OK] Architecture event-driven
- [OK] API REST design et implémentation
- [OK] Base de données NoSQL (modélisation)
- [OK] Monitoring et observabilité
- [OK] DevOps serverless

**Architecture déployée :**
```
[OK] 5 fonctions Lambda (CRUD + rapport)
[OK] API Gateway REST (5 routes)
[OK] DynamoDB table + GSI
[OK] SNS topic + email subscription
[OK] EventBridge cron rule
[OK] CloudWatch logs + dashboard
[OK] IAM role avec permissions granulaires
[OK] API Keys + throttling
[OK] DLQ configurée
```

**Coût estimé :**
- **Free Tier : 0$/mois** [OK]
- **Après Free Tier (50k req/mois) : ~0.50$/mois**
- **Production (1M req/mois) : ~12$/mois**

**vs Architecture traditionnelle (EC2 + RDS) : ~30$/mois**

**Économie : 60%+ [OK]**

**Temps moyen de réalisation :** 5-6 heures

**Prochaine étape :** Exercice 5 - Réseau avancé (VPC Peering, NAT Gateway, Multi-tier) ! [WEB]

---

**[ATTENTION] NETTOYAGE DES RESSOURCES**

```bash
# 1. Supprimer l'API Gateway
API Gateway -> todo-api -> Actions -> Delete API

# 2. Supprimer les fonctions Lambda
Lambda -> Functions -> Sélectionner toutes -> Actions -> Delete

# 3. Supprimer la table DynamoDB
DynamoDB -> Tasks -> Delete table

# 4. Supprimer le topic SNS
SNS -> task-notifications -> Delete

# 5. Supprimer la règle EventBridge
EventBridge -> Rules -> daily-report-trigger -> Delete

# 6. Supprimer la queue SQS DLQ
SQS -> lambda-dlq -> Delete queue

# 7. Supprimer le rôle IAM
IAM -> Roles -> lambda-todo-api-role -> Delete

# 8. Supprimer les log groups CloudWatch
CloudWatch -> Logs -> Log groups -> Sélectionner /aws/lambda/* -> Delete

# 9. Supprimer le dashboard CloudWatch
CloudWatch -> Dashboards -> Serverless-Todo-API -> Delete
```

---

**FIN DE L'EXERCICE 4**

═══════════════════════════════════════════════════════════════

Veux-tu que je continue avec l'**Exercice 5 : Réseau avancé (VPC Peering, NAT Gateway, Architecture Multi-tier)** ?

# [JAUNE] EXERCICE 5 : RÉSEAU AVANCÉ AWS (VPC PEERING, NAT GATEWAY, MULTI-TIER)

## [LISTE] ÉNONCÉ

### Contexte professionnel

La startup pour laquelle tu travailles connaît une croissance rapide. Le CTO décide d'adopter une architecture réseau professionnelle pour :

**Problèmes actuels :**
- [X] **Isolation insuffisante** : Toutes les ressources dans le même VPC (dev + prod mélangés)
- [X] **Sécurité faible** : Instances privées qui téléchargent des packages passent par internet public
- [X] **Coûts élevés** : Data transfer OUT vers internet facturé
- [X] **Pas de séparation des environnements** : Dev peut affecter prod accidentellement
- [X] **Architecture plate** : Pas de tiers (web, app, data) clairement séparés

**Nouvelle architecture demandée :**

Le directeur technique impose :
1. **3 VPCs distincts** : Production, Staging, Développement
2. **VPC Peering** pour communication inter-VPC (prod <-> staging, partage de ressources)
3. **Architecture 3-tiers** dans le VPC Production :
   - **Web Tier** (subnet public) : Load Balancer + instances web
   - **Application Tier** (subnet privé) : Instances applicatives
   - **Database Tier** (subnet privé isolé) : RDS
4. **NAT Gateway** pour instances privées (mises à jour, APIs externes)
5. **VPC Endpoints** pour S3 et DynamoDB (pas de trafic internet)
6. **Network ACLs** pour sécurité supplémentaire (en plus des Security Groups)
7. **VPC Flow Logs** pour audit et troubleshooting réseau
8. **Bastion Host** pour accès SSH sécurisé
9. **Transit Gateway** (optionnel, architecture avancée)

### Architecture cible

```
┌────────────────────────────────────────────────────────────────────┐
│                            INTERNET                                 │
└──────────────┬────────────────────────────────┬────────────────────┘
               │                                │
               │                                │
    ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐         ┌──────────[BLACK_DOWN-POINTING_TRIANGLE]──────────┐
    │  Internet Gateway   │         │  Internet Gateway   │
    │   (VPC Production)  │         │  (VPC Staging)      │
    └──────────┬──────────┘         └──────────┬──────────┘
               │                                │
┌──────────────┴────────────────────────────────┴───────────────────┐
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐  │
│  │              VPC PRODUCTION (10.0.0.0/16)                    │  │
│  │                                                               │  │
│  │  ┌────────────────────────────────────────────────────────┐ │  │
│  │  │  WEB TIER (Public Subnets)                             │ │  │
│  │  │  ┌────────────────────────────────────────────────┐    │ │  │
│  │  │  │  Public Subnet 1a (10.0.1.0/24)               │    │ │  │
│  │  │  │  - Application Load Balancer                   │    │ │  │
│  │  │  │  - NAT Gateway (1a)                            │    │ │  │
│  │  │  │  - Bastion Host                                │    │ │  │
│  │  │  └────────────────────────────────────────────────┘    │ │  │
│  │  │  ┌────────────────────────────────────────────────┐    │ │  │
│  │  │  │  Public Subnet 1b (10.0.2.0/24)               │    │ │  │
│  │  │  │  - NAT Gateway (1b) - Haute dispo             │    │ │  │
│  │  │  └────────────────────────────────────────────────┘    │ │  │
│  │  └────────────────────────────────────────────────────────┘ │  │
│  │                                                               │  │
│  │  ┌────────────────────────────────────────────────────────┐ │  │
│  │  │  APPLICATION TIER (Private Subnets)                    │ │  │
│  │  │  ┌────────────────────────────────────────────────┐    │ │  │
│  │  │  │  App Subnet 1a (10.0.11.0/24)                 │    │ │  │
│  │  │  │  - EC2 instances (Flask app)                   │    │ │  │
│  │  │  │  - Auto Scaling Group                          │    │ │  │
│  │  │  │  - Internet via NAT Gateway 1a                 │    │ │  │
│  │  │  └────────────────────────────────────────────────┘    │ │  │
│  │  │  ┌────────────────────────────────────────────────┐    │ │  │
│  │  │  │  App Subnet 1b (10.0.12.0/24)                 │    │ │  │
│  │  │  │  - EC2 instances (Flask app)                   │    │ │  │
│  │  │  │  - Internet via NAT Gateway 1b                 │    │ │  │
│  │  │  └────────────────────────────────────────────────┘    │ │  │
│  │  └────────────────────────────────────────────────────────┘ │  │
│  │                                                               │  │
│  │  ┌────────────────────────────────────────────────────────┐ │  │
│  │  │  DATABASE TIER (Private Subnets - Isolated)            │ │  │
│  │  │  ┌────────────────────────────────────────────────┐    │ │  │
│  │  │  │  DB Subnet 1a (10.0.21.0/24)                  │    │ │  │
│  │  │  │  - RDS Primary                                 │    │ │  │
│  │  │  │  - NO internet access                          │    │ │  │
│  │  │  └────────────────────────────────────────────────┘    │ │  │
│  │  │  ┌────────────────────────────────────────────────┐    │ │  │
│  │  │  │  DB Subnet 1b (10.0.22.0/24)                  │    │ │  │
│  │  │  │  - RDS Standby (Multi-AZ)                      │    │ │  │
│  │  │  └────────────────────────────────────────────────┘    │ │  │
│  │  └────────────────────────────────────────────────────────┘ │  │
│  │                                                               │  │
│  │  VPC Endpoints:                                              │  │
│  │  - S3 Gateway Endpoint (pas de trafic internet)             │  │
│  │  - DynamoDB Gateway Endpoint                                │  │
│  │  - Secrets Manager Interface Endpoint                       │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐  │
│  │              VPC STAGING (10.1.0.0/16)                       │  │
│  │  - Architecture simplifiée (2-tier)                          │  │
│  │  - Subnets: Public (10.1.1.0/24), Private (10.1.11.0/24)    │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                    ^v VPC Peering Connection                         │
│  ┌─────────────────────────────────────────────────────────────┐  │
│  │              VPC DEVELOPMENT (10.2.0.0/16)                   │  │
│  │  - Architecture minimale (1 public subnet)                   │  │
│  │  - Subnet: Public (10.2.1.0/24)                              │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Flow Logs -> CloudWatch Logs Group -> S3 (long-term storage)

Security Layers:
├── Security Groups (Instance-level, stateful)
├── Network ACLs (Subnet-level, stateless)
└── Route Tables (Routing control)
```

### Contraintes techniques

- Régions : `eu-west-1` (Irlande)
- CIDR non-overlapping (10.0.x.x, 10.1.x.x, 10.2.x.x)
- NAT Gateway : High Availability (1 par AZ)
- Bastion Host : t3.micro (Free Tier)
- VPC Flow Logs : Vers CloudWatch + S3
- Budget : Optimiser les coûts (NAT Gateway = cher)
- Temps estimé : 6-7 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Concevoir une architecture réseau multi-tier
- [OK] Créer et configurer plusieurs VPCs
- [OK] Mettre en place VPC Peering
- [OK] Déployer et configurer NAT Gateway
- [OK] Comprendre la différence NAT Gateway vs NAT Instance
- [OK] Créer VPC Endpoints (Gateway et Interface)
- [OK] Configurer Network ACLs (vs Security Groups)
- [OK] Implémenter VPC Flow Logs
- [OK] Déployer un Bastion Host sécurisé
- [OK] Gérer les route tables complexes
- [OK] Troubleshooter les problèmes réseau
- [OK] Optimiser les coûts réseau AWS
- [OK] Comprendre Transit Gateway (architecture avancée)

---

## [DOCS] PRÉREQUIS

- Exercices 1-4 terminés (concepts AWS de base)
- Compréhension réseau (CIDR, subnets, routing)
- Notions TCP/IP et OSI model
- Connaissance des concepts VPC de base

---

## [ARGENT] ESTIMATION DES COÛTS

**NAT Gateway ([ATTENTION] ATTENTION - Service le plus coûteux) :**

| Ressource | Coût | Remarque |
|-----------|------|----------|
| NAT Gateway (1 instance) | 0.045$/h (~33$/mois) | [X] PAS de Free Tier |
| Data processing | 0.045$/GB | Pour chaque GB traité |
| NAT Gateway HA (2 AZs) | 0.09$/h (~66$/mois) | Double le coût |

**VPC :**
- VPC lui-même : **Gratuit** [OK]
- Subnets : **Gratuit** [OK]
- Route tables : **Gratuit** [OK]
- Internet Gateway : **Gratuit** [OK]

**VPC Peering :**
- Connection : **Gratuit** [OK]
- Data transfer (même région) : **Gratuit** [OK]
- Data transfer (cross-region) : 0.01$/GB

**VPC Endpoints :**
- Gateway Endpoints (S3, DynamoDB) : **Gratuit** [OK]
- Interface Endpoints : 0.01$/h (~7$/mois) par endpoint

**VPC Flow Logs :**
- Logs CloudWatch : 0.50$/GB ingéré
- Storage S3 : 0.023$/GB-mois

**Bastion Host :**
- Instance t3.micro : **Gratuit** (Free Tier 750h/mois) [OK]

**Scénario de cet exercice :**

**Option 1 : Sans NAT Gateway (économique)**
```
VPC × 3 : 0$
Subnets × 10 : 0$
Bastion t3.micro : 0$ (Free Tier)
VPC Peering : 0$
VPC Endpoints (Gateway) : 0$
Flow Logs : ~2$/mois (léger)

Total : ~2$/mois [OK]
```

**Option 2 : Avec 1 NAT Gateway (production minimale)**
```
NAT Gateway × 1 : 33$/mois
Data processing (10 GB) : 0.45$/mois
Autres : 2$/mois

Total : ~35$/mois
```

**Option 3 : Avec 2 NAT Gateway (HA complète)**
```
NAT Gateway × 2 : 66$/mois
Data processing (20 GB) : 0.90$/mois
Autres : 2$/mois

Total : ~69$/mois
```

**[IDEE] Recommandation pour apprendre :**
- Créer 1 NAT Gateway pour tester (~1h = 0.045$)
- Le supprimer après tests
- En production : Toujours 2 NAT Gateway (HA)

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Comprendre les concepts réseau avancés

#### VPC vs Subnet vs Route Table

**VPC (Virtual Private Cloud) :**
- Réseau virtuel isolé logiquement
- Plage CIDR définie (ex: 10.0.0.0/16)
- Contient des subnets
- 1 VPC = 1 région (peut couvrir plusieurs AZs)

**Subnet :**
- Subdivision du VPC
- Plage CIDR plus petite (ex: 10.0.1.0/24)
- 1 subnet = 1 AZ (pas multi-AZ)
- Type : Public ou Privé

**Route Table :**
- Définit où le trafic réseau va
- Associée à un ou plusieurs subnets
- Contient des routes (destination -> target)

---

#### Public Subnet vs Private Subnet

**Public Subnet :**
```
Caractéristiques :
- Route vers Internet Gateway (0.0.0.0/0 -> igw-xxx)
- Instances peuvent avoir IP publique
- Accessible depuis internet (si Security Group autorise)

Usage :
- Load Balancers
- NAT Gateways
- Bastion Hosts
- Ressources web publiques
```

**Private Subnet :**
```
Caractéristiques :
- PAS de route vers Internet Gateway
- Pas d'IP publique
- Peut avoir route vers NAT Gateway (internet sortant)
- Jamais accessible depuis internet directement

Usage :
- Application servers
- Databases
- Workers
- Microservices internes
```

**Exemple de routes :**

**Public Subnet Route Table :**
```
Destination      Target
10.0.0.0/16      local (VPC)
0.0.0.0/0        igw-abc123 (Internet Gateway)
```

**Private Subnet Route Table :**
```
Destination      Target
10.0.0.0/16      local (VPC)
0.0.0.0/0        nat-xyz789 (NAT Gateway)
```

---

#### NAT Gateway vs NAT Instance

**Besoin :** Instances privées doivent accéder à internet (apt update, API calls, etc.)

**2 solutions :**

**NAT Gateway (Managed by AWS) :**

| Critère | Détails |
|---------|---------|
| **Gestion** | Fully managed (AWS s'occupe de tout) |
| **Performance** | Jusqu'à 45 Gbps |
| **Haute dispo** | 1 par AZ (créer plusieurs pour HA) |
| **Scaling** | Automatique |
| **Maintenance** | Aucune (AWS gère) |
| **Coût** | 0.045$/h + 0.045$/GB |
| **IP** | Elastic IP requise |

**NAT Instance (EC2 custom) :**

| Critère | Détails |
|---------|---------|
| **Gestion** | Tu gères l'instance EC2 |
| **Performance** | Limité par instance type |
| **Haute dispo** | Tu configures (scripts, failover) |
| **Scaling** | Manuel |
| **Maintenance** | Patches, monitoring, etc. |
| **Coût** | Instance EC2 (~10$/mois t3.small) |
| **IP** | Elastic IP requise |

**Recommandation :**
- **Production : NAT Gateway** (fiabilité, performance)
- **Dev/Test budget serré : NAT Instance** (moins cher mais + effort)

---

#### VPC Peering

**VPC Peering = Connexion réseau privée entre 2 VPCs**

**Caractéristiques :**
- Trafic ne passe PAS par internet public
- Utilise le réseau AWS interne (backbone)
- Latence faible
- Pas de single point of failure
- Support cross-account et cross-region

**Limitations :**
- [X] Pas de transitive peering
  ```
  VPC A <--> VPC B <--> VPC C
  
  A peut parler à B [OK]
  B peut parler à C [OK]
  A peut parler à C ? [X] (pas de transitivité)
  
  Solution : Créer A <--> C peering
  Ou : Transit Gateway (hub central)
  ```

- [X] CIDR ne doit PAS se chevaucher
  ```
  [OK] BON :
    VPC A : 10.0.0.0/16
    VPC B : 10.1.0.0/16
  
  [X] MAUVAIS :
    VPC A : 10.0.0.0/16
    VPC B : 10.0.0.0/24 (overlap !)
  ```

**Use cases :**
- Prod <-> Staging (partage ressources)
- Multi-account (central services VPC)
- Disaster recovery (cross-region)

---

#### VPC Endpoints

**Problème :** Accès S3/DynamoDB depuis subnet privé passe par internet

```
Instance privée -> NAT Gateway -> Internet -> S3
  (coûteux, latence, sécurité)
```

**Solution : VPC Endpoint**

```
Instance privée -> VPC Endpoint -> S3 (AWS backbone)
  (gratuit pour Gateway, privé, rapide)
```

**2 types :**

**Gateway Endpoint (S3, DynamoDB) :**
- Gratuit [OK]
- Ajouté dans route table
- Scalable automatiquement
- Régional

**Interface Endpoint (autres services) :**
- 0.01$/h par endpoint (~7$/mois)
- ENI (Elastic Network Interface) dans subnet
- IP privée
- Supporte tous les services AWS

**Exemple route avec Gateway Endpoint :**

```
Destination           Target
10.0.0.0/16           local
pl-xxx (S3 prefix)    vpce-abc123
```

`pl-xxx` = Prefix list AWS (toutes les IPs S3 de la région)

---

#### Network ACLs vs Security Groups

**2 couches de sécurité :**

**Security Groups (Instance-level) :**

| Caractéristique | Valeur |
|-----------------|--------|
| **Niveau** | Instance (ENI) |
| **Stateful** | [OK] Oui (réponse auto-autorisée) |
| **Règles** | Allow seulement |
| **Par défaut** | Deny all inbound, allow all outbound |
| **Évaluation** | Toutes les règles évaluées |

**Network ACLs (Subnet-level) :**

| Caractéristique | Valeur |
|-----------------|--------|
| **Niveau** | Subnet (toutes instances) |
| **Stateful** | [X] Non (réponse doit être explicite) |
| **Règles** | Allow ET Deny |
| **Par défaut** | Allow all (par défaut AWS) |
| **Évaluation** | Par numéro (ordre) |

**Exemple :**

```
Traffic: Client (internet) -> Instance EC2 (port 80)

Security Group:
  Inbound: HTTP (80) from 0.0.0.0/0 -> Allow
  Outbound: (réponse auto-autorisée) -> Allow
  
Network ACL:
  Inbound: HTTP (80) from 0.0.0.0/0 -> Allow
  Outbound: Ephemeral ports (1024-65535) -> Allow ([ATTENTION] Important!)
    (La réponse HTTP part sur un port éphémère)
```

**Quand utiliser :**
- **Security Groups** : Toujours (contrôle par instance)
- **Network ACLs** : Sécurité additionnelle (deny spécifique)

**Exemple use case Network ACL :**

```
Bloquer une IP malveillante sur tout un subnet :

Network ACL inbound:
  Rule 10: Deny TCP 0-65535 from 1.2.3.4/32
  Rule 100: Allow all
```

---

#### VPC Flow Logs

**VPC Flow Logs = Capture du trafic réseau**

**Informations capturées :**
```
<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.5 172.217.14.206 52000 443 6 10 5200 1702820000 1702820060 ACCEPT OK

Décodage :
- Source: 10.0.1.5:52000 (instance privée)
- Destination: 172.217.14.206:443 (Google, HTTPS)
- Protocol: 6 (TCP)
- Packets: 10
- Bytes: 5200
- Action: ACCEPT (autorisé)
```

**3 niveaux de capture :**
1. **VPC-level** : Tout le trafic du VPC
2. **Subnet-level** : Trafic d'un subnet spécifique
3. **ENI-level** : Trafic d'une instance spécifique

**Destinations :**
- CloudWatch Logs (analyse temps réel)
- S3 (archivage long terme, Athena queries)
- Kinesis Data Firehose (streaming)

**Use cases :**
- Troubleshooting (pourquoi connexion refusée ?)
- Sécurité (détecter scans de ports)
- Compliance (audit trafic)
- Cost optimization (identifier gros consommateurs bandwidth)

---

### ÉTAPE 2 : Créer le VPC Production (3-tier)

#### Planifier les CIDR

**VPC Production : 10.0.0.0/16 (65,536 IPs)**

**Découpage :**

```
Web Tier (Public):
  10.0.1.0/24  - Public Subnet 1a (AZ eu-west-1a) - 256 IPs
  10.0.2.0/24  - Public Subnet 1b (AZ eu-west-1b) - 256 IPs
  10.0.3.0/24  - Public Subnet 1c (AZ eu-west-1c) - 256 IPs (optionnel)

Application Tier (Private):
  10.0.11.0/24 - App Subnet 1a (AZ eu-west-1a) - 256 IPs
  10.0.12.0/24 - App Subnet 1b (AZ eu-west-1b) - 256 IPs
  10.0.13.0/24 - App Subnet 1c (AZ eu-west-1c) - 256 IPs (optionnel)

Database Tier (Private Isolated):
  10.0.21.0/24 - DB Subnet 1a (AZ eu-west-1a) - 256 IPs
  10.0.22.0/24 - DB Subnet 1b (AZ eu-west-1b) - 256 IPs

Réservé pour futur:
  10.0.31.0/24, 10.0.32.0/24, etc.
```

**Pourquoi ces numéros ?**
- `10.0.1.x` = Tier 1 (Web, public)
- `10.0.11.x` = Tier 2 (App, privé)
- `10.0.21.x` = Tier 3 (DB, ultra privé)
- Schéma clair et scalable

---

#### Créer le VPC

**VPC -> Your VPCs -> Create VPC**

```
Name tag: vpc-production

IPv4 CIDR block: 10.0.0.0/16

IPv6 CIDR block: No IPv6 CIDR block

Tenancy: Default (shared hardware)
```

**Create VPC**

**[OK] VPC créé !**

```
VPC ID: vpc-prod-abc123
State: Available
```

---

**Créer l'Internet Gateway :**

**VPC -> Internet Gateways -> Create internet gateway**

```
Name tag: igw-production
```

**Create internet gateway**

---

**Attacher au VPC :**

**Sélectionner igw-production -> Actions -> Attach to VPC**

```
VPC: vpc-production
```

**Attach internet gateway**

**[OK] Internet Gateway attaché !**

---

#### Créer les subnets

**Subnet 1 : Public 1a**

**VPC -> Subnets -> Create subnet**

```
VPC: vpc-production

Subnet name: subnet-public-1a
Availability Zone: eu-west-1a
IPv4 CIDR block: 10.0.1.0/24
```

**Create subnet**

---

**Activer l'auto-assign IP publique :**

**Subnet -> subnet-public-1a -> Actions -> Modify auto-assign IP settings**

```
[x] Enable auto-assign public IPv4 address
```

**Save**

**Maintenant, toute instance lancée dans ce subnet aura une IP publique automatiquement.**

---

**Répéter pour tous les subnets :**

**Public Subnets :**
```
subnet-public-1b : eu-west-1b, 10.0.2.0/24, auto-assign IP: enabled
```

**App Subnets (Private) :**
```
subnet-app-1a : eu-west-1a, 10.0.11.0/24, auto-assign IP: disabled
subnet-app-1b : eu-west-1b, 10.0.12.0/24, auto-assign IP: disabled
```

**DB Subnets (Private Isolated) :**
```
subnet-db-1a : eu-west-1a, 10.0.21.0/24, auto-assign IP: disabled
subnet-db-1b : eu-west-1b, 10.0.22.0/24, auto-assign IP: disabled
```

**[OK] 6 subnets créés ! (2 public + 4 private)**

---

#### Créer les route tables

**Route Table 1 : Public**

**VPC -> Route Tables -> Create route table**

```
Name tag: rtb-public-production
VPC: vpc-production
```

**Create route table**

---

**Ajouter la route vers Internet :**

**Routes -> Edit routes -> Add route**

```
Destination: 0.0.0.0/0
Target: Internet Gateway -> igw-production
```

**Save changes**

---

**Associer aux subnets publics :**

**Subnet associations -> Edit subnet associations**

```
[x] subnet-public-1a
[x] subnet-public-1b
```

**Save associations**

**[OK] Subnets publics routent vers internet !**

---

**Route Table 2 : App (Private - avec NAT)**

**On créera cette route table après avoir déployé le NAT Gateway**

---

**Route Table 3 : DB (Private - Isolé)**

**Create route table**

```
Name tag: rtb-db-production
VPC: vpc-production
```

**Routes : Seulement local (pas d'internet)**

```
Destination: 10.0.0.0/16
Target: local
```

**(Pas d'autre route, le subnet DB est complètement isolé)**

---

**Associer aux subnets DB :**

```
[x] subnet-db-1a
[x] subnet-db-1b
```

**Save associations**

**[OK] Subnets DB isolés !**

---

### ÉTAPE 3 : Déployer NAT Gateway

#### Comprendre les coûts

**[ATTENTION] NAT Gateway = Service le plus cher de l'exercice**

**Coût :**
```
0.045$/h = 1.08$/jour = 32.40$/mois
+ 0.045$/GB data processing
```

**Pour apprendre sans trop dépenser :**
1. Créer le NAT Gateway
2. Tester (30 min = 0.023$)
3. Supprimer immédiatement après

**En production : Toujours 2 NAT Gateway (1 par AZ) pour HA**

---

#### Créer une Elastic IP pour le NAT Gateway

**VPC -> Elastic IPs -> Allocate Elastic IP address**

```
Network Border Group: eu-west-1

Public IPv4 address pool: Amazon's pool

Tags:
  Name: eip-nat-gateway-1a
```

**Allocate**

**[OK] Elastic IP allouée !**

```
Allocated IPv4 address: 52.18.123.45 (exemple)
```

**[ATTENTION] Elastic IP non-attachée = 0.005$/h (~3.6$/mois)**

**Toujours attacher rapidement ou libérer si non utilisée !**

---

#### Créer le NAT Gateway

**VPC -> NAT Gateways -> Create NAT gateway**

```
Name: nat-gateway-1a

Subnet: subnet-public-1a
  ([ATTENTION] DOIT être un subnet public !)

Connectivity type: Public
  (Pas Private - c'est pour PrivateLink)

Elastic IP allocation ID: eip-nat-gateway-1a
  (L'EIP qu'on vient de créer)
```

**Create NAT gateway**

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

**Status :**

```
Pending -> Available [OK]
```

**[OK] NAT Gateway déployé !**

```
NAT gateway ID: nat-0abc123def456
State: Available
Elastic IP: 52.18.123.45
Subnet: subnet-public-1a
```

---

#### Créer route table pour App Tier

**VPC -> Route Tables -> Create route table**

```
Name tag: rtb-app-1a-production
VPC: vpc-production
```

**Create**

---

**Ajouter routes :**

**Edit routes -> Add route**

```
Route 1:
  Destination: 10.0.0.0/16
  Target: local

Route 2:
  Destination: 0.0.0.0/0
  Target: NAT Gateway -> nat-gateway-1a
```

**Save changes**

---

**Associer au subnet App :**

**Subnet associations -> Edit**

```
[x] subnet-app-1a
```

**Save**

**[OK] Instances dans subnet-app-1a peuvent maintenant accéder à internet via NAT Gateway !**

---

**Pour haute disponibilité (optionnel, coûte 2×) :**

**Créer 2ème NAT Gateway dans eu-west-1b :**

```
1. Allouer 2ème Elastic IP (eip-nat-gateway-1b)
2. Créer NAT Gateway dans subnet-public-1b
3. Créer rtb-app-1b-production
4. Route 0.0.0.0/0 -> nat-gateway-1b
5. Associer à subnet-app-1b
```

**Avantage : Si AZ 1a tombe, instances en 1b continuent d'avoir internet [OK]**

---

### ÉTAPE 4 : Tester la connectivité NAT Gateway

#### Lancer une instance de test dans subnet privé

**EC2 -> Launch instance**

```
Name: test-instance-private

AMI: Ubuntu 22.04 LTS

Instance type: t2.micro (Free Tier)

Key pair: sirrdev-ec2-key (de l'exercice 1)

Network settings:
  VPC: vpc-production
  Subnet: subnet-app-1a (PRIVÉ)
  Auto-assign public IP: Disable
  
Security group: Create new
  Name: sg-app-tier
  Inbound: SSH (22) from 10.0.0.0/16 (VPC seulement)
  Outbound: All (default)
```

**Launch instance**

---

**Problème : Comment se connecter à une instance sans IP publique ?**

**Solution : Bastion Host (on le créera à l'étape suivante)**

**Pour l'instant, temporairement :**

**Modifier le subnet pour ajouter temporairement une IP publique :**

**EC2 -> test-instance-private -> Actions -> Networking -> Manage IP addresses**

```
[x] Allocate Elastic IP
```

**(Juste pour tester, on la libèrera après)**

---

**Se connecter en SSH :**

```bash
ssh -i ~/.ssh/sirrdev-ec2-key.pem ubuntu@52.x.x.x
```

---

**Tester la connectivité internet (via NAT Gateway) :**

```bash
# Test ping
ping -c 3 8.8.8.8
```

**Résultat :**

```
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=112 time=2.34 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=112 time=2.45 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=112 time=2.67 ms

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss
```

**[OK] Internet fonctionne via NAT Gateway !**

---

**Test apt update :**

```bash
sudo apt update
```

**Résultat :**

```
Hit:1 http://eu-west-1.ec2.archive.ubuntu.com/ubuntu jammy InRelease
Get:2 http://eu-west-1.ec2.archive.ubuntu.com/ubuntu jammy-updates InRelease [119 kB]
...
Fetched 12.3 MB in 3s (4.1 MB/s)
Reading package lists... Done
```

**[OK] Peut télécharger des packages !**

---

**Vérifier l'IP sortante (vue par internet) :**

```bash
curl https://api.ipify.org
```

**Résultat :**

```
52.18.123.45
```

**C'est l'Elastic IP du NAT Gateway ! [OK]**

**(Pas l'IP privée 10.0.11.x de l'instance)**

---

**Libérer l'Elastic IP temporaire de l'instance :**

**EC2 -> Elastic IPs -> Sélectionner l'IP de test-instance-private**

```
Actions -> Disassociate Elastic IP address
Actions -> Release Elastic IP address
```

**Instance revient en mode privé (pas d'IP publique) [OK]**

---

### ÉTAPE 5 : Déployer un Bastion Host

#### Qu'est-ce qu'un Bastion Host ?

**Bastion Host = Jump server pour accès SSH sécurisé**

**Problème :**
```
Tu (internet) -> Instance privée (10.0.11.5)
  [X] Impossible directement (pas d'IP publique)
```

**Solution avec Bastion :**
```
Tu (internet) -> Bastion (subnet public, IP publique)
  -> SSH forward -> Instance privée (10.0.11.5)
  [OK] Accessible via le bastion
```

**Architecture sécurisée :**
```
Internet
  v (SSH depuis ton IP seulement)
Bastion Host (subnet public)
  v (SSH vers VPC seulement)
Instances privées (app, db)
```

---

#### Créer le Bastion Host

**EC2 -> Launch instance**

```
Name: bastion-host-production

AMI: Ubuntu 22.04 LTS (ou Amazon Linux 2023)

Instance type: t3.micro (Free Tier)

Key pair: sirrdev-ec2-key

Network settings:
  VPC: vpc-production
  Subnet: subnet-public-1a (PUBLIC)
  Auto-assign public IP: Enable
  
Security group: Create new
  Name: sg-bastion-host
  Description: SSH access to bastion
  
  Inbound rules:
    Type: SSH
    Port: 22
    Source: My IP (ton IP actuelle)
    Description: SSH from my IP only
  
  Outbound rules:
    Type: SSH
    Port: 22
    Destination: 10.0.0.0/16 (VPC)
    Description: SSH to instances in VPC
```

**Launch instance**

---

**[ATTENTION] Sécurité importante :**

**Restreindre l'accès SSH au bastion :**
- [OK] Source : Ton IP uniquement (pas 0.0.0.0/0)
- [OK] Utiliser clés SSH (pas mot de passe)
- [OK] Changer port SSH (optionnel, 2222 au lieu de 22)
- [OK] Installer fail2ban (ban après X tentatives)
- [OK] Activer CloudWatch Logs (audit SSH)

---

#### Configurer le SSH forwarding

**Sur ton PC (pas sur le bastion), éditer `~/.ssh/config` :**

```bash
nano ~/.ssh/config
```

**Ajouter :**

```
# Bastion Host
Host bastion-prod
    HostName 34.245.67.89  # IP publique du bastion
    User ubuntu
    IdentityFile ~/.ssh/sirrdev-ec2-key.pem
    ForwardAgent yes

# Instances privées via bastion
Host 10.0.*
    User ubuntu
    IdentityFile ~/.ssh/sirrdev-ec2-key.pem
    ProxyJump bastion-prod
```

**Sauvegarder**

---

**Tester la connexion au bastion :**

```bash
ssh bastion-prod
```

**Résultat :**

```
Welcome to Ubuntu 22.04 LTS
...
ubuntu@ip-10-0-1-x:~$
```

**[OK] Connecté au bastion !**

---

**Se connecter à l'instance privée (via bastion) :**

```bash
# Depuis ton PC (pas depuis le bastion)
ssh ubuntu@10.0.11.5
```

**(Remplace par l'IP privée de test-instance-private)**

**Grâce à `ProxyJump`, SSH passe automatiquement par le bastion [OK]**

---

**Alternative : SSH en 2 étapes (manuel) :**

```bash
# 1. Se connecter au bastion
ssh ubuntu@34.245.67.89 -i ~/.ssh/sirrdev-ec2-key.pem

# 2. Depuis le bastion, SSH vers instance privée
ssh ubuntu@10.0.11.5
```

**[ATTENTION] Problème : La clé privée n'est pas sur le bastion !**

**Solution : SSH Agent Forwarding**

```bash
# Sur ton PC, démarrer ssh-agent
eval $(ssh-agent)
ssh-add ~/.ssh/sirrdev-ec2-key.pem

# Se connecter avec -A (agent forwarding)
ssh -A ubuntu@34.245.67.89

# Depuis le bastion, SSH fonctionne maintenant
ssh ubuntu@10.0.11.5
```

---

### ÉTAPE 6 : Créer les VPCs Staging et Development

#### VPC Staging

**VPC -> Create VPC**

```
Name tag: vpc-staging
IPv4 CIDR: 10.1.0.0/16
```

**Create VPC**

---

**Internet Gateway :**

```
Name: igw-staging
Attach to: vpc-staging
```

---

**Subnets (architecture simplifiée) :**

```
subnet-staging-public-1a : 10.1.1.0/24, eu-west-1a, auto-assign IP: enabled
subnet-staging-private-1a : 10.1.11.0/24, eu-west-1a, auto-assign IP: disabled
```

---

**Route Tables :**

**Public :**
```
Name: rtb-staging-public
Routes:
  10.1.0.0/16 -> local
  0.0.0.0/0 -> igw-staging
Subnets: subnet-staging-public-1a
```

**Private (sans NAT pour économiser) :**
```
Name: rtb-staging-private
Routes:
  10.1.0.0/16 -> local
Subnets: subnet-staging-private-1a
```

**(Pas de NAT Gateway en staging pour économiser, on peut en ajouter si nécessaire)**

---

#### VPC Development

**VPC -> Create VPC**

```
Name tag: vpc-development
IPv4 CIDR: 10.2.0.0/16
```

---

**Internet Gateway :**

```
Name: igw-development
Attach to: vpc-development
```

---

**Subnet (minimal, tout public) :**

```
subnet-dev-public-1a : 10.2.1.0/24, eu-west-1a, auto-assign IP: enabled
```

---

**Route Table :**

```
Name: rtb-dev-public
Routes:
  10.2.0.0/16 -> local
  0.0.0.0/0 -> igw-development
Subnets: subnet-dev-public-1a
```

---

**[OK] 3 VPCs créés !**

```
vpc-production : 10.0.0.0/16 (3-tier, 6 subnets)
vpc-staging : 10.1.0.0/16 (2-tier, 2 subnets)
vpc-development : 10.2.0.0/16 (minimal, 1 subnet)
```

---

### ÉTAPE 7 : Configurer VPC Peering

#### Créer le VPC Peering Connection

**Scénario : Production doit communiquer avec Staging**

**VPC -> Peering Connections -> Create peering connection**

```
Name tag: peer-prod-staging

VPC (Requester): vpc-production

Account: My account

Region: This region (eu-west-1)

VPC (Accepter): vpc-staging
```

**Create peering connection**

---

**État :**

```
Status: Pending Acceptance
```

---

#### Accepter le peering

**Peering Connections -> peer-prod-staging -> Actions -> Accept Request**

```
Status: Active [OK]
```

**[OK] VPC Peering établi !**

---

#### Configurer les routes

**Sans routes, le peering ne sert à rien !**

**Il faut ajouter des routes dans chaque VPC :**

---

**VPC Production -> Route Table (rtb-app-1a-production) :**

**Routes -> Edit routes -> Add route**

```
Destination: 10.1.0.0/16 (CIDR du VPC Staging)
Target: Peering Connection -> peer-prod-staging
```

**Save**

---

**VPC Staging -> Route Table (rtb-staging-private) :**

**Routes -> Edit routes -> Add route**

```
Destination: 10.0.0.0/16 (CIDR du VPC Production)
Target: Peering Connection -> peer-prod-staging
```

**Save**

---

**Répéter pour toutes les route tables concernées dans les 2 VPCs**

---

#### Tester le VPC Peering

**Lancer une instance de test dans Staging :**

```
Name: test-instance-staging
VPC: vpc-staging
Subnet: subnet-staging-private-1a (10.1.11.0/24)
Security Group: Allow ICMP + SSH from 10.0.0.0/16
```

---

**Depuis une instance dans Production (10.0.11.5), ping l'instance Staging :**

```bash
# Se connecter à l'instance prod
ssh ubuntu@10.0.11.5

# Ping instance staging (10.1.11.x)
ping -c 3 10.1.11.20
```

**Résultat :**

```
PING 10.1.11.20 (10.1.11.20) 56(84) bytes of data.
64 bytes from 10.1.11.20: icmp_seq=1 ttl=64 time=0.345 ms
64 bytes from 10.1.11.20: icmp_seq=2 ttl=64 time=0.298 ms
64 bytes from 10.1.11.20: icmp_seq=3 ttl=64 time=0.312 ms
```

**[OK] Communication via VPC Peering fonctionnelle !**

---

**[ATTENTION] Vérifier aussi les Security Groups :**

**Production instance SG : Autoriser ICMP/SSH depuis 10.1.0.0/16**
**Staging instance SG : Autoriser ICMP/SSH depuis 10.0.0.0/16**

---

### ÉTAPE 8 : Créer VPC Endpoints

#### Gateway Endpoint pour S3

**Problème : Instances privées accèdent S3 via NAT Gateway**

```
Instance (10.0.11.5) -> NAT Gateway -> Internet -> S3
  (Coût: 0.045$/GB)
```

**Solution : S3 Gateway Endpoint**

```
Instance (10.0.11.5) -> VPC Endpoint -> S3 (AWS backbone)
  (Gratuit [OK])
```

---

**VPC -> Endpoints -> Create endpoint**

```
Name tag: vpce-s3-production

Service category: AWS services

Services: Filter by "s3"
  com.amazonaws.eu-west-1.s3 (Type: Gateway)

VPC: vpc-production

Route tables:
  [x] rtb-app-1a-production
  [x] rtb-app-1b-production (si existe)
  [x] rtb-db-production
  
  (Toutes les route tables privées)

Policy: Full Access (pour l'instant)
```

**Create endpoint**

---

**Vérifier la route ajoutée automatiquement :**

**Route Tables -> rtb-app-1a-production -> Routes**

```
Destination           Target
10.0.0.0/16           local
0.0.0.0/0             nat-xxx (NAT Gateway)
pl-6ca54005 (S3)      vpce-xxx (VPC Endpoint) <- Nouvelle route
```

`pl-6ca54005` = Prefix list S3 (toutes les IPs S3 de eu-west-1)

---

**Tester depuis instance privée :**

```bash
# Se connecter à instance privée
ssh ubuntu@10.0.11.5

# Installer AWS CLI si pas déjà fait
sudo apt install awscli -y

# Lister buckets S3
aws s3 ls
```

**Le trafic passe maintenant par le VPC Endpoint (pas NAT Gateway) [OK]**

---

**Vérifier avec VPC Flow Logs (on configurera à l'étape 9) :**

```
srcaddr=10.0.11.5 dstaddr=52.218.x.x (IP S3) action=ACCEPT
  (Via VPC Endpoint, pas NAT Gateway)
```

---

#### Gateway Endpoint pour DynamoDB

**Même processus :**

**Create endpoint**

```
Name: vpce-dynamodb-production
Service: com.amazonaws.eu-west-1.dynamodb (Gateway)
VPC: vpc-production
Route tables: (mêmes que S3)
```

**[OK] DynamoDB accessible via VPC Endpoint (gratuit) !**

---

#### Interface Endpoint pour Secrets Manager (optionnel)

**Pour autres services (pas S3/DynamoDB), utiliser Interface Endpoint**

**Create endpoint**

```
Name: vpce-secretsmanager-production
Service: com.amazonaws.eu-west-1.secretsmanager (Interface)
VPC: vpc-production
Subnets:
  [x] subnet-app-1a
  [x] subnet-app-1b

Security groups: Créer nouveau
  Name: sg-vpce-secretsmanager
  Inbound: HTTPS (443) from 10.0.0.0/16
```

**Create**

---

**Coût : 0.01$/h × 2 subnets = 0.02$/h (~14$/mois)**

**Interface Endpoint crée des ENIs (Elastic Network Interfaces) dans les subnets**

**Résolution DNS :**

```
secretsmanager.eu-west-1.amazonaws.com
  -> 10.0.11.x (IP privée dans VPC) [OK]
```

**Trafic reste dans le VPC, ne sort jamais vers internet [OK]**

---

### ÉTAPE 9 : Configurer VPC Flow Logs

#### Créer un Log Group CloudWatch

**CloudWatch -> Logs -> Log groups -> Create log group**

```
Log group name: /aws/vpc/flow-logs-production

Retention setting: 7 days
  (Ou 1 day pour économiser)

Log group class: Standard
```

**Create**

---

#### Créer IAM Role pour Flow Logs

**IAM -> Roles -> Create role**

```
Trusted entity: AWS service
Use case: VPC (sous "EC2" dans la liste)
  OU Custom trust policy:
```

**Custom trust policy :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "vpc-flow-logs.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

**Next**

---

**Permissions : Create inline policy**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogGroups",
        "logs:DescribeLogStreams"
      ],
      "Resource": "*"
    }
  ]
}
```

**Name : VPCFlowLogsPolicy**

---

**Role name : VPCFlowLogsRole**

**Create role**

---

#### Activer Flow Logs sur le VPC

**VPC -> vpc-production -> Flow logs -> Create flow log**

```
Name: flow-log-production

Filter:
  [x] All (Accept + Reject)
  
  OU
  [ ] Accept (seulement trafic autorisé)
  [ ] Reject (seulement trafic bloqué)

Maximum aggregation interval: 1 minute
  (Ou 10 minutes pour moins de logs)

Destination: Send to CloudWatch Logs

Destination log group: /aws/vpc/flow-logs-production

IAM role: VPCFlowLogsRole

Log record format: AWS default format

Tags: (optionnel)
```

**Create flow log**

---

**[OK] Flow Logs activé !**

**Après quelques minutes, les logs commencent à apparaître**

---

#### Analyser les Flow Logs

**CloudWatch -> Logs -> Log groups -> /aws/vpc/flow-logs-production**

**Cliquer sur un log stream récent**

---

**Exemple de logs :**

```
2 123456789012 eni-abc123 10.0.11.5 8.8.8.8 52000 53 17 1 84 1702820100 1702820160 ACCEPT OK
2 123456789012 eni-abc123 10.0.11.5 10.0.21.10 45678 5432 6 10 5200 1702820200 1702820260 ACCEPT OK
2 123456789012 eni-abc123 203.0.113.5 10.0.11.5 12345 22 6 1 60 1702820300 1702820360 REJECT OK
```

**Décodage :**

**Log 1 :**
```
Source: 10.0.11.5:52000 (instance app)
Dest: 8.8.8.8:53 (Google DNS)
Protocol: 17 (UDP)
Action: ACCEPT
```

**Log 2 :**
```
Source: 10.0.11.5:45678 (instance app)
Dest: 10.0.21.10:5432 (PostgreSQL)
Protocol: 6 (TCP)
Action: ACCEPT (connexion DB autorisée)
```

**Log 3 :**
```
Source: 203.0.113.5:12345 (internet)
Dest: 10.0.11.5:22 (SSH)
Action: REJECT (instance privée, SSH refusé depuis internet)
```

---

**Query avec CloudWatch Insights :**

**CloudWatch -> Logs -> Insights**

```
fields @timestamp, srcaddr, dstaddr, srcport, dstport, action
| filter action = "REJECT"
| stats count() as reject_count by srcaddr
| sort reject_count desc
```

**Résultat : Top IPs qui se font bloquer (potentielles attaques)**

---

**Query : Trafic par instance**

```
fields @timestamp, interfaceId, bytes
| stats sum(bytes) as total_bytes by interfaceId
| sort total_bytes desc
```

**Résultat : Instances consommant le plus de bande passante**

---

### ÉTAPE 10 : Configurer Network ACLs

#### Comprendre le besoin

**Security Groups suffisent pour la plupart des cas**

**Network ACLs = Couche additionnelle pour :**
- Bloquer IPs malveillantes (deny rules)
- Sécurité defense-in-depth
- Compliance (certains standards l'exigent)

---

#### Créer un Network ACL pour App Tier

**VPC -> Network ACLs -> Create network ACL**

```
Name: nacl-app-tier-production
VPC: vpc-production
```

**Create**

---

**Par défaut, le NACL créé DENY ALL (contrairement au default NACL)**

**Il faut ajouter des règles explicites :**

---

**Inbound rules -> Edit inbound rules**

**Ajouter ces règles (dans l'ordre) :**

```
Rule #   Type          Protocol   Port Range   Source          Allow/Deny
100      HTTP          TCP        80           0.0.0.0/0       ALLOW
110      HTTPS         TCP        443          0.0.0.0/0       ALLOW
120      SSH           TCP        22           10.0.0.0/16     ALLOW (VPC seulement)
130      PostgreSQL    TCP        5432         10.0.0.0/16     ALLOW
140      Custom TCP    TCP        1024-65535   0.0.0.0/0       ALLOW (ephemeral ports)
*        All traffic   All        All          0.0.0.0/0       DENY (implicit)
```

**[ATTENTION] Rule 140 est CRITIQUE !**

**Ephemeral ports = Ports utilisés pour les réponses**

**Sans ça, les réponses HTTP/HTTPS seraient bloquées !**

---

**Outbound rules -> Edit outbound rules**

```
Rule #   Type          Protocol   Port Range   Destination     Allow/Deny
100      HTTP          TCP        80           0.0.0.0/0       ALLOW
110      HTTPS         TCP        443          0.0.0.0/0       ALLOW
120      PostgreSQL    TCP        5432         10.0.21.0/24    ALLOW (DB subnet)
130      Custom TCP    TCP        1024-65535   0.0.0.0/0       ALLOW (ephemeral)
*        All traffic   All        All          0.0.0.0/0       DENY
```

**Save**

---

**Associer aux subnets App :**

**Subnet associations -> Edit**

```
[x] subnet-app-1a
[x] subnet-app-1b
```

**Save**

---

#### Exemple : Bloquer une IP malveillante

**Scénario : L'IP `203.0.113.50` fait du scan de ports**

**Network ACL -> nacl-app-tier-production -> Inbound rules -> Edit**

**Ajouter en PREMIER (rule # plus petit) :**

```
Rule # 10
Type: All traffic
Protocol: All
Port Range: All
Source: 203.0.113.50/32
Allow/Deny: DENY
```

**Cette règle s'exécute AVANT rule #100, donc bloque tout de cette IP [OK]**

---

### ÉTAPE 11 : Tester l'architecture complète

#### Déployer l'application 3-tier

**Je vais décrire le déploiement (pas en détail car ça reprend les exercices précédents) :**

**Tier 1 : Web (Load Balancer)**

**Créer Application Load Balancer :**
```
EC2 -> Load Balancers -> Create ALB
  Name: alb-production
  Scheme: Internet-facing
  Subnets: subnet-public-1a, subnet-public-1b
  Security Group: HTTP (80), HTTPS (443)
  Target Group: tg-app-instances
```

---

**Tier 2 : Application (EC2 Auto Scaling)**

**Créer Launch Template :**
```
Name: lt-flask-app-production
AMI: Ubuntu 22.04
Instance type: t3.micro
User data: (script installation Flask)
Security Group: sg-app-tier
```

**Créer Auto Scaling Group :**
```
Launch template: lt-flask-app-production
VPC: vpc-production
Subnets: subnet-app-1a, subnet-app-1b (PRIVÉS)
Min: 2, Desired: 2, Max: 4
Target group: tg-app-instances
```

---

**Tier 3 : Database (RDS)**

**Créer RDS instance :**
```
Engine: PostgreSQL 15
Instance: db.t3.micro
VPC: vpc-production
Subnet group: DB subnets (10.0.21.0/24, 10.0.22.0/24)
Multi-AZ: Yes
Security Group: sg-db-tier (port 5432 depuis app tier)
```

---

**Flow de test :**

```
Internet -> ALB (10.0.1.x) -> App instances (10.0.11.x) -> RDS (10.0.21.x)
```

**App instances :**
- [OK] Accès internet sortant via NAT Gateway
- [OK] Accès S3 via VPC Endpoint (gratuit)
- [OK] Accès DynamoDB via VPC Endpoint (gratuit)
- [OK] Accès RDS dans subnet DB

**RDS :**
- [OK] Complètement isolé (pas d'internet)
- [OK] Accessible seulement depuis app tier

---

Je vais continuer dans le prochain message avec les tests, troubleshooting, optimisation des coûts, et concepts avancés (Transit Gateway). Veux-tu que je continue ?

### ÉTAPE 12 : Tests et validation de l'architecture

#### Test 1 : Connectivité internet depuis App Tier

**Se connecter à une instance app (via bastion) :**

```bash
# Depuis ton PC
ssh -J bastion-prod ubuntu@10.0.11.5
```

---

**Tester l'accès internet (via NAT Gateway) :**

```bash
# Test DNS
nslookup google.com

# Résultat attendu :
Server:    127.0.0.53
Address:   127.0.0.53#53

Non-authoritative answer:
Name:    google.com
Address: 142.250.185.46
```

**[OK] DNS fonctionne**

---

**Tester téléchargement :**

```bash
curl -I https://api.github.com
```

**Résultat :**

```
HTTP/2 200
server: GitHub.com
date: Sun, 16 Dec 2024 16:30:00 GMT
content-type: application/json
...
```

**[OK] HTTPS sortant fonctionne via NAT Gateway**

---

**Vérifier l'IP source vue par internet :**

```bash
curl https://api.ipify.org
```

**Résultat :**

```
52.18.123.45
```

**C'est l'Elastic IP du NAT Gateway [OK]**

**(Pas l'IP privée 10.0.11.5)**

---

#### Test 2 : Connectivité vers RDS (DB Tier)

**Installer PostgreSQL client :**

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

---

**Se connecter à RDS :**

```bash
psql \
  --host=sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com \
  --port=5432 \
  --username=postgres \
  --dbname=todo_production
```

**Résultat :**

```
Password for user postgres:
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384)
Type "help" for help.

todo_production=>
```

**[OK] Connexion RDS depuis App Tier fonctionnelle**

---

**Vérifier que DB n'a PAS d'accès internet :**

**Se connecter à une instance dans DB subnet (si tu en as une pour test) :**

```bash
# Lancer instance test dans subnet-db-1a
# Puis tenter ping internet
ping 8.8.8.8
```

**Résultat attendu :**

```
connect: Network is unreachable
```

**[X] Pas d'internet (normal, DB tier isolé) [OK]**

---

#### Test 3 : VPC Endpoint S3

**Tester upload S3 depuis instance app :**

```bash
# Créer fichier test
echo "Test VPC Endpoint S3" > test-vpce.txt

# Upload vers S3
aws s3 cp test-vpce.txt s3://sirrdev-todo-uploads/test-vpce.txt
```

**Résultat :**

```
upload: ./test-vpce.txt to s3://sirrdev-todo-uploads/test-vpce.txt
```

**[OK] Upload réussi**

---

**Vérifier que ça passe par VPC Endpoint (pas NAT) :**

**Méthode 1 : Vérifier les métriques NAT Gateway**

**CloudWatch -> Metrics -> NAT Gateway**

```
BytesOutToDestination (pendant le upload S3)
  -> Devrait rester à 0 ou faible [OK]
```

**Si trafic S3 passait par NAT, BytesOut augmenterait significativement**

---

**Méthode 2 : VPC Flow Logs**

**CloudWatch -> Logs -> /aws/vpc/flow-logs-production**

**Chercher les logs avec destination = IP S3 :**

```
srcaddr=10.0.11.5 dstaddr=52.218.x.x (IP S3) action=ACCEPT
```

**L'interface (eni-xxx) devrait être celle de l'instance (pas NAT Gateway) [OK]**

---

**Méthode 3 : Route table**

**VPC -> Route Tables -> rtb-app-1a-production -> Routes**

```
Destination         Target
pl-xxx (S3)         vpce-xxx (VPC Endpoint)
```

**Trafic S3 matche cette route en premier (plus spécifique que 0.0.0.0/0) [OK]**

---

#### Test 4 : VPC Peering

**Depuis instance Production, ping instance Staging :**

```bash
# Instance prod (10.0.11.5)
ping -c 3 10.1.11.20
```

**Résultat :**

```
PING 10.1.11.20 (10.1.11.20) 56(84) bytes of data.
64 bytes from 10.1.11.20: icmp_seq=1 ttl=64 time=0.412 ms
64 bytes from 10.1.11.20: icmp_seq=2 ttl=64 time=0.389 ms
64 bytes from 10.1.11.20: icmp_seq=3 ttl=64 time=0.401 ms
```

**[OK] VPC Peering fonctionnel**

---

**Tester transfert de données (mesurer performance) :**

```bash
# Sur instance staging, démarrer serveur HTTP simple
python3 -m http.server 8080

# Sur instance prod, télécharger un fichier
time wget http://10.1.11.20:8080/large-file.bin
```

**Résultat :**

```
100%[===================>] 100.00M  125MB/s   in 0.8s

real    0m0.812s
```

**Latence très faible, bande passante élevée (réseau AWS interne) [OK]**

---

#### Test 5 : Network ACL (deny rules)

**Ajouter une règle de blocage pour tester :**

**NACL -> nacl-app-tier-production -> Inbound rules**

**Ajouter :**

```
Rule # 5
Type: SSH
Protocol: TCP
Port: 22
Source: 10.0.1.0/24 (public subnet)
Allow/Deny: DENY
```

**Save**

---

**Tester SSH depuis bastion (10.0.1.x) vers instance app (10.0.11.5) :**

```bash
ssh ubuntu@10.0.11.5
```

**Résultat :**

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

**[X] Bloqué par Network ACL (comme attendu) [OK]**

---

**Supprimer la règle de test :**

**Delete rule #5**

**SSH fonctionne à nouveau [OK]**

---

#### Test 6 : Bastion Host sécurité

**Vérifier que bastion n'est accessible que depuis ton IP :**

**Depuis un autre PC/réseau (ou via VPN pour changer IP) :**

```bash
ssh ubuntu@<bastion-ip>
```

**Résultat attendu :**

```
ssh: connect to host x.x.x.x port 22: Connection timed out
```

**[X] Bloqué (Security Group limite à ton IP) [OK]**

---

**Vérifier logs SSH sur bastion :**

```bash
# Sur le bastion
sudo tail -f /var/log/auth.log
```

**Surveiller tentatives de connexion suspectes**

---

**Installer fail2ban (ban automatique après échecs) :**

```bash
sudo apt install fail2ban -y

# Configurer
sudo nano /etc/fail2ban/jail.local
```

**Ajouter :**

```ini
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
```

**Restart :**

```bash
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
```

**Après 3 échecs SSH, l'IP est bannie 1h [OK]**

---

### ÉTAPE 13 : Troubleshooting réseau

#### Outils de diagnostic

**1. Reachability Analyzer (AWS)**

**VPC -> Reachability Analyzer -> Create and analyze path**

```
Source type: Instance
Source: test-instance-private (10.0.11.5)

Destination type: Instance
Destination: RDS endpoint (10.0.21.10:5432)

Protocol: TCP
Destination port: 5432
```

**Analyze path**

---

**Résultat :**

```
[OK] Reachable

Path:
1. Source instance (eni-abc)
2. Source subnet (10.0.11.0/24)
3. Route table (rtb-app-1a)
4. Destination subnet (10.0.21.0/24)
5. Destination instance (eni-xyz)

Security Groups: ALLOWED
Network ACLs: ALLOWED
Route Tables: VALID
```

**Ou si problème :**

```
[X] Not reachable

Analysis:
- Security group sg-db-tier (eni-xyz) DENIES TCP 5432 from 10.0.11.5
  Suggested fix: Add inbound rule allowing TCP 5432 from 10.0.11.0/24
```

**Très utile pour diagnostiquer sans tester en live [OK]**

---

**2. VPC Flow Logs analysis**

**Scénario : Instance ne peut pas joindre RDS**

**CloudWatch Insights query :**

```
fields @timestamp, srcaddr, dstaddr, srcport, dstport, action
| filter srcaddr = "10.0.11.5" and dstaddr = "10.0.21.10" and dstport = 5432
| sort @timestamp desc
| limit 100
```

**Résultats possibles :**

```
action=REJECT -> Security Group ou NACL bloque
action=ACCEPT -> Connexion autorisée (problème ailleurs)
```

---

**3. tcpdump / Wireshark**

**Capturer paquets sur l'instance :**

```bash
# Installer tcpdump
sudo apt install tcpdump -y

# Capturer trafic vers RDS
sudo tcpdump -i any host 10.0.21.10 and port 5432 -w rds-traffic.pcap

# Dans autre terminal, tenter connexion
psql --host=...

# Arrêter capture (Ctrl+C)
```

**Télécharger pcap :**

```bash
# Sur ton PC
scp -J bastion-prod ubuntu@10.0.11.5:rds-traffic.pcap .
```

**Ouvrir avec Wireshark :**

**Analyser les paquets SYN, SYN-ACK, ACK -> Voir où ça échoue**

---

**4. Route troubleshooting**

**Vérifier route effective d'une instance :**

**EC2 -> Instance -> Networking -> View route table**

**Ou via CLI :**

```bash
aws ec2 describe-route-tables \
  --route-table-ids rtb-xxx \
  --query 'RouteTables[0].Routes'
```

---

**5. Security Group effective rules**

**EC2 -> Instance -> Security -> Security groups -> View inbound/outbound rules**

**Vérifier :**
- Source/Dest CIDR correcte ?
- Port correct ?
- Protocol correct (TCP vs UDP) ?

---

#### Problèmes fréquents et solutions

**Problème 1 : Instance privée sans internet**

**Symptômes :**

```bash
ping 8.8.8.8
# connect: Network is unreachable

curl https://google.com
# Could not resolve host
```

**Diagnostic :**

**1. Vérifier route table du subnet**

```
VPC -> Subnets -> subnet-app-1a -> Route table
```

**Doit avoir :**

```
0.0.0.0/0 -> nat-xxx (NAT Gateway)
```

**Si manquant -> Créer la route**

---

**2. Vérifier NAT Gateway status**

```
VPC -> NAT Gateways -> nat-gateway-1a
State: Available (pas Failed)
```

**Si Failed -> Supprimer et recréer**

---

**3. Vérifier Elastic IP du NAT**

```
VPC -> Elastic IPs
```

**L'EIP doit être associée au NAT Gateway**

**Si pas associée -> Réassocier**

---

**4. Vérifier Security Group outbound**

**Instance SG doit autoriser :**

```
Outbound: All traffic -> 0.0.0.0/0
```

---

**Problème 2 : VPC Peering ne fonctionne pas**

**Symptômes :**

```bash
ping 10.1.11.20
# Destination Host Unreachable
```

**Diagnostic :**

**1. Peering connection acceptée ?**

```
VPC -> Peering Connections -> Status: Active
```

**Si Pending -> Accepter dans l'autre compte/VPC**

---

**2. Routes configurées des DEUX côtés ?**

**VPC A route table :**

```
10.1.0.0/16 -> pcx-xxx
```

**VPC B route table :**

```
10.0.0.0/16 -> pcx-xxx
```

**Si manquant -> Ajouter la route**

---

**3. Security Groups autorisent trafic ?**

**Instance A SG :**

```
Inbound: Allow from 10.1.0.0/16
```

**Instance B SG :**

```
Inbound: Allow from 10.0.0.0/16
```

---

**4. CIDR ne se chevauche pas ?**

```
VPC A: 10.0.0.0/16
VPC B: 10.1.0.0/16
  [OK] Pas de chevauchement

VPC A: 10.0.0.0/16
VPC B: 10.0.1.0/24
  [X] Chevauchement ! (10.0.1.x existe dans les deux)
```

**Si chevauchement -> Recréer VPC avec CIDR différent**

---

**Problème 3 : VPC Endpoint S3 ne fonctionne pas**

**Symptômes :**

```bash
aws s3 ls
# Unable to connect to endpoint URL
```

**Diagnostic :**

**1. Endpoint créé et Available ?**

```
VPC -> Endpoints -> vpce-xxx
State: Available
```

---

**2. Route table associée ?**

```
VPC -> Endpoints -> vpce-s3 -> Route tables
```

**Doit inclure la route table du subnet**

**Si manquant -> Edit route table associations**

---

**3. Policy du endpoint permet l'accès ?**

**Edit policy :**

```json
{
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": "*"
    }
  ]
}
```

**Ou restreindre à buckets spécifiques :**

```json
{
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::sirrdev-todo-uploads",
        "arn:aws:s3:::sirrdev-todo-uploads/*"
      ]
    }
  ]
}
```

---

**4. Instance a IAM role avec permissions S3 ?**

```
EC2 -> Instance -> Security -> IAM role
```

**Doit avoir policy avec s3:* permissions**

---

**Problème 4 : Network ACL bloque trafic**

**Symptômes :**

```bash
curl http://example.com
# Connection timed out (pas refused)
```

**Diagnostic :**

**1. Vérifier NACL du subnet**

```
VPC -> Network ACLs -> nacl-xxx
```

---

**2. Inbound rules permettent le trafic ?**

```
Rule 100: TCP 80 from 0.0.0.0/0 -> ALLOW
Rule 110: TCP 443 from 0.0.0.0/0 -> ALLOW
```

---

**3. Outbound rules permettent ephemeral ports ?**

**[ATTENTION] CRITIQUE pour NACL (stateless) :**

```
Rule 140: TCP 1024-65535 to 0.0.0.0/0 -> ALLOW
```

**Sans ça, les réponses HTTP sont bloquées !**

---

**4. Ordre des règles correct ?**

**NACL évalue par numéro croissant**

**Si :**

```
Rule 10: DENY all from 0.0.0.0/0
Rule 100: ALLOW TCP 80 from 0.0.0.0/0
```

**Rule 10 s'exécute en premier -> Tout bloqué !**

**Solution : Mettre DENY rules APRÈS allow rules**

---

### ÉTAPE 14 : Optimisation des coûts réseau

#### Analyse des coûts

**Cost Explorer -> Filters**

```
Service: Amazon VPC, Amazon EC2 (NAT Gateway)
Time: Last month
Group by: Usage type
```

**Breakdown typique :**

```
NAT Gateway (1 instance):
  NatGateway-Hours: 720h × 0.045$ = 32.40$/mois
  NatGateway-Bytes: 100 GB × 0.045$ = 4.50$/mois
  Total NAT: ~37$/mois

VPC Endpoints (Interface):
  VpcEndpoint-Hours: 2 endpoints × 720h × 0.01$ = 14.40$/mois

Data Transfer OUT:
  DataTransfer-Out-Bytes: 50 GB × 0.09$ = 4.50$/mois

Total réseau: ~56$/mois
```

**[ATTENTION] NAT Gateway = 65% du coût réseau !**

---

#### Stratégies d'optimisation

**1. Réduire le nombre de NAT Gateways**

**Scénario actuel (HA complète) :**

```
2 NAT Gateways (1 par AZ) = 2 × 32.40$ = 64.80$/mois
```

**Option économique (non-HA) :**

```
1 NAT Gateway = 32.40$/mois
Économie: 50% [OK]
```

**[ATTENTION] Compromis : Si AZ tombe, instances dans autre AZ perdent internet**

**Recommandation :**
- **Dev/Staging : 1 NAT Gateway**
- **Production : 2 NAT Gateways (HA)**

---

**2. Utiliser NAT Instance au lieu de NAT Gateway**

**NAT Instance (EC2 t3.small) :**

```
Instance: t3.small (~0.02$/h) = 14.40$/mois
Data processing: Gratuit
Total: ~14$/mois

vs NAT Gateway: ~37$/mois

Économie: 62% [OK]
```

**[ATTENTION] Compromis :**
- Tu gères l'instance (patches, monitoring)
- Performance limitée (vs 45 Gbps NAT Gateway)
- Pas de HA automatique

**Implémentation NAT Instance :**

```bash
# 1. Lancer instance dans subnet public
EC2 -> Launch instance
  AMI: Amazon Linux 2023
  Type: t3.small
  Subnet: subnet-public-1a
  Security Group: Allow all outbound, SSH inbound

# 2. Désactiver source/destination check
EC2 -> Instance -> Actions -> Networking -> Change source/dest check
  [ ] Enable (désactiver)

# 3. Configurer iptables
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
sudo iptables -A FORWARD -i eth0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o eth0 -j ACCEPT

# 4. Persister config
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sudo service iptables save

# 5. Modifier route table
VPC -> Route Tables -> rtb-app-1a
  0.0.0.0/0 -> Instance: i-natinstance
```

**NAT Instance opérationnel [OK]**

---

**3. VPC Endpoints pour réduire data transfer**

**Trafic S3/DynamoDB via NAT Gateway :**

```
100 GB/mois × 0.045$/GB = 4.50$/mois
```

**Avec VPC Gateway Endpoints :**

```
Gratuit [OK]
Économie: 4.50$/mois
```

**Toujours créer Gateway Endpoints pour S3 et DynamoDB [OK]**

---

**4. Optimiser data transfer OUT**

**Data transfer vers internet = cher (0.09$/GB)**

**Stratégies :**

**a) Utiliser CloudFront (CDN)**

```
Direct: Data transfer OUT = 0.09$/GB
Via CloudFront: 0.085$/GB (légèrement moins cher)
  + Cache (requêtes suivantes gratuites)
```

---

**b) Compression**

```python
# Dans Flask, activer compression
from flask_compress import Compress

app = Flask(__name__)
Compress(app)

# Réponses JSON compressées (gzip)
# 1 MB -> 100 KB (90% économie)
```

---

**c) Limiter backups vers internet**

```
Backup RDS vers S3 (même région) : Gratuit [OK]
Backup RDS vers internet : 0.09$/GB [X]

Toujours backuper dans S3, pas vers on-premise direct
```

---

**5. Supprimer ressources inutilisées**

**Ressources qui coûtent même inactives :**

```
Elastic IPs non-attachées: 0.005$/h (~3.6$/mois)
NAT Gateway idle: 0.045$/h (plein tarif)
Interface Endpoints non-utilisés: 0.01$/h
```

**Auditer régulièrement :**

```bash
# Trouver EIPs non-attachées
aws ec2 describe-addresses \
  --query 'Addresses[?AssociationId==null]'

# Supprimer
aws ec2 release-address --allocation-id eipalloc-xxx
```

---

**6. Consolider VPCs si possible**

**3 VPCs = 3× complexité/coûts**

**Si Dev et Staging peuvent partager :**

```
Avant:
  vpc-dev (10.2.0.0/16)
  vpc-staging (10.1.0.0/16)

Après:
  vpc-non-prod (10.1.0.0/16)
    Subnets: dev (10.1.1.x), staging (10.1.11.x)
  
Économie: Moins de NAT Gateway, moins de VPC Endpoints
```

---

**Budget mensuel optimisé :**

```
Architecture HA complète (production):
  NAT Gateway × 2: 65$/mois
  VPC Endpoints (Interface) × 3: 21$/mois
  Data transfer: 5$/mois
  Total: ~91$/mois

Architecture économique (dev/staging):
  NAT Instance × 1: 14$/mois
  VPC Endpoints (Gateway only): 0$/mois
  Data transfer: 2$/mois
  Total: ~16$/mois

Économie: 82% [OK]
```

---

### ÉTAPE 15 : Concepts avancés

#### AWS Transit Gateway

**Problème avec VPC Peering à grande échelle :**

```
4 VPCs nécessitent 6 peering connections:
  A <-> B
  A <-> C
  A <-> D
  B <-> C
  B <-> D
  C <-> D

10 VPCs nécessitent 45 peering connections ! (n(n-1)/2)
```

**Solution : Transit Gateway (hub-and-spoke)**

```
         Transit Gateway (hub)
         /       |       \
       VPC A   VPC B   VPC C   VPC D
       
Seulement 4 connections (1 par VPC) [OK]
```

---

**Créer Transit Gateway :**

**VPC -> Transit Gateways -> Create transit gateway**

```
Name tag: tgw-central

Description: Central hub for all VPCs

Amazon side ASN: 64512 (default)

DNS support: [x] Enable

VPN ECMP support: [x] Enable (Equal-cost multi-path)

Default route table association: [x] Enable
Default route table propagation: [x] Enable

Auto accept shared attachments: [ ] Disable (manual control)
```

**Create transit gateway**

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

---

**Attacher VPCs au Transit Gateway :**

**Transit gateway attachments -> Create transit gateway attachment**

```
Transit gateway ID: tgw-central

Attachment type: VPC

VPC ID: vpc-production

Subnet IDs:
  [x] subnet-app-1a (10.0.11.0/24)
  [x] subnet-app-1b (10.0.12.0/24)
  (Choisir 1 subnet par AZ pour HA)

DNS support: Enable
```

**Create**

---

**Répéter pour vpc-staging et vpc-development**

---

**Configurer route tables :**

**VPC Production -> rtb-app-1a -> Routes**

```
Destination: 10.1.0.0/16 (staging)
Target: Transit Gateway -> tgw-central

Destination: 10.2.0.0/16 (development)
Target: Transit Gateway -> tgw-central
```

---

**VPC Staging -> rtb-staging-private -> Routes**

```
Destination: 10.0.0.0/16 (production)
Target: Transit Gateway -> tgw-central

Destination: 10.2.0.0/16 (development)
Target: Transit Gateway -> tgw-central
```

---

**Résultat : Communication transitive !**

```
VPC A <-> Transit Gateway <-> VPC B [OK]
VPC A <-> Transit Gateway <-> VPC C [OK]
VPC B <-> Transit Gateway <-> VPC C [OK]

Toutes les combinaisons fonctionnent via le hub central
```

---

**Coût Transit Gateway :**

```
Attachment: 0.05$/h par VPC = 36$/mois par VPC
Data processing: 0.02$/GB

3 VPCs = 3 × 36$ = 108$/mois

vs VPC Peering (gratuit dans même région)

[ATTENTION] Transit Gateway plus cher, mais simplifie gestion à grande échelle
```

**Recommandation :**
- **< 5 VPCs : VPC Peering** (gratuit)
- **5+ VPCs : Transit Gateway** (scalabilité)

---

#### AWS PrivateLink

**Exposer un service dans VPC A vers VPC B sans peering**

**Use case : Service provider -> Customers**

```
Provider VPC (service private)
  -> PrivateLink
    -> Customer VPC (consomme via endpoint)
    
Pas besoin de peering, pas de chevauchement CIDR [OK]
```

---

**Architecture :**

```
VPC Provider:
  - Service (ex: API interne sur port 8080)
  - Network Load Balancer (NLB)
  - VPC Endpoint Service (PrivateLink)

VPC Customer:
  - VPC Interface Endpoint
  - Résolution DNS privée
  - Accès au service via IP privée
```

---

**Créer PrivateLink :**

**1. Dans VPC Provider, créer NLB :**

```
EC2 -> Load Balancers -> Create NLB
  Name: nlb-private-service
  Scheme: Internal
  Subnets: Private subnets
  Target group: Service instances
```

---

**2. Créer VPC Endpoint Service :**

```
VPC -> Endpoint Services -> Create endpoint service

Load balancer type: Network
Load balancers:
  [x] nlb-private-service

Acceptance required: [x] Yes
  (Manuel approval pour contrôle)

Allowed principals: (optionnel)
  arn:aws:iam::customer-account-id:root
```

**Create**

**Service name généré :**

```
com.amazonaws.vpce.eu-west-1.vpce-svc-abc123
```

---

**3. Dans VPC Customer, créer Interface Endpoint :**

```
VPC -> Endpoints -> Create endpoint

Service category: Find service by name

Service name: com.amazonaws.vpce.eu-west-1.vpce-svc-abc123

VPC: vpc-customer

Subnets: Private subnets

Security groups: Allow TCP 8080 from customer instances
```

**Create**

---

**4. Provider accepte la connexion :**

```
VPC -> Endpoint Services -> vpce-svc-abc123
Endpoint connections -> Accept connection request
```

---

**5. Customer accède au service :**

```bash
# DNS privé résolu automatiquement
curl http://vpce-xxx.vpce-svc-abc123.eu-west-1.vpce.amazonaws.com:8080
```

**Ou via IP privée de l'endpoint**

**[OK] Service accessible sans peering, sans exposition internet !**

---

#### IPv6 dans VPC

**Activer IPv6 sur VPC existant :**

**VPC -> vpc-production -> Actions -> Edit CIDRs**

```
IPv6 CIDR block: Amazon-provided IPv6 CIDR
```

**AWS assigne automatiquement un /56 :**

```
2600:1f18:1234:5600::/56
```

---

**Associer IPv6 aux subnets :**

```
Subnet -> subnet-public-1a -> Edit IPv6 CIDRs

IPv6 CIDR: 2600:1f18:1234:5600::/64
  (Premier /64 du /56)
```

**Répéter pour chaque subnet (incrémenter le bloc)**

```
subnet-public-1b: 2600:1f18:1234:5601::/64
subnet-app-1a: 2600:1f18:1234:5602::/64
...
```

---

**Activer auto-assign IPv6 :**

```
Subnet -> Modify auto-assign IP settings
[x] Enable auto-assign IPv6 address
```

---

**Modifier route tables :**

```
Route table -> Add route

Destination: ::/0 (tout IPv6)
Target: Internet Gateway (pour public) ou Egress-only IGW (pour private)
```

**Egress-only Internet Gateway :**
- Comme NAT pour IPv6
- Permet sortie, bloque entrée
- Gratuit [OK]

---

**Lancer instance avec IPv6 :**

```
Instance obtient:
  - IPv4 privée: 10.0.11.5
  - IPv6: 2600:1f18:1234:5602::a1b2
  - IPv6 publique (si subnet public): 2600:1f18:1234:5600::c3d4
```

---

**Tester connectivité IPv6 :**

```bash
ping6 google.com
curl -6 https://ipv6.google.com
```

**Avantage IPv6 :**
- Pas besoin de NAT (chaque instance a IP publique)
- Économie sur NAT Gateway [OK]
- Future-proof

---

### [OK] TESTS DE VALIDATION

**Infrastructure réseau :**
- [ ] 3 VPCs créés (Production, Staging, Development)
- [ ] Subnets créés dans 2+ AZs (haute disponibilité)
- [ ] Internet Gateways attachés aux VPCs
- [ ] NAT Gateway déployé et fonctionnel
- [ ] Bastion Host déployé et accessible

**Routing :**
- [ ] Route tables créées (public, private, db)
- [ ] Routes vers IGW (subnets publics)
- [ ] Routes vers NAT Gateway (subnets privés)
- [ ] Routes locales (VPC CIDR)

**VPC Peering :**
- [ ] Peering connection créée et acceptée
- [ ] Routes configurées dans les 2 VPCs
- [ ] Connectivité testée (ping, SSH)

**VPC Endpoints :**
- [ ] S3 Gateway Endpoint créé
- [ ] DynamoDB Gateway Endpoint créé
- [ ] Trafic S3 passe par endpoint (pas NAT)

**Sécurité :**
- [ ] Security Groups configurés (tiers séparés)
- [ ] Network ACLs configurés (app tier)
- [ ] Bastion accessible seulement depuis IP autorisée
- [ ] DB tier isolé (pas d'internet)

**Monitoring :**
- [ ] VPC Flow Logs activés
- [ ] Logs visibles dans CloudWatch
- [ ] Reachability Analyzer testé

**Architecture 3-tier :**
- [ ] Web Tier (public, ALB)
- [ ] App Tier (private, instances)
- [ ] DB Tier (private isolé, RDS)
- [ ] Communication entre tiers fonctionnelle

**Performance :**
- [ ] Latence internet < 100ms (via NAT)
- [ ] Latence inter-VPC < 5ms (peering)
- [ ] Bande passante suffisante

**Coûts :**
- [ ] NAT Gateway coût estimé
- [ ] Elastic IPs non-attachées supprimées
- [ ] Ressources inutilisées identifiées
- [ ] Budget alert configurée

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Route table modification n'a pas d'effet

**Symptôme :**

```
Route ajoutée mais trafic ne passe toujours pas
```

**Cause : Route table pas associée au bon subnet**

**Solution :**

```
1. VPC -> Subnets -> subnet-app-1a
2. Route table: (vérifier le nom)
3. Si mauvais -> Subnet associations -> Edit
4. Associer au bon route table
```

---

#### Erreur 2 : NAT Gateway coûte même si aucun trafic

**Symptôme :**

```
Facture NAT Gateway = 32$/mois alors que pas utilisé
```

**Cause : NAT Gateway facturé à l'heure (pas à l'usage)**

**Solution :**

```
Delete NAT Gateway si pas utilisé
  (Contrairement à Lambda qui facture seulement si invoqué)

Si besoin temporaire:
  - Créer NAT Gateway
  - Tester (1h = 0.045$)
  - Supprimer immédiatement
```

---

#### Erreur 3 : VPC Peering transitive ne fonctionne pas

**Symptôme :**

```
VPC A <-> VPC B [OK]
VPC B <-> VPC C [OK]
VPC A -> VPC C [X]
```

**Cause : Peering n'est PAS transitif**

**Solution :**

```
Option 1: Créer peering direct A <-> C
Option 2: Utiliser Transit Gateway
```

---

#### Erreur 4 : CIDR overlap empêche peering

**Symptôme :**

```
Cannot create peering: CIDR ranges overlap
```

**Cause :**

```
VPC A: 10.0.0.0/16
VPC B: 10.0.1.0/24
  ^ 10.0.1.x existe dans les 2 !
```

**Solution :**

```
Recréer VPC B avec CIDR différent:
  10.1.0.0/16 (pas de overlap)

[ATTENTION] Planifier CIDRs AVANT de créer VPCs
```

---

#### Erreur 5 : Instance privée inaccessible via bastion

**Symptôme :**

```bash
ssh -J bastion-prod ubuntu@10.0.11.5
# Connection refused
```

**Causes possibles :**

**1. Security Group instance privée bloque SSH depuis bastion**

**Solution :**

```
SG instance privée:
  Inbound: SSH (22) from sg-bastion [OK]
  (Ou from 10.0.1.0/24 si tu utilises CIDR)
```

---

**2. SSH key pas forwarded**

**Solution :**

```bash
# Vérifier ssh-agent
ssh-add -l

# Si vide, ajouter clé
ssh-add ~/.ssh/sirrdev-ec2-key.pem

# Se connecter avec -A
ssh -A ubuntu@bastion-ip
```

---

**3. ProxyJump mal configuré**

**Solution :**

```
~/.ssh/config:

Host bastion-prod
    HostName <IP-publique-bastion>
    User ubuntu
    IdentityFile ~/.ssh/sirrdev-ec2-key.pem

Host 10.0.*
    User ubuntu
    ProxyJump bastion-prod
    IdentityFile ~/.ssh/sirrdev-ec2-key.pem
```

---

#### Erreur 6 : Ephemeral ports oubliés dans NACL

**Symptôme :**

```bash
curl https://google.com
# Connection timeout (pas refused)
```

**Cause : NACL bloque réponses (ports éphémères)**

**Solution :**

```
NACL Outbound rules:
  Rule 140: TCP 1024-65535 to 0.0.0.0/0 -> ALLOW

Sans ça, la réponse HTTPS (port éphémère) est bloquée
```

---

#### Erreur 7 : VPC Endpoint policy trop restrictive

**Symptôme :**

```bash
aws s3 ls s3://my-bucket
# Access Denied
```

**Cause : Endpoint policy bloque**

**Solution :**

```json
VPC Endpoint -> Edit policy

{
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": "*"
    }
  ]
}
```

**Ou combiner avec IAM role permissions**

---

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

**VPC Architecture :**
- VPC = Réseau virtuel isolé
- Subnets = Subdivision par AZ
- Public subnet = Route vers IGW
- Private subnet = Pas de route IGW (NAT ou isolé)
- 1 subnet = 1 AZ (haute dispo = multiple AZs)

**NAT Gateway :**
- Permet internet sortant pour instances privées
- Coût : 0.045$/h + 0.045$/GB
- Haute dispo = 1 NAT par AZ
- Alternative économique = NAT Instance

**VPC Peering :**
- Connexion privée entre 2 VPCs
- Gratuit (même région)
- Pas transitif (A<->B, B<->C ≠ A<->C)
- CIDR ne doit pas overlap

**VPC Endpoints :**
- Gateway (S3, DynamoDB) = Gratuit
- Interface (autres services) = 0.01$/h
- Évite trafic internet (économie + sécurité)

**Sécurité multi-couches :**
- Security Groups = Instance-level, stateful
- Network ACLs = Subnet-level, stateless
- Route Tables = Contrôle du routing
- Bastion Host = Jump server pour accès SSH

**Monitoring :**
- VPC Flow Logs = Capture trafic réseau
- Reachability Analyzer = Test connectivité
- CloudWatch Logs Insights = Analyse logs

**Coûts :**
- NAT Gateway = Service le plus cher (65% coût réseau)
- VPC/Subnets/IGW = Gratuit
- Gateway Endpoints = Gratuit
- Optimiser avec NAT Instance ou réduire nombre NAT

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. AWS Direct Connect**

**Connexion réseau dédiée datacenter -> AWS**

```
On-premise
  -> Direct Connect Location (point d'échange)
    -> AWS backbone
      -> VPC
```

**Avantages :**
- Bande passante garantie (1-100 Gbps)
- Latence plus faible que internet public
- Coût data transfer réduit (0.02$/GB vs 0.09$/GB)
- Sécurité (pas internet public)

**Use cases :**
- Hybrid cloud (workloads on-prem + AWS)
- Migration massive de données
- Accès DB temps réel

**Coût :**
```
Port fee: 0.30$/h (1 Gbps)
Data transfer OUT: 0.02$/GB (vs 0.09$/GB internet)
```

---

**2. Site-to-Site VPN**

**Connexion VPN entre datacenter et VPC**

```
On-premise
  -> VPN tunnel (IPsec)
    -> Virtual Private Gateway (VPC)
```

**Avantages :**
- Setup rapide (vs Direct Connect)
- Économique (0.05$/h)
- Chiffrement automatique

**Inconvénients :**
- Bande passante limitée (1.25 Gbps max)
- Latence variable (internet public)

**Créer VPN :**

```
VPC -> Virtual Private Gateway -> Create
  Attach to: vpc-production

VPC -> Customer Gateway -> Create
  IP address: <IP-publique-on-premise>
  BGP ASN: 65000

VPC -> Site-to-Site VPN Connections -> Create
  Virtual Private Gateway: vgw-xxx
  Customer Gateway: cgw-xxx
  Routing: Static or Dynamic (BGP)
```

---

**3. AWS Network Firewall**

**Firewall managé pour inspection trafic**

```
Internet
  v
Internet Gateway
  v
Network Firewall (inspection)
  v
VPC resources
```

**Features :**
- Stateful inspection
- IDS/IPS (Intrusion Detection/Prevention)
- Domain filtering (bloquer domaines malveillants)
- TLS inspection

**Règles exemple :**

```
BLOCK domain .malware.com
ALERT on suspicious traffic patterns
ALLOW only approved protocols (HTTP, HTTPS, SSH)
```

**Coût : ~300$/mois (firewall endpoint + règles)**

---

**4. Traffic Mirroring**

**Dupliquer trafic pour analyse (IDS, monitoring)**

```
Production instance
  -> Traffic mirroring session
    -> Mirror traffic
      -> Analysis instance (Wireshark, Zeek, etc.)
```

**Use cases :**
- Security monitoring
- Troubleshooting réseau
- Compliance (audit complet trafic)

---

**5. IPAM (IP Address Manager)**

**Gestion centralisée des CIDRs**

**Problème :**
```
10 VPCs, chacun avec son CIDR
  -> Risque d'overlap
  -> Difficile de tracer utilisation IPs
```

**Solution : AWS VPC IPAM**

```
IPAM Pool:
  10.0.0.0/8
    ├─ Production: 10.0.0.0/16
    ├─ Staging: 10.1.0.0/16
    ├─ Dev: 10.2.0.0/16
    └─ (Auto-assign, pas d'overlap)
```

**Features :**
- Détection overlap automatique
- Audit utilisation IP
- Multi-account (AWS Organizations)

---

**6. DNS avancé avec Route 53 Resolver**

**Résoudre DNS entre VPC et on-premise**

```
VPC A
  -> Route 53 Resolver Endpoint (inbound)
    <- DNS queries from on-premise

On-premise
  -> Route 53 Resolver Endpoint (outbound)
    <- DNS queries to VPC
```

**Use case : Hybrid DNS**

```
app.internal.company.com
  -> Résolu dans VPC [OK]
  -> Résolu on-premise [OK]
```

---

**7. VPC Sharing (AWS RAM - Resource Access Manager)**

**Partager VPC entre comptes AWS**

```
Central Networking Account (VPC owner)
  -> Partage subnets
    -> Application Account A (utilise subnets)
    -> Application Account B (utilise subnets)
```

**Avantages :**
- Centraliser réseau (1 seul VPC à gérer)
- Isolation applicative (comptes séparés)
- Économie (1 NAT Gateway partagé)

**Configuration :**

```
AWS RAM -> Create resource share
  Name: shared-vpc-subnets
  Resources:
    [x] subnet-app-1a
    [x] subnet-app-1b
  Principals (accounts): 123456789012, 210987654321
```

---

**8. Carrier Gateway (5G/LTE)**

**VPC pour Wavelength Zones (edge 5G)**

```
Mobile user (5G)
  -> Carrier Gateway
    -> Wavelength Zone (edge)
      -> VPC subnet
        -> Application ultra low-latency
```

**Use cases :**
- Gaming (< 10ms latency)
- AR/VR
- IoT temps réel

---

## [COURS] CONCLUSION DE L'EXERCICE 5

**[BRAVO] FÉLICITATIONS ! Tu maîtrises maintenant le réseau AWS avancé ! [BRAVO]**

**Ce que tu as appris :**
- [OK] Architecture réseau multi-tier (web, app, DB)
- [OK] VPCs multiples avec isolation
- [OK] NAT Gateway et NAT Instance
- [OK] VPC Peering (connexions inter-VPC)
- [OK] VPC Endpoints (S3, DynamoDB, autres services)
- [OK] Bastion Host pour accès sécurisé
- [OK] Network ACLs vs Security Groups
- [OK] VPC Flow Logs (monitoring réseau)
- [OK] Troubleshooting réseau
- [OK] Optimisation des coûts réseau
- [OK] Transit Gateway (architecture hub-spoke)
- [OK] PrivateLink (exposition services)

**Compétences acquises :**
- [OK] Network architecture design
- [OK] Sécurité réseau multi-couches
- [OK] Troubleshooting avancé
- [OK] Cost optimization
- [OK] High availability networking

**Architecture déployée :**
```
[OK] 3 VPCs (Production 3-tier, Staging 2-tier, Dev minimal)
[OK] 8+ subnets (public, private, db dans 2 AZs)
[OK] NAT Gateway avec haute disponibilité
[OK] VPC Peering entre environnements
[OK] VPC Endpoints (S3, DynamoDB)
[OK] Bastion Host sécurisé
[OK] Network ACLs configurés
[OK] VPC Flow Logs activés
[OK] Security Groups par tier
```

**Coût estimé :**
- **Option économique (1 NAT) : ~35$/mois**
- **Option HA complète (2 NAT) : ~70$/mois**
- **Option NAT Instance : ~16$/mois**

**Temps moyen de réalisation :** 6-7 heures

**Prochaine étape :** Exercice 6 - Load Balancing et Auto Scaling ! [SCALES]

---

**[ATTENTION] NETTOYAGE DES RESSOURCES**

**[ATTENTION] IMPORTANT : Supprimer dans cet ordre pour éviter erreurs**

```bash
# 1. Supprimer Transit Gateway attachments (si créé)
VPC -> Transit gateway attachments -> Delete

# 2. Supprimer Transit Gateway (si créé)
VPC -> Transit Gateways -> Delete

# 3. Supprimer VPC Peering connections
VPC -> Peering Connections -> Delete

# 4. Terminer toutes les instances EC2
EC2 -> Instances -> Terminate

# 5. Supprimer NAT Gateways ([ATTENTION] arrête facturation horaire)
VPC -> NAT Gateways -> Delete
  (Attendre 5 min que status = Deleted)

# 6. Libérer Elastic IPs
VPC -> Elastic IPs -> Release

# 7. Supprimer VPC Endpoints
VPC -> Endpoints -> Delete

# 8. Supprimer Load Balancers (si créés)
EC2 -> Load Balancers -> Delete

# 9. Supprimer Target Groups
EC2 -> Target Groups -> Delete

# 10. Supprimer Auto Scaling Groups
EC2 -> Auto Scaling Groups -> Delete

# 11. Supprimer Launch Templates
EC2 -> Launch Templates -> Delete

# 12. Supprimer RDS instances
RDS -> Databases -> Delete (Skip final snapshot pour dev)

# 13. Supprimer DB Subnet Groups
RDS -> Subnet groups -> Delete

# 14. Supprimer VPC Flow Logs
VPC -> Each VPC -> Flow logs -> Delete

# 15. Supprimer CloudWatch Log Groups
CloudWatch -> Logs -> Log groups -> Delete

# 16. Supprimer Security Groups (custom seulement)
EC2 -> Security Groups -> Delete
  (Default SG ne peut pas être supprimé)

# 17. Supprimer Network ACLs (custom seulement)
VPC -> Network ACLs -> Delete
  (Default NACL ne peut pas être supprimé)

# 18. Détacher Internet Gateways
VPC -> Internet Gateways -> Detach from VPC

# 19. Supprimer Internet Gateways
VPC -> Internet Gateways -> Delete

# 20. Supprimer les subnets
VPC -> Subnets -> Delete

# 21. Supprimer les VPCs
VPC -> Your VPCs -> Delete VPC

# 22. Supprimer IAM roles créés
IAM -> Roles -> VPCFlowLogsRole -> Delete
```

**Vérification finale :**

```
VPC -> Your VPCs -> Doit afficher seulement le default VPC [OK]
EC2 -> Instances -> Aucune instance running [OK]
VPC -> Elastic IPs -> Aucune IP allouée [OK]
VPC -> NAT Gateways -> Aucun NAT [OK]
```

---

**FIN DE L'EXERCICE 5**

═══════════════════════════════════════════════════════════════

Veux-tu que je continue avec l'**Exercice 6 : Load Balancing et Auto Scaling (ELB + ASG + CloudWatch)** ?

# [JAUNE] EXERCICE 6 : LOAD BALANCING ET AUTO SCALING (ELB + ASG)

## [LISTE] ÉNONCÉ

### Contexte professionnel

L'application de gestion de tâches connaît un succès croissant. Le CTO fait face à de nouveaux défis :

**Problèmes actuels :**
- [X] **Disponibilité faible** : 1 seule instance EC2 (si elle tombe, tout tombe)
- [X] **Pas de scalabilité** : Traffic peak -> Instance overloaded -> 503 errors
- [X] **Gaspillage de ressources** : Instance t3.medium 24/7 même si traffic faible la nuit
- [X] **Déploiements risqués** : Update = downtime (arrêt instance -> deploy -> redémarrage)
- [X] **Pas de répartition de charge** : 1 instance = 1 point de congestion
- [X] **Pas de redondance géographique** : Toutes ressources en 1 seule AZ

**Nouvelle architecture demandée :**

Le directeur technique impose :
1. **Application Load Balancer (ALB)** pour distribuer le trafic
2. **Auto Scaling Group (ASG)** pour ajuster le nombre d'instances automatiquement
3. **Multi-AZ deployment** (minimum 2 AZs pour haute disponibilité)
4. **Health checks** pour détecter instances défaillantes
5. **Scaling policies** :
   - Scale OUT si CPU > 70% (ajouter instances)
   - Scale IN si CPU < 30% (supprimer instances)
   - Scheduled scaling (+ instances pendant business hours)
6. **Rolling deployments** (zero-downtime updates)
7. **CloudWatch monitoring** avec alarmes proactives
8. **Cost optimization** : Min 2 instances, Max 10 instances
9. **Target Tracking** pour maintenir performance cible

### Architecture cible

```
┌─────────────────────────────────────────────────────────────────┐
│                         INTERNET USERS                           │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ HTTPS (443)
                       │ HTTP (80) -> Redirect to HTTPS
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│         APPLICATION LOAD BALANCER (ALB)                          │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Listeners:                                               │   │
│  │  - HTTP:80 -> Redirect to HTTPS                           │   │
│  │  - HTTPS:443 -> Forward to target group                   │   │
│  │                                                            │   │
│  │  Features:                                                │   │
│  │  - Path-based routing (/api -> app, /static -> S3)        │   │
│  │  - Host-based routing (api.example.com -> app)           │   │
│  │  - Sticky sessions (cookie-based)                        │   │
│  │  - Connection draining (300 sec)                         │   │
│  │  - Access logs -> S3                                      │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  Public Subnets:                                                │
│  - Subnet AZ 1a (10.0.1.0/24)                                   │
│  - Subnet AZ 1b (10.0.2.0/24)                                   │
│  - Subnet AZ 1c (10.0.3.0/24) - Optional                        │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ Health Checks (HTTP GET /health)
                       │ Interval: 30s, Timeout: 5s, Healthy: 2, Unhealthy: 3
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│                    TARGET GROUP: tg-flask-app                    │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Protocol: HTTP (80)                                      │   │
│  │  Health check path: /health                               │   │
│  │  Deregistration delay: 300 sec                            │   │
│  │  Stickiness: Enabled (1 hour TTL)                         │   │
│  └──────────────────────────────────────────────────────────┘   │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ Distributes traffic
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│              AUTO SCALING GROUP (ASG)                            │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Configuration:                                           │   │
│  │  - Launch Template: lt-flask-app-v2                      │   │
│  │  - Desired capacity: 2 instances                         │   │
│  │  - Minimum: 2 (high availability)                        │   │
│  │  - Maximum: 10 (cost control)                            │   │
│  │                                                            │   │
│  │  Scaling Policies:                                        │   │
│  │  1. Target Tracking (CPU 50%)                            │   │
│  │  2. Step Scaling (CPU thresholds)                        │   │
│  │  3. Scheduled (business hours boost)                     │   │
│  │                                                            │   │
│  │  Health Checks:                                           │   │
│  │  - EC2 status checks (default)                           │   │
│  │  - ELB health checks (enabled)                           │   │
│  │  - Grace period: 300 seconds                             │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  Multi-AZ Distribution:                                         │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────┐  │
│  │  AZ eu-west-1a   │  │  AZ eu-west-1b   │  │ eu-west-1c   │  │
│  │  ┌────────────┐  │  │  ┌────────────┐  │  │ ┌──────────┐ │  │
│  │  │ Instance 1 │  │  │  │ Instance 2 │  │  │ │Instance 3│ │  │
│  │  │ 10.0.11.5  │  │  │  │ 10.0.12.8  │  │  │ │10.0.13.4 │ │  │
│  │  │ t3.micro   │  │  │  │ t3.micro   │  │  │ │t3.micro  │ │  │
│  │  └────────────┘  │  │  └────────────┘  │  │ └──────────┘ │  │
│  └──────────────────┘  └──────────────────┘  └──────────────┘  │
│                                                                  │
│  Private Subnets:                                               │
│  - App Subnet 1a (10.0.11.0/24)                                 │
│  - App Subnet 1b (10.0.12.0/24)                                 │
│  - App Subnet 1c (10.0.13.0/24)                                 │
└──────────────────────┬──────────────────────────────────────────┘
                       │
                       │ Database connections
                       [BLACK_DOWN-POINTING_TRIANGLE]
┌─────────────────────────────────────────────────────────────────┐
│                    RDS POSTGRESQL (Multi-AZ)                     │
│  - Primary: eu-west-1a                                          │
│  - Standby: eu-west-1b                                          │
│  - Endpoint: sirrdev-todo-db.xxx.rds.amazonaws.com              │
└─────────────────────────────────────────────────────────────────┘

CloudWatch Monitoring:
┌─────────────────────────────────────────────────────────────────┐
│  Metrics:                                                        │
│  - ASG: GroupDesiredCapacity, GroupInServiceInstances            │
│  - ALB: TargetResponseTime, HealthyHostCount, RequestCount      │
│  - EC2: CPUUtilization, NetworkIn/Out, StatusCheckFailed        │
│                                                                  │
│  Alarms:                                                         │
│  - High CPU (> 70%) -> Scale OUT                                 │
│  - Low CPU (< 30%) -> Scale IN                                   │
│  - Unhealthy targets -> SNS notification                         │
│  - High latency (> 1 sec) -> SNS notification                    │
└─────────────────────────────────────────────────────────────────┘
```

### Contraintes techniques

- Région : `eu-west-1` (Irlande)
- Multi-AZ : Minimum 2 AZs (eu-west-1a, eu-west-1b)
- Load Balancer : Application Load Balancer (Layer 7)
- Instance type : t3.micro (Free Tier)
- AMI : Amazon Linux 2023 ou Ubuntu 22.04
- SSL/TLS : Self-signed certificate (ou Let's Encrypt)
- Health check : HTTP GET /health endpoint
- Auto Scaling : Target tracking + Step scaling
- Budget : Optimiser pour rester proche Free Tier
- Temps estimé : 5-6 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre les types de Load Balancers (ALB, NLB, CLB)
- [OK] Créer et configurer Application Load Balancer
- [OK] Configurer Target Groups avec health checks
- [OK] Créer Launch Templates pour ASG
- [OK] Déployer Auto Scaling Groups
- [OK] Configurer scaling policies (Target, Step, Scheduled)
- [OK] Implémenter health checks multi-niveaux
- [OK] Configurer CloudWatch alarmes
- [OK] Tester scaling automatique
- [OK] Implémenter zero-downtime deployments
- [OK] Optimiser les coûts ASG
- [OK] Troubleshooter les problèmes de scaling

---

## [DOCS] PRÉREQUIS

- Exercices 1-5 terminés (VPC, EC2, RDS, réseau)
- VPC avec subnets publics et privés dans 2+ AZs
- Application Flask fonctionnelle
- RDS PostgreSQL déployé
- Connaissances HTTP/HTTPS

---

## [ARGENT] ESTIMATION DES COÛTS

**Application Load Balancer :**

| Ressource | Coût | Remarque |
|-----------|------|----------|
| ALB (fixed) | 0.0225$/h (~16$/mois) | [X] PAS de Free Tier |
| LCU (Load Balancer Capacity Units) | 0.008$/LCU-h | Basé sur trafic |

**LCU = Max de :**
- Nouvelles connexions/sec
- Connexions actives
- Bande passante (GB)
- Rule evaluations

**Exemple faible trafic :**
```
25 nouvelles connexions/sec
3,000 connexions actives
1 GB/h bandwidth
1,000 rule evaluations/sec

-> ~1 LCU × 0.008$/h = 0.19$/jour
```

**Auto Scaling Group :**
- ASG lui-même : **Gratuit** [OK]
- Instances EC2 : Facturées normalement

**EC2 Instances (ASG) :**

| Configuration | Coût Free Tier | Coût après Free Tier |
|---------------|----------------|----------------------|
| 2 × t3.micro (24/7) | 0$ (750h/mois) | 2 × 0.0104$/h = ~15$/mois |
| 3 × t3.micro (peak) | 0$ (dans limite) | 3 × 0.0104$/h = ~23$/mois |
| 10 × t3.micro (stress test) | N/A | 10 × 0.0104$/h = ~75$/mois (temporaire) |

**CloudWatch :**
- Métriques standard : **Gratuit** [OK]
- Alarmes : 10 gratuites, puis 0.10$/alarme/mois
- Logs : 0.50$/GB ingéré

**Scénario de cet exercice :**

**Développement/Test (2 instances minimum) :**
```
ALB: 16$/mois
2 × t3.micro: 0$/mois (Free Tier) ou 15$/mois (après)
CloudWatch alarmes: 0$/mois (< 10 alarmes)

Total Free Tier: 16$/mois
Total après Free Tier: 31$/mois
```

**Production (2-5 instances dynamiques) :**
```
ALB: 16$/mois
Average 3 instances: 23$/mois
CloudWatch: 1$/mois

Total: ~40$/mois
```

**Comparaison avec architecture statique :**
```
Architecture statique (1 instance 24/7):
  EC2 t3.medium: ~30$/mois
  Pas de HA
  Pas de scaling

Architecture ASG (2-5 instances):
  ~40$/mois
  [OK] Haute disponibilité
  [OK] Auto-scaling
  [OK] Zero-downtime deployments
  
Coût additionnel: +33%, mais valeur bien supérieure [OK]
```

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Comprendre les Load Balancers AWS

#### Types de Load Balancers

**3 types d'ELB (Elastic Load Balancing) :**

| Type | Layer OSI | Protocol | Use case | Prix |
|------|-----------|----------|----------|------|
| **Application LB (ALB)** | Layer 7 (Application) | HTTP/HTTPS, gRPC | Web apps, microservices | 0.0225$/h |
| **Network LB (NLB)** | Layer 4 (Transport) | TCP, UDP, TLS | Ultra high performance | 0.0225$/h |
| **Classic LB (CLB)** | Layer 4 & 7 | HTTP, HTTPS, TCP | Legacy (deprecated) | 0.025$/h |

---

#### Application Load Balancer (ALB) - Détails

**Niveau : Layer 7 (HTTP/HTTPS)**

**Features :**

**1. Path-based routing**
```
http://example.com/api/* -> Target Group A (API servers)
http://example.com/static/* -> Target Group B (Static content)
http://example.com/* -> Target Group C (Web servers)
```

**2. Host-based routing**
```
api.example.com -> Target Group A
www.example.com -> Target Group B
admin.example.com -> Target Group C
```

**3. Query string routing**
```
http://example.com/?version=v1 -> Target Group A
http://example.com/?version=v2 -> Target Group B
```

**4. Header-based routing**
```
Header: X-User-Type: premium -> Target Group A
Header: X-User-Type: free -> Target Group B
```

**5. HTTP/2 et WebSocket support**
```
Upgrade: websocket -> Persistent connection
```

**6. Sticky sessions (Session affinity)**
```
Cookie: AWSALB=xxx -> Toujours la même instance
```

**7. SSL/TLS termination**
```
Client -> HTTPS -> ALB -> HTTP -> Instances (backend)
  (ALB déchiffre, instances reçoivent HTTP clair)
```

**8. AWS WAF integration**
```
ALB + WAF -> Protection DDoS, SQL injection, XSS
```

---

#### Network Load Balancer (NLB) - Quand l'utiliser ?

**Use cases NLB :**

**1. Ultra low latency (< 100 µs)**
```
Gaming servers
Real-time trading
VoIP/SIP
```

**2. Millions de requests/sec**
```
Layer 4 -> Moins de processing qu'ALB
TCP direct forwarding
```

**3. Static IP requirement**
```
NLB peut avoir Elastic IP
  (ALB a seulement DNS, pas IP fixe)
```

**4. Preserve source IP**
```
Instance voit la vraie IP client
  (ALB masque avec X-Forwarded-For header)
```

**5. TCP/UDP protocols (non-HTTP)**
```
MySQL/PostgreSQL load balancing
Custom TCP protocols
```

---

#### Classic Load Balancer (CLB) - [X] Éviter

**Deprecated depuis 2016**

**Problèmes :**
- Pas de path-based routing
- Pas de host-based routing
- Pas de WebSocket
- Pas de HTTP/2
- Moins de features

**Migration : CLB -> ALB toujours recommandée**

---

### ÉTAPE 2 : Préparer l'application pour Load Balancing

#### Créer un health check endpoint

**L'application doit exposer un endpoint /health pour health checks**

**Éditer l'application Flask :**

```bash
# Sur instance EC2 (ou localement)
cd ~/flask-todo-app
nano app.py
```

**Ajouter le endpoint /health :**

```python
# ═══════════════════════════════════════════════════════════════
# HEALTH CHECK ENDPOINT
# ═══════════════════════════════════════════════════════════════

@app.route('/health')
def health_check():
    """
    Health check endpoint pour ALB
    
    Vérifie:
    - Application répond (200 OK)
    - Database accessible
    - Dépendances critiques OK
    
    Returns:
        200 OK si healthy
        503 Service Unavailable si unhealthy
    """
    
    health_status = {
        'status': 'healthy',
        'timestamp': datetime.now().isoformat(),
        'checks': {}
    }
    
    # ───────────────────────────────────────────────────────────
    # 1. CHECK DATABASE CONNECTION
    # ───────────────────────────────────────────────────────────
    
    try:
        # Test simple query
        if DB_TYPE == 'postgresql':
            result = execute_query('SELECT 1', fetch=True)
        else:
            result = execute_query('SELECT 1', fetch=True)
        
        health_status['checks']['database'] = 'ok'
    except Exception as e:
        health_status['status'] = 'unhealthy'
        health_status['checks']['database'] = f'error: {str(e)}'
        return health_status, 503
    
    # ───────────────────────────────────────────────────────────
    # 2. CHECK DISK SPACE (optionnel)
    # ───────────────────────────────────────────────────────────
    
    import shutil
    disk = shutil.disk_usage('/')
    disk_percent = (disk.used / disk.total) * 100
    
    if disk_percent > 90:
        health_status['status'] = 'unhealthy'
        health_status['checks']['disk'] = f'critical: {disk_percent:.1f}% used'
        return health_status, 503
    else:
        health_status['checks']['disk'] = f'ok: {disk_percent:.1f}% used'
    
    # ───────────────────────────────────────────────────────────
    # 3. CHECK MEMORY (optionnel)
    # ───────────────────────────────────────────────────────────
    
    import psutil
    memory = psutil.virtual_memory()
    
    if memory.percent > 95:
        health_status['status'] = 'unhealthy'
        health_status['checks']['memory'] = f'critical: {memory.percent}% used'
        return health_status, 503
    else:
        health_status['checks']['memory'] = f'ok: {memory.percent}% used'
    
    # ───────────────────────────────────────────────────────────
    # 4. RETURN HEALTHY STATUS
    # ───────────────────────────────────────────────────────────
    
    return health_status, 200


# ═══════════════════════════════════════════════════════════════
# METADATA ENDPOINT (pour debugging)
# ═══════════════════════════════════════════════════════════════

@app.route('/info')
def instance_info():
    """
    Retourne infos sur l'instance (pour identifier quelle instance répond)
    """
    
    import socket
    import requests
    
    try:
        # Instance metadata (AWS)
        metadata_base = 'http://169.254.169.254/latest/meta-data/'
        instance_id = requests.get(f'{metadata_base}instance-id', timeout=1).text
        az = requests.get(f'{metadata_base}placement/availability-zone', timeout=1).text
        instance_type = requests.get(f'{metadata_base}instance-type', timeout=1).text
        private_ip = requests.get(f'{metadata_base}local-ipv4', timeout=1).text
    except:
        instance_id = 'unknown'
        az = 'unknown'
        instance_type = 'unknown'
        private_ip = socket.gethostbyname(socket.gethostname())
    
    return {
        'instance_id': instance_id,
        'availability_zone': az,
        'instance_type': instance_type,
        'private_ip': private_ip,
        'hostname': socket.gethostname(),
        'timestamp': datetime.now().isoformat()
    }
```

**Installer psutil (pour memory check) :**

```bash
pip install psutil
```

---

**Tester le health check localement :**

```bash
# Démarrer Flask
python app.py

# Dans autre terminal, tester
curl http://localhost:5000/health
```

**Résultat attendu :**

```json
{
  "status": "healthy",
  "timestamp": "2024-12-16T18:30:00",
  "checks": {
    "database": "ok",
    "disk": "ok: 45.3% used",
    "memory": "ok: 62.1% used"
  }
}
```

**HTTP 200 OK [OK]**

---

**Si unhealthy (DB down par exemple) :**

```json
{
  "status": "unhealthy",
  "timestamp": "2024-12-16T18:30:00",
  "checks": {
    "database": "error: could not connect to server",
    "disk": "ok: 45.3% used",
    "memory": "ok: 62.1% used"
  }
}
```

**HTTP 503 Service Unavailable [OK]**

**ALB retire automatiquement cette instance du pool [OK]**

---

#### Rendre l'application stateless

**Problème avec sessions :**

**Session stockée en mémoire (Flask default) :**

```
User -> ALB -> Instance A (login, session en RAM)
User -> ALB -> Instance B (session perdue, re-login)
  [X] Mauvaise expérience utilisateur
```

**Solutions :**

**1. Cookie-based sessions (simple)**

```python
# Flask config
app.config['SESSION_TYPE'] = 'filesystem'
# Ou mieux, signer les cookies
app.secret_key = 'super-secret-key-from-env'
```

**Sessions stockées côté client (cookie signé) [OK]**

---

**2. Redis/Memcached sessions (production)**

```python
from flask_session import Session
import redis

app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url('redis://elasticache-endpoint:6379')

Session(app)
```

**Sessions stockées dans Redis (partagé entre instances) [OK]**

---

**3. Database sessions**

```python
app.config['SESSION_TYPE'] = 'sqlalchemy'
app.config['SESSION_SQLALCHEMY'] = db
```

**Sessions stockées dans PostgreSQL [OK]**

---

**Pour cet exercice, on utilisera sticky sessions (ALB feature)**

**Sticky sessions = ALB route toujours le même user vers la même instance**

**On configurera ça plus tard dans le Target Group**

---

### ÉTAPE 3 : Créer un Launch Template

#### Qu'est-ce qu'un Launch Template ?

**Launch Template = "Recette" pour lancer des instances EC2**

**Contient :**
- AMI ID
- Instance type
- Key pair
- Security groups
- User data (script démarrage)
- Network settings
- IAM role
- EBS volumes
- Tags

**Launch Template vs Launch Configuration :**

| Critère | Launch Template | Launch Configuration |
|---------|-----------------|----------------------|
| **Versions** | [OK] Multiples versions | [X] Pas de versions |
| **Modification** | [OK] Peut être modifié | [X] Immuable |
| **Spot instances** | [OK] Support | [X] Pas de support |
| **T2/T3 Unlimited** | [OK] Support | [X] Pas de support |
| **Recommandation** | [OK] Utiliser | [X] Deprecated |

**Toujours utiliser Launch Template (pas Launch Configuration) [OK]**

---

#### Créer une AMI custom (Golden Image)

**Option 1 : Créer AMI depuis instance existante**

**Si tu as une instance Flask configurée :**

```
EC2 -> Instances -> Sélectionner instance
Actions -> Image and templates -> Create image

Image name: ami-flask-app-v1
Image description: Flask todo app with PostgreSQL
[ ] No reboot (pour pas interrompre service)

Create image
```

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

**AMI ID : ami-0abc123...**

---

**Option 2 : User Data script (on va utiliser ça)**

**Pas besoin de créer AMI, on utilise Amazon Linux 2023 + User Data**

**User Data = Script bash exécuté au premier démarrage**

---

#### Créer le Launch Template

**EC2 -> Launch Templates -> Create launch template**

```
Launch template name: lt-flask-app-production

Template version description: v1 - Initial version

[x] Provide guidance to help me set up a template that I can use with EC2 Auto Scaling
```

---

**Application and OS Images (AMI) :**

```
[x] Quick Start

Amazon Linux:
  Amazon Linux 2023 AMI (latest)
  
Architecture: 64-bit (x86)

AMI ID: ami-0d421d84814b7d51c (exemple, varie selon région)
```

---

**Instance type :**

```
Instance type: t3.micro

[ ] Don't include in launch template
  (Pour pouvoir override dans ASG si besoin)
```

---

**Key pair (login) :**

```
Key pair name: sirrdev-ec2-key

[ ] Don't include in launch template
  (Optionnel, pour debugging via SSH)
```

---

**Network settings :**

```
Networking platform: VPC

[ ] Don't include in launch template
  (ASG spécifiera les subnets)

Security groups:
  [x] Select existing security group
  
  Security groups: sg-app-tier
    (Créé dans exercice 5)
    
  Si pas existe, créer:
    Name: sg-app-tier
    Inbound:
      - HTTP (80) from ALB security group
      - SSH (22) from Bastion (optionnel)
    Outbound:
      - All traffic
```

---

**Storage (volumes) :**

```
Volume 1 (Root):
  Size (GiB): 8
  Volume type: gp3
  IOPS: 3000
  Throughput (MB/s): 125
  Delete on termination: Yes
  Encrypted: Yes (optionnel)
```

---

**Resource tags :**

```
Key: Name | Value: flask-app-instance | Resource types: Instances, Volumes

Key: Environment | Value: production

Key: ManagedBy | Value: AutoScaling
```

---

**Advanced details -> IAM instance profile :**

```
IAM instance profile: EC2-S3-Full-Access
  (Créé dans exercice 2)
  
Permissions:
  - S3 read/write
  - DynamoDB read/write
  - CloudWatch Logs write
```

---

**Advanced details -> User data :**

**[ATTENTION] CRUCIAL : Script qui configure l'instance au démarrage**

```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════
# USER DATA SCRIPT - FLASK APP BOOTSTRAP
# ═══════════════════════════════════════════════════════════════
# Ce script s'exécute au premier démarrage de l'instance
# Il installe et configure l'application Flask
# ═══════════════════════════════════════════════════════════════

set -e  # Arrêter si erreur

# ───────────────────────────────────────────────────────────────
# 1. LOGGING
# ───────────────────────────────────────────────────────────────

exec > >(tee /var/log/user-data.log)
exec 2>&1

echo "========================================="
echo "Starting user data script..."
echo "Date: $(date)"
echo "========================================="

# ───────────────────────────────────────────────────────────────
# 2. UPDATE SYSTEM
# ───────────────────────────────────────────────────────────────

echo "Updating system packages..."
yum update -y

# ───────────────────────────────────────────────────────────────
# 3. INSTALL DEPENDENCIES
# ───────────────────────────────────────────────────────────────

echo "Installing dependencies..."
yum install -y python3 python3-pip git postgresql15

# ───────────────────────────────────────────────────────────────
# 4. CLONE APPLICATION CODE
# ───────────────────────────────────────────────────────────────

echo "Cloning application code..."
cd /home/ec2-user

# Option A: Depuis Git
# git clone https://github.com/votre-repo/flask-todo-app.git

# Option B: Depuis S3 (plus rapide)
aws s3 cp s3://sirrdev-deployments/flask-app-latest.tar.gz .
tar -xzf flask-app-latest.tar.gz
cd flask-todo-app

# Option C: Code inline (pour l'exemple)
mkdir -p /home/ec2-user/flask-todo-app
cd /home/ec2-user/flask-todo-app

cat > app.py << 'EOF'
# Code Flask complet ici (simplifié pour user data)
from flask import Flask, jsonify
import psycopg2
import os

app = Flask(__name__)

DB_CONFIG = {
    'host': os.getenv('DB_HOST', 'sirrdev-todo-db.xxx.rds.amazonaws.com'),
    'port': int(os.getenv('DB_PORT', '5432')),
    'database': os.getenv('DB_NAME', 'todo_production'),
    'user': os.getenv('DB_USER', 'postgres'),
    'password': os.getenv('DB_PASSWORD', 'your-password')
}

@app.route('/health')
def health():
    try:
        conn = psycopg2.connect(**DB_CONFIG)
        cur = conn.cursor()
        cur.execute('SELECT 1')
        conn.close()
        return {'status': 'healthy'}, 200
    except:
        return {'status': 'unhealthy'}, 503

@app.route('/')
def index():
    import socket
    return {
        'message': 'Hello from Flask!',
        'hostname': socket.gethostname(),
        'instance': os.getenv('INSTANCE_ID', 'unknown')
    }

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)
EOF

cat > requirements.txt << 'EOF'
Flask==3.0.0
psycopg2-binary==2.9.9
boto3==1.34.0
requests==2.31.0
EOF

# ───────────────────────────────────────────────────────────────
# 5. INSTALL PYTHON DEPENDENCIES
# ───────────────────────────────────────────────────────────────

echo "Installing Python packages..."
pip3 install -r requirements.txt

# ───────────────────────────────────────────────────────────────
# 6. CONFIGURE ENVIRONMENT VARIABLES
# ───────────────────────────────────────────────────────────────

echo "Configuring environment..."

# Get instance metadata
export INSTANCE_ID=$(ec2-metadata --instance-id | cut -d " " -f 2)
export AZ=$(ec2-metadata --availability-zone | cut -d " " -f 2)

# Database config ([ATTENTION] Production : Utiliser Secrets Manager)
export DB_HOST="sirrdev-todo-db.c1abc2defgh.eu-west-1.rds.amazonaws.com"
export DB_PORT="5432"
export DB_NAME="todo_production"
export DB_USER="postgres"
export DB_PASSWORD="PostgresAdminSecure2024!"

# ───────────────────────────────────────────────────────────────
# 7. CREATE SYSTEMD SERVICE
# ───────────────────────────────────────────────────────────────

echo "Creating systemd service..."

cat > /etc/systemd/system/flask-app.service << 'EOF'
[Unit]
Description=Flask Todo Application
After=network.target

[Service]
Type=simple
User=ec2-user
WorkingDirectory=/home/ec2-user/flask-todo-app
Environment="PATH=/usr/local/bin:/usr/bin"
ExecStart=/usr/bin/python3 app.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

# ───────────────────────────────────────────────────────────────
# 8. START SERVICE
# ───────────────────────────────────────────────────────────────

echo "Starting Flask application..."
systemctl daemon-reload
systemctl enable flask-app
systemctl start flask-app

# ───────────────────────────────────────────────────────────────
# 9. VERIFY SERVICE
# ───────────────────────────────────────────────────────────────

sleep 5
systemctl status flask-app

# Test health endpoint
curl -f http://localhost/health || echo "Health check failed!"

# ───────────────────────────────────────────────────────────────
# 10. CLOUDWATCH LOGS (optionnel)
# ───────────────────────────────────────────────────────────────

echo "Configuring CloudWatch logs..."
# Instructions pour installer CloudWatch agent si besoin

# ───────────────────────────────────────────────────────────────
# 11. DONE
# ───────────────────────────────────────────────────────────────

echo "========================================="
echo "User data script completed successfully!"
echo "Date: $(date)"
echo "========================================="
```

**[ATTENTION] Remplacer :**
- `DB_HOST` : Ton endpoint RDS
- `DB_PASSWORD` : Ton mot de passe RDS

**Production : Utiliser AWS Secrets Manager pour credentials [OK]**

---

**Cliquer "Create launch template"**

**[OK] Launch Template créé !**

```
Launch template ID: lt-0abc123...
Launch template name: lt-flask-app-production
Default version: 1
```

---

### ÉTAPE 4 : Créer le Target Group

**Target Group = Ensemble d'instances vers lesquelles le Load Balancer envoie le trafic**

**EC2 -> Target Groups -> Create target group**

```
Choose a target type:
  [x] Instances
  
Target group name: tg-flask-app-production

Protocol: HTTP
Port: 80

VPC: vpc-production

Protocol version: HTTP1
```

**Next**

---

**Health checks :**

```
Health check protocol: HTTP

Health check path: /health

Advanced health check settings:
  Port: Traffic port (80)
  
  Healthy threshold: 2
    (2 checks successifs = healthy)
  
  Unhealthy threshold: 3
    (3 checks échoués = unhealthy)
  
  Timeout: 5 seconds
  
  Interval: 30 seconds
    (Check toutes les 30 secondes)
  
  Success codes: 200
```

**Explication thresholds :**

```
Instance démarre:
  Check 1 (t=30s): FAIL (app pas encore prête)
  Check 2 (t=60s): FAIL
  Check 3 (t=90s): SUCCESS (app prête)
  Check 4 (t=120s): SUCCESS
  -> Healthy (2 succès consécutifs) [OK]

Instance a problème:
  Check 1: SUCCESS
  Check 2: FAIL (DB timeout)
  Check 3: FAIL
  Check 4: FAIL
  -> Unhealthy (3 échecs consécutifs) [X]
  -> ALB retire instance du pool
```

---

**Attributes (optionnel mais recommandé) :**

**Click "Attributes" en bas**

```
Deregistration delay: 300 seconds
  (Temps d'attendre avant de terminer connexions existantes)
  
  Instance à supprimer:
    - ALB arrête d'envoyer nouvelles requêtes
    - Attend 300s pour connexions en cours
    - Puis termine instance

Stickiness: Enabled
  Stickiness type: Load balancer generated cookie
  Stickiness duration: 3600 seconds (1 heure)
  
  User -> Instance A (cookie AWSALB=xxx)
  Requêtes suivantes -> Toujours Instance A (même cookie)
```

**Stickiness utile pour sessions (évite re-login) [OK]**

---

**Next**

---

**Register targets :**

**Pour l'instant, ne pas ajouter d'instances manuellement**

**L'Auto Scaling Group ajoutera automatiquement les instances [OK]**

**Create target group**

**[OK] Target Group créé !**

```
Target group ARN: arn:aws:elasticloadbalancing:...
State: unused (pas encore de targets)
Registered targets: 0
```

---

Je vais continuer dans le prochain message avec la création de l'ALB, l'Auto Scaling Group, les scaling policies, et les tests. Veux-tu que je continue ?

### ÉTAPE 5 : Créer l'Application Load Balancer

#### Créer le Load Balancer

**EC2 -> Load Balancers -> Create load balancer**

```
Load balancer types:

[x] Application Load Balancer
  HTTP and HTTPS traffic
  
Create
```

---

**Basic Configuration :**

```
Load balancer name: alb-production

Scheme:
  [x] Internet-facing
    (Accessible depuis internet)
  
  [ ] Internal
    (Seulement depuis VPC)

IP address type:
  [x] IPv4
```

---

**Network mapping :**

```
VPC: vpc-production

Mappings (Availability Zones):
  
  [x] eu-west-1a
    Subnet: subnet-public-1a (10.0.1.0/24)
  
  [x] eu-west-1b
    Subnet: subnet-public-1b (10.0.2.0/24)
  
  [ ] eu-west-1c (optionnel)
    Subnet: subnet-public-1c (10.0.3.0/24)
```

**[ATTENTION] ALB DOIT être dans subnets publics (avec route vers IGW)**

**[ATTENTION] Minimum 2 AZs pour haute disponibilité**

---

**Security groups :**

**Create new security group : sg-alb-production**

```
Inbound rules:
  Type: HTTP
  Port: 80
  Source: 0.0.0.0/0
  Description: Allow HTTP from internet
  
  Type: HTTPS
  Port: 443
  Source: 0.0.0.0/0
  Description: Allow HTTPS from internet

Outbound rules:
  Type: All traffic
  Destination: 0.0.0.0/0
```

**Ou si déjà créé, sélectionner sg-alb-production**

---

**Listeners and routing :**

**Listener 1 : HTTP**

```
Protocol: HTTP
Port: 80

Default action: Redirect to HTTPS
  Status code: 301 (Permanent redirect)
  Port: 443
  
  (Ou pour l'instant, Forward to target group)
```

**Pour l'instant, configurer :**

```
Default action: Forward to
  Target group: tg-flask-app-production
```

**On configurera HTTPS plus tard**

---

**Summary :**

```
Load balancer: alb-production
Scheme: Internet-facing
IP address type: IPv4
Availability Zones: 2 (eu-west-1a, eu-west-1b)
Security groups: sg-alb-production
Listeners: HTTP:80 -> tg-flask-app-production
```

**Create load balancer**

---

**Durée de création : 3-5 minutes**

**État :**

```
State: Provisioning -> Active [OK]
```

---

**DNS name :**

```
alb-production-1234567890.eu-west-1.elb.amazonaws.com
```

**[ATTENTION] ALB n'a PAS d'IP publique statique (seulement DNS)**

**Le DNS résout vers plusieurs IPs (multi-AZ)**

---

#### Configurer HTTPS (optionnel mais recommandé)

**Pour HTTPS, il faut un certificat SSL/TLS**

**Option 1 : AWS Certificate Manager (ACM) - Gratuit**

**Nécessite un domaine (ex: example.com)**

**ACM -> Request certificate**

```
Certificate type: Request a public certificate

Domain names:
  *.example.com (wildcard)
  example.com

Validation method: DNS validation
  (Ou Email validation si tu as accès aux emails)
```

**Request**

**Valider via DNS (ajouter CNAME dans Route 53)**

**Certificat status : Issued [OK]**

---

**Option 2 : Self-signed certificate (dev/test)**

```bash
# Générer certificat self-signed
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -subj "/C=FR/ST=Paris/L=Paris/O=SirrDev/CN=alb-production.local"

# Uploader dans ACM
aws acm import-certificate \
  --certificate fileb://selfsigned.crt \
  --private-key fileb://selfsigned.key
```

---

**Ajouter listener HTTPS sur ALB :**

**EC2 -> Load Balancers -> alb-production -> Listeners**

**Add listener**

```
Protocol: HTTPS
Port: 443

Default action: Forward to
  Target group: tg-flask-app-production

Secure listener settings:
  Security policy: ELBSecurityPolicy-TLS13-1-2-2021-06
    (TLS 1.2 et 1.3, sécurisé moderne)
  
  Default SSL/TLS certificate:
    From ACM: arn:aws:acm:...:certificate/xxx
    (Le certificat créé plus haut)
```

**Add**

---

**Modifier listener HTTP pour redirect vers HTTPS :**

**Listeners -> HTTP:80 -> Edit**

```
Default actions:
  Remove: Forward to tg-flask-app-production
  
  Add: Redirect to HTTPS
    Protocol: HTTPS
    Port: 443
    Status code: 301 - Moved Permanently
```

**Save changes**

---

**Maintenant :**

```
http://alb-production-xxx.elb.amazonaws.com
  -> Redirect 301 -> https://alb-production-xxx.elb.amazonaws.com
```

**[OK] HTTPS fonctionnel !**

---

#### Modifier Security Group des instances

**Les instances doivent accepter trafic DEPUIS l'ALB**

**EC2 -> Security Groups -> sg-app-tier -> Edit inbound rules**

```
Supprimer: HTTP (80) from 0.0.0.0/0

Ajouter:
  Type: HTTP
  Port: 80
  Source: sg-alb-production (security group de l'ALB)
  Description: Allow HTTP from ALB only
```

**Save rules**

**Maintenant, instances acceptent seulement trafic de l'ALB [OK]**

**Utilisateurs NE PEUVENT PAS accéder instances directement (seulement via ALB) [OK]**

---

### ÉTAPE 6 : Créer l'Auto Scaling Group

#### Créer l'Auto Scaling Group

**EC2 -> Auto Scaling Groups -> Create Auto Scaling group**

```
Auto Scaling group name: asg-flask-app-production

Launch template:
  lt-flask-app-production
  Version: Latest (ou Default)
```

**Next**

---

**Choose instance launch options :**

```
Network:
  VPC: vpc-production
  
  Availability Zones and subnets:
    [x] subnet-app-1a (10.0.11.0/24) | eu-west-1a
    [x] subnet-app-1b (10.0.12.0/24) | eu-west-1b
    [ ] subnet-app-1c (optionnel)

Instance type requirements: (optionnel)
  [ ] Override launch template
  
  (Laisser vide pour utiliser t3.micro du template)
```

**Next**

---

**Configure advanced options :**

```
Load balancing:
  [x] Attach to an existing load balancer
  
  Choose from your load balancer target groups:
    [x] tg-flask-app-production
  
  [ ] Attach to a new load balancer

VPC Lattice integration: (optionnel)
  [ ] No VPC Lattice service

Health checks:
  [x] Turn on Elastic Load Balancing health checks
  
  Health check grace period: 300 seconds
    (Temps d'attendre avant de commencer health checks)
    (Important : Donne le temps à l'instance de booter)

Additional settings:
  [x] Enable group metrics collection within CloudWatch
  
  [ ] Enable default instance warmup
    (Ou mettre 180 seconds si tu veux)
```

**Health check grace period expliqué :**

```
t=0: Instance démarre
t=0-300s: Grace period (pas de health checks)
t=300s: Premier health check
t=330s: Deuxième check (si 200 OK)
t=360s: Instance considérée healthy [OK]

Sans grace period:
  t=0: Instance démarre
  t=30s: Health check FAIL (app pas prête)
  t=60s: Health check FAIL
  t=90s: Health check FAIL
  -> Instance terminée (unhealthy) [X]
  -> Boucle infinie de création/suppression !
```

**Next**

---

**Configure group size and scaling :**

```
Group size:
  Desired capacity: 2
  Minimum capacity: 2
  Maximum capacity: 10

Scaling policies:
  [x] Target tracking scaling policy
  
  (On configurera les policies après)
  
  Pour l'instant, laisser:
    [ ] None
```

**Explication capacités :**

```
Minimum: 2
  -> Toujours AU MOINS 2 instances (haute dispo)
  -> Même si trafic = 0, garde 2 instances

Desired: 2
  -> État actuel/cible
  -> ASG ajuste pour atteindre ce nombre

Maximum: 10
  -> JAMAIS plus de 10 instances
  -> Limite de coût et de capacité
  
Scénario:
  Normal: 2 instances
  Peak (CPU élevé): Scale OUT -> 5 instances
  Ultra peak: Scale OUT -> 10 instances (max atteint)
  Nuit (CPU faible): Scale IN -> 2 instances (minimum atteint)
```

---

**Automatic scaling:
  Desired capacity: 2
  Min: 2, Max: 10

Instance maintenance policy: (nouveau)
  [ ] No policy
  
  (Ou configurer pour rolling updates)
```

**Next**

---

**Add notifications (optionnel) :**

```
[x] Add notification

SNS Topic: Create a topic
  Topic name: asg-events-production
  
  Email endpoints: ton-email@example.com

Events:
  [x] Launch
  [x] Terminate
  [x] Fail to launch
  [x] Fail to terminate
```

**Tu recevras email quand instances créées/supprimées [OK]**

**Next**

---

**Add tags :**

```
Key: Name
Value: flask-app-asg-instance
Tag new instances: Yes

Key: Environment
Value: production
Tag new instances: Yes

Key: ManagedBy
Value: AutoScaling
Tag new instances: Yes
```

**Next**

---

**Review :**

```
Auto Scaling group name: asg-flask-app-production
Launch template: lt-flask-app-production
Group size: Min=2, Desired=2, Max=10
Subnets: 2 (subnet-app-1a, subnet-app-1b)
Load balancing: tg-flask-app-production
Health checks: ELB enabled, grace period 300s
Notifications: SNS topic configured
```

**Create Auto Scaling group**

---

**[OK] Auto Scaling Group créé !**

**Durée : Immédiatement créé, puis lance 2 instances**

**Activity :**

```
Activity history:
  Launching a new EC2 instance: i-abc123 (subnet-app-1a)
  Launching a new EC2 instance: i-def456 (subnet-app-1b)
  
Status: Successful (après 5-10 min)
```

---

**Vérifier les instances :**

**EC2 -> Instances**

```
Instance 1:
  Instance ID: i-abc123
  Name: flask-app-asg-instance
  State: Running
  AZ: eu-west-1a
  Private IP: 10.0.11.25
  Public IP: (none)

Instance 2:
  Instance ID: i-def456
  Name: flask-app-asg-instance
  State: Running
  AZ: eu-west-1b
  Private IP: 10.0.12.18
  Public IP: (none)
```

**[OK] 2 instances lancées automatiquement !**

---

**Vérifier Target Group :**

**EC2 -> Target Groups -> tg-flask-app-production -> Targets**

```
Registered targets: 2

Instance ID    | AZ          | Health status
i-abc123       | eu-west-1a  | initial (-> healthy après ~5 min)
i-def456       | eu-west-1b  | initial (-> healthy après ~5 min)
```

**Attendre que health status = healthy [OK]**

**Health status transition :**

```
initial (0-300s: grace period)
  -> unhealthy (premiers checks échouent)
    -> healthy (2 checks consécutifs OK) [OK]
```

---

**Tester l'ALB :**

```bash
curl http://alb-production-1234567890.eu-west-1.elb.amazonaws.com/
```

**Résultat :**

```json
{
  "message": "Hello from Flask!",
  "hostname": "ip-10-0-11-25",
  "instance": "i-abc123"
}
```

**Requête suivante :**

```json
{
  "message": "Hello from Flask!",
  "hostname": "ip-10-0-12-18",
  "instance": "i-def456"
}
```

**Load balancer distribue entre instances [OK]**

---

**Tester health check :**

```bash
curl http://alb-production-xxx.elb.amazonaws.com/health
```

**Résultat :**

```json
{
  "status": "healthy",
  "timestamp": "2024-12-16T19:30:00",
  "checks": {
    "database": "ok",
    "disk": "ok: 35.2% used",
    "memory": "ok: 45.8% used"
  }
}
```

**[OK] Application fonctionnelle derrière ALB !**

---

### ÉTAPE 7 : Configurer les Scaling Policies

#### Types de Scaling Policies

**3 types de policies :**

| Type | Trigger | Use case | Exemple |
|------|---------|----------|---------|
| **Target Tracking** | Métrique cible | Maintenir CPU à 50% | CPU > 50% -> +1 instance |
| **Step Scaling** | Alarme CloudWatch | Scaling progressif | CPU 60-70% -> +1, 70-80% -> +2, 80%+ -> +3 |
| **Scheduled** | Horaire fixe | Traffic prévisible | Lundi-Vendredi 9h -> +5 instances |

**On va configurer les 3 types [OK]**

---

#### 1. Target Tracking Scaling Policy

**Maintient une métrique à une valeur cible**

**ASG -> asg-flask-app-production -> Automatic scaling -> Create dynamic scaling policy**

```
Policy type: Target tracking scaling

Scaling policy name: target-tracking-cpu-50

Metric type: Average CPU utilization

Target value: 50
  (Maintenir CPU moyen à 50%)

Instances need: 300 seconds
  (Warmup time avant que nouvelle instance compte dans métrique)

[x] Disable scale-in
  (Décocher : On veut scale IN aussi)
```

**Create**

---

**Comment ça marche :**

```
État actuel: 2 instances, CPU moyen = 30%
  -> Rien (en dessous de cible)

Traffic augmente: CPU moyen = 65%
  -> Scale OUT +1 instance (pour baisser CPU vers 50%)

Traffic continue: CPU = 70% avec 3 instances
  -> Scale OUT +1 instance (total 4)

Traffic diminue: CPU = 25% avec 4 instances
  -> Scale IN -1 instance (remonter CPU vers 50%)
  -> Scale IN -1 instance (total 2, minimum atteint)
```

**CloudWatch crée automatiquement 2 alarmes :**

```
AlarmHigh-asg-flask-app-production-xxx
  CPU > 50% -> Scale OUT

AlarmLow-asg-flask-app-production-xxx
  CPU < 50% (avec marge) -> Scale IN
```

**[OK] Target tracking policy créée !**

---

#### 2. Step Scaling Policy

**Scaling progressif selon seuils**

**Créer CloudWatch Alarm d'abord :**

**CloudWatch -> Alarms -> Create alarm**

```
Select metric:
  EC2 -> By Auto Scaling Group
  
  Metric name: CPUUtilization
  AutoScalingGroupName: asg-flask-app-production
  Statistic: Average
  Period: 1 minute
```

**Conditions :**

```
Threshold type: Static

Whenever CPUUtilization is...
  Greater than 70
```

**Actions :**

```
In alarm:
  [ ] Send notification to SNS (optionnel)
  
  (On configurera action dans scaling policy)
```

**Alarm name :**

```
asg-cpu-high-70
```

**Create alarm**

---

**Créer la Step Scaling Policy :**

**ASG -> Automatic scaling -> Create dynamic scaling policy**

```
Policy type: Step scaling

Scaling policy name: step-scaling-cpu-tiers

CloudWatch alarm: asg-cpu-high-70

Take the action:
  Add 1 capacity units when 70 <= CPU < 80
  Add 2 capacity units when 80 <= CPU < 90
  Add 3 capacity units when CPU >= 90

Instances need: 300 seconds warmup
```

**Create**

---

**Scénario :**

```
CPU = 75%:
  -> Alarm triggers
  -> Add 1 instance (70-80% tier)

CPU = 85% (après scale OUT):
  -> Add 2 instances (80-90% tier)
  -> Total = Original + 1 + 2 = 3 instances supplémentaires

CPU = 95%:
  -> Add 3 instances (90%+ tier)
```

**Scaling plus agressif pour pics de traffic [OK]**

---

**Créer aussi Scale IN step policy :**

**CloudWatch alarm :**

```
Metric: CPUUtilization
Condition: Less than 30
Alarm name: asg-cpu-low-30
```

**Step scaling policy :**

```
Policy name: step-scaling-cpu-low

Take the action:
  Remove 1 capacity units when 20 <= CPU < 30
  Remove 2 capacity units when CPU < 20

Wait: 300 seconds
```

---

#### 3. Scheduled Scaling

**Ajuster capacité selon horaire prévisible**

**Use case : Application B2B (traffic bureau seulement)**

```
Lundi-Vendredi 9h-18h: Peak traffic
Nuit et weekend: Trafic minimal
```

**ASG -> Scheduled actions -> Create scheduled action**

---

**Action 1 : Scale OUT le matin**

```
Name: morning-scale-out

Desired capacity: 5
Min: 2
Max: 10

Recurrence: Cron expression
  0 7 * * 1-5
  
  (Tous les jours ouvrables à 7h UTC = 8h Paris hiver)

Time zone: Europe/Paris
```

**Create**

---

**Action 2 : Scale IN le soir**

```
Name: evening-scale-in

Desired capacity: 2
Min: 2
Max: 10

Recurrence: 0 19 * * 1-5
  (19h UTC = 20h Paris hiver)
```

**Create**

---

**Résultat :**

```
7h00: Desired = 5 (augmente de 2 -> 5)
  -> 3 nouvelles instances lancées

19h00: Desired = 2 (diminue de 5 -> 2)
  -> 3 instances terminées

Weekend: Reste à 2 (minimum)
```

**Économie : 8$/jour (12h × 3 instances × 0.0208$/h) [OK]**

---

### ÉTAPE 8 : Configurer CloudWatch Monitoring

#### Métriques importantes à surveiller

**Auto Scaling Group :**

```
GroupDesiredCapacity: Nombre cible d'instances
GroupInServiceInstances: Instances actuellement running
GroupMinSize / GroupMaxSize: Limites configurées
GroupPendingInstances: Instances en cours de lancement
GroupTerminatingInstances: Instances en cours de suppression
```

**Application Load Balancer :**

```
TargetResponseTime: Latence des instances (ms)
HealthyHostCount: Nombre d'instances healthy
UnHealthyHostCount: Nombre d'instances unhealthy
RequestCount: Nombre de requêtes total
HTTPCode_Target_2XX_Count: Réponses 2xx (succès)
HTTPCode_Target_4XX_Count: Réponses 4xx (erreurs client)
HTTPCode_Target_5XX_Count: Réponses 5xx (erreurs serveur)
ActiveConnectionCount: Connexions actives
ProcessedBytes: Data transfer
```

**EC2 Instances :**

```
CPUUtilization: % CPU utilisé
NetworkIn / NetworkOut: Trafic réseau
StatusCheckFailed: Checks système échoués
```

---

#### Créer un Dashboard CloudWatch

**CloudWatch -> Dashboards -> Create dashboard**

```
Dashboard name: Production-LoadBalancing-Monitoring
```

**Create dashboard**

---

**Add widget 1 : ASG Capacity**

**Add widget -> Line**

```
Metrics:
  Auto Scaling -> Group Metrics
  
  [x] GroupDesiredCapacity (asg-flask-app-production)
  [x] GroupInServiceInstances
  [x] GroupMinSize
  [x] GroupMaxSize

Graph options:
  Title: Auto Scaling Group Capacity
  Y-axis: Count (instances)
  Legend: Bottom
```

**Create widget**

---

**Add widget 2 : ALB Latency**

```
Metrics:
  ApplicationELB -> Per AppELB Metrics
  
  [x] TargetResponseTime (alb-production)
  Statistic: Average

Graph options:
  Title: Application Response Time
  Y-axis: Milliseconds
```

---

**Add widget 3 : ALB Request Count**

```
Metrics:
  [x] RequestCount (alb-production)
  Statistic: Sum
  Period: 1 minute

Graph options:
  Title: Requests per Minute
```

---

**Add widget 4 : Healthy Hosts**

```
Metrics:
  TargetGroups -> Per TG Metrics
  
  [x] HealthyHostCount (tg-flask-app-production)
  [x] UnHealthyHostCount

Graph options:
  Title: Target Health Status
```

---

**Add widget 5 : EC2 CPU (all instances)**

```
Metrics:
  EC2 -> By Auto Scaling Group
  
  [x] CPUUtilization (asg-flask-app-production)
  Statistic: Average

Graph options:
  Title: Average CPU Utilization
```

---

**Add widget 6 : HTTP Status Codes**

```
Metrics:
  [x] HTTPCode_Target_2XX_Count
  [x] HTTPCode_Target_4XX_Count
  [x] HTTPCode_Target_5XX_Count
  
  Statistic: Sum
  Period: 1 minute

Graph options:
  Title: HTTP Response Codes
  Stacked: Yes
```

---

**Dashboard complet [OK]**

**Save dashboard**

---

#### Créer des Alarmes CloudWatch

**Alarme 1 : High Response Time**

**CloudWatch -> Alarms -> Create alarm**

```
Metric:
  ApplicationELB -> Per AppELB Metrics
  TargetResponseTime (alb-production)
  Statistic: Average
  Period: 1 minute

Conditions:
  Threshold type: Static
  Whenever TargetResponseTime is...
    Greater than: 1000 (milliseconds = 1 seconde)
  
  Datapoints to alarm: 2 out of 3
    (2 points sur 3 périodes consécutives)

Actions:
  In alarm:
    Send notification to: SNS topic
    Topic: production-alerts
    Email: ton-email@example.com

Alarm name: alb-high-latency-1sec
```

**Create alarm**

---

**Alarme 2 : No Healthy Targets**

```
Metric: HealthyHostCount (tg-flask-app-production)
Statistic: Minimum
Period: 1 minute

Conditions:
  Lower than: 1
  (Moins de 1 instance healthy = critique)

Alarm name: alb-no-healthy-targets
```

---

**Alarme 3 : High 5xx Errors**

```
Metric: HTTPCode_Target_5XX_Count
Statistic: Sum
Period: 5 minutes

Conditions:
  Greater than: 10
  (Plus de 10 erreurs 5xx en 5 min)

Alarm name: alb-high-5xx-errors
```

---

**Alarme 4 : ASG At Max Capacity**

```
Metric: GroupDesiredCapacity (asg-flask-app-production)
Statistic: Maximum
Period: 1 minute

Conditions:
  Greater/Equal: 10
  (Atteint capacité max = besoin scale davantage)

Alarm name: asg-at-max-capacity
```

---

**Tu recevras emails quand alarmes déclenchées [OK]**

---

### ÉTAPE 9 : Tester le Scaling

#### Test 1 : Scaling manuel

**Modifier desired capacity manuellement :**

**ASG -> asg-flask-app-production -> Group details -> Edit**

```
Desired capacity: 4
```

**Update**

---

**Activity history :**

```
Launching a new EC2 instance: i-ghi789 (subnet-app-1a)
Launching a new EC2 instance: i-jkl012 (subnet-app-1b)

Status: Successful
```

**Après 5-10 min : 4 instances running [OK]**

---

**Target Group :**

```
Registered targets: 4

All healthy [OK]
```

---

**Tester load balancing :**

```bash
for i in {1..10}; do
  curl -s http://alb-production-xxx.elb.amazonaws.com/info | jq '.instance_id'
done
```

**Résultat :**

```
"i-abc123"
"i-def456"
"i-ghi789"
"i-jkl012"
"i-abc123"
"i-def456"
...
```

**Trafic distribué entre 4 instances [OK]**

---

**Rescale down :**

```
Desired capacity: 2
```

**Activity :**

```
Terminating EC2 instance: i-ghi789
Terminating EC2 instance: i-jkl012

Reason: A user request explicitly set group desired capacity changing the desired capacity from 4 to 2.
```

**Après 5 min : 2 instances seulement [OK]**

---

#### Test 2 : Stress test (CPU spike)

**Installer stress tool sur instances :**

**Se connecter à une instance (via bastion) :**

```bash
ssh -J bastion-prod ubuntu@10.0.11.25
```

**Installer stress :**

```bash
sudo yum install stress -y  # Amazon Linux
# Ou
sudo apt install stress -y  # Ubuntu
```

---

**Générer charge CPU :**

```bash
# Utiliser 100% CPU sur tous les cores pendant 5 minutes
stress --cpu 8 --timeout 300s
```

**Monitoring :**

**CloudWatch -> Metrics -> EC2 -> CPUUtilization**

**Graphique montre CPU -> 100% [OK]**

---

**Auto Scaling réagit :**

**Après 1-2 minutes (dépend de alarm period) :**

```
CloudWatch Alarm: AlarmHigh-asg-xxx -> In alarm
  Reason: CPUUtilization > 70%

Auto Scaling Activity:
  Launching a new EC2 instance: i-mno345
  Reason: Instance was launched in response to a target tracking scaling policy.
  
  Launching a new EC2 instance: i-pqr678
```

**Desired capacity augmente : 2 -> 3 -> 4 [OK]**

---

**Arrêter stress :**

```bash
# Ctrl+C ou attendre timeout
```

**CPU redescend : 100% -> 30% -> 10%**

---

**Scale IN automatique :**

**Après ~10 minutes (scale in plus conservateur) :**

```
CloudWatch Alarm: AlarmLow-asg-xxx -> In alarm
  Reason: CPUUtilization < 30%

Auto Scaling Activity:
  Terminating EC2 instance: i-pqr678
  Reason: Instance was terminated in response to a target tracking scaling policy.
```

**Desired capacity diminue : 4 -> 3 -> 2 (minimum) [OK]**

---

#### Test 3 : Load testing avec Apache Bench

**Installer Apache Bench (ab) :**

```bash
# Sur ton PC ou instance bastion
sudo apt install apache2-utils -y  # Ubuntu
# Ou
sudo yum install httpd-tools -y    # Amazon Linux
```

---

**Test simple (100 requêtes) :**

```bash
ab -n 100 -c 10 http://alb-production-xxx.elb.amazonaws.com/
```

**Paramètres :**
- `-n 100` : 100 requêtes total
- `-c 10` : 10 requêtes concurrentes

**Résultat :**

```
Concurrency Level:      10
Time taken for tests:   2.345 seconds
Complete requests:      100
Failed requests:        0
Total transferred:      25600 bytes
Requests per second:    42.64 [#/sec] (mean)
Time per request:       234.5 [ms] (mean)
Time per request:       23.45 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%    215
  66%    230
  75%    245
  80%    255
  90%    280
  95%    310
  98%    345
  99%    380
 100%    450 (longest request)
```

**[OK] Load Balancer gère bien le trafic**

---

**Test intensif (déclencher scaling) :**

```bash
# 10,000 requêtes, 100 concurrent
ab -n 10000 -c 100 http://alb-production-xxx.elb.amazonaws.com/

# Ou stress pendant 5 minutes
ab -t 300 -c 200 http://alb-production-xxx.elb.amazonaws.com/
```

**Monitoring CloudWatch pendant le test :**

```
RequestCount: 2000+/min (spike)
TargetResponseTime: 500ms (augmente)
CPUUtilization: 80% (instances surchargées)

Auto Scaling:
  Launching instances...
  Desired: 2 -> 5 -> 8

Après scale OUT:
  TargetResponseTime: 150ms (baisse, plus d'instances)
  CPUUtilization: 40% (réparti sur plus d'instances)
```

**[OK] Auto Scaling maintient performance [OK]**

---

#### Test 4 : Instance failure

**Simuler instance failure :**

**Méthode 1 : Arrêter l'app**

```bash
# Sur instance
sudo systemctl stop flask-app
```

**Health check :**

```
/health endpoint -> Connection refused
Target Group: healthy -> unhealthy
```

---

**Méthode 2 : Terminer instance directement**

```
EC2 -> Instances -> i-abc123 -> Terminate
```

**ASG détecte :**

```
Activity:
  Terminating EC2 instance: i-abc123
  Reason: Instance failed a health check.
  
  Launching a new EC2 instance: i-stu901
  Reason: Instance was launched to replace an unhealthy instance.
```

**Desired capacity maintenue : 2 instances [OK]**

**Nouvelle instance remplace automatiquement [OK]**

---

**Pendant le remplacement :**

```
t=0: 2 instances healthy
t=1min: Instance A unhealthy, ASG lance Instance C
t=5min: Instance C booting (grace period)
t=8min: Instance C healthy
t=8min: Instance A terminée
  
Résultat: 2 instances healthy (Instance B + Instance C)
```

**Zero downtime : Instance B continue de servir pendant remplacement [OK]**

---

### ÉTAPE 10 : Zero-Downtime Deployments

#### Stratégies de déploiement

**4 stratégies principales :**

| Stratégie | Downtime | Risk | Rollback | Coût |
|-----------|----------|------|----------|------|
| **In-place** | [X] Oui | High | Hard | Low |
| **Rolling** | [OK] Non | Medium | Medium | Low |
| **Blue/Green** | [OK] Non | Low | Instant | High (2×) |
| **Canary** | [OK] Non | Very Low | Easy | Medium |

---

#### Stratégie 1 : Rolling Deployment (ASG)

**Déployer nouvelle version progressivement**

**Update Launch Template avec nouvelle version :**

**EC2 -> Launch Templates -> lt-flask-app-production -> Actions -> Modify template (Create new version)**

```
Source template version: 1

Changes:
  User data: (nouvelle version app)
  
  # Changer dans le user data:
  git clone -b v2.0 https://github.com/.../flask-todo-app.git
  # Ou
  aws s3 cp s3://sirrdev-deployments/flask-app-v2.tar.gz .

Template version description: v2 - New features
```

**Create template version**

**Version 2 créée [OK]**

---

**Configurer Instance Refresh (rolling update) :**

**ASG -> asg-flask-app-production -> Instance refresh -> Start instance refresh**

```
Settings:
  Minimum healthy percentage: 50%
    (Toujours garder 50% instances up)
  
  Instance warmup: 300 seconds
    (Temps avant de considérer instance ready)
  
  Skip matching: No
    (Forcer remplacement même si version identique)

Checkpoints:
  [x] Enable checkpoints
  Checkpoint delay: 3600 seconds
    (Pause après chaque batch pour validation)
  
  Checkpoint percentages: 50, 100
    (Pause après 50% et 100%)
```

**Start**

---

**Déroulement :**

```
État initial: 2 instances (v1)

Batch 1 (50%):
  t=0: Terminate instance A (v1)
  t=0: Launch instance C (v2)
  t=5min: Instance C healthy [OK]
  t=5min: CHECKPOINT -> Pause 1h pour validation
  
Validation manuelle:
  Tester instance C avec nouvelle version
  Si OK -> Continue
  Si NOK -> Cancel (rollback automatique)

Batch 2 (50% restant):
  t=1h: Terminate instance B (v1)
  t=1h: Launch instance D (v2)
  t=1h05: Instance D healthy [OK]
  
État final: 2 instances (v2) [OK]
```

**Pendant toute l'opération : Au moins 1 instance up [OK]**

---

**Monitoring Instance Refresh :**

**ASG -> Instance refresh tab**

```
Status: In progress
  Successful: 1/2 (50%)
  Failed: 0
  Waiting for checkpoint: Yes
  
Actions:
  [Continue] [Cancel] [Rollback]
```

---

**Si problème détecté :**

**Click "Cancel and rollback"**

```
Rolling back:
  Terminate new instances (v2)
  Launch old instances (v1)
  
Retour à état initial [OK]
```

---

#### Stratégie 2 : Blue/Green Deployment

**Déployer version parallèle complète**

**Créer 2ème Auto Scaling Group (Green) :**

```
Name: asg-flask-app-production-green
Launch template: lt-flask-app-production (version 2)
Desired: 2
Min: 2, Max: 10
Target group: tg-flask-app-production-green (nouveau)
```

---

**Tester version Green isolément :**

**Créer ALB listener rule temporaire :**

```
ALB -> Listeners -> HTTP:80 -> View/edit rules

Add rule:
  IF Host header is test.example.com
  THEN Forward to tg-flask-app-production-green

(Ou IF Query string version=green)
```

**Tester :**

```bash
curl -H "Host: test.example.com" http://alb-xxx.elb.amazonaws.com/
# Ou
curl http://alb-xxx.elb.amazonaws.com/?version=green
```

**Valider nouvelle version [OK]**

---

**Basculer traffic de Blue vers Green :**

```
ALB -> Listeners -> HTTP:80 -> Edit default action

Old: Forward to tg-flask-app-production (Blue)
New: Forward to tg-flask-app-production-green (Green)

Save
```

**100% trafic -> Green instantanément [OK]**

---

**Rollback instantané si problème :**

```
Edit default action:
  Forward to tg-flask-app-production (Blue)

Save
```

**Retour à Blue en <1 seconde [OK]**

---

**Nettoyer après validation complète :**

```
1. Supprimer ASG Blue
2. Renommer Green -> Blue
3. Supprimer target group Blue
```

---

#### Stratégie 3 : Canary Deployment

**Déployer vers petit % utilisateurs d'abord**

**Weighted target groups :**

**ALB -> Listeners -> HTTP:80 -> View/edit rules**

```
Default action -> Edit

Forward to:
  tg-flask-app-production (Blue): Weight 90%
  tg-flask-app-production-green (Canary): Weight 10%

Enable group-level stickiness: Yes
```

**Save**

---

**Résultat :**

```
90% users -> Blue (v1 stable)
10% users -> Green (v2 canary)
```

**Monitoring canary :**

```
CloudWatch -> Metrics -> Target Groups
  Compare:
    HTTPCode_Target_5XX_Count (Blue vs Green)
    TargetResponseTime (Blue vs Green)
```

**Si metrics similaires -> Augmenter canary**

---

**Progression graduelle :**

```
Phase 1: Blue 90%, Green 10%  -> Monitoring 1h
Phase 2: Blue 70%, Green 30%  -> Monitoring 1h
Phase 3: Blue 50%, Green 50%  -> Monitoring 2h
Phase 4: Blue 0%, Green 100%  -> Full deployment [OK]
```

**Si problème : Revenir phase précédente [OK]**

---

### ÉTAPE 11 : Optimisation des coûts ASG

#### Analyser les coûts

**Cost Explorer -> Filters**

```
Service: Amazon EC2
Usage type: BoxUsage (running instances)
Tag: ManagedBy = AutoScaling

Time: Last month
Group by: Instance type
```

**Résultat exemple :**

```
t3.micro BoxUsage: 1440 hours (2 instances × 720h)
  Cost: 0$ (Free Tier)
  
After Free Tier: 1440h × 0.0104$/h = 14.98$/month
```

---

#### Stratégies d'optimisation

**1. Right-sizing instances**

**Vérifier utilisation CPU/Memory réelle :**

```
CloudWatch -> Metrics -> EC2 -> CPUUtilization
  Average over 30 days: 25%
  
CloudWatch Insights -> Query:
  fields @timestamp, @message
  | filter MetricName = "MemoryUtilization"
  | stats avg(Value) as AvgMemory
  
Result: 40% memory used
```

**Analyse :**

```
t3.micro: 1 vCPU, 1 GB RAM
  CPU used: 25% -> 0.25 vCPU needed
  Memory used: 0.4 GB -> OK
  
Conclusion: t3.micro correct [OK] (pas de downsize possible)
```

**Si surdimensionné :**

```
t3.medium: 2 vCPU, 4 GB RAM
  CPU: 15% -> 0.3 vCPU needed
  Memory: 25% -> 1 GB needed
  
Downsize to: t3.micro
Économie: 0.0416$/h - 0.0104$/h = 0.0312$/h
  -> 22.46$/month saved [OK]
```

---

**2. Spot Instances (jusqu'à 90% d'économie)**

**Modifier Launch Template pour mix On-Demand + Spot :**

**ASG -> asg-flask-app-production -> Edit**

```
Scroll to: Purchase options and instance types

[x] Combine purchase options and instance types

On-Demand base: 1
  (Toujours 1 On-Demand minimum pour stabilité)

On-Demand percentage above base: 0%
  (Reste en Spot)

Spot allocation strategy: Lowest price
  (Ou capacity-optimized pour stability)

Instance types:
  [x] t3.micro
  [x] t3a.micro (AMD, moins cher)
  [x] t2.micro (fallback)

Capacity Rebalancing: Enable
  (Remplace instances Spot risquant termination)
```

**Save**

---

**Résultat :**

```
Configuration:
  Desired: 3 instances
  
  Instance 1: t3.micro On-Demand (0.0104$/h)
  Instance 2: t3.micro Spot (0.0031$/h, 70% discount)
  Instance 3: t3a.micro Spot (0.0028$/h, 73% discount)

Coût:
  On-Demand only: 3 × 0.0104$/h = 0.0312$/h (22.46$/month)
  Mixed: 0.0104 + 0.0031 + 0.0028 = 0.0163$/h (11.74$/month)
  
Économie: 47.7% [OK]
```

**[ATTENTION] Spot instances peuvent être terminées (2 min notice)**

**Recommandation :**
- Production critique : 50% On-Demand, 50% Spot
- Dev/Test : 100% Spot

---

**3. Reserved Instances (1-3 ans)**

**Si usage stable (baseline instances) :**

```
Baseline: 2 instances 24/7 (toujours running)
Peak: +5 instances (seulement quelques heures/jour)

Stratégie:
  Reserve 2 × t3.micro (1 year, No upfront)
  Peak: On-Demand ou Spot

Reserved: 2 × 0.0062$/h = 0.0124$/h (8.93$/month)
On-Demand: 2 × 0.0104$/h = 0.0208$/h (14.98$/month)

Économie: 40% sur baseline [OK]
```

---

**4. Savings Plans (flexible)**

**Alternative aux Reserved Instances :**

```
Commit to: 10$/month compute usage (any instance type/region)
Discount: 30-40% vs On-Demand

Avantage: Pas besoin de choisir instance type à l'avance
```

**Acheter :**

```
AWS Cost Explorer -> Savings Plans -> Purchase Savings Plans
  Commitment: 10$/month
  Term: 1 year
  Payment: No upfront
```

---

**5. Ajuster Min/Max selon usage**

**Si traffic prévisible :**

```
Business hours (9h-18h): Min=5, Max=10
Night/Weekend: Min=2, Max=5
```

**Utiliser scheduled scaling :**

```
Scheduled action 1:
  Cron: 0 7 * * 1-5 (9h Paris)
  Min: 5, Desired: 5, Max: 10

Scheduled action 2:
  Cron: 0 19 * * 1-5 (21h Paris)
  Min: 2, Desired: 2, Max: 5
```

**Économie : ~30% vs baseline constant [OK]**

---

**6. Arrêter instances dev la nuit**

**Pour environnement dev/staging (pas prod) :**

**Lambda function pour arrêter/démarrer ASG :**

```python
import boto3

asg = boto3.client('autoscaling')

def lambda_handler(event, context):
    action = event['action']  # 'stop' ou 'start'
    
    if action == 'stop':
        # Sauvegarder desired capacity
        response = asg.describe_auto_scaling_groups(
            AutoScalingGroupNames=['asg-flask-app-staging']
        )
        desired = response['AutoScalingGroups'][0]['DesiredCapacity']
        
        # Stocker dans Parameter Store
        ssm = boto3.client('ssm')
        ssm.put_parameter(
            Name='/asg/staging/previous-desired',
            Value=str(desired),
            Type='String',
            Overwrite=True
        )
        
        # Scale to 0
        asg.update_auto_scaling_group(
            AutoScalingGroupName='asg-flask-app-staging',
            MinSize=0,
            DesiredCapacity=0
        )
    
    elif action == 'start':
        # Récupérer desired capacity précédente
        ssm = boto3.client('ssm')
        desired = int(ssm.get_parameter(
            Name='/asg/staging/previous-desired'
        )['Parameter']['Value'])
        
        # Restaurer
        asg.update_auto_scaling_group(
            AutoScalingGroupName='asg-flask-app-staging',
            MinSize=2,
            DesiredCapacity=desired
        )
```

**EventBridge rules :**

```
Rule 1: cron(0 19 * * ? *)  -> Lambda(action=stop)
Rule 2: cron(0 7 * * ? *)   -> Lambda(action=start)
```

**Économie : 14h/jour × 30 jours = ~58% [OK]**

---

**Budget mensuel optimisé :**

```
Production (baseline optimisé):
  2 × Reserved t3.micro: 9$/month
  3 × Spot t3.micro (peak): 3$/month
  ALB: 16$/month
  Data transfer: 2$/month
  Total: ~30$/month

vs Architecture statique:
  1 × t3.medium 24/7: 30$/month
  Pas de HA, pas de scaling

Résultat: Même coût mais 10× plus robuste [OK]
```

---

### [OK] TESTS DE VALIDATION

**Infrastructure :**
- [ ] Application Load Balancer créé et actif
- [ ] Target Group créé avec health checks configurés
- [ ] Launch Template créé avec user data
- [ ] Auto Scaling Group déployé (min 2 instances)
- [ ] Security Groups configurés (ALB -> instances)

**Load Balancing :**
- [ ] ALB distribue trafic entre instances
- [ ] Health checks détectent instances unhealthy
- [ ] Sticky sessions fonctionnelles (si activées)
- [ ] HTTPS configuré avec certificat
- [ ] HTTP redirect vers HTTPS

**Auto Scaling :**
- [ ] Target tracking policy fonctionnelle
- [ ] Step scaling policy configurée
- [ ] Scheduled scaling configurée
- [ ] Scale OUT automatique (CPU > 70%)
- [ ] Scale IN automatique (CPU < 30%)
- [ ] Instances remplacées automatiquement si unhealthy

**Monitoring :**
- [ ] CloudWatch Dashboard créé
- [ ] Alarmes configurées (latency, health, errors)
- [ ] Notifications SNS fonctionnelles
- [ ] Métriques visibles (ASG, ALB, EC2)

**Tests :**
- [ ] Load testing avec Apache Bench réussi
- [ ] Stress test CPU déclenche scaling
- [ ] Instance failure -> Remplacement automatique
- [ ] Zero-downtime deployment testé
- [ ] Rollback testé

**Coûts :**
- [ ] Spot instances configurées (si applicable)
- [ ] Scheduled scaling réduit coûts
- [ ] Budget alert configurée
- [ ] Right-sizing validé

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Instances unhealthy en boucle

**Symptôme :**

```
Activity:
  Launching instance i-abc
  Instance failed health check
  Terminating instance i-abc
  Launching instance i-def
  Instance failed health check
  ...
```

**Causes possibles :**

**1. Health check grace period trop court**

```
Grace period: 60 seconds
App boot time: 120 seconds

-> Instance terminée avant d'être prête [X]
```

**Solution :**

```
ASG -> Edit
  Health check grace period: 300 seconds [OK]
```

---

**2. Health check path incorrect**

```
Target Group health check: /health
App n'a pas de /health endpoint [X]
```

**Solution :**

```
Ajouter endpoint /health dans l'app
Ou changer health check path: /
```

---

**3. Security Group bloque health checks**

```
Instance SG:
  Inbound: HTTP (80) from 0.0.0.0/0 [X] (ALB pas dans 0.0.0.0/0)
```

**Solution :**

```
Inbound: HTTP (80) from sg-alb-production [OK]
```

---

**4. App ne démarre pas (user data bug)**

**Vérifier logs user data :**

```bash
ssh ubuntu@instance-ip
sudo cat /var/log/user-data.log
```

**Erreur typique :**

```
Error: pip: command not found
```

**Fix user data :**

```bash
#!/bin/bash
yum install -y python3-pip  # <- Manquait
pip3 install flask
...
```

---

#### Erreur 2 : Scaling ne se déclenche pas

**Symptôme :**

```
CPU = 85%
Pas de scale OUT
```

**Diagnostic :**

**1. Vérifier CloudWatch alarm**

```
CloudWatch -> Alarms -> asg-cpu-high-70

State: OK (devrait être In alarm) [X]
```

**Raison possible :**

```
Alarm condition: CPU > 70% for 3 datapoints out of 3

Réalité:
  t=1min: CPU 85% (1/3)
  t=2min: CPU 75% (2/3)
  t=3min: CPU 65% (0/3, reset)
  
-> Jamais 3 consécutifs [X]
```

**Solution :**

```
Change to: 2 out of 3 datapoints [OK]
```

---

**2. ASG at max capacity**

```
Current: 10 instances
Max: 10
CPU: 90%

-> Cannot scale further [X]
```

**Solution :**

```
Augmenter Max capacity: 15
```

---

**3. Cooldown period**

```
t=0: Scale OUT +1 (desired 2->3)
t=1min: CPU still high, need more
  -> Cooldown active (must wait 5 min)
t=5min: Scale OUT +1 (desired 3->4)
```

**Ajuster cooldown :**

```
Default cooldown: 300 seconds
Reduce to: 180 seconds (si scaling lent)
```

---

#### Erreur 3 : ALB retourne 502/503

**Symptôme :**

```
curl http://alb-xxx.elb.amazonaws.com/
HTTP/1.1 502 Bad Gateway
```

**Causes :**

**1. Aucune instance healthy**

```
Target Group -> Targets
  Healthy: 0
  Unhealthy: 2 [X]
```

**Check health status reason :**

```
Health status details: Connection timeout
```

**-> Security Group bloque ALB -> instances**

**Solution :**

```
Instance SG:
  Inbound: HTTP (80) from sg-alb-production
```

---

**2. App crashed**

```bash
ssh instance
sudo systemctl status flask-app

[BLACK_CIRCLE] flask-app.service - Flask Todo Application
   Active: failed (Result: exit-code)
```

**Check logs :**

```bash
sudo journalctl -u flask-app -n 50
```

**Fix app et restart :**

```bash
sudo systemctl restart flask-app
```

---

**3. App écoute sur mauvais port**

```python
app.run(host='0.0.0.0', port=5000)  # [X] ALB envoie sur port 80
```

**Fix :**

```python
app.run(host='0.0.0.0', port=80)  # [OK]
```

**Ou changer Target Group port : 5000**

---

#### Erreur 4 : Spot instances terminées

**Symptôme :**

```
EC2 Spot Instance Interruption Notice
Your Spot instance will be terminated in 2 minutes.
```

**Cause : Prix Spot augmenté (demande haute)**

**Solution automatique (si enabled) :**

```
ASG Capacity Rebalancing: Enabled

Actions:
  1. ASG détecte interruption warning
  2. Lance nouvelle instance (Spot ou On-Demand)
  3. Attend qu'elle soit healthy
  4. Laisse ancienne instance terminer gracieusement
  
-> Pas de downtime [OK]
```

---

**Si pas enabled :**

```
Enable Capacity Rebalancing:
  ASG -> Edit -> Capacity Rebalancing: Yes
```

---

**Backup : Mix On-Demand + Spot**

```
On-Demand base: 2
  (Toujours 2 On-Demand stables)

Spot: instances au-dessus
  (Peak traffic seulement)
```

---

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

**Load Balancing :**
- ALB = Layer 7 (HTTP/HTTPS)
- Distribue trafic entre instances
- Health checks automatiques
- SSL/TLS termination
- Path/host-based routing
- Sticky sessions pour sessions

**Auto Scaling :**
- Ajuste capacité automatiquement
- Min/Desired/Max capacity
- Multi-AZ pour haute dispo
- Health checks EC2 + ELB
- Grace period crucial

**Scaling Policies :**
- Target Tracking : Simple, maintient métrique
- Step Scaling : Contrôle fin, tiers
- Scheduled : Traffic prévisible

**Zero-downtime Deployments :**
- Rolling : Remplacement progressif
- Blue/Green : Environnements parallèles
- Canary : Déploiement graduel

**Coûts :**
- ALB : ~16$/mois (fixed)
- Instances : Optimiser avec Spot/Reserved
- Right-sizing crucial
- Scheduled scaling économise 30-50%

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. AWS App Mesh (Service Mesh)**

**Microservices communication layer**

```
Service A -> App Mesh
  -> Traffic routing
  -> Retry logic
  -> Circuit breaker
    -> Service B
```

**Features :**
- Traffic shifting (canary)
- Outlier detection
- Distributed tracing
- mTLS encryption

---

**2. AWS Global Accelerator**

**Améliorer latence globale**

```
User (Asia)
  -> Anycast IP (Global Accelerator)
    -> AWS Edge Location (nearest)
      -> ALB (ap-southeast-1)
        -> Instances
```

**vs CloudFront :**
- Global Accelerator : TCP/UDP (Layer 4)
- CloudFront : HTTP/HTTPS cache

---

**3. Network Load Balancer (NLB)**

**Ultra low latency**

```
Gaming server / Trading platform
  -> NLB (< 100 µs latency)
    -> Instances (preserve source IP)
```

**Use cases :**
- Millions req/sec
- Static IP needed
- TCP/UDP (non-HTTP)

---

**4. Target Group Weighting**

**Traffic distribution avancée**

```
ALB Listener:
  Forward to:
    TG-v1: 70% weight
    TG-v2: 20% weight
    TG-canary: 10% weight
```

**A/B testing, progressive rollout**

---

**5. Lambda Target Groups**

**Serverless behind ALB**

```
ALB
  -> /api/users -> Lambda function
  -> /api/orders -> Lambda function
  -> /static -> S3
```

**Combine EC2 + Lambda [OK]**

---

**6. AWS Elastic Beanstalk**

**PaaS abstraction sur ALB + ASG**

```
Upload code -> Beanstalk
  -> Crée automatiquement:
    - ALB
    - ASG
    - Launch template
    - CloudWatch
    - RDS (optionnel)
```

**Trade-off : Moins de contrôle, plus simple**

---

## [COURS] CONCLUSION DE L'EXERCICE 6

**[BRAVO] EXCELLENT ! Tu maîtrises maintenant Load Balancing et Auto Scaling ! [BRAVO]**

**Ce que tu as appris :**
- [OK] Application Load Balancer (ALB)
- [OK] Target Groups avec health checks
- [OK] Launch Templates
- [OK] Auto Scaling Groups
- [OK] Scaling policies (Target, Step, Scheduled)
- [OK] CloudWatch monitoring et alarmes
- [OK] Zero-downtime deployments
- [OK] Load testing et stress testing
- [OK] Optimisation des coûts
- [OK] Troubleshooting scaling

**Compétences acquises :**
- [OK] High availability architecture
- [OK] Elastic infrastructure
- [OK] Performance optimization
- [OK] Cost optimization
- [OK] Production-grade deployments

**Architecture déployée :**
```
[OK] Application Load Balancer (multi-AZ)
[OK] Target Group avec health checks
[OK] Auto Scaling Group (2-10 instances)
[OK] 3 scaling policies
[OK] CloudWatch dashboard + 5 alarmes
[OK] Zero-downtime deployment strategy
[OK] Cost-optimized avec Spot/Scheduled
```

**Performance :**
- Latence : < 200ms
- Availability : 99.9%+
- Scalabilité : 2 -> 10 instances automatique
- Coût : ~30$/mois optimisé

**Temps moyen de réalisation :** 5-6 heures

**Prochaine étape :** Exercice 7 - Containers (ECS, Fargate, Docker) ! [DOCKER]

---

**[ATTENTION] NETTOYAGE DES RESSOURCES**

```bash
# 1. Supprimer Auto Scaling Group
EC2 -> Auto Scaling Groups -> asg-flask-app-production -> Delete
  (Coche "Force delete" pour terminer instances)

# 2. Supprimer Launch Template
EC2 -> Launch Templates -> lt-flask-app-production -> Delete

# 3. Supprimer Load Balancer
EC2 -> Load Balancers -> alb-production -> Delete

# 4. Supprimer Target Groups
EC2 -> Target Groups -> tg-flask-app-production -> Delete

# 5. Supprimer CloudWatch Alarms
CloudWatch -> Alarms -> Sélectionner toutes -> Delete

# 6. Supprimer CloudWatch Dashboard
CloudWatch -> Dashboards -> Production-LoadBalancing-Monitoring -> Delete

# 7. Supprimer SNS Topics
SNS -> Topics -> asg-events-production -> Delete
SNS -> Subscriptions -> Confirm deletion dans email

# 8. Supprimer Security Groups
EC2 -> Security Groups -> sg-alb-production -> Delete

# 9. Vérifier aucune instance running
EC2 -> Instances -> (doit être vide)
```

**Vérification finale :**
```
EC2 -> Load Balancers -> Aucun [OK]
EC2 -> Auto Scaling Groups -> Aucun [OK]
EC2 -> Instances -> Aucune running [OK]
```

---

**FIN DE L'EXERCICE 6**

═══════════════════════════════════════════════════════════════

Veux-tu que je continue avec l'**Exercice 7 : Containers et Orchestration (Docker + ECS/Fargate)** ?

# [JAUNE] EXERCICE 7 : CONTAINERS ET ORCHESTRATION (DOCKER + ECS/FARGATE)

## [LISTE] ÉNONCÉ

### Contexte professionnel

La startup a décidé de moderniser son infrastructure en adoptant les containers. Le CTO veut :

**Problèmes actuels avec EC2 classique :**
- [X] **"Works on my machine"** : App fonctionne en dev mais pas en prod
- [X] **Déploiements lents** : User data script = 5-10 min par instance
- [X] **Dépendances fragiles** : Conflits de versions Python/packages
- [X] **Portabilité limitée** : Code lié à Amazon Linux spécifiquement
- [X] **Scaling lent** : Temps de boot EC2 = plusieurs minutes
- [X] **Gaspillage ressources** : 1 app par instance (sous-utilisation)

**Nouvelle architecture containerisée demandée :**

Le directeur technique impose :
1. **Docker** pour packager l'application (image immuable)
2. **Amazon ECR** pour stocker les images Docker (registry privé)
3. **Amazon ECS** pour orchestrer les containers
4. **AWS Fargate** pour mode serverless (pas de gestion instances)
5. **Service Discovery** pour communication inter-services
6. **Application Load Balancer** intégré avec ECS
7. **Auto Scaling** basé sur CPU/Memory des containers
8. **CloudWatch Logs** pour centraliser les logs
9. **CI/CD Pipeline** pour déploiements automatisés
10. **Multi-environment** (dev, staging, prod) avec même image

### Architecture cible

```
┌────────────────────────────────────────────────────────────────┐
│                      DEVELOPER WORKFLOW                         │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Developer PC                                                   │
│  ├─ Code application (Flask)                                   │
│  ├─ Write Dockerfile                                           │
│  ├─ docker build -t flask-app:latest                           │
│  ├─ docker run -p 5000:5000 flask-app                          │
│  └─ Test locally [OK]                                             │
│                                                                 │
│  Push to Git -> Triggers CI/CD                                  │
│  └─ GitHub / CodeCommit                                        │
│                     v                                           │
└─────────────────────┼───────────────────────────────────────────┘
                      │
                      [BLACK_DOWN-POINTING_TRIANGLE]
┌────────────────────────────────────────────────────────────────┐
│                    CI/CD PIPELINE (CodePipeline)                │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Source Stage:                                                  │
│  └─ CodeCommit / GitHub webhook                                │
│                     v                                           │
│  Build Stage (CodeBuild):                                       │
│  ├─ Pull source code                                           │
│  ├─ Run tests (pytest)                                         │
│  ├─ docker build -t flask-app:$COMMIT_SHA                      │
│  ├─ docker tag flask-app:$SHA $ECR_URI:$SHA                    │
│  ├─ docker push $ECR_URI:$SHA                                  │
│  └─ Update task definition with new image                      │
│                     v                                           │
│  Deploy Stage (CodeDeploy):                                     │
│  ├─ Blue/Green deployment                                      │
│  ├─ Update ECS service                                         │
│  └─ Health check -> Rollback if failed                          │
│                                                                 │
└─────────────────────┼───────────────────────────────────────────┘
                      │
                      [BLACK_DOWN-POINTING_TRIANGLE]
┌────────────────────────────────────────────────────────────────┐
│              AMAZON ECR (Elastic Container Registry)            │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Repository: flask-app-production                               │
│  ├─ Image: flask-app:abc1234 (latest commit)                  │
│  ├─ Image: flask-app:def5678 (previous)                       │
│  ├─ Image: flask-app:v1.2.0 (tagged release)                  │
│  └─ Image: flask-app:latest                                    │
│                                                                 │
│  Features:                                                      │
│  - Vulnerability scanning (AWS Inspector)                      │
│  - Lifecycle policies (keep last 10 images)                    │
│  - Encryption at rest                                          │
│  - Cross-region replication                                    │
│                                                                 │
└─────────────────────┼───────────────────────────────────────────┘
                      │
                      │ Pull images
                      [BLACK_DOWN-POINTING_TRIANGLE]
┌────────────────────────────────────────────────────────────────┐
│                    AMAZON ECS CLUSTER                           │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Launch Type Options:                                           │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  OPTION 1: ECS with EC2 (manual capacity)                │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  EC2 Instances (ECS-optimized AMI)                 │  │  │
│  │  │  - t3.medium × 2 instances                          │  │  │
│  │  │  - Docker daemon running                            │  │  │
│  │  │  - ECS agent communicates with control plane       │  │  │
│  │  │  - Can run multiple containers per instance        │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │                                                            │  │
│  │  Pros: Lower cost, more control                          │  │
│  │  Cons: Manage instances, less elastic                    │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  OPTION 2: AWS Fargate (serverless) [OK] RECOMMENDED      │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  NO EC2 instances to manage                        │  │  │
│  │  │  - Define: CPU + Memory per task                   │  │  │
│  │  │  - AWS provisions compute automatically            │  │  │
│  │  │  - Pay per vCPU-second + GB-second                 │  │  │
│  │  │  - Fully serverless                                │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │                                                            │  │
│  │  Pros: No infra management, elastic, secure              │  │
│  │  Cons: ~30% more expensive than EC2                      │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
└─────────────────────┼───────────────────────────────────────────┘
                      │
                      [BLACK_DOWN-POINTING_TRIANGLE]
┌────────────────────────────────────────────────────────────────┐
│                     ECS SERVICE ARCHITECTURE                    │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  Application Load Balancer                               │  │
│  │  - DNS: alb-ecs-prod.elb.amazonaws.com                   │  │
│  │  - Listener: HTTPS:443 -> Target Group (ECS tasks)        │  │
│  │  - Health checks: /health every 30s                      │  │
│  └──────────────────┬───────────────────────────────────────┘  │
│                     │                                           │
│                     │ Distributes traffic                       │
│                     [BLACK_DOWN-POINTING_TRIANGLE]                                           │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │         ECS Service: flask-app-service                   │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  Configuration:                                     │  │  │
│  │  │  - Desired count: 3 tasks                           │  │  │
│  │  │  - Min healthy: 100% (rolling update)               │  │  │
│  │  │  - Max: 200% (blue/green support)                   │  │  │
│  │  │  - Deployment circuit breaker: Enabled              │  │  │
│  │  │  - Auto Scaling: Target CPU 50%                     │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │                                                            │  │
│  │  Running Tasks (Fargate):                                 │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │  │
│  │  │   Task 1     │  │   Task 2     │  │   Task 3     │   │  │
│  │  │  AZ: 1a      │  │  AZ: 1b      │  │  AZ: 1c      │   │  │
│  │  │  ┌────────┐  │  │  ┌────────┐  │  │  ┌────────┐  │   │  │
│  │  │  │Container│ │  │  │Container│ │  │  │Container│ │   │  │
│  │  │  │flask-app│ │  │  │flask-app│ │  │  │flask-app│ │   │  │
│  │  │  │Port:5000│ │  │  │Port:5000│ │  │  │Port:5000│ │   │  │
│  │  │  │CPU:0.25 │ │  │  │CPU:0.25 │ │  │  │CPU:0.25 │ │   │  │
│  │  │  │Mem:0.5GB│ │  │  │Mem:0.5GB│ │  │  │Mem:0.5GB│ │   │  │
│  │  │  └────────┘  │  │  └────────┘  │  │  └────────┘  │   │  │
│  │  │  IP:10.0.x  │  │  IP:10.0.y  │  │  IP:10.0.z  │   │  │
│  │  └──────────────┘  └──────────────┘  └──────────────┘   │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Service Discovery (AWS Cloud Map):                            │
│  ├─ Namespace: production.local                                │
│  ├─ Service: flask-app.production.local                        │
│  └─ Auto DNS registration: task IPs                            │
│                                                                 │
└─────────────────────┼───────────────────────────────────────────┘
                      │
                      │ Database connections
                      [BLACK_DOWN-POINTING_TRIANGLE]
┌────────────────────────────────────────────────────────────────┐
│                   RDS POSTGRESQL (Multi-AZ)                     │
│  - Endpoint via Secrets Manager                                │
│  - Security Group: Allow from ECS tasks only                   │
└────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────┐
│                    MONITORING & LOGGING                         │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  CloudWatch Logs:                                               │
│  ├─ /ecs/flask-app-production                                  │
│  │  ├─ Task 1 logs (stdout/stderr)                             │
│  │  ├─ Task 2 logs                                             │
│  │  └─ Task 3 logs                                             │
│  └─ Retention: 7 days                                          │
│                                                                 │
│  CloudWatch Metrics:                                            │
│  ├─ ECS: CPUUtilization, MemoryUtilization                     │
│  ├─ ALB: TargetResponseTime, HealthyTaskCount                  │
│  └─ Custom: Request count, Error rate                          │
│                                                                 │
│  X-Ray (Distributed Tracing):                                  │
│  └─ Request flow: ALB -> Task -> RDS -> Response                  │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

Cost Comparison:
┌────────────────────────────────────────────────────────────────┐
│  EC2 (t3.medium × 2):        ~30$/month                        │
│  ECS EC2 (t3.medium × 2):    ~30$/month (same)                 │
│  Fargate (0.25 vCPU × 3):    ~18$/month (more efficient)       │
│  ALB:                        ~16$/month                        │
│  ECR:                        ~1$/month (< 10 GB images)        │
│  CloudWatch Logs:            ~2$/month (< 5 GB)                │
│                                                                 │
│  Total Fargate architecture: ~37$/month                        │
│  Total EC2 architecture:     ~46$/month                        │
│                                                                 │
│  Savings: 20% + No server management [OK]                         │
└────────────────────────────────────────────────────────────────┘
```

### Contraintes techniques

- Région : `eu-west-1` (Irlande)
- Container runtime : Docker
- Orchestration : Amazon ECS (Fargate)
- Registry : Amazon ECR
- Load Balancer : Application Load Balancer
- Logging : CloudWatch Logs
- Secrets : AWS Secrets Manager
- Service Discovery : AWS Cloud Map
- CI/CD : AWS CodePipeline (optionnel)
- Budget : < 40$/mois
- Temps estimé : 6-7 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre les concepts containers (vs VMs)
- [OK] Créer des Dockerfiles optimisés
- [OK] Builder et tester des images Docker
- [OK] Créer et gérer Amazon ECR repositories
- [OK] Comprendre ECS Task Definitions
- [OK] Déployer ECS Services avec Fargate
- [OK] Configurer Service Discovery
- [OK] Intégrer ECS avec ALB
- [OK] Implémenter Auto Scaling pour containers
- [OK] Centraliser logs avec CloudWatch
- [OK] Gérer secrets avec Secrets Manager
- [OK] Optimiser images Docker (multi-stage builds)
- [OK] Troubleshooter containers

---

## [DOCS] PRÉREQUIS

- Exercices 1-6 terminés (EC2, VPC, RDS, ALB)
- Docker installé localement (pour développement)
- AWS CLI configuré
- Connaissances Linux de base
- VPC avec subnets publics et privés

---

## [ARGENT] ESTIMATION DES COÛTS

**AWS Fargate :**

| Ressource | Coût | Calcul |
|-----------|------|--------|
| vCPU | 0.04048$/vCPU-hour | 0.25 vCPU × 0.04048 × 720h = 7.29$/month |
| Memory | 0.004445$/GB-hour | 0.5 GB × 0.004445 × 720h = 1.60$/month |
| **Par task** | | **8.89$/month** |
| **3 tasks 24/7** | | **26.67$/month** |

**Amazon ECR :**

| Ressource | Coût | Remarque |
|-----------|------|----------|
| Storage | 0.10$/GB-month | < 10 GB images = 1$/month |
| Data transfer OUT | 0.09$/GB | Dans région = gratuit [OK] |

**Application Load Balancer :**
- ALB : 16$/month (comme exercice 6)

**CloudWatch Logs :**
- Ingestion : 0.50$/GB
- Storage : 0.03$/GB-month
- Estimation : ~2$/month (< 5 GB logs)

**ECS Service :**
- Gratuit [OK] (seulement Fargate/EC2 facturés)

**Scénario de cet exercice :**

```
Fargate (3 tasks × 0.25 vCPU × 0.5 GB): 27$/month
ECR (< 10 GB images): 1$/month
ALB: 16$/month
CloudWatch Logs: 2$/month

Total: ~46$/month

vs EC2 ASG (exercice 6): 30$/month (2 instances)
  Mais: Fargate = Plus simple, plus élastique [OK]
```

**Free Tier (premiers 12 mois) :**
- Fargate : Pas de Free Tier [X]
- ECR : 500 MB storage/month [OK]
- CloudWatch Logs : 5 GB ingestion [OK]

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Comprendre les Containers

#### Containers vs Virtual Machines

**Virtual Machines (VMs) :**

```
┌────────────────────────────────────────┐
│         Physical Server (Host)          │
├────────────────────────────────────────┤
│           Hardware (CPU, RAM)           │
├────────────────────────────────────────┤
│         Host Operating System           │
├────────────────────────────────────────┤
│            Hypervisor (KVM)             │
├──────────┬──────────┬──────────────────┤
│   VM 1   │   VM 2   │      VM 3        │
├──────────┼──────────┼──────────────────┤
│ Guest OS │ Guest OS │   Guest OS       │
│ (Ubuntu) │ (CentOS) │   (Amazon Linux) │
├──────────┼──────────┼──────────────────┤
│  App A   │  App B   │      App C       │
└──────────┴──────────┴──────────────────┘

Caractéristiques VMs:
- Chaque VM = OS complet (plusieurs GB)
- Boot time: 30-60 secondes
- Isolation forte (hardware-level)
- Overhead: ~1-2 GB RAM par VM
```

---

**Containers :**

```
┌────────────────────────────────────────┐
│         Physical Server (Host)          │
├────────────────────────────────────────┤
│           Hardware (CPU, RAM)           │
├────────────────────────────────────────┤
│         Host Operating System           │
├────────────────────────────────────────┤
│          Container Runtime              │
│            (Docker Engine)              │
├──────────┬──────────┬──────────────────┤
│Container1│Container2│   Container 3    │
├──────────┼──────────┼──────────────────┤
│  App A   │  App B   │      App C       │
│  + libs  │  + libs  │    + libs        │
└──────────┴──────────┴──────────────────┘

Caractéristiques Containers:
- Partage le kernel de l'hôte
- Size: Quelques MB (vs plusieurs GB VM)
- Boot time: < 1 seconde
- Isolation: Process-level (namespaces, cgroups)
- Overhead: ~50 MB RAM par container
```

---

**Comparaison :**

| Critère | VM | Container |
|---------|-----|-----------|
| **OS** | OS complet | Partage kernel hôte |
| **Taille** | GB (2-10 GB) | MB (50-500 MB) |
| **Boot** | 30-60s | < 1s |
| **Isolation** | Forte (hardware) | Moyenne (process) |
| **Performance** | Overhead ~10% | Overhead ~2% |
| **Portabilité** | Moyenne | Excellente |
| **Densité** | 10-20 VMs/host | 100+ containers/host |

---

#### Docker Architecture

```
┌──────────────────────────────────────────────────────────┐
│                   DOCKER ARCHITECTURE                     │
├──────────────────────────────────────────────────────────┤
│                                                           │
│  ┌────────────────────────────────────────────────────┐  │
│  │              Docker Client (CLI)                    │  │
│  │  $ docker build -t myapp .                          │  │
│  │  $ docker run -p 5000:5000 myapp                    │  │
│  │  $ docker push myregistry/myapp:latest              │  │
│  └───────────────────┬──────────────────────────────────┘  │
│                      │ REST API                            │
│                      [BLACK_DOWN-POINTING_TRIANGLE]                                     │
│  ┌────────────────────────────────────────────────────┐  │
│  │           Docker Daemon (dockerd)                   │  │
│  │  - Manages images, containers, networks, volumes   │  │
│  │  - Listens on Unix socket or TCP                   │  │
│  └────────────────────┬──────────────────────────────┘  │
│                       │                                   │
│         ┌─────────────┼─────────────┐                     │
│         │             │             │                     │
│         [BLACK_DOWN-POINTING_TRIANGLE]             [BLACK_DOWN-POINTING_TRIANGLE]             [BLACK_DOWN-POINTING_TRIANGLE]                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐               │
│  │  Images  │  │Containers│  │ Networks │               │
│  └──────────┘  └──────────┘  └──────────┘               │
│                                                           │
└──────────────────────────────────────────────────────────┘

Container Filesystem:
┌──────────────────────────────────────────┐
│         Container (Running)               │
├──────────────────────────────────────────┤
│      Container Layer (R/W)                │  <- Modifications
├──────────────────────────────────────────┤
│      Image Layer 3 (R/O)                  │
├──────────────────────────────────────────┤
│      Image Layer 2 (R/O)                  │  <- Image layers
├──────────────────────────────────────────┤
│      Image Layer 1 (R/O)                  │     (read-only)
├──────────────────────────────────────────┤
│      Base Layer (R/O)                     │  <- Base OS
└──────────────────────────────────────────┘

Copy-on-Write:
- Layers partagés entre images (économie espace)
- Modifications dans container layer seulement
```

---

#### Concepts clés Docker

**Image :**
- Template read-only
- Contient : code + dépendances + OS minimal
- Construit depuis Dockerfile
- Stocké dans registry (ECR, Docker Hub)
- Versionné avec tags

**Container :**
- Instance running d'une image
- Processus isolé avec son filesystem
- Éphémère (peut être détruit/recréé)
- État : running, stopped, paused

**Dockerfile :**
- Fichier texte définissant comment builder l'image
- Instructions : FROM, RUN, COPY, CMD, etc.

**Registry :**
- Repository centralisé pour images
- AWS ECR, Docker Hub, Harbor, etc.

**Volume :**
- Stockage persistant (survive à container)
- Partagé entre containers

**Network :**
- Isolation réseau entre containers
- Bridge, host, overlay modes

---

### ÉTAPE 2 : Installer Docker localement

#### Installation sur Ubuntu/Debian

```bash
# Mettre à jour apt
sudo apt update

# Installer dépendances
sudo apt install -y \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

# Ajouter GPG key Docker
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# Ajouter repository Docker
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Installer Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Vérifier installation
docker --version
# Docker version 24.0.7, build afdd53b

sudo systemctl status docker
# [BLACK_CIRCLE] docker.service - Docker Application Container Engine
#    Active: active (running)
```

---

#### Configuration post-installation

**Ajouter user au groupe docker (éviter sudo) :**

```bash
# Ajouter user actuel au groupe docker
sudo usermod -aG docker $USER

# Recharger groupes (ou logout/login)
newgrp docker

# Tester sans sudo
docker run hello-world
```

**Résultat :**

```
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
...
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.
```

**[OK] Docker installé et fonctionnel !**

---

#### Installation sur macOS

```bash
# Télécharger Docker Desktop
# https://www.docker.com/products/docker-desktop/

# Ou via Homebrew
brew install --cask docker

# Lancer Docker Desktop app
open -a Docker

# Vérifier
docker --version
docker run hello-world
```

---

#### Installation sur Windows

```powershell
# Installer WSL2 d'abord
wsl --install

# Télécharger Docker Desktop
# https://www.docker.com/products/docker-desktop/

# Lancer Docker Desktop

# Vérifier (PowerShell ou WSL)
docker --version
docker run hello-world
```

---

### ÉTAPE 3 : Créer le Dockerfile pour l'application Flask

#### Structure du projet

```bash
mkdir ~/flask-app-docker
cd ~/flask-app-docker

# Structure
flask-app-docker/
├── Dockerfile
├── requirements.txt
├── app.py
├── .dockerignore
└── docker-compose.yml (optionnel, pour dev local)
```

---

#### Créer requirements.txt

```bash
nano requirements.txt
```

**Contenu :**

```
Flask==3.0.0
psycopg2-binary==2.9.9
boto3==1.34.0
requests==2.31.0
gunicorn==21.2.0
```

**Sauvegarder**

---

#### Créer app.py

```bash
nano app.py
```

**Contenu (version simplifiée pour container) :**

```python
from flask import Flask, jsonify, request
import psycopg2
import os
import socket
import time

app = Flask(__name__)

# ═══════════════════════════════════════════════════════════════
# DATABASE CONNECTION
# ═══════════════════════════════════════════════════════════════

def get_db_connection():
    """
    Connexion PostgreSQL depuis variables d'environnement
    (injectées par ECS Task Definition)
    """
    return psycopg2.connect(
        host=os.environ.get('DB_HOST', 'localhost'),
        port=int(os.environ.get('DB_PORT', '5432')),
        database=os.environ.get('DB_NAME', 'postgres'),
        user=os.environ.get('DB_USER', 'postgres'),
        password=os.environ.get('DB_PASSWORD', ''),
    )

# ═══════════════════════════════════════════════════════════════
# HEALTH CHECK ENDPOINT
# ═══════════════════════════════════════════════════════════════

@app.route('/health')
def health_check():
    """Health check pour ALB Target Group"""
    
    health_status = {
        'status': 'healthy',
        'timestamp': time.time(),
        'container_id': socket.gethostname(),
        'checks': {}
    }
    
    # Check database
    try:
        conn = get_db_connection()
        cur = conn.cursor()
        cur.execute('SELECT 1')
        conn.close()
        health_status['checks']['database'] = 'ok'
    except Exception as e:
        health_status['status'] = 'unhealthy'
        health_status['checks']['database'] = f'error: {str(e)}'
        return jsonify(health_status), 503
    
    return jsonify(health_status), 200

# ═══════════════════════════════════════════════════════════════
# INFO ENDPOINT (pour identifier container)
# ═══════════════════════════════════════════════════════════════

@app.route('/info')
def container_info():
    """Retourne infos sur le container (debugging)"""
    
    # ECS Task Metadata (Fargate)
    metadata_uri = os.environ.get('ECS_CONTAINER_METADATA_URI_V4')
    
    task_metadata = {}
    if metadata_uri:
        import requests
        try:
            task_metadata = requests.get(f"{metadata_uri}/task").json()
        except:
            pass
    
    return jsonify({
        'container_id': socket.gethostname(),
        'container_ip': socket.gethostbyname(socket.gethostname()),
        'task_arn': task_metadata.get('TaskARN', 'unknown'),
        'availability_zone': task_metadata.get('AvailabilityZone', 'unknown'),
        'timestamp': time.time()
    })

# ═══════════════════════════════════════════════════════════════
# ROOT ENDPOINT
# ═══════════════════════════════════════════════════════════════

@app.route('/')
def index():
    return jsonify({
        'message': 'Flask app running in Docker container!',
        'container': socket.gethostname(),
        'version': '1.0.0'
    })

# ═══════════════════════════════════════════════════════════════
# RUN APPLICATION
# ═══════════════════════════════════════════════════════════════

if __name__ == '__main__':
    # Development mode (ne pas utiliser en production)
    app.run(host='0.0.0.0', port=5000, debug=True)
```

**Sauvegarder**

---

#### Créer Dockerfile (version basique)

```bash
nano Dockerfile
```

**Contenu :**

```dockerfile
# ═══════════════════════════════════════════════════════════════
# DOCKERFILE - FLASK APPLICATION (Basic version)
# ═══════════════════════════════════════════════════════════════

# Base image
FROM python:3.11-slim

# Métadonnées
LABEL maintainer="sirrdev@example.com"
LABEL version="1.0"
LABEL description="Flask todo application"

# Définir workdir
WORKDIR /app

# Copier requirements et installer dépendances
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier code application
COPY app.py .

# Exposer port
EXPOSE 5000

# Variables d'environnement par défaut (override par ECS)
ENV DB_HOST=localhost
ENV DB_PORT=5432
ENV DB_NAME=postgres
ENV DB_USER=postgres
ENV DB_PASSWORD=''

# Commande de démarrage
# Production: Utiliser Gunicorn (WSGI server)
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--threads", "2", "app:app"]

# Alternative dev (Flask built-in server):
# CMD ["python", "app.py"]
```

**Sauvegarder**

---

**Explication instructions Dockerfile :**

| Instruction | Rôle | Exemple |
|-------------|------|---------|
| `FROM` | Image de base | `python:3.11-slim` |
| `LABEL` | Métadonnées | `maintainer="..."` |
| `WORKDIR` | Répertoire de travail | `/app` |
| `COPY` | Copier fichiers locaux -> image | `COPY app.py .` |
| `RUN` | Exécuter commande (build time) | `RUN pip install ...` |
| `ENV` | Variables d'environnement | `ENV DB_HOST=localhost` |
| `EXPOSE` | Documenter port | `EXPOSE 5000` |
| `CMD` | Commande par défaut (runtime) | `CMD ["python", "app.py"]` |
| `ENTRYPOINT` | Commande fixe (+ CMD args) | `ENTRYPOINT ["gunicorn"]` |

---

#### Créer .dockerignore

**Exclure fichiers inutiles du build context :**

```bash
nano .dockerignore
```

**Contenu :**

```
# Python
__pycache__/
*.py[cod]
*.so
.Python
*.egg-info/
.pytest_cache/
.coverage

# Virtual env
venv/
env/

# IDE
.vscode/
.idea/
*.swp

# Git
.git/
.gitignore

# Docker
Dockerfile
docker-compose.yml

# Logs
*.log

# OS
.DS_Store
Thumbs.db
```

**Sauvegarder**

---

### ÉTAPE 4 : Builder et tester l'image Docker localement

#### Builder l'image

```bash
cd ~/flask-app-docker

# Builder l'image
docker build -t flask-app:latest .
```

**Output :**

```
[+] Building 45.2s (10/10) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 789B
 => [internal] load .dockerignore
 => => transferring context: 123B
 => [internal] load metadata for docker.io/library/python:3.11-slim
 => [1/5] FROM docker.io/library/python:3.11-slim
 => [internal] load build context
 => => transferring context: 1.23kB
 => [2/5] WORKDIR /app
 => [3/5] COPY requirements.txt .
 => [4/5] RUN pip install --no-cache-dir -r requirements.txt
 => [5/5] COPY app.py .
 => exporting to image
 => => exporting layers
 => => writing image sha256:abc123...
 => => naming to docker.io/library/flask-app:latest
```

**[OK] Image créée !**

---

**Vérifier l'image :**

```bash
docker images
```

**Résultat :**

```
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
flask-app    latest    abc123def456   2 minutes ago    215MB
python       3.11-slim 789ghi012jkl   3 weeks ago      125MB
```

---

**Inspecter l'image :**

```bash
docker inspect flask-app:latest
```

**Ou voir l'historique des layers :**

```bash
docker history flask-app:latest
```

**Résultat :**

```
IMAGE          CREATED          CREATED BY                                      SIZE
abc123def456   5 minutes ago    CMD ["gunicorn" "--bind" "0.0.0.0:5000"...]    0B
abc123def456   5 minutes ago    EXPOSE 5000                                     0B
abc123def456   5 minutes ago    COPY app.py . # buildkit                        2.1kB
abc123def456   6 minutes ago    RUN /bin/sh -c pip install --no-cache-dir...   85MB
abc123def456   6 minutes ago    COPY requirements.txt . # buildkit              123B
abc123def456   6 minutes ago    WORKDIR /app                                    0B
789ghi012jkl   3 weeks ago      /bin/sh -c #(nop)  CMD ["python3"]              0B
...
```

---

#### Tester le container localement

**Démarrer le container :**

```bash
docker run -d \
  --name flask-test \
  -p 5000:5000 \
  -e DB_HOST=host.docker.internal \
  -e DB_PORT=5432 \
  -e DB_NAME=postgres \
  -e DB_USER=postgres \
  -e DB_PASSWORD=YourPassword \
  flask-app:latest
```

**Paramètres :**
- `-d` : Detached mode (background)
- `--name` : Nom du container
- `-p 5000:5000` : Port mapping (host:container)
- `-e` : Variables d'environnement
- `host.docker.internal` : Accéder à localhost de l'hôte depuis container

---

**Vérifier que le container tourne :**

```bash
docker ps
```

**Résultat :**

```
CONTAINER ID   IMAGE              COMMAND                  CREATED          STATUS          PORTS                    NAMES
abc123def456   flask-app:latest   "gunicorn --bind 0.0…"   10 seconds ago   Up 9 seconds    0.0.0.0:5000->5000/tcp   flask-test
```

**[OK] Container running !**

---

**Tester l'application :**

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

**Résultat :**

```json
{
  "message": "Flask app running in Docker container!",
  "container": "abc123def456",
  "version": "1.0.0"
}
```

**[OK] App fonctionne !**

---

**Tester health check :**

```bash
curl http://localhost:5000/health
```

**Résultat :**

```json
{
  "status": "healthy",
  "timestamp": 1702820000.123,
  "container_id": "abc123def456",
  "checks": {
    "database": "ok"
  }
}
```

---

**Voir les logs du container :**

```bash
docker logs flask-test
```

**Résultat :**

```
[2024-12-16 20:00:00 +0000] [1] [INFO] Starting gunicorn 21.2.0
[2024-12-16 20:00:00 +0000] [1] [INFO] Listening at: http://0.0.0.0:5000 (1)
[2024-12-16 20:00:00 +0000] [1] [INFO] Using worker: sync
[2024-12-16 20:00:00 +0000] [7] [INFO] Booting worker with pid: 7
[2024-12-16 20:00:00 +0000] [8] [INFO] Booting worker with pid: 8
```

---

**Logs en temps réel :**

```bash
docker logs -f flask-test
```

**(`-f` = follow, comme `tail -f`)**

---

**Exécuter commande dans container running :**

```bash
# Ouvrir shell interactif
docker exec -it flask-test /bin/bash

# Une fois dans container
root@abc123def456:/app# ls
app.py  requirements.txt

root@abc123def456:/app# python --version
Python 3.11.7

root@abc123def456:/app# pip list
Package    Version
---------- -------
Flask      3.0.0
gunicorn   21.2.0
...

root@abc123def456:/app# exit
```

---

**Arrêter et supprimer container :**

```bash
# Arrêter
docker stop flask-test

# Supprimer
docker rm flask-test

# Ou en une commande
docker rm -f flask-test
```

---

Je vais continuer dans le prochain message avec la création du repository ECR, le push de l'image, la configuration ECS/Fargate, et le déploiement complet. Veux-tu que je continue ?

### ÉTAPE 5 : Créer un repository Amazon ECR

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

**ECR = Elastic Container Registry**

**Registry Docker privé, fully managed par AWS**

**Fonctionnalités :**
- Stockage images Docker/OCI
- Intégration IAM (authentification/autorisation)
- Encryption at rest (KMS)
- Vulnerability scanning
- Lifecycle policies (auto-cleanup)
- Cross-region replication
- Pull Through Cache (proxy vers Docker Hub)

**ECR vs Docker Hub :**

| Critère | Docker Hub | Amazon ECR |
|---------|------------|------------|
| **Prix** | Gratuit (public), 5$/mois (private) | 0.10$/GB-month |
| **Sécurité** | Basique | IAM intégré, KMS |
| **Scanning** | Paid tiers | Gratuit (AWS Inspector) |
| **Intégration AWS** | Moyenne | Native |
| **Bandwidth** | Limité | Gratuit dans région |

---

#### Créer le repository ECR

**ECR -> Repositories -> Create repository**

```
Visibility settings:
  [x] Private
  
Repository name: flask-app-production

Tag immutability:
  [ ] Enabled
  (Si enabled, ne peut pas overwrite tag existant)

Image scan settings:
  [x] Scan on push
  (Scan vulnérabilités automatique)

KMS encryption:
  [ ] Disabled (AES-256 default OK)
  
  (Ou enable si besoin KMS custom key)
```

**Create repository**

**[OK] Repository créé !**

```
Repository URI: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production
```

**[ATTENTION] Note le URI, on en aura besoin pour push**

---

#### Configurer AWS CLI pour ECR

**Installer AWS CLI si pas déjà fait :**

```bash
# Ubuntu/Debian
sudo apt install awscli -y

# macOS
brew install awscli

# Vérifier
aws --version
# aws-cli/2.13.0
```

---

**Configurer credentials :**

```bash
aws configure
```

**Entrer :**

```
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: eu-west-1
Default output format [None]: json
```

---

**Obtenir credentials IAM :**

**IAM -> Users -> ton-user -> Security credentials -> Create access key**

```
Use case: Command Line Interface (CLI)

Access key created:
  Access key ID: AKIA...
  Secret access key: wJal... (afficher une seule fois)
```

**[ATTENTION] Sauvegarder le secret key immédiatement (pas ré-affichable)**

---

#### Authentifier Docker avec ECR

**ECR nécessite authentification pour push/pull images**

**Méthode 1 : AWS CLI v2 (recommandé)**

```bash
# Obtenir token et login Docker
aws ecr get-login-password --region eu-west-1 | \
  docker login --username AWS --password-stdin \
  123456789012.dkr.ecr.eu-west-1.amazonaws.com
```

**Résultat :**

```
Login Succeeded
```

**[OK] Docker authentifié avec ECR !**

---

**Méthode 2 : AWS CLI v1 (legacy)**

```bash
# Générer commande docker login
$(aws ecr get-login --no-include-email --region eu-west-1)
```

---

**Token valide 12 heures**

**Après expiration, re-run `get-login-password`**

---

### ÉTAPE 6 : Pusher l'image vers ECR

#### Tagger l'image pour ECR

**L'image doit être taggée avec l'URI du repository ECR**

```bash
# Format: REGISTRY/REPOSITORY:TAG
# Exemple: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v1.0.0

# Tag avec version
docker tag flask-app:latest \
  123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v1.0.0

# Tag avec "latest"
docker tag flask-app:latest \
  123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:latest

# Tag avec commit SHA (best practice CI/CD)
docker tag flask-app:latest \
  123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:abc1234
```

---

**Vérifier tags :**

```bash
docker images | grep flask-app
```

**Résultat :**

```
REPOSITORY                                                        TAG       IMAGE ID       SIZE
flask-app                                                         latest    abc123def456   215MB
123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production v1.0.0    abc123def456   215MB
123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production latest    abc123def456   215MB
123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production abc1234   abc123def456   215MB
```

**Même Image ID -> C'est le même filesystem (pas de duplication) [OK]**

---

#### Pusher l'image vers ECR

```bash
# Push version spécifique
docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v1.0.0

# Push latest
docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:latest
```

**Output :**

```
The push refers to repository [123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production]
5f70bf18a086: Pushed
d32f6c60d4f7: Pushed
8e3ba11ec2a2: Pushed
v1.0.0: digest: sha256:abc123... size: 1234
```

**Durée : 1-3 minutes (selon bande passante)**

---

**Vérifier dans ECR :**

**ECR -> Repositories -> flask-app-production -> Images**

```
Image tags:
  v1.0.0    |  Pushed: 2 minutes ago  |  Size: 215 MB  |  Scan: In progress
  latest    |  Pushed: 1 minute ago   |  Size: 215 MB  |  Scan: In progress
  abc1234   |  Pushed: 30 seconds ago |  Size: 215 MB  |  Scan: Complete
```

**[OK] Images dans ECR !**

---

**Vulnerability scan results :**

**Après quelques minutes, scan termine**

**Click sur image -> Vulnerabilities**

```
Scan status: Complete

Severity breakdown:
  Critical: 0
  High: 2
  Medium: 5
  Low: 12
  Informational: 8

Total findings: 27
```

**Détails :**

```
CVE-2023-1234  |  High      |  Package: openssl
  Description: Buffer overflow in OpenSSL 1.1.1
  Fix: Upgrade to openssl 1.1.1w

CVE-2023-5678  |  High      |  Package: libxml2
  Description: XML parsing vulnerability
  Fix: Upgrade to libxml2 2.11.5
```

**[ATTENTION] Production : Toujours fixer vulnerabilities Critical/High**

---

#### Configurer Lifecycle Policy

**Auto-supprimer vieilles images (économiser storage)**

**ECR -> Repositories -> flask-app-production -> Lifecycle policy -> Create rule**

```
Rule 1:
  Rule priority: 1
  Description: Keep last 10 tagged images
  
  Image status: Tagged
  
  Match criteria:
    Image count more than: 10
  
  Action: Expire
```

**Rule 2:**

```
Rule priority: 2
  Description: Remove untagged images after 7 days
  
  Image status: Untagged
  
  Match criteria:
    Since image pushed: 7 days
  
  Action: Expire
```

**Save**

---

**Test dry run (simulate) :**

**Lifecycle policy -> Preview**

```
Images that would be expired:
  abc1234  |  Untagged  |  Pushed: 15 days ago  |  Size: 215 MB
  def5678  |  v0.9.0    |  Pushed: 30 days ago  |  Size: 210 MB
  
Total savings: 425 MB
```

**Enable rule**

**Lifecycle exécuté daily à 00:00 UTC**

---

### ÉTAPE 7 : Créer un ECS Cluster

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

**ECS = Elastic Container Service**

**Service d'orchestration de containers**

**Composants ECS :**

```
ECS Cluster
  └─ Capacity Provider (EC2 ou Fargate)
      └─ ECS Service
          └─ Task Definition (blueprint)
              └─ Tasks (containers running)
```

**Cluster :**
- Groupe logique de ressources compute
- Peut mélanger EC2 + Fargate
- Multi-AZ

**Task Definition :**
- Blueprint du container
- Définit : image, CPU, memory, env vars, ports, etc.
- Versionné (immutable)

**Task :**
- Instance d'une Task Definition
- 1+ containers
- Éphémère

**Service :**
- Maintient N tasks running
- Intégration ALB
- Auto Scaling
- Rolling deployments

---

#### Créer le Cluster

**ECS -> Clusters -> Create cluster**

```
Cluster name: ecs-cluster-production

Infrastructure:
  [x] AWS Fargate (serverless)
  
  [ ] Amazon EC2 instances
    (Décocher, on utilise Fargate)

Monitoring:
  [x] Use Container Insights
    (CloudWatch metrics détaillées)
    
Tags:
  Key: Environment | Value: production
  Key: ManagedBy | Value: ECS
```

**Create**

**[OK] Cluster créé !**

```
Cluster: ecs-cluster-production
Status: Active
Registered container instances: 0 (Fargate = serverless)
Running tasks count: 0
Services: 0
```

---

**Container Insights (si activé) :**

**CloudWatch -> Insights -> Container Insights**

**Dashboard automatique avec :**
- CPU/Memory utilization
- Network traffic
- Task count
- Performance metrics

---

### ÉTAPE 8 : Créer une Task Definition

#### Créer la Task Definition

**ECS -> Task Definitions -> Create new Task Definition**

```
Task definition family: flask-app-task

Launch type:
  [x] AWS Fargate
  
  [ ] Amazon EC2 instances

Operating system/Architecture:
  Linux/X86_64
  
  (Ou ARM64 si ton image supporte, moins cher)

Task size:
  CPU: 0.25 vCPU
    (Minimum Fargate, suffit pour Flask léger)
  
  Memory: 0.5 GB
    (512 MB)

Task role:
  ecsTaskExecutionRole (créé automatiquement)
  
  (Ou créer custom role si besoin S3/DynamoDB/etc)

Task execution role:
  ecsTaskExecutionRole
  
  (Nécessaire pour pull ECR, logs CloudWatch)
```

---

**Container definitions :**

**Container - 1**

```
Name: flask-app-container

Image URI: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:latest

Essential container: Yes
  (Si ce container crash, toute la task arrête)

Port mappings:
  Container port: 5000
  Protocol: TCP
  Port name: flask-http (optionnel)
  App protocol: HTTP

Environment variables:
  Key: DB_HOST        | Value: sirrdev-todo-db.xxx.rds.amazonaws.com
  Key: DB_PORT        | Value: 5432
  Key: DB_NAME        | Value: todo_production
  Key: DB_USER        | Value: postgres
  
  [ATTENTION] DB_PASSWORD: Ne PAS mettre ici (use Secrets Manager)

Resource allocation limits:
  CPU: (inherited from task)
  Memory soft limit: 512 MB
  Memory hard limit: 512 MB
  
  GPU: 0

HealthCheck:
  Command: CMD-SHELL, curl -f http://localhost:5000/health || exit 1
  Interval: 30 seconds
  Timeout: 5 seconds
  Start period: 60 seconds
  Retries: 3
```

---

**Logging :**

```
Log configuration:
  [x] Auto-configure CloudWatch Logs

Log driver: awslogs

Log options:
  awslogs-group: /ecs/flask-app-production
  awslogs-region: eu-west-1
  awslogs-stream-prefix: ecs
```

**ECS créera automatiquement le log group**

---

**Storage (optionnel) :**

```
[ ] Add volume
  (Pas nécessaire si app stateless)
  
  Si besoin:
    Volume type: Bind mount (ephemeral)
    Name: tmp
    Mount point: /tmp
```

---

**Click "Next"**

---

**Environment variables depuis Secrets Manager (sécurisé) :**

**D'abord, créer secret dans Secrets Manager :**

**Secrets Manager -> Store a new secret**

```
Secret type: Other type of secret

Key/value pairs:
  DB_PASSWORD | PostgresAdminSecure2024!

Encryption key: aws/secretsmanager (default)

Secret name: production/db/password

Description: RDS PostgreSQL password for production

Create secret
```

**Secret ARN :**

```
arn:aws:secretsmanager:eu-west-1:123456789012:secret:production/db/password-abc123
```

---

**Retour à Task Definition, ajouter secret :**

**Container -> Environment variables**

```
Type: ValueFrom

Key: DB_PASSWORD
Value: arn:aws:secretsmanager:eu-west-1:123456789012:secret:production/db/password-abc123:DB_PASSWORD::
```

**Format ValueFrom :**

```
arn:aws:secretsmanager:REGION:ACCOUNT:secret:SECRET_NAME:JSON_KEY:VERSION_STAGE:VERSION_ID

Exemples:
  Full secret: arn:...secret:my-secret::
  JSON key: arn:...secret:my-secret:password::
  Specific version: arn:...secret:my-secret:password:AWSCURRENT:
```

---

**Modifier IAM Task Execution Role :**

**ECS Task Execution Role doit avoir permissions Secrets Manager**

**IAM -> Roles -> ecsTaskExecutionRole -> Add permissions -> Attach policies**

**Chercher : SecretsManagerReadWrite**

**Ou créer inline policy :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": [
        "arn:aws:secretsmanager:eu-west-1:123456789012:secret:production/db/password-*"
      ]
    }
  ]
}
```

**Attach policy**

---

**Click "Create"**

**[OK] Task Definition créée !**

```
Task definition: flask-app-task:1
  (Version 1)

Status: Active
Launch type: Fargate
Task CPU: 0.25 vCPU
Task memory: 0.5 GB
Containers: 1 (flask-app-container)
```

---

### ÉTAPE 9 : Créer un ECS Service avec Fargate

#### Préparer le Load Balancer

**Réutiliser ALB de l'exercice 6, ou créer nouveau :**

**EC2 -> Load Balancers -> Create load balancer**

```
Type: Application Load Balancer

Name: alb-ecs-production

Scheme: Internet-facing

VPC: vpc-production

Subnets:
  [x] subnet-public-1a
  [x] subnet-public-1b

Security groups: sg-alb-ecs (créer nouveau)
  Inbound:
    HTTP (80) from 0.0.0.0/0
    HTTPS (443) from 0.0.0.0/0
  Outbound:
    All traffic

Listeners:
  HTTP:80 -> Forward to tg-ecs-flask-app (créer nouveau)
```

**Create target group :**

```
Target type: IP
  ([ATTENTION] Fargate utilise IP, pas Instance)

Target group name: tg-ecs-flask-app

Protocol: HTTP
Port: 5000

VPC: vpc-production

Health checks:
  Protocol: HTTP
  Path: /health
  Port: Traffic port (5000)
  Healthy threshold: 2
  Unhealthy threshold: 3
  Timeout: 5 seconds
  Interval: 30 seconds
  Success codes: 200

Advanced settings:
  Deregistration delay: 30 seconds
    (Plus court que EC2, containers démarrent vite)

Create target group
```

---

**Retour à ALB, finir création**

**Create load balancer**

**ALB DNS :**

```
alb-ecs-production-123456789.eu-west-1.elb.amazonaws.com
```

---

#### Créer le Security Group pour ECS Tasks

**EC2 -> Security Groups -> Create security group**

```
Name: sg-ecs-tasks-production

Description: Security group for ECS Fargate tasks

VPC: vpc-production

Inbound rules:
  Type: Custom TCP
  Port: 5000
  Source: sg-alb-ecs (security group de l'ALB)
  Description: Allow traffic from ALB only
  
  Type: Custom TCP (optionnel, pour debugging)
  Port: 5000
  Source: sg-bastion
  Description: Allow direct access from bastion

Outbound rules:
  Type: All traffic
  Destination: 0.0.0.0/0
  Description: Allow all outbound (RDS, internet, etc.)
```

**Create security group**

---

#### Créer le ECS Service

**ECS -> Clusters -> ecs-cluster-production -> Services -> Create**

```
Deployment configuration:
  Service: (default)

Task definition:
  Family: flask-app-task
  Revision: 1 (latest)

Service name: flask-app-service

Service type: Replica
  (Vs Daemon = 1 task per instance)

Desired tasks: 3
  (Nombre de tasks à maintenir running)
```

---

**Deployment options :**

```
Deployment type: Rolling update

Min running tasks: 100%
  (Pendant update, garde 100% tasks running)

Max running tasks: 200%
  (Peut monter à 200% pendant déploiement)

Deployment circuit breaker:
  [x] Turn on deployment circuit breaker
  [x] Rollback on failure
  
  (Auto rollback si deployment échoue)
```

---

**Networking :**

```
VPC: vpc-production

Subnets:
  [x] subnet-app-1a (10.0.11.0/24) - Private
  [x] subnet-app-1b (10.0.12.0/24) - Private
  [x] subnet-app-1c (10.0.13.0/24) - Private (optionnel)

Security group: sg-ecs-tasks-production

Public IP: DISABLED
  (Tasks privés, accès via ALB seulement)
```

---

**Load balancing :**

```
Load balancer type: Application Load Balancer

Container: flask-app-container 5000:5000

[x] Use an existing load balancer

Load balancer: alb-ecs-production

[x] Use an existing target group

Target group: tg-ecs-flask-app

Health check grace period: 60 seconds
  (Temps avant de commencer health checks)
```

---

**Service auto scaling (optionnel, on configurera après) :**

```
[ ] Use service auto scaling
```

**On configurera après la création du service**

---

**Tags :**

```
Key: Environment | Value: production
Key: Application | Value: flask-app
```

---

**Click "Create"**

**[OK] Service créé !**

---

**Déploiement en cours :**

**Services -> flask-app-service -> Deployments**

```
Status: In progress
Running count: 0 -> 1 -> 2 -> 3

Events:
  (service flask-app-service) has started 3 tasks: (task abc123...).
  (service flask-app-service) registered 3 targets in (target-group arn:aws:...)
```

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

---

**Vérifier tasks :**

**Tasks tab**

```
Task ID          | Status  | Desired Status | Health | AZ
abc123def456     | RUNNING | RUNNING        | HEALTHY| eu-west-1a
ghi789jkl012     | RUNNING | RUNNING        | HEALTHY| eu-west-1b
mno345pqr678     | RUNNING | RUNNING        | HEALTHY| eu-west-1c

Last status: RUNNING [OK]
Health status: HEALTHY [OK]
```

---

**Vérifier Target Group :**

**EC2 -> Target Groups -> tg-ecs-flask-app -> Targets**

```
Registered targets: 3

Target                    | Port | AZ          | Health status
10.0.11.25 (task abc123)  | 5000 | eu-west-1a  | healthy [OK]
10.0.12.18 (task ghi789)  | 5000 | eu-west-1b  | healthy [OK]
10.0.13.42 (task mno345)  | 5000 | eu-west-1c  | healthy [OK]
```

**[OK] Tous targets healthy !**

---

**Tester via ALB :**

```bash
curl http://alb-ecs-production-123456789.eu-west-1.elb.amazonaws.com/
```

**Résultat :**

```json
{
  "message": "Flask app running in Docker container!",
  "container": "ip-10-0-11-25.eu-west-1.compute.internal",
  "version": "1.0.0"
}
```

**[OK] Application accessible via ALB !**

---

**Requêtes suivantes load-balanced :**

```bash
for i in {1..10}; do
  curl -s http://alb-ecs-production-xxx.elb.amazonaws.com/info | jq '.container_id'
done
```

**Résultat :**

```
"ip-10-0-11-25.eu-west-1.compute.internal"
"ip-10-0-12-18.eu-west-1.compute.internal"
"ip-10-0-13-42.eu-west-1.compute.internal"
"ip-10-0-11-25.eu-west-1.compute.internal"
"ip-10-0-12-18.eu-west-1.compute.internal"
...
```

**Trafic distribué entre 3 tasks [OK]**

---

### ÉTAPE 10 : Configurer Auto Scaling pour ECS Service

#### Service Auto Scaling Setup

**ECS -> Clusters -> ecs-cluster-production -> Services -> flask-app-service**

**Auto Scaling tab -> Create Auto Scaling**

```
Service auto scaling:
  [x] Enable service auto scaling

Minimum number of tasks: 2
  (Jamais moins de 2 pour HA)

Desired number of tasks: 3
  (État actuel/cible)

Maximum number of tasks: 10
  (Limite haute)
```

---

**Automatic task scaling policies :**

**Scaling policy type: Target tracking**

```
Policy name: target-tracking-cpu-50

ECS service metric: ECSServiceAverageCPUUtilization

Target value: 50
  (Maintenir CPU moyen à 50%)

Scale-out cooldown period: 300 seconds
  (Attendre 5 min avant nouveau scale OUT)

Scale-in cooldown period: 300 seconds
  (Attendre 5 min avant scale IN)
```

**Create**

---

**Ajouter 2ème policy (Memory tracking) :**

```
Policy name: target-tracking-memory-60

ECS service metric: ECSServiceAverageMemoryUtilization

Target value: 60

Cooldown periods: 300 seconds
```

**Create**

---

**Résultat :**

**Auto Scaling prend le MAX des 2 policies**

```
CPU = 70% -> Scale OUT +1
Memory = 50% -> Pas de scale

-> Action: Scale OUT (CPU policy déclenche)

CPU = 40% -> Pas de scale
Memory = 75% -> Scale OUT +1

-> Action: Scale OUT (Memory policy déclenche)
```

---

**Scheduled Scaling (optionnel) :**

**Create scheduled action**

```
Name: morning-scale-out

Desired count: 5

Schedule:
  Cron expression: 0 7 * * MON-FRI
  (7h UTC = 8h Paris hiver, Lundi-Vendredi)

Timezone: Europe/Paris
```

**Create**

---

**Scheduled action 2 :**

```
Name: evening-scale-in

Desired count: 2

Schedule: 0 19 * * MON-FRI
```

---

### ÉTAPE 11 : Configurer CloudWatch Logs et Monitoring

#### Vérifier les logs

**CloudWatch -> Logs -> Log groups**

**Log group : `/ecs/flask-app-production`**

**Log streams :**

```
ecs/flask-app-container/abc123def456
ecs/flask-app-container/ghi789jkl012
ecs/flask-app-container/mno345pqr678
```

**Cliquer sur un stream**

---

**Logs :**

```
2024-12-16T21:00:00.123Z [INFO] Starting gunicorn 21.2.0
2024-12-16T21:00:00.456Z [INFO] Listening at: http://0.0.0.0:5000 (1)
2024-12-16T21:00:00.789Z [INFO] Using worker: sync
2024-12-16T21:00:01.012Z [INFO] Booting worker with pid: 7
2024-12-16T21:00:01.234Z [INFO] Booting worker with pid: 8
2024-12-16T21:05:30.567Z 10.0.1.15 - - [16/Dec/2024:21:05:30 +0000] "GET /health HTTP/1.1" 200 -
2024-12-16T21:06:00.890Z 10.0.1.15 - - [16/Dec/2024:21:06:00 +0000] "GET /health HTTP/1.1" 200 -
```

**[OK] Logs centralisés dans CloudWatch !**

---

**Query logs avec Insights :**

**CloudWatch -> Logs -> Insights**

```
Select log group: /ecs/flask-app-production

Query:
fields @timestamp, @message
| filter @message like /ERROR/
| sort @timestamp desc
| limit 100
```

**Run query**

**Résultat : Toutes les erreurs des 3 tasks agrégées [OK]**

---

#### Créer CloudWatch Dashboard

**CloudWatch -> Dashboards -> Create dashboard**

```
Dashboard name: ECS-Production-Monitoring
```

---

**Widget 1 : ECS Service CPU/Memory**

**Add widget -> Line**

```
Metrics:
  ECS -> ClusterName, ServiceName
  
  [x] CPUUtilization (flask-app-service)
  [x] MemoryUtilization

Graph options:
  Title: Service CPU and Memory
  Y-axis: Percent
```

---

**Widget 2 : Task Count**

```
Metrics:
  [x] RunningTaskCount
  [x] DesiredTaskCount
  [x] PendingTaskCount

Graph options:
  Title: Task Count
  Y-axis: Count
```

---

**Widget 3 : ALB Metrics**

```
Metrics:
  ApplicationELB -> Per AppELB Metrics
  
  [x] TargetResponseTime (alb-ecs-production)
  [x] RequestCount

Graph options:
  Title: Load Balancer Performance
```

---

**Widget 4 : Target Health**

```
Metrics:
  TargetGroups -> Per TG Metrics
  
  [x] HealthyHostCount (tg-ecs-flask-app)
  [x] UnHealthyHostCount

Graph options:
  Title: Target Health
```

---

**Widget 5 : Container Insights (si activé)**

```
Container Insights -> ECS Services
  
  [x] ContainerCPU
  [x] ContainerMemory
  [x] NetworkRxBytes
  [x] NetworkTxBytes
```

---

**Save dashboard**

---

#### Créer CloudWatch Alarms

**Alarm 1 : High CPU**

```
Metric: ECSServiceAverageCPUUtilization
Statistic: Average
Period: 1 minute

Condition: Greater than 80

Datapoints: 2 out of 3

Actions:
  In alarm: SNS topic -> production-alerts
  
Alarm name: ecs-high-cpu-80
```

---

**Alarm 2 : Low Healthy Task Count**

```
Metric: RunningTaskCount
Statistic: Minimum
Period: 1 minute

Condition: Lower than 2

Alarm name: ecs-low-task-count
```

---

**Alarm 3 : Task Failed**

**Utiliser CloudWatch Log Metric Filter :**

**Log group : /ecs/flask-app-production**

**Create metric filter**

```
Filter pattern: [time, request_id, level = ERROR*, ...]

Ou simplement: ERROR

Metric namespace: ECS/Tasks
Metric name: ErrorCount
Metric value: 1
```

**Create filter**

---

**Créer alarm basée sur métrique :**

```
Metric: ECS/Tasks/ErrorCount
Statistic: Sum
Period: 5 minutes

Condition: Greater than 10

Alarm name: ecs-high-error-rate
```

---

### ÉTAPE 12 : Optimiser l'image Docker (Multi-stage builds)

#### Problème avec Dockerfile actuel

**Image actuelle : 215 MB**

**Contient :**
- Base Python (125 MB)
- Pip, setuptools, wheel
- Tous les headers C (gcc, etc.) pour compiler psycopg2
- Dépendances build inutiles en production

**Solution : Multi-stage build**

---

#### Créer Dockerfile optimisé

```bash
nano Dockerfile.optimized
```

**Contenu :**

```dockerfile
# ═══════════════════════════════════════════════════════════════
# MULTI-STAGE DOCKERFILE - OPTIMIZED
# ═══════════════════════════════════════════════════════════════

# ───────────────────────────────────────────────────────────────
# STAGE 1: Builder (compile dependencies)
# ───────────────────────────────────────────────────────────────

FROM python:3.11-slim AS builder

# Install build dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Copy and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ───────────────────────────────────────────────────────────────
# STAGE 2: Runtime (final image)
# ───────────────────────────────────────────────────────────────

FROM python:3.11-slim

# Install only runtime dependencies (no gcc, no build tools)
RUN apt-get update && apt-get install -y \
    libpq5 \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv

# Set PATH to use virtual environment
ENV PATH="/opt/venv/bin:$PATH"

# Create non-root user (security best practice)
RUN useradd -m -u 1000 appuser && \
    mkdir -p /app && \
    chown -R appuser:appuser /app

# Switch to non-root user
USER appuser

# Set working directory
WORKDIR /app

# Copy application code
COPY --chown=appuser:appuser app.py .

# Expose port
EXPOSE 5000

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:5000/health || exit 1

# Run with Gunicorn
CMD ["gunicorn", \
     "--bind", "0.0.0.0:5000", \
     "--workers", "2", \
     "--threads", "2", \
     "--worker-class", "sync", \
     "--access-logfile", "-", \
     "--error-logfile", "-", \
     "--log-level", "info", \
     "app:app"]
```

---

**Rebuild avec Dockerfile optimisé :**

```bash
docker build -f Dockerfile.optimized -t flask-app:optimized .
```

---

**Comparer tailles :**

```bash
docker images | grep flask-app
```

**Résultat :**

```
REPOSITORY   TAG        IMAGE ID       SIZE
flask-app    optimized  xyz789abc123   145MB  <- 33% plus petit [OK]
flask-app    latest     abc123def456   215MB
```

**Économie : 70 MB (33%) [OK]**

---

**Avantages multi-stage :**
- Image plus petite (pull plus rapide)
- Pas de build tools (surface d'attaque réduite)
- Layers mieux organisés
- Cache Docker optimisé

---

**Rebuild et push nouvelle version :**

```bash
# Tag
docker tag flask-app:optimized \
  123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v1.1.0

# Push
docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v1.1.0
```

---

**Mettre à jour Task Definition avec nouvelle image :**

**ECS -> Task Definitions -> flask-app-task -> Create new revision**

```
Container image URI: 
  123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v1.1.0
  
  (Changer :latest -> :v1.1.0)

Create
```

**Task Definition v2 créée [OK]**

---

**Déployer nouvelle version :**

**ECS -> Services -> flask-app-service -> Update service**

```
Revision: 2 (latest)

Force new deployment: [x] Yes
  (Force recréation tasks même si config identique)

Update
```

**Rolling deployment démarre :**

```
Desired count: 3
Running count: 3 (old) + 3 (new) = 6 (temporary)

Tasks old version stopping...
Tasks new version starting...

After ~2 minutes:
  Running count: 3 (new version only) [OK]
```

**Zero-downtime deployment [OK]**

---

### ÉTAPE 13 : Service Discovery avec AWS Cloud Map

#### Qu'est-ce que Service Discovery ?

**Problème :**

```
Service A veut appeler Service B

Option 1: IP hardcodée
  IP: 10.0.11.25
  [X] Si task redémarre -> nouvelle IP

Option 2: ALB
  URL: alb-service-b.elb.amazonaws.com
  [OK] Stable mais overhead (Layer 7)

Option 3: Service Discovery
  URL: service-b.production.local
  [OK] DNS automatique, résout IPs actuelles
```

**AWS Cloud Map = Service Discovery managé**

---

#### Créer un namespace

**Cloud Map -> Namespaces -> Create namespace**

```
Namespace type:
  [x] API calls and DNS queries in VPCs
  
Namespace name: production.local

VPC: vpc-production

Tags:
  Environment: production
```

**Create namespace**

**Namespace créé :**

```
Namespace ID: ns-abc123
Namespace: production.local
Type: DNS (private)
```

---

#### Activer Service Discovery sur ECS Service

**ECS -> Services -> flask-app-service -> Update service**

```
Service discovery:
  [x] Enable service discovery integration

Namespace: production.local

Service discovery name: flask-app
  (Crée DNS: flask-app.production.local)

DNS record type: A (IPv4)

TTL: 60 seconds

Enable ECS task health propagation: Yes
  (Unhealthy tasks retirés du DNS automatiquement)
```

**Update**

---

**Service Discovery créé :**

**Cloud Map -> Namespaces -> production.local -> Services**

```
Service name: flask-app
DNS name: flask-app.production.local
Instances: 3
```

---

**Tester Service Discovery :**

**Depuis un autre container ECS (ou EC2 dans VPC) :**

```bash
# Résoudre DNS
nslookup flask-app.production.local
```

**Résultat :**

```
Server:    10.0.0.2
Address:   10.0.0.2#53

Name:   flask-app.production.local
Address: 10.0.11.25
Name:   flask-app.production.local
Address: 10.0.12.18
Name:   flask-app.production.local
Address: 10.0.13.42
```

**3 IPs retournées (round-robin DNS) [OK]**

---

**Appeler service via DNS :**

```bash
curl http://flask-app.production.local:5000/
```

**Résultat : Fonctionne ! [OK]**

---

**Use case : Microservices**

```
Service API -> flask-app.production.local
Service Auth -> auth-service.production.local
Service Payment -> payment.production.local

Chaque service découvre les autres via DNS
Pas besoin de hardcoder IPs [OK]
```

---

### ÉTAPE 14 : CI/CD avec AWS CodePipeline (optionnel)

#### Architecture CI/CD

```
Developer
  v git push
GitHub / CodeCommit
  v webhook
CodePipeline
  ├─ Source stage (pull code)
  ├─ Build stage (CodeBuild)
  │   ├─ docker build
  │   ├─ docker tag
  │   ├─ docker push ECR
  │   └─ Update task definition
  └─ Deploy stage (ECS rolling update)
      └─ Update service with new task definition
```

---

#### Créer buildspec.yml

```bash
nano buildspec.yml
```

**Contenu :**

```yaml
version: 0.2

env:
  variables:
    AWS_DEFAULT_REGION: eu-west-1
    AWS_ACCOUNT_ID: 123456789012
    IMAGE_REPO_NAME: flask-app-production
    IMAGE_TAG: latest
    
phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
      - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
      
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $REPOSITORY_URI:latest .
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
      
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo Writing image definitions file...
      - printf '[{"name":"flask-app-container","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
      
artifacts:
  files:
    - imagedefinitions.json
```

**Commit à Git**

---

#### Créer CodeBuild Project

**CodeBuild -> Build projects -> Create project**

```
Project name: flask-app-build

Source:
  Provider: GitHub (ou CodeCommit)
  Repository: your-repo-url
  
Environment:
  Environment image: Managed image
  Operating system: Ubuntu
  Runtime: Standard
  Image: aws/codebuild/standard:7.0
  
  [x] Privileged (required for Docker)

Service role:
  New service role: codebuild-flask-app-role

Buildspec:
  Use a buildspec file
  Buildspec name: buildspec.yml

Artifacts:
  Type: No artifacts (CodePipeline gère)

Logs:
  CloudWatch logs: Enabled
```

**Create build project**

---

#### Créer CodePipeline

**CodePipeline -> Pipelines -> Create pipeline**

```
Pipeline name: flask-app-pipeline

Service role: New service role

Source stage:
  Source provider: GitHub (ou CodeCommit)
  Repository: your-repo
  Branch: main
  Change detection: GitHub webhooks

Build stage:
  Build provider: AWS CodeBuild
  Project name: flask-app-build

Deploy stage:
  Deploy provider: Amazon ECS
  Cluster name: ecs-cluster-production
  Service name: flask-app-service
  Image definitions file: imagedefinitions.json
```

**Create pipeline**

---

**Pipeline exécute automatiquement :**

```
Source -> Build -> Deploy

Status: Succeeded [OK]
```

**Désormais : git push -> Auto deploy vers ECS [OK]**

---

Je vais continuer avec les tests, troubleshooting, optimisations avancées et la conclusion dans le prochain message. Veux-tu que je continue ?

### ÉTAPE 15 : Tests et Validation

#### Test 1 : Load Testing avec Apache Bench

**Tester performance containers vs EC2 :**

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

# Test baseline (100 requests, 10 concurrent)
ab -n 100 -c 10 http://alb-ecs-production-xxx.elb.amazonaws.com/
```

**Résultat :**

```
Concurrency Level:      10
Time taken for tests:   1.234 seconds
Complete requests:      100
Failed requests:        0
Requests per second:    81.04 [#/sec] (mean)
Time per request:       123.4 [ms] (mean)

Percentage of requests served within a certain time (ms)
  50%    105
  66%    118
  75%    130
  80%    138
  90%    165
  95%    190
  98%    215
  99%    230
 100%    245 (longest request)
```

**[OK] Performance excellente (Fargate vs EC2 similaire)**

---

**Test intensif (déclencher auto scaling) :**

```bash
# 10,000 requests, 200 concurrent
ab -n 10000 -c 200 http://alb-ecs-production-xxx.elb.amazonaws.com/
```

**Monitoring pendant le test :**

**CloudWatch -> Metrics -> ECS Service**

```
CPUUtilization: 25% -> 65% -> 85%
MemoryUtilization: 40% -> 55% -> 70%

RunningTaskCount: 3 -> 5 -> 7 (auto scaling) [OK]

TargetResponseTime (ALB): 150ms -> 180ms -> 140ms
  (Latence augmente puis diminue après scale OUT)
```

**[OK] Auto Scaling fonctionne !**

---

#### Test 2 : Container Failure Simulation

**Crasher un container manuellement :**

**ECS -> Clusters -> ecs-cluster-production -> Tasks**

**Sélectionner une task -> Stop**

```
Reason: Testing failure recovery
```

**Stop selected**

---

**ECS Service réagit :**

```
Events:
  (service flask-app-service) has stopped 1 running tasks: (task abc123...)
  (service flask-app-service) has started 1 tasks: (task xyz789...)

Desired: 3
Running: 2 -> 3 [OK]

Duration: ~30 seconds pour remplacement complet
```

**Pendant le remplacement :**
- 2 tasks continuent de servir le trafic
- Nouvelle task démarre
- ALB health check -> healthy
- Ancienne task retirée

**Zero downtime [OK]**

---

#### Test 3 : Rolling Deployment

**Déployer nouvelle version :**

**Modifier app.py :**

```python
@app.route('/')
def index():
    return jsonify({
        'message': 'Flask app running in Docker container!',
        'container': socket.gethostname(),
        'version': '2.0.0'  # <- Changé de 1.0.0 -> 2.0.0
    })
```

---

**Rebuild et push :**

```bash
# Build
docker build -t flask-app:v2.0.0 .

# Tag
docker tag flask-app:v2.0.0 \
  123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v2.0.0

# Push
docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v2.0.0
```

---

**Créer nouvelle Task Definition :**

**ECS -> Task Definitions -> flask-app-task -> Create new revision**

```
Image URI: .../flask-app-production:v2.0.0

Create
```

**Revision 3 créée**

---

**Update service :**

**Services -> flask-app-service -> Update**

```
Revision: 3 (latest)
Force new deployment: Yes

Update
```

---

**Observer rolling deployment :**

**Deployments tab**

```
Status: In progress

Primary deployment (v3):
  Running count: 1 -> 2 -> 3
  Pending count: 0
  
Active deployment (v2):
  Running count: 3 -> 2 -> 1 -> 0

Total running: 3 -> 4 -> 5 -> 4 -> 3 [OK]
```

**Pendant déploiement, tester versions :**

```bash
while true; do
  curl -s http://alb-ecs-production-xxx.elb.amazonaws.com/ | jq '.version'
  sleep 1
done
```

**Output :**

```
"1.0.0"
"1.0.0"
"2.0.0"  <- Nouvelle version apparaît
"1.0.0"
"2.0.0"
"2.0.0"
"2.0.0"  <- Toutes en v2.0.0 après 3-5 minutes
```

**[OK] Rolling deployment sans downtime !**

---

#### Test 4 : Service Discovery

**Lancer task test pour tester DNS :**

**ECS -> Task Definitions -> Create new task definition**

```
Task definition family: debug-task

Launch type: Fargate

Task size: 0.25 vCPU, 0.5 GB

Container:
  Name: debug
  Image: amazon/aws-cli:latest
  Command override: ["sleep", "3600"]
  
  Network mode: awsvpc
```

**Create**

---

**Run task :**

**ECS -> Clusters -> ecs-cluster-production -> Tasks -> Run new task**

```
Launch type: Fargate
Task definition: debug-task
Cluster VPC: vpc-production
Subnets: subnet-app-1a (private)
Security group: sg-ecs-tasks-production
Public IP: DISABLED
```

**Run task**

---

**Exec dans task :**

**Tasks -> debug task -> Enable Execute Command (si pas activé)**

**Ou utiliser ECS Exec (voir ÉTAPE 17)**

**Depuis AWS CLI :**

```bash
# Get task ID
TASK_ID=$(aws ecs list-tasks --cluster ecs-cluster-production --family debug-task --query 'taskArns[0]' --output text | cut -d'/' -f3)

# Exec
aws ecs execute-command \
  --cluster ecs-cluster-production \
  --task $TASK_ID \
  --container debug \
  --interactive \
  --command "/bin/sh"
```

---

**Dans le container debug :**

```bash
# Test DNS resolution
nslookup flask-app.production.local
```

**Résultat :**

```
Server:    10.0.0.2
Address:   10.0.0.2#53

Name:   flask-app.production.local
Address: 10.0.11.25
Address: 10.0.12.18
Address: 10.0.13.42
```

---

**Test HTTP :**

```bash
# Installer curl
apk add curl

# Call service
curl http://flask-app.production.local:5000/
```

**Résultat :**

```json
{
  "message": "Flask app running in Docker container!",
  "container": "ip-10-0-11-25.eu-west-1.compute.internal",
  "version": "2.0.0"
}
```

**[OK] Service Discovery fonctionne !**

---

### ÉTAPE 16 : Troubleshooting Containers et ECS

#### Problème 1 : Task ne démarre pas

**Symptôme :**

```
Task status: STOPPED
Stopped reason: Essential container in task exited
Exit code: 1
```

---

**Diagnostic :**

**1. Vérifier CloudWatch Logs :**

**CloudWatch -> Logs -> /ecs/flask-app-production**

**Logs montrent :**

```
Error: Unable to connect to database
psycopg2.OperationalError: could not connect to server: Connection refused
```

**-> Problème : DB_HOST incorrect ou Security Group bloque**

---

**2. Vérifier Task Definition :**

**ECS -> Task Definitions -> flask-app-task -> Latest revision**

**Environment variables :**

```
DB_HOST: sirrdev-todo-db.xxx.rds.amazonaws.com [OK]
DB_PORT: 5432 [OK]
DB_NAME: todo_production [OK]
DB_USER: postgres [OK]
DB_PASSWORD: (from Secrets Manager) [OK]
```

**Variables correctes**

---

**3. Vérifier Security Groups :**

**RDS Security Group (sg-db-tier) :**

```
Inbound rules:
  PostgreSQL (5432) from 10.0.0.0/16 [OK]
  
  Ou mieux:
  PostgreSQL (5432) from sg-ecs-tasks-production [OK]
```

**Si manquant -> Ajouter la règle**

---

**4. Vérifier Network ACLs :**

**VPC -> Network ACLs**

**NACL du subnet app doit permettre :**

```
Outbound: PostgreSQL (5432) to DB subnet [OK]
Inbound: Ephemeral ports (1024-65535) from DB subnet [OK]
```

---

**5. Tester connectivité depuis task :**

**Run debug task dans même subnet :**

```bash
# Dans debug container
apk add postgresql-client

psql -h sirrdev-todo-db.xxx.rds.amazonaws.com \
     -U postgres \
     -d todo_production
```

**Si connexion OK -> Problème dans app**
**Si connexion KO -> Problème réseau/security group**

---

#### Problème 2 : Task crashe après démarrage

**Symptôme :**

```
Task running -> healthy -> stopped
Stopped reason: Task failed ELB health checks in (target-group ...)
```

---

**Diagnostic :**

**1. Vérifier health check endpoint :**

**Target Group -> Health checks**

```
Path: /health
Port: 5000
Protocol: HTTP
Success codes: 200
```

---

**2. Tester health check localement :**

```bash
# Run container locally
docker run -p 5000:5000 \
  -e DB_HOST=host.docker.internal \
  flask-app:latest

# Test health check
curl http://localhost:5000/health
```

**Si 503 -> Bug dans health check endpoint**
**Si 200 -> Health check fonctionne localement**

---

**3. Vérifier logs ECS task :**

**CloudWatch Logs montrent :**

```
2024-12-16T22:00:00.123Z [ERROR] Database connection failed
2024-12-16T22:00:05.456Z [ERROR] Database connection failed
...
```

**-> Health check échoue car DB inaccessible**

---

**4. Augmenter health check grace period :**

**Si app lente à démarrer :**

**Service -> Edit**

```
Health check grace period: 60 seconds -> 120 seconds
```

**Donne plus de temps à l'app pour se connecter à DB**

---

#### Problème 3 : Image pull errors

**Symptôme :**

```
Task status: STOPPED
Stopped reason: CannotPullContainerError: Error response from daemon: pull access denied
```

---

**Causes :**

**1. IAM Task Execution Role manque permissions ECR**

**IAM -> Roles -> ecsTaskExecutionRole**

**Doit avoir policy : `AmazonECSTaskExecutionRolePolicy`**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
```

**Si manquant -> Attach policy**

---

**2. Image URI incorrect**

**Task Definition -> Container definition**

```
Image URI: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:latest [OK]

Pas:
  flask-app:latest [X] (local image)
  docker.io/flask-app:latest [X] (Docker Hub)
```

---

**3. Image n'existe pas dans ECR**

**ECR -> Repositories -> flask-app-production -> Images**

**Vérifier que tag existe :**

```
Tags: latest, v2.0.0, abc1234 [OK]
```

**Si tag manquant -> Push l'image**

---

#### Problème 4 : Out of Memory (OOM)

**Symptôme :**

```
Task status: STOPPED
Stopped reason: OutOfMemoryError: Container killed due to memory usage
```

---

**Diagnostic :**

**CloudWatch Container Insights :**

**Container memory utilization : 100% (maxed out)**

---

**Solutions :**

**1. Augmenter Task memory**

**Task Definition -> Create new revision**

```
Task memory: 0.5 GB -> 1 GB
```

---

**2. Optimiser app (memory leaks)**

**Ajouter profiling :**

```python
import tracemalloc

tracemalloc.start()

@app.route('/memory')
def memory_stats():
    current, peak = tracemalloc.get_traced_memory()
    return {
        'current_mb': current / 1024 / 1024,
        'peak_mb': peak / 1024 / 1024
    }
```

**Identifier memory leaks**

---

**3. Limiter Gunicorn workers**

**Dockerfile :**

```dockerfile
# 2 workers × ~100 MB = 200 MB
# + Flask ~50 MB = ~250 MB total

CMD ["gunicorn", \
     "--workers", "2", \  # <- Réduire si OOM
     "--threads", "2", \
     "app:app"]
```

---

#### Problème 5 : Secrets Manager access denied

**Symptôme :**

```
Task logs:
Error: Unable to retrieve secret: AccessDeniedException
```

---

**Solution :**

**IAM Task Execution Role doit avoir permissions Secrets Manager**

**IAM -> Roles -> ecsTaskExecutionRole -> Add inline policy**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Resource": [
        "arn:aws:secretsmanager:eu-west-1:123456789012:secret:production/db/password-*"
      ]
    }
  ]
}
```

**Save policy**

**Redémarrer tasks : Service -> Update -> Force new deployment**

---

### ÉTAPE 17 : ECS Exec (SSH-like dans containers)

#### Activer ECS Exec

**ECS Exec = SSM Session Manager pour containers**

**Permet d'exec dans container running (comme `docker exec`)**

---

**Prérequis :**

**1. Installer AWS CLI v2 (support ECS Exec)**

```bash
aws --version
# aws-cli/2.13.0 ou supérieur
```

---

**2. Installer session-manager-plugin**

```bash
# Ubuntu/Debian
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
sudo dpkg -i session-manager-plugin.deb

# macOS
brew install --cask session-manager-plugin

# Vérifier
session-manager-plugin --version
```

---

**3. Activer ECS Exec sur service**

**CLI (plus simple) :**

```bash
aws ecs update-service \
  --cluster ecs-cluster-production \
  --service flask-app-service \
  --enable-execute-command \
  --region eu-west-1
```

**Ou via Console :**

**Services -> flask-app-service -> Update**

```
Deployment configuration:
  [x] Enable Execute Command
```

**Update**

---

**4. Task Definition doit avoir initProcessEnabled**

**Task Definition -> Create new revision**

```json
"containerDefinitions": [
  {
    "name": "flask-app-container",
    "linuxParameters": {
      "initProcessEnabled": true  <- Ajouter
    }
  }
]
```

**Ou via Console :**

**Container definitions -> Advanced container configuration**

```
[x] Enable init process
```

---

**5. IAM Task Role permissions**

**Créer Task Role (différent de Task Execution Role) :**

**IAM -> Roles -> Create role**

```
Trusted entity: Elastic Container Service Task

Policy: Inline policy
```

**Policy JSON :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel"
      ],
      "Resource": "*"
    }
  ]
}
```

**Role name : ecsTaskRole**

---

**Attacher à Task Definition :**

```
Task role: ecsTaskRole
```

---

#### Utiliser ECS Exec

**Lister tasks :**

```bash
aws ecs list-tasks \
  --cluster ecs-cluster-production \
  --service-name flask-app-service \
  --region eu-west-1
```

**Résultat :**

```json
{
  "taskArns": [
    "arn:aws:ecs:eu-west-1:123456789012:task/ecs-cluster-production/abc123def456",
    "arn:aws:ecs:eu-west-1:123456789012:task/ecs-cluster-production/ghi789jkl012",
    "arn:aws:ecs:eu-west-1:123456789012:task/ecs-cluster-production/mno345pqr678"
  ]
}
```

---

**Exec dans task :**

```bash
aws ecs execute-command \
  --cluster ecs-cluster-production \
  --task abc123def456 \
  --container flask-app-container \
  --interactive \
  --command "/bin/bash"
```

**Résultat :**

```
Starting session with SessionId: ecs-execute-command-abc123...

appuser@ip-10-0-11-25:/app$
```

**[OK] Shell interactif dans container running ! (comme `docker exec`) [OK]**

---

**Commandes utiles :**

```bash
# Voir processus
ps aux

# Variables d'environnement
env | grep DB_

# Tester DB connection
python3 -c "import psycopg2; conn = psycopg2.connect(host='...'); print('OK')"

# Logs Gunicorn
cat /proc/1/fd/1  # stdout
cat /proc/1/fd/2  # stderr

# Network debugging
apt-get update && apt-get install -y curl netcat
curl http://localhost:5000/health
nc -zv sirrdev-todo-db.xxx.rds.amazonaws.com 5432

# Exit
exit
```

---

### ÉTAPE 18 : Optimisations avancées

#### 1. Fargate Spot (jusqu'à 70% d'économie)

**Fargate Spot = Capacity excédentaire AWS (peut être interrompu avec 2 min notice)**

**Créer Capacity Provider Strategy :**

**ECS -> Clusters -> ecs-cluster-production -> Capacity providers**

**Add capacity provider strategy :**

```
Capacity provider: FARGATE_SPOT
Base: 0
Weight: 1

Capacity provider: FARGATE (On-Demand)
Base: 1  (Toujours 1 On-Demand minimum)
Weight: 1
```

---

**Mettre à jour service :**

**Services -> flask-app-service -> Update**

```
Capacity provider strategy:
  [x] Use custom (Advanced)
  
  FARGATE: Base=1, Weight=1
  FARGATE_SPOT: Base=0, Weight=4
  
  (Ratio: 1 On-Demand, 4 Spot)
```

**Update**

---

**Résultat :**

```
Desired count: 5 tasks

Tasks:
  1 × FARGATE (On-Demand)
  4 × FARGATE_SPOT (70% discount) [OK]

Coût:
  On-Demand: 5 × 8.89$/month = 44.45$/month
  Mixed: (1 × 8.89) + (4 × 2.67) = 19.57$/month
  
Économie: 56% [OK]
```

**[ATTENTION] Spot peut être interrompu (use case : dev/staging, tasks non-critiques)**

---

#### 2. ARM64 / Graviton2 (20% moins cher)

**Fargate supporte ARM64 (processeurs AWS Graviton2)**

**Rebuild image pour ARM64 :**

```bash
# Build multi-arch image
docker buildx build --platform linux/amd64,linux/arm64 \
  -t 123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app-production:v2.0.0-multiarch \
  --push .
```

**Ou utiliser ECR image scanning pour auto-convert**

---

**Task Definition -> Architecture :**

```
Operating system/Architecture: Linux/ARM64
```

**Coût :**

```
x86_64: 0.25 vCPU × 0.04048$/vCPU-h = 0.01012$/h
ARM64:  0.25 vCPU × 0.03238$/vCPU-h = 0.008095$/h

Économie: 20% [OK]
```

---

#### 3. Compression d'image avec Slim/Distroless

**Base image alternatives :**

| Base | Size | Use case |
|------|------|----------|
| `python:3.11` | 1.02 GB | Full tools |
| `python:3.11-slim` | 125 MB | Production [OK] |
| `python:3.11-alpine` | 50 MB | Minimal (complexe) |
| `gcr.io/distroless/python3` | 55 MB | Ultra minimal |

**On utilise déjà `slim` [OK]**

---

**Alternative : Distroless**

```dockerfile
FROM python:3.11-slim AS builder
# ... build stage ...

FROM gcr.io/distroless/python3-debian12

COPY --from=builder /opt/venv /opt/venv
COPY app.py .

ENV PATH="/opt/venv/bin:$PATH"

CMD ["/opt/venv/bin/gunicorn", "app:app"]
```

**Avantages Distroless :**
- Pas de shell (sécurité ++)
- Pas de package manager
- Surface d'attaque minimale
- Taille réduite

**Inconvénients :**
- Debugging difficile (pas de shell)
- Pas de `apt install` pour troubleshooting

---

#### 4. Layer caching et Build optimization

**Ordonner Dockerfile pour maximiser cache :**

```dockerfile
# [X] MAUVAIS (cache souvent invalidé)
FROM python:3.11-slim
COPY . .  # <- Change à chaque modif code
RUN pip install -r requirements.txt

# [OK] BON (cache requirements séparément)
FROM python:3.11-slim
COPY requirements.txt .
RUN pip install -r requirements.txt  # <- Cache stable
COPY app.py .  # <- Change souvent, mais après pip
```

**Règle : Commandes qui changent rarement en premier**

---

**BuildKit (Docker build avancé) :**

```bash
# Activer BuildKit
export DOCKER_BUILDKIT=1

# Build avec cache mount (pip cache persistant)
docker build \
  --build-arg BUILDKIT_INLINE_CACHE=1 \
  -t flask-app:latest .
```

**Dockerfile avec BuildKit :**

```dockerfile
# syntax=docker/dockerfile:1

FROM python:3.11-slim

COPY requirements.txt .

# Cache pip downloads
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

COPY app.py .
```

**Build ~50% plus rapide avec cache [OK]**

---

#### 5. Read-only root filesystem

**Sécurité : Container filesystem read-only**

**Task Definition :**

```json
"containerDefinitions": [
  {
    "readonlyRootFilesystem": true,
    "mountPoints": [
      {
        "sourceVolume": "tmp",
        "containerPath": "/tmp",
        "readOnly": false
      }
    ]
  }
]

"volumes": [
  {
    "name": "tmp",
    "host": {}
  }
]
```

**Avantages :**
- Empêche modifications malveillantes
- Immutabilité garantie
- Compliance (PCI-DSS, HIPAA)

**Requires : App écrit dans `/tmp` seulement (pas de logs dans `/var/log`)**

---

### ÉTAPE 19 : Monitoring avancé avec X-Ray

#### Activer AWS X-Ray tracing

**X-Ray = Distributed tracing (APM)**

**Trace requêtes à travers ALB -> ECS -> RDS**

---

**Installer X-Ray daemon sidecar :**

**Task Definition -> Add container**

```
Container name: xray-daemon

Image: amazon/aws-xray-daemon:latest

Port mappings:
  2000/udp (X-Ray daemon protocol)

CPU: 32
Memory soft limit: 256 MB

Links to: flask-app-container
```

---

**Instrumenter Flask app :**

**requirements.txt :**

```
aws-xray-sdk==2.12.0
```

**app.py :**

```python
from aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.ext.flask.middleware import XRayMiddleware

app = Flask(__name__)

# Configure X-Ray
xray_recorder.configure(
    service='flask-app-production',
    daemon_address='127.0.0.1:2000'
)

# Add middleware
XRayMiddleware(app, xray_recorder)

# Instrument DB calls
from aws_xray_sdk.core import patch_all
patch_all()  # Auto-instrument boto3, psycopg2, requests, etc.

@app.route('/')
@xray_recorder.capture('index_handler')
def index():
    # Custom subsegment
    with xray_recorder.capture('database_query'):
        conn = get_db_connection()
        # ... query ...
    
    return jsonify({...})
```

---

**Rebuild et déployer nouvelle image**

---

**Visualiser traces :**

**X-Ray -> Service map**

```
Client
  v (250ms avg)
ALB
  v (180ms avg)
ECS Task (flask-app)
  v (120ms avg - database_query segment)
RDS PostgreSQL
```

**Trace details :**

```
Total duration: 250ms

Segments:
  ALB: 20ms (overhead)
  flask-app-container: 210ms
    ├─ index_handler: 210ms
    │  ├─ database_query: 120ms
    │  ├─ response_formatting: 15ms
    │  └─ logging: 5ms
    └─ overhead: 70ms
  RDS: 115ms (actual query time)
```

**Identifier bottlenecks [OK]**

---

### ÉTAPE 20 : Sécurité Containers

#### 1. Vulnerability Scanning

**ECR scan automatique (AWS Inspector) :**

**ECR -> Repositories -> flask-app-production**

```
Scan on push: Enabled [OK]
```

**Chaque push -> Auto scan**

---

**Scan results :**

```
Critical: 0 [OK]
High: 2
Medium: 5
Low: 12

CVE-2023-1234 (High):
  Package: openssl 1.1.1t
  Fix: Upgrade to 1.1.1w
  CVSS: 7.5
```

---

**Bloquer déploiements avec vulnérabilités :**

**CodePipeline -> Add approval stage**

```
Stage: Security-Scan-Approval

Manual approval IF:
  Critical vulnerabilities > 0
  High vulnerabilities > 5
```

---

#### 2. IAM Task Roles (Least Privilege)

**Séparer Task Execution Role vs Task Role :**

**Task Execution Role :**
- Pull ECR images
- Write CloudWatch Logs
- Get Secrets Manager secrets

**Task Role :**
- Permissions pour l'app (S3, DynamoDB, SNS, etc.)

**Créer Task Role minimal :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::sirrdev-app-uploads/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:eu-west-1:123456789012:table/Tasks"
    }
  ]
}
```

**Pas de `"Resource": "*"` [OK]**

---

#### 3. Network Isolation

**Security Groups granulaires :**

```
sg-ecs-tasks:
  Inbound:
    - Port 5000 from sg-alb ONLY
    - NO SSH (pas de port 22)
  
  Outbound:
    - Port 443 to 0.0.0.0/0 (HTTPS pour APIs externes)
    - Port 5432 to sg-db-tier (PostgreSQL)
    - NO port 80 (force HTTPS)
```

---

**Network ACLs restrictifs :**

```
NACL subnet-app:
  Inbound:
    Rule 100: Allow TCP 5000 from ALB subnets
    Rule 200: Allow TCP 1024-65535 (ephemeral)
    Rule *: Deny all
  
  Outbound:
    Rule 100: Allow TCP 5432 to DB subnet
    Rule 200: Allow TCP 443 to 0.0.0.0/0
    Rule 300: Allow TCP 1024-65535
    Rule *: Deny all
```

---

#### 4. Secrets rotation

**Automatic secrets rotation :**

**Secrets Manager -> production/db/password -> Rotation configuration**

```
[x] Enable automatic rotation

Rotation schedule: 30 days

Lambda function: SecretsManagerRDSPostgreSQLRotationSingleUser

Create
```

**Secrets Manager crée Lambda qui :**
1. Génère nouveau password
2. Update RDS
3. Update secret
4. Teste connexion

**ECS tasks récupèrent nouveau secret automatiquement (force new deployment) [OK]**

---

#### 5. Runtime protection (Fargate Runtime Threat Detection)

**GuardDuty Fargate threat detection :**

**GuardDuty -> Settings**

```
[x] Enable ECS Runtime Monitoring
  
  [x] Manage agent automatically
```

**Détecte :**
- Process anomalies (crypto miners, etc.)
- Network anomalies (C&C communication)
- File access anomalies

**Alerte CloudWatch Event -> SNS -> Email [OK]**

---

### [OK] TESTS DE VALIDATION

**Infrastructure :**
- [ ] ECR repository créé avec images
- [ ] ECS Cluster créé (Fargate)
- [ ] Task Definition créée (3+ revisions)
- [ ] ECS Service running (min 2 tasks)
- [ ] Application Load Balancer intégré
- [ ] Target Group avec health checks

**Containers :**
- [ ] Image Docker créée et optimisée
- [ ] Multi-stage build utilisé
- [ ] Image < 200 MB
- [ ] Non-root user configuré
- [ ] Health check intégré

**Networking :**
- [ ] Tasks déployés dans subnets privés
- [ ] Security Groups configurés (ALB -> Tasks -> RDS)
- [ ] Service Discovery activé (Cloud Map)
- [ ] Tasks accessibles via DNS interne

**Auto Scaling :**
- [ ] Target tracking policy configurée
- [ ] Scheduled scaling configurée (optionnel)
- [ ] Scale OUT testé (CPU > 70%)
- [ ] Scale IN testé (CPU < 30%)

**Monitoring :**
- [ ] CloudWatch Logs centralisés
- [ ] Container Insights activé
- [ ] Dashboard CloudWatch créé
- [ ] Alarmes configurées (CPU, Memory, Health)
- [ ] X-Ray tracing activé (optionnel)

**Security :**
- [ ] Secrets Manager pour credentials
- [ ] IAM Task Role avec least privilege
- [ ] Vulnerability scanning activé
- [ ] Read-only root filesystem (optionnel)
- [ ] GuardDuty runtime monitoring (optionnel)

**Deployments :**
- [ ] Rolling deployment testé
- [ ] Blue/Green deployment capable
- [ ] Circuit breaker activé
- [ ] Zero-downtime confirmé
- [ ] Rollback testé

**CI/CD (optionnel) :**
- [ ] CodePipeline créé
- [ ] CodeBuild configuré
- [ ] Auto deploy sur git push
- [ ] buildspec.yml fonctionnel

**Performance :**
- [ ] Response time < 200ms
- [ ] Container start time < 60s
- [ ] Load testing réussi (1000+ req)
- [ ] CPU/Memory optimisés

---

### [ROUGE] ERREURS COURANTES ET SOLUTIONS

#### Erreur 1 : Task stopped with exit code 139 (SIGSEGV)

**Symptôme :**

```
Task stopped
Exit code: 139 (Segmentation fault)
```

**Causes :**

**1. Memory limit trop bas**

```
Task memory: 512 MB
App nécessite: 700 MB

-> OOM Killer termine process
```

**Solution : Augmenter task memory**

---

**2. Incompatibilité architecture**

```
Image: Built for ARM64
Task: Running on x86_64

-> Segfault
```

**Solution : Vérifier architecture**

```bash
docker inspect flask-app:latest | jq '.[0].Architecture'
# "arm64" ou "amd64"
```

**Match task definition architecture**

---

#### Erreur 2 : Tasks démarrent puis s'arrêtent immédiatement

**Symptôme :**

```
Task PENDING -> RUNNING -> STOPPED (dans 10 secondes)
```

**Cause : CMD/ENTRYPOINT exit immédiatement**

**Vérifier Dockerfile :**

```dockerfile
# [X] MAUVAIS
CMD ["echo", "Hello"]
  -> Exécute, print, exit

# [OK] BON
CMD ["gunicorn", "app:app"]
  -> Process long-running
```

---

**Ou app crash au startup :**

**CloudWatch Logs :**

```
Traceback (most recent call last):
  File "app.py", line 5, in <module>
    import missing_module
ModuleNotFoundError: No module named 'missing_module'
```

**Solution : Fixer l'app ou requirements.txt**

---

#### Erreur 3 : Cannot pull ECR image (403 Forbidden)

**Symptôme :**

```
CannotPullContainerError: 403 Forbidden
```

**Causes :**

**1. Task Execution Role manque permissions ECR**

**Vérifier :**

```bash
aws iam get-role-policy \
  --role-name ecsTaskExecutionRole \
  --policy-name ECSTaskExecutionPolicy
```

**Doit contenir :**

```json
{
  "Action": [
    "ecr:GetAuthorizationToken",
    "ecr:BatchCheckLayerAvailability",
    "ecr:GetDownloadUrlForLayer",
    "ecr:BatchGetImage"
  ],
  "Resource": "*",
  "Effect": "Allow"
}
```

---

**2. ECR repository dans autre compte AWS**

**ECR repository policy :**

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCOUNT_B:role/ecsTaskExecutionRole"
      },
      "Action": [
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer"
      ]
    }
  ]
}
```

---

#### Erreur 4 : Health check failing mais app fonctionne

**Symptôme :**

```
Target Group: Unhealthy
Mais: curl localhost:5000/health -> 200 OK
```

**Causes :**

**1. Security Group bloque ALB -> Tasks**

```
sg-ecs-tasks inbound:
  Port 5000 from 0.0.0.0/0 [X] (pas ALB SG)

Fix:
  Port 5000 from sg-alb-ecs [OK]
```

---

**2. Health check path incorrect**

```
Target Group health check: /health
App endpoint: /healthcheck (différent)

-> 404 Not Found
```

**Solution : Aligner path**

---

**3. Container port mismatch**

```
Task Definition port: 5000
App listen on: 8080

-> Connection refused
```

**Solution : Match ports**

---

#### Erreur 5 : Fargate Spot task interrupted

**Symptôme :**

```
Task status: DEPROVISIONING
Stopped reason: Spot interruption
```

**Cause : Fargate reprend capacity**

**Solution : Capacity Provider mix**

```
Base: 2 FARGATE (toujours On-Demand)
Weight: 3 FARGATE_SPOT

-> 2 tasks critiques toujours On-Demand
-> Scale OUT = Spot (économie)
```

---

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

**Containers vs VMs :**
- Containers = Processus isolés (pas OS complet)
- Boot rapide (< 1 sec vs 30-60 sec)
- Léger (MB vs GB)
- Portables (même image dev -> prod)

**Docker :**
- Image = Template immuable
- Container = Instance running d'image
- Dockerfile = Recette build
- Multi-stage = Optimisation taille
- Layer caching = Build rapide

**Amazon ECS :**
- Orchestration containers AWS
- Task Definition = Blueprint
- Service = Maintient N tasks running
- Cluster = Groupe logique ressources

**Fargate :**
- Serverless containers (pas d'EC2 à gérer)
- Pay-per-use (vCPU-sec + GB-sec)
- Auto-scaling natif
- ~30% plus cher qu'EC2 mais 0 gestion

**ECR :**
- Docker registry privé AWS
- Vulnerability scanning intégré
- Lifecycle policies (cleanup auto)
- IAM intégration

**Networking :**
- Tasks = IPs privées dynamiques
- Service Discovery = DNS automatique
- ALB integration = Load balancing
- Security Groups = Firewall instance-level

**Monitoring :**
- CloudWatch Logs = Logs centralisés
- Container Insights = Métriques détaillées
- X-Ray = Distributed tracing
- Health checks = ELB + Task-level

**Sécurité :**
- Vulnerability scanning (ECR)
- Secrets Manager (credentials)
- IAM Task Roles (least privilege)
- Non-root user dans container
- Read-only root filesystem

**Cost Optimization :**
- Fargate Spot = 70% discount
- ARM64/Graviton2 = 20% discount
- Right-sizing (CPU/Memory)
- Multi-stage builds = Images plus petites

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Amazon EKS (Kubernetes on AWS)**

**ECS vs EKS :**

| Critère | ECS | EKS |
|---------|-----|-----|
| **Learning curve** | Simple | Complex |
| **Portabilité** | AWS-only | Multi-cloud |
| **Coût** | Gratuit (sauf compute) | 0.10$/h/cluster (~73$/mois) |
| **Ecosystem** | AWS services | Kubernetes ecosystem |
| **Use case** | AWS-native apps | Hybrid/multi-cloud |

**Quand utiliser EKS :**
- Multi-cloud strategy
- Existing Kubernetes expertise
- Complex orchestration needs
- Large-scale (100+ services)

---

**2. App Mesh (Service Mesh)**

**Service mesh = Couche communication microservices**

```
Service A -> App Mesh Envoy Proxy
  -> Traffic routing
  -> Retry logic
  -> Circuit breaker
  -> mTLS encryption
    -> Service B Envoy Proxy
      -> Service B
```

**Features :**
- Traffic shifting (canary, blue/green)
- Observability (métriques, traces)
- Security (mTLS automatique)
- Resilience (retries, timeouts, circuit breakers)

---

**3. AWS Copilot (CLI simplifiée)**

**Copilot = Infrastructure as Code simplifié pour containers**

```bash
# Installer
brew install aws/tap/copilot-cli

# Init app
copilot init

# Deploy
copilot deploy
```

**Copilot crée automatiquement :**
- VPC
- ALB
- ECS Cluster
- Task Definition
- Service
- CloudWatch Logs
- IAM Roles

**Trade-off : Moins de contrôle, plus simple**

---

**4. AWS Batch (Jobs batch)**

**Use case : Processing jobs (non-web apps)**

```
Exemple:
  - Video transcoding
  - Data processing
  - ML training
  - Report generation
```

**AWS Batch :**
- Queue jobs
- Auto-scale compute
- Retry failed jobs
- Job dependencies (DAG)

**vs ECS Service :**
- Batch = One-time jobs
- Service = Long-running apps

---

**5. Lambda + Containers**

**Lambda supporte containers (max 10 GB) :**

```dockerfile
FROM public.ecr.aws/lambda/python:3.11

COPY requirements.txt .
RUN pip install -r requirements.txt

COPY app.py .

CMD ["app.lambda_handler"]
```

**Deploy :**

```bash
aws lambda create-function \
  --function-name flask-app-lambda \
  --package-type Image \
  --code ImageUri=123456789012.dkr.ecr.eu-west-1.amazonaws.com/flask-app:latest \
  --role arn:aws:iam::123456789012:role/lambda-role
```

**Avantages :**
- Scaling automatique (0 -> ∞)
- Pay-per-request
- Max 15 min execution

**Use case : APIs sporadiques, event-driven**

---

**6. Bottlerocket OS**

**OS optimisé pour containers (par AWS) :**

```
Standard Amazon Linux 2: 2.5 GB
Bottlerocket: 500 MB

Boot time: 50% plus rapide
Security: Immutable OS, auto-updates
```

**ECS optimized Bottlerocket AMI disponible [OK]**

---

**7. Firecracker (MicroVMs)**

**Firecracker = Lightweight VMs pour containers**

**Fargate utilise Firecracker sous le capot :**

```
Container -> Firecracker MicroVM
  -> Isolation VM-level
  -> Performance container-level
  
Best of both worlds [OK]
```

---

## [COURS] CONCLUSION DE L'EXERCICE 7

**[BRAVO] EXCELLENT ! Tu maîtrises maintenant les containers et ECS/Fargate ! [BRAVO]**

**Ce que tu as appris :**
- [OK] Concepts containers vs VMs
- [OK] Docker (Dockerfile, images, build, run)
- [OK] Multi-stage builds
- [OK] Amazon ECR (registry privé)
- [OK] Amazon ECS (orchestration)
- [OK] AWS Fargate (serverless containers)
- [OK] Task Definitions
- [OK] ECS Services avec ALB
- [OK] Auto Scaling containers
- [OK] Service Discovery (Cloud Map)
- [OK] ECS Exec (debugging)
- [OK] CloudWatch monitoring
- [OK] Secrets Manager
- [OK] Security containers
- [OK] CI/CD avec CodePipeline

**Compétences acquises :**
- [OK] Containerization
- [OK] Container orchestration
- [OK] Serverless architecture
- [OK] Microservices deployment
- [OK] DevOps practices

**Architecture déployée :**
```
[OK] Application containerisée (Flask)
[OK] Image optimisée (< 150 MB)
[OK] ECR repository avec lifecycle policies
[OK] ECS Cluster Fargate
[OK] 3 tasks running (multi-AZ)
[OK] Application Load Balancer
[OK] Auto Scaling (2-10 tasks)
[OK] Service Discovery
[OK] CloudWatch Logs + Insights
[OK] Secrets Manager intégration
[OK] CI/CD pipeline (optionnel)
```

**Performance :**
- Start time : < 60 secondes
- Response time : < 200ms
- Availability : 99.9%+
- Scalabilité : 2 -> 10 tasks automatique

**Coût estimé :**
- **Fargate (3 tasks 24/7) :** ~27$/mois
- **ECR (< 10 GB) :** ~1$/mois
- **ALB :** ~16$/mois
- **CloudWatch Logs :** ~2$/mois
- **Total :** ~46$/mois

**vs EC2 traditionnel : Même ordre grandeur mais 0 gestion serveur [OK]**

**Temps moyen de réalisation :** 6-7 heures

**Prochaine étape :** Exercice 8 - Infrastructure as Code (CloudFormation + Terraform) ! [DOC]

---

**[ATTENTION] NETTOYAGE DES RESSOURCES**

```bash
# 1. Supprimer ECS Service (stop tasks)
ECS -> Services -> flask-app-service -> Delete
  [x] Force delete

# 2. Supprimer ECS Cluster
ECS -> Clusters -> ecs-cluster-production -> Delete

# 3. Supprimer Task Definitions (optionnel, ne coûtent rien)
ECS -> Task Definitions -> Deregister all revisions

# 4. Supprimer ECR images
ECR -> Repositories -> flask-app-production -> Delete repository
  [x] Delete images

# 5. Supprimer ALB
EC2 -> Load Balancers -> alb-ecs-production -> Delete

# 6. Supprimer Target Groups
EC2 -> Target Groups -> tg-ecs-flask-app -> Delete

# 7. Supprimer CloudWatch Log Groups
CloudWatch -> Logs -> /ecs/flask-app-production -> Delete

# 8. Supprimer Cloud Map namespace
Cloud Map -> Namespaces -> production.local -> Delete

# 9. Supprimer Secrets Manager secrets
Secrets Manager -> production/db/password -> Delete
  Schedule deletion: 7 days (minimum)

# 10. Supprimer Security Groups
EC2 -> Security Groups -> sg-ecs-tasks-production -> Delete
EC2 -> Security Groups -> sg-alb-ecs -> Delete

# 11. Supprimer IAM Roles
IAM -> Roles -> ecsTaskRole -> Delete
IAM -> Roles -> ecsTaskExecutionRole -> Delete (si créé custom)

# 12. Supprimer CodePipeline (si créé)
CodePipeline -> Pipelines -> flask-app-pipeline -> Delete

# 13. Supprimer CodeBuild project
CodeBuild -> Build projects -> flask-app-build -> Delete
```

**Vérification finale :**
```
ECS Clusters -> Aucun [OK]
ECR Repositories -> Aucun [OK]
ALB -> Aucun [OK]
Running tasks -> 0 [OK]
```

---

**FIN DE L'EXERCICE 7**

═══════════════════════════════════════════════════════════════

Veux-tu que je continue avec l'**Exercice 8 : Infrastructure as Code (CloudFormation, Terraform, CDK)** ?

# [JAUNE] EXERCICE 8 : INFRASTRUCTURE AS CODE (CloudFormation + Terraform + CDK)

## [LISTE] ÉNONCÉ

### Contexte professionnel

La startup a grandi et gère maintenant plusieurs environnements (dev, staging, production). Le CTO fait face à de nouveaux défis :

**Problèmes avec gestion manuelle via Console AWS :**
- [X] **"ClickOps"** : Configuration manuelle = erreurs humaines, incohérences
- [X] **Pas de versioning** : Impossible de revenir en arrière, pas d'audit trail
- [X] **Pas de documentation** : Infrastructure = "tribal knowledge" dans têtes des devs
- [X] **Environnements divergents** : Dev ≠ Staging ≠ Production (drift)
- [X] **Onboarding lent** : Nouveau dev = semaines pour comprendre infra
- [X] **Disaster recovery complexe** : Recréer infra = jours de travail manuel
- [X] **Pas de code review** : Changements infra non reviewés comme code applicatif
- [X] **Multi-région difficile** : Répliquer infra manuellement = cauchemar

**Nouvelle approche Infrastructure as Code demandée :**

Le directeur technique impose :
1. **Infrastructure versionnable** (Git)
2. **Infrastructure reproductible** (même template -> même infra)
3. **Infrastructure testable** (validation avant apply)
4. **Infrastructure documentée** (code = documentation)
5. **Multi-environnements** (dev, staging, prod) avec même template
6. **Code review obligatoire** (Pull Requests)
7. **CI/CD pour infrastructure** (auto-deploy après merge)
8. **State management** (tracking des ressources)
9. **Modules réutilisables** (DRY principle)
10. **Disaster recovery automatisé** (1 commande = rebuild complet)

### Architecture cible

```
┌────────────────────────────────────────────────────────────────┐
│                   INFRASTRUCTURE AS CODE                        │
├────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │               SOURCE CONTROL (Git)                        │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  Repository: infrastructure-repo                    │  │  │
│  │  │  ├── cloudformation/                                │  │  │
│  │  │  │   ├── vpc.yaml                                   │  │  │
│  │  │  │   ├── rds.yaml                                   │  │  │
│  │  │  │   ├── ecs.yaml                                   │  │  │
│  │  │  │   └── master.yaml (nested stacks)               │  │  │
│  │  │  ├── terraform/                                     │  │  │
│  │  │  │   ├── modules/                                   │  │  │
│  │  │  │   │   ├── vpc/                                   │  │  │
│  │  │  │   │   ├── rds/                                   │  │  │
│  │  │  │   │   └── ecs/                                   │  │  │
│  │  │  │   ├── environments/                              │  │  │
│  │  │  │   │   ├── dev.tfvars                             │  │  │
│  │  │  │   │   ├── staging.tfvars                         │  │  │
│  │  │  │   │   └── production.tfvars                      │  │  │
│  │  │  │   ├── main.tf                                    │  │  │
│  │  │  │   ├── variables.tf                               │  │  │
│  │  │  │   └── outputs.tf                                 │  │  │
│  │  │  └── cdk/                                           │  │  │
│  │  │      ├── lib/                                       │  │  │
│  │  │      │   ├── vpc-stack.ts                           │  │  │
│  │  │      │   ├── rds-stack.ts                           │  │  │
│  │  │      │   └── ecs-stack.ts                           │  │  │
│  │  │      ├── bin/app.ts                                 │  │  │
│  │  │      ├── cdk.json                                   │  │  │
│  │  │      └── package.json                               │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                             v                                   │
│                       git push / PR merge                       │
│                             v                                   │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │              CI/CD PIPELINE (GitHub Actions)             │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  Validation Stage:                                  │  │  │
│  │  │  ├─ Syntax check (cloudformation validate)         │  │  │
│  │  │  ├─ Linting (cfn-lint, terraform fmt, tflint)      │  │  │
│  │  │  ├─ Security scan (checkov, tfsec)                 │  │  │
│  │  │  └─ Cost estimation (infracost)                    │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │                         v                                  │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  Plan Stage:                                        │  │  │
│  │  │  ├─ CloudFormation: Create change set              │  │  │
│  │  │  ├─ Terraform: terraform plan                       │  │  │
│  │  │  └─ CDK: cdk diff                                   │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │                         v                                  │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  Manual Approval (Production only)                  │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │                         v                                  │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  Apply Stage:                                       │  │  │
│  │  │  ├─ CloudFormation: Execute change set             │  │  │
│  │  │  ├─ Terraform: terraform apply                      │  │  │
│  │  │  └─ CDK: cdk deploy                                 │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                             v                                   │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │              STATE MANAGEMENT                            │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  CloudFormation:                                    │  │  │
│  │  │  - State: AWS-managed (automatic)                  │  │  │
│  │  │  - Location: AWS CloudFormation service            │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  Terraform:                                         │  │  │
│  │  │  - State: terraform.tfstate                        │  │  │
│  │  │  - Backend: S3 + DynamoDB locking                  │  │  │
│  │  │  - Encryption: Yes (KMS)                           │  │  │
│  │  │  - Versioning: Enabled                             │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  │  ┌────────────────────────────────────────────────────┐  │  │
│  │  │  CDK:                                               │  │  │
│  │  │  - Synthesizes to -> CloudFormation                 │  │  │
│  │  │  - State: AWS-managed (via CFN)                    │  │  │
│  │  └────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────┘  │
│                             v                                   │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                  AWS INFRASTRUCTURE                      │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │  │
│  │  │     DEV      │  │   STAGING    │  │  PRODUCTION  │  │  │
│  │  │  VPC 10.0/16 │  │  VPC 10.1/16 │  │  VPC 10.2/16 │  │  │
│  │  │  RDS t3.micro│  │  RDS t3.small│  │  RDS r6.large│  │  │
│  │  │  ECS 1 task  │  │  ECS 2 tasks │  │  ECS 5 tasks │  │  │
│  │  └──────────────┘  └──────────────┘  └──────────────┘  │  │
│  │         ^                  ^                  ^          │  │
│  │    Same template, different parameters                  │  │
│  └──────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────┘

Comparison Table:
┌─────────────────────────────────────────────────────────────┐
│                CloudFormation vs Terraform vs CDK            │
├────────────────┬──────────────┬──────────────┬──────────────┤
│ Critère        │CloudFormation│  Terraform   │     CDK      │
├────────────────┼──────────────┼──────────────┼──────────────┤
│ Provider       │ AWS          │ HashiCorp    │ AWS          │
│ Language       │ YAML/JSON    │ HCL          │ TypeScript/  │
│                │              │              │ Python/Java  │
│ Learning curve │ Medium       │ Medium       │ Easy (for    │
│                │              │              │ developers)  │
│ Multi-cloud    │ No (AWS only)│ Yes          │ No (AWS only)│
│ State          │ AWS-managed  │ Self-managed │ AWS-managed  │
│ Cost           │ Free         │ Free (OSS)   │ Free         │
│ Maturity       │ High         │ Very High    │ Medium       │
│ Community      │ AWS docs     │ Huge         │ Growing      │
│ Abstractions   │ Low-level    │ Low-level    │ High-level   │
│ Testing        │ Limited      │ Good         │ Excellent    │
│ Modularity     │ Nested stacks│ Modules      │ Constructs   │
└────────────────┴──────────────┴──────────────┴──────────────┘
```

### Contraintes techniques

- Région : `eu-west-1` (Irlande)
- Git : GitHub ou GitLab
- CloudFormation : YAML format
- Terraform : v1.6+
- CDK : TypeScript (Node.js 18+)
- State backend : S3 + DynamoDB (Terraform)
- CI/CD : GitHub Actions ou GitLab CI
- Environnements : dev, staging, production
- Budget : Optimiser pour rester dans Free Tier
- Temps estimé : 7-8 heures

---

## [OBJECTIF] OBJECTIFS PÉDAGOGIQUES

À la fin de cet exercice, tu sauras :

- [OK] Comprendre les concepts Infrastructure as Code
- [OK] Écrire templates CloudFormation (YAML)
- [OK] Utiliser nested stacks et modules
- [OK] Écrire configurations Terraform (HCL)
- [OK] Gérer state Terraform avec S3 backend
- [OK] Créer modules Terraform réutilisables
- [OK] Utiliser AWS CDK (TypeScript)
- [OK] Comprendre CDK Constructs et Stacks
- [OK] Comparer CloudFormation vs Terraform vs CDK
- [OK] Implémenter CI/CD pour IaC
- [OK] Gérer multi-environnements
- [OK] Tester infrastructure code
- [OK] Troubleshooter IaC deployments

---

## [DOCS] PRÉREQUIS

- Exercices 1-7 terminés (concepts AWS)
- Git installé et configuré
- AWS CLI configuré
- Terraform installé (v1.6+)
- Node.js 18+ installé (pour CDK)
- Connaissances YAML/JSON
- Connaissances programmation (pour CDK)

---

## [ARGENT] ESTIMATION DES COÛTS

**Infrastructure as Code services :**

| Service | Coût | Remarque |
|---------|------|----------|
| CloudFormation | **Gratuit** [OK] | AWS-managed |
| Terraform OSS | **Gratuit** [OK] | Open-source |
| CDK | **Gratuit** [OK] | Wrapper CloudFormation |
| Terraform Cloud | 0$ (Free tier) | 500 resources limit |

**S3 Backend (Terraform state) :**

| Ressource | Coût | Remarque |
|-----------|------|----------|
| S3 storage | ~0.023$/GB-month | State ~1 MB = 0.02$/mois |
| DynamoDB | Gratuit (Free Tier) | < 25 GB |
| Versioning | Inclus | Rollback safety |

**Infrastructure déployée (même coût que exercices précédents) :**

```
VPC: Gratuit [OK]
EC2 t3.micro (2): ~15$/mois (ou Free Tier)
RDS t3.micro: ~15$/mois (ou Free Tier)
ALB: ~16$/mois
ECS Fargate: ~27$/mois
Total: ~73$/mois (ou ~16$/mois avec Free Tier)
```

**Scénario de cet exercice :**

```
IaC tools: 0$/mois [OK]
S3 state backend: 0.02$/mois
Infrastructure (dev): ~10$/mois (minimal sizing)

Total: ~10$/mois
```

---

## [OK] SOLUTION COMPLÈTE

### ÉTAPE 1 : Comprendre Infrastructure as Code

#### Qu'est-ce que l'IaC ?

**Infrastructure as Code = Gérer infrastructure via code (pas clics manuels)**

**Principes :**

```
Infrastructure traditionnelle (ClickOps):
  Developer -> AWS Console -> Click Click Click -> Resources créées
  [X] Pas de versioning
  [X] Pas de reproduction
  [X] Pas de documentation
  [X] Erreurs humaines

Infrastructure as Code:
  Developer -> Write code -> Git commit -> Apply -> Resources créées
  [OK] Versionné (Git)
  [OK] Reproductible (même code = même infra)
  [OK] Documenté (code = documentation)
  [OK] Testé (validation automatique)
  [OK] Code review (Pull Requests)
```

---

#### Avantages IaC

**1. Versioning et Audit Trail**

```
git log infrastructure/

commit abc123 (HEAD -> main)
Author: SirrDev
Date: 2024-12-16

    Add production RDS with Multi-AZ
    
    - Increase instance size to r6.large
    - Enable automated backups (7 days retention)
    - Add read replica in eu-west-1b

commit def456
Author: SirrDev
Date: 2024-12-15

    Initial VPC setup
    
    - 3 public subnets
    - 3 private subnets
    - NAT Gateways in 2 AZs
```

**Historique complet des changements [OK]**

---

**2. Disaster Recovery**

```
Scénario: Région AWS complètement down

Traditionnelle:
  1. Recréer VPC manuellement
  2. Recréer subnets, route tables, IGW...
  3. Recréer security groups...
  4. Recréer EC2, RDS, ALB...
  Durée: Jours/semaines [X]

IaC:
  1. terraform apply -var="region=us-east-1"
  Durée: 30 minutes [OK]
```

---

**3. Multi-environnements cohérents**

```
Dev:
  terraform apply -var-file=dev.tfvars
  -> VPC 10.0.0.0/16, t3.micro instances

Staging:
  terraform apply -var-file=staging.tfvars
  -> VPC 10.1.0.0/16, t3.small instances

Production:
  terraform apply -var-file=production.tfvars
  -> VPC 10.2.0.0/16, r6.large instances

Même code, paramètres différents [OK]
Garantie cohérence architecture [OK]
```

---

**4. Code Review et Collaboration**

```
Developer A: Modify RDS instance size
  -> Create Pull Request
    -> CI runs: terraform plan
      -> Shows: t3.micro -> t3.large
        -> Team reviews
          -> DBA: "Too expensive, use t3.medium"
            -> Developer updates
              -> Approved [OK]
                -> Merged
                  -> Auto-deployed

Infrastructure changes reviewed comme code applicatif [OK]
```

---

**5. Documentation auto-générée**

```
Code Terraform = Documentation

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = {
    Name        = "vpc-production"
    Environment = "production"
    ManagedBy   = "Terraform"
  }
}

-> Documentation toujours à jour (code = source of truth) [OK]
```

---

#### Concepts clés

**Declarative vs Imperative**

**Imperative (scripts bash) :**

```bash
# HOW to create infrastructure (step-by-step)

#!/bin/bash
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.VpcId' --output text)
aws ec2 create-tags --resources $VPC_ID --tags Key=Name,Value=vpc-production

SUBNET_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 --query 'Subnet.SubnetId' --output text)
aws ec2 create-tags --resources $SUBNET_ID --tags Key=Name,Value=subnet-public-1a

# If script fails midway -> Partial state [X]
# Run twice -> Duplicate resources [X]
```

**Declarative (IaC) :**

```hcl
# WHAT infrastructure should look like (desired state)

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags = { Name = "vpc-production" }
}

resource "aws_subnet" "public_1a" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
  tags = { Name = "subnet-public-1a" }
}

# Terraform compares desired state vs current state
# Creates/Updates/Deletes to reach desired state [OK]
# Idempotent: Run twice -> Same result [OK]
```

---

**Idempotence**

```
Idempotent = Running multiple times has same effect as running once

Non-idempotent (imperative):
  CREATE VPC cidr 10.0.0.0/16
  Run 1: Creates VPC [OK]
  Run 2: Error "VPC already exists" [X]

Idempotent (declarative):
  Desired state: VPC cidr 10.0.0.0/16
  Run 1: Creates VPC [OK]
  Run 2: No change (already exists) [OK]
  Run 3: No change [OK]
```

---

**State Management**

```
State = Current inventory of resources

CloudFormation:
  State stored: AWS CloudFormation service
  Management: Automatic [OK]
  
Terraform:
  State stored: terraform.tfstate file
  Management: Manual (S3 backend recommended)
  
  terraform.tfstate:
  {
    "version": 4,
    "resources": [
      {
        "type": "aws_vpc",
        "name": "main",
        "provider": "aws",
        "instances": [{
          "attributes": {
            "id": "vpc-abc123",
            "cidr_block": "10.0.0.0/16"
          }
        }]
      }
    ]
  }
  
  Terraform uses state to know what exists
  terraform plan: Compares desired (code) vs current (state)
```

---

### ÉTAPE 2 : AWS CloudFormation

#### Installation et setup

**CloudFormation CLI (déjà inclus dans AWS CLI)**

```bash
# Vérifier AWS CLI
aws --version
# aws-cli/2.13.0

# Vérifier CloudFormation disponible
aws cloudformation help
```

---

#### Créer un template CloudFormation simple

**Créer structure projet :**

```bash
mkdir -p ~/iac-workshop/cloudformation
cd ~/iac-workshop/cloudformation
```

---

**Créer `vpc.yaml` :**

```bash
nano vpc.yaml
```

**Contenu :**

```yaml
# ═══════════════════════════════════════════════════════════════
# CLOUDFORMATION TEMPLATE - VPC
# ═══════════════════════════════════════════════════════════════

AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC with public and private subnets across 2 AZs'

# ───────────────────────────────────────────────────────────────
# PARAMETERS (variables d'entrée)
# ───────────────────────────────────────────────────────────────

Parameters:
  EnvironmentName:
    Type: String
    Default: production
    Description: Environment name (dev, staging, production)
    AllowedValues:
      - dev
      - staging
      - production
  
  VpcCIDR:
    Type: String
    Default: 10.0.0.0/16
    Description: CIDR block for VPC
    AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'
  
  PublicSubnet1CIDR:
    Type: String
    Default: 10.0.1.0/24
    Description: CIDR for public subnet in AZ1
  
  PublicSubnet2CIDR:
    Type: String
    Default: 10.0.2.0/24
    Description: CIDR for public subnet in AZ2
  
  PrivateSubnet1CIDR:
    Type: String
    Default: 10.0.11.0/24
    Description: CIDR for private subnet in AZ1
  
  PrivateSubnet2CIDR:
    Type: String
    Default: 10.0.12.0/24
    Description: CIDR for private subnet in AZ2

# ───────────────────────────────────────────────────────────────
# RESOURCES (ressources AWS à créer)
# ───────────────────────────────────────────────────────────────

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub 'vpc-${EnvironmentName}'
        - Key: Environment
          Value: !Ref EnvironmentName
        - Key: ManagedBy
          Value: CloudFormation

  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub 'igw-${EnvironmentName}'

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  # Public Subnet 1
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select 
        - 0
        - !GetAZs ''
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-public-subnet-1a'
        - Key: Type
          Value: Public

  # Public Subnet 2
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select 
        - 1
        - !GetAZs ''
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-public-subnet-1b'
        - Key: Type
          Value: Public

  # Private Subnet 1
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select 
        - 0
        - !GetAZs ''
      CidrBlock: !Ref PrivateSubnet1CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-private-subnet-1a'
        - Key: Type
          Value: Private

  # Private Subnet 2
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select 
        - 1
        - !GetAZs ''
      CidrBlock: !Ref PrivateSubnet2CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-private-subnet-1b'
        - Key: Type
          Value: Private

  # NAT Gateway Elastic IP 1
  NatGateway1EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-nat-eip-1a'

  # NAT Gateway 1
  NatGateway1:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway1EIP.AllocationId
      SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-nat-1a'

  # Public Route Table
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-public-routes'

  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

  # Private Route Table 1
  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-private-routes-1a'

  DefaultPrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet1

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet2

# ───────────────────────────────────────────────────────────────
# OUTPUTS (valeurs exportées pour autres stacks)
# ───────────────────────────────────────────────────────────────

Outputs:
  VPC:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub '${EnvironmentName}-VPC'

  PublicSubnets:
    Description: List of public subnet IDs
    Value: !Join 
      - ','
      - - !Ref PublicSubnet1
        - !Ref PublicSubnet2
    Export:
      Name: !Sub '${EnvironmentName}-PublicSubnets'

  PrivateSubnets:
    Description: List of private subnet IDs
    Value: !Join 
      - ','
      - - !Ref PrivateSubnet1
        - !Ref PrivateSubnet2
    Export:
      Name: !Sub '${EnvironmentName}-PrivateSubnets'

  PublicSubnet1:
    Description: Public Subnet 1 ID
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub '${EnvironmentName}-PublicSubnet1'

  PublicSubnet2:
    Description: Public Subnet 2 ID
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub '${EnvironmentName}-PublicSubnet2'

  PrivateSubnet1:
    Description: Private Subnet 1 ID
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub '${EnvironmentName}-PrivateSubnet1'

  PrivateSubnet2:
    Description: Private Subnet 2 ID
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub '${EnvironmentName}-PrivateSubnet2'
```

**Sauvegarder**

---

**Explication des sections CloudFormation :**

| Section | Rôle | Exemple |
|---------|------|---------|
| `AWSTemplateFormatVersion` | Version format | '2010-09-09' |
| `Description` | Description template | 'VPC setup' |
| `Parameters` | Variables d'entrée | VpcCIDR, EnvironmentName |
| `Resources` | Ressources à créer | VPC, Subnets, NAT Gateway |
| `Outputs` | Valeurs exportées | VPC ID, Subnet IDs |
| `Mappings` | Lookup tables | AMI par région |
| `Conditions` | Logique conditionnelle | Si Prod -> Multi-AZ |

---

**Fonctions intrinsèques CloudFormation :**

| Fonction | Rôle | Exemple |
|----------|------|---------|
| `!Ref` | Référence ressource/param | `!Ref VPC` |
| `!GetAtt` | Attribut d'une ressource | `!GetAtt VPC.CidrBlock` |
| `!Sub` | Substitution variable | `!Sub 'vpc-${Env}'` |
| `!Join` | Concaténation | `!Join [',', [a, b]]` |
| `!Select` | Sélection dans liste | `!Select [0, !GetAZs]` |
| `!GetAZs` | Liste AZs région | `!GetAZs 'eu-west-1'` |
| `!If` | Condition | `!If [IsProd, t3.large, t3.micro]` |

---

#### Valider le template

**Validation syntaxe :**

```bash
aws cloudformation validate-template \
  --template-body file://vpc.yaml
```

**Résultat si OK :**

```json
{
  "Parameters": [
    {
      "ParameterKey": "EnvironmentName",
      "DefaultValue": "production",
      "NoEcho": false,
      "Description": "Environment name (dev, staging, production)"
    },
    ...
  ],
  "Description": "VPC with public and private subnets across 2 AZs",
  "Capabilities": [],
  "CapabilitiesReason": "The following resource(s) require capabilities: []"
}
```

**[OK] Template valide !**

---

**Si erreur :**

```json
{
  "Code": "ValidationError",
  "Message": "Template format error: YAML not well-formed. (line 45, column 3)"
}
```

**Fixer l'erreur YAML et re-valider**

---

#### Déployer le stack

**Créer stack :**

```bash
aws cloudformation create-stack \
  --stack-name vpc-production \
  --template-body file://vpc.yaml \
  --parameters \
    ParameterKey=EnvironmentName,ParameterValue=production \
    ParameterKey=VpcCIDR,ParameterValue=10.0.0.0/16 \
  --region eu-west-1
```

**Résultat :**

```json
{
  "StackId": "arn:aws:cloudformation:eu-west-1:123456789012:stack/vpc-production/abc123-def456-..."
}
```

---

**Surveiller création :**

```bash
aws cloudformation describe-stacks \
  --stack-name vpc-production \
  --query 'Stacks[0].StackStatus'
```

**Résultat :**

```
"CREATE_IN_PROGRESS"
-> (attendre 3-5 minutes)
"CREATE_COMPLETE" [OK]
```

---

**Voir événements :**

```bash
aws cloudformation describe-stack-events \
  --stack-name vpc-production \
  --max-items 10
```

**Résultat :**

```json
{
  "StackEvents": [
    {
      "StackId": "arn:aws:cloudformation:...",
      "EventId": "abc123",
      "StackName": "vpc-production",
      "LogicalResourceId": "vpc-production",
      "PhysicalResourceId": "arn:aws:cloudformation:...",
      "ResourceType": "AWS::CloudFormation::Stack",
      "Timestamp": "2024-12-16T22:00:00.000Z",
      "ResourceStatus": "CREATE_COMPLETE"
    },
    {
      "LogicalResourceId": "NatGateway1",
      "PhysicalResourceId": "nat-0abc123def456",
      "ResourceType": "AWS::EC2::NatGateway",
      "ResourceStatus": "CREATE_COMPLETE"
    },
    ...
  ]
}
```

---

**Voir outputs :**

```bash
aws cloudformation describe-stacks \
  --stack-name vpc-production \
  --query 'Stacks[0].Outputs'
```

**Résultat :**

```json
[
  {
    "OutputKey": "VPC",
    "OutputValue": "vpc-0abc123def456",
    "Description": "VPC ID",
    "ExportName": "production-VPC"
  },
  {
    "OutputKey": "PublicSubnets",
    "OutputValue": "subnet-0abc123,subnet-0def456",
    "Description": "List of public subnet IDs",
    "ExportName": "production-PublicSubnets"
  },
  ...
]
```

**[OK] Stack créé, ressources disponibles !**

---

Je vais continuer dans le prochain message avec la mise à jour de stacks, nested stacks, puis Terraform et CDK. Veux-tu que je continue ?

#### Mettre à jour un stack CloudFormation

**Modifier le template (ajouter une 3ème AZ) :**

```bash
nano vpc.yaml
```

**Ajouter dans Parameters :**

```yaml
  PublicSubnet3CIDR:
    Type: String
    Default: 10.0.3.0/24
    Description: CIDR for public subnet in AZ3
  
  PrivateSubnet3CIDR:
    Type: String
    Default: 10.0.13.0/24
    Description: CIDR for private subnet in AZ3
```

**Ajouter dans Resources :**

```yaml
  # Public Subnet 3
  PublicSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select 
        - 2
        - !GetAZs ''
      CidrBlock: !Ref PublicSubnet3CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-public-subnet-1c'

  # Private Subnet 3
  PrivateSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select 
        - 2
        - !GetAZs ''
      CidrBlock: !Ref PrivateSubnet3CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName}-private-subnet-1c'

  PublicSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet3

  PrivateSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet3
```

**Sauvegarder**

---

**Créer un change set (preview des changements) :**

```bash
aws cloudformation create-change-set \
  --stack-name vpc-production \
  --change-set-name add-third-az \
  --template-body file://vpc.yaml \
  --parameters \
    ParameterKey=EnvironmentName,ParameterValue=production \
    ParameterKey=VpcCIDR,ParameterValue=10.0.0.0/16
```

**Résultat :**

```json
{
  "Id": "arn:aws:cloudformation:eu-west-1:123456789012:changeSet/add-third-az/...",
  "StackId": "arn:aws:cloudformation:eu-west-1:123456789012:stack/vpc-production/..."
}
```

---

**Voir les changements :**

```bash
aws cloudformation describe-change-set \
  --change-set-name add-third-az \
  --stack-name vpc-production
```

**Résultat :**

```json
{
  "Changes": [
    {
      "Type": "Resource",
      "ResourceChange": {
        "Action": "Add",
        "LogicalResourceId": "PublicSubnet3",
        "ResourceType": "AWS::EC2::Subnet",
        "Replacement": "False"
      }
    },
    {
      "Type": "Resource",
      "ResourceChange": {
        "Action": "Add",
        "LogicalResourceId": "PrivateSubnet3",
        "ResourceType": "AWS::EC2::Subnet",
        "Replacement": "False"
      }
    },
    ...
  ],
  "ChangeSetName": "add-third-az",
  "Status": "CREATE_COMPLETE"
}
```

**Preview : 2 nouveaux subnets seront créés [OK]**

---

**Exécuter le change set :**

```bash
aws cloudformation execute-change-set \
  --change-set-name add-third-az \
  --stack-name vpc-production
```

**Monitoring :**

```bash
aws cloudformation describe-stacks \
  --stack-name vpc-production \
  --query 'Stacks[0].StackStatus'
```

**Résultat :**

```
"UPDATE_IN_PROGRESS"
-> (attendre 2-3 minutes)
"UPDATE_COMPLETE" [OK]
```

**[OK] Stack mis à jour, 3ème AZ ajoutée !**

---

**Alternative : Update direct sans change set**

```bash
aws cloudformation update-stack \
  --stack-name vpc-production \
  --template-body file://vpc.yaml \
  --parameters ParameterKey=EnvironmentName,ParameterValue=production
```

**[ATTENTION] Moins sûr (pas de preview), préférer change sets**

---

#### Supprimer un stack

```bash
aws cloudformation delete-stack \
  --stack-name vpc-production
```

**Monitoring :**

```bash
aws cloudformation describe-stacks \
  --stack-name vpc-production \
  --query 'Stacks[0].StackStatus'
```

**Résultat :**

```
"DELETE_IN_PROGRESS"
-> (attendre 3-5 minutes)

An error occurred (ValidationError) when calling the DescribeStacks operation: 
Stack with id vpc-production does not exist
```

**[OK] Stack supprimé (toutes ressources détruites) [OK]**

---

#### Nested Stacks (modularité)

**Problème avec templates monolithiques :**

```
vpc.yaml = 500 lignes
  + rds.yaml = 300 lignes
    + ecs.yaml = 400 lignes
      + alb.yaml = 200 lignes
        = 1400 lignes total [X]

-> Difficile à maintenir
-> Difficile à réutiliser
```

**Solution : Nested stacks**

```
master.yaml (100 lignes)
  ├─ Calls vpc.yaml (500 lignes)
  ├─ Calls rds.yaml (300 lignes)
  ├─ Calls ecs.yaml (400 lignes)
  └─ Calls alb.yaml (200 lignes)

Chaque stack = module réutilisable [OK]
```

---

**Uploader templates sur S3 (requis pour nested stacks) :**

```bash
# Créer bucket
aws s3 mb s3://sirrdev-cloudformation-templates

# Uploader templates
aws s3 cp vpc.yaml s3://sirrdev-cloudformation-templates/
aws s3 cp rds.yaml s3://sirrdev-cloudformation-templates/
aws s3 cp ecs.yaml s3://sirrdev-cloudformation-templates/
```

---

**Créer `master.yaml` :**

```bash
nano master.yaml
```

**Contenu :**

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Master stack - Deploys VPC, RDS, ECS'

Parameters:
  EnvironmentName:
    Type: String
    Default: production

Resources:
  # VPC Nested Stack
  VPCStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.eu-west-1.amazonaws.com/sirrdev-cloudformation-templates/vpc.yaml
      Parameters:
        EnvironmentName: !Ref EnvironmentName
        VpcCIDR: 10.0.0.0/16
      TimeoutInMinutes: 30

  # RDS Nested Stack
  RDSStack:
    Type: AWS::CloudFormation::Stack
    DependsOn: VPCStack
    Properties:
      TemplateURL: https://s3.eu-west-1.amazonaws.com/sirrdev-cloudformation-templates/rds.yaml
      Parameters:
        EnvironmentName: !Ref EnvironmentName
        VPCId: !GetAtt VPCStack.Outputs.VPC
        PrivateSubnets: !GetAtt VPCStack.Outputs.PrivateSubnets
      TimeoutInMinutes: 30

  # ECS Nested Stack
  ECSStack:
    Type: AWS::CloudFormation::Stack
    DependsOn: VPCStack
    Properties:
      TemplateURL: https://s3.eu-west-1.amazonaws.com/sirrdev-cloudformation-templates/ecs.yaml
      Parameters:
        EnvironmentName: !Ref EnvironmentName
        VPCId: !GetAtt VPCStack.Outputs.VPC
        PublicSubnets: !GetAtt VPCStack.Outputs.PublicSubnets
        PrivateSubnets: !GetAtt VPCStack.Outputs.PrivateSubnets
      TimeoutInMinutes: 30

Outputs:
  VPCId:
    Description: VPC ID from nested stack
    Value: !GetAtt VPCStack.Outputs.VPC
  
  RDSEndpoint:
    Description: RDS endpoint from nested stack
    Value: !GetAtt RDSStack.Outputs.DBEndpoint
```

---

**Déployer master stack :**

```bash
aws cloudformation create-stack \
  --stack-name master-production \
  --template-body file://master.yaml \
  --parameters ParameterKey=EnvironmentName,ParameterValue=production \
  --capabilities CAPABILITY_IAM
```

**CloudFormation crée :**
1. Stack `master-production`
2. Nested stack `master-production-VPCStack-ABC123`
3. Nested stack `master-production-RDSStack-DEF456`
4. Nested stack `master-production-ECSStack-GHI789`

**[OK] Infrastructure complète en 1 commande !**

---

### ÉTAPE 3 : Terraform

#### Installation Terraform

**Ubuntu/Debian :**

```bash
# Installer dependencies
sudo apt-get update
sudo apt-get install -y gnupg software-properties-common

# Add HashiCorp GPG key
wget -O- https://apt.releases.hashicorp.com/gpg | \
  gpg --dearmor | \
  sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg

# Add HashiCorp repository
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
  https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
  sudo tee /etc/apt/sources.list.d/hashicorp.list

# Install Terraform
sudo apt-get update
sudo apt-get install terraform

# Verify
terraform version
# Terraform v1.6.5
```

---

**macOS :**

```bash
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

terraform version
```

---

**Windows :**

```powershell
# Chocolatey
choco install terraform

# Ou télécharger binary
# https://www.terraform.io/downloads
```

---

#### Créer configuration Terraform

**Structure projet :**

```bash
mkdir -p ~/iac-workshop/terraform
cd ~/iac-workshop/terraform

# Structure
terraform/
├── main.tf          # Ressources principales
├── variables.tf     # Variables d'entrée
├── outputs.tf       # Outputs
├── providers.tf     # Configuration providers
├── terraform.tfvars # Valeurs variables (dev/staging/prod)
└── modules/         # Modules réutilisables
    ├── vpc/
    ├── rds/
    └── ecs/
```

---

**Créer `providers.tf` :**

```bash
nano providers.tf
```

**Contenu :**

```hcl
# ═══════════════════════════════════════════════════════════════
# TERRAFORM PROVIDERS CONFIGURATION
# ═══════════════════════════════════════════════════════════════

terraform {
  required_version = ">= 1.6.0"
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  
  # Backend configuration (S3 + DynamoDB)
  # On configurera après
  # backend "s3" {
  #   bucket         = "sirrdev-terraform-state"
  #   key            = "production/terraform.tfstate"
  #   region         = "eu-west-1"
  #   encrypt        = true
  #   dynamodb_table = "terraform-state-lock"
  # }
}

provider "aws" {
  region = var.aws_region
  
  default_tags {
    tags = {
      ManagedBy   = "Terraform"
      Environment = var.environment
      Project     = "SirrDev-IaC-Workshop"
    }
  }
}
```

---

**Créer `variables.tf` :**

```bash
nano variables.tf
```

**Contenu :**

```hcl
# ═══════════════════════════════════════════════════════════════
# TERRAFORM VARIABLES
# ═══════════════════════════════════════════════════════════════

variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "eu-west-1"
}

variable "environment" {
  description = "Environment name (dev, staging, production)"
  type        = string
  
  validation {
    condition     = contains(["dev", "staging", "production"], var.environment)
    error_message = "Environment must be dev, staging, or production."
  }
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
  default     = "10.0.0.0/16"
  
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Must be valid IPv4 CIDR."
  }
}

variable "availability_zones" {
  description = "List of availability zones"
  type        = list(string)
  default     = ["eu-west-1a", "eu-west-1b"]
}

variable "public_subnet_cidrs" {
  description = "CIDR blocks for public subnets"
  type        = list(string)
  default     = ["10.0.1.0/24", "10.0.2.0/24"]
}

variable "private_subnet_cidrs" {
  description = "CIDR blocks for private subnets"
  type        = list(string)
  default     = ["10.0.11.0/24", "10.0.12.0/24"]
}

variable "enable_nat_gateway" {
  description = "Enable NAT Gateway for private subnets"
  type        = bool
  default     = true
}

variable "single_nat_gateway" {
  description = "Use single NAT Gateway (cost optimization)"
  type        = bool
  default     = false
}

variable "tags" {
  description = "Additional tags"
  type        = map(string)
  default     = {}
}
```

---

**Créer `main.tf` :**

```bash
nano main.tf
```

**Contenu :**

```hcl
# ═══════════════════════════════════════════════════════════════
# TERRAFORM MAIN CONFIGURATION - VPC
# ═══════════════════════════════════════════════════════════════

# ───────────────────────────────────────────────────────────────
# VPC
# ───────────────────────────────────────────────────────────────

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  tags = merge(
    var.tags,
    {
      Name = "${var.environment}-vpc"
    }
  )
}

# ───────────────────────────────────────────────────────────────
# Internet Gateway
# ───────────────────────────────────────────────────────────────

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  
  tags = merge(
    var.tags,
    {
      Name = "${var.environment}-igw"
    }
  )
}

# ───────────────────────────────────────────────────────────────
# Public Subnets
# ───────────────────────────────────────────────────────────────

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)
  
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true
  
  tags = merge(
    var.tags,
    {
      Name = "${var.environment}-public-subnet-${count.index + 1}"
      Type = "Public"
    }
  )
}

# ───────────────────────────────────────────────────────────────
# Private Subnets
# ───────────────────────────────────────────────────────────────

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)
  
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.private_subnet_cidrs[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = false
  
  tags = merge(
    var.tags,
    {
      Name = "${var.environment}-private-subnet-${count.index + 1}"
      Type = "Private"
    }
  )
}

# ───────────────────────────────────────────────────────────────
# Elastic IPs for NAT Gateways
# ───────────────────────────────────────────────────────────────

resource "aws_eip" "nat" {
  count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0
  
  domain = "vpc"
  
  tags = merge(
    var.tags,
    {
      Name = "${var.environment}-nat-eip-${count.index + 1}"
    }
  )
  
  depends_on = [aws_internet_gateway.main]
}

# ───────────────────────────────────────────────────────────────
# NAT Gateways
# ───────────────────────────────────────────────────────────────

resource "aws_nat_gateway" "main" {
  count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0
  
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  
  tags = merge(
    var.tags,
    {
      Name = "${var.environment}-nat-${count.index + 1}"
    }
  )
  
  depends_on = [aws_internet_gateway.main]
}

# ───────────────────────────────────────────────────────────────
# Public Route Table
# ───────────────────────────────────────────────────────────────

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  
  tags = merge(
    var.tags,
    {
      Name = "${var.environment}-public-routes"
    }
  )
}

resource "aws_route" "public_internet_gateway" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.main.id
}

resource "aws_route_table_association" "public" {
  count = length(var.public_subnet_cidrs)
  
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# ───────────────────────────────────────────────────────────────
# Private Route Tables
# ───────────────────────────────────────────────────────────────

resource "aws_route_table" "private" {
  count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 1
  
  vpc_id = aws_vpc.main.id
  
  tags = merge(
    var.tags,
    {
      Name = "${var.environment}-private-routes-${count.index + 1}"
    }
  )
}

resource "aws_route" "private_nat_gateway" {
  count = var.enable_nat_gateway ? (var.single_nat_gateway ? 1 : length(var.availability_zones)) : 0
  
  route_table_id         = aws_route_table.private[count.index].id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.main[count.index].id
}

resource "aws_route_table_association" "private" {
  count = length(var.private_subnet_cidrs)
  
  subnet_id      = aws_subnet.private[count.index].id
  route_table_id = var.single_nat_gateway ? aws_route_table.private[0].id : aws_route_table.private[count.index].id
}
```

---

**Créer `outputs.tf` :**

```bash
nano outputs.tf
```

**Contenu :**

```hcl
# ═══════════════════════════════════════════════════════════════
# TERRAFORM OUTPUTS
# ═══════════════════════════════════════════════════════════════

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

output "vpc_cidr" {
  description = "VPC CIDR block"
  value       = aws_vpc.main.cidr_block
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "List of private subnet IDs"
  value       = aws_subnet.private[*].id
}

output "nat_gateway_ids" {
  description = "List of NAT Gateway IDs"
  value       = aws_nat_gateway.main[*].id
}

output "nat_gateway_ips" {
  description = "List of NAT Gateway public IPs"
  value       = aws_eip.nat[*].public_ip
}

output "internet_gateway_id" {
  description = "Internet Gateway ID"
  value       = aws_internet_gateway.main.id
}
```

---

**Créer `terraform.tfvars` (production) :**

```bash
nano terraform.tfvars
```

**Contenu :**

```hcl
environment          = "production"
aws_region           = "eu-west-1"
vpc_cidr             = "10.0.0.0/16"
availability_zones   = ["eu-west-1a", "eu-west-1b"]
public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway   = true
single_nat_gateway   = false

tags = {
  Owner = "SirrDev"
  CostCenter = "Engineering"
}
```

---

**Créer `dev.tfvars` (dev environment) :**

```bash
nano environments/dev.tfvars
```

**Contenu :**

```hcl
environment          = "dev"
aws_region           = "eu-west-1"
vpc_cidr             = "10.1.0.0/16"
availability_zones   = ["eu-west-1a"]
public_subnet_cidrs  = ["10.1.1.0/24"]
private_subnet_cidrs = ["10.1.11.0/24"]
enable_nat_gateway   = true
single_nat_gateway   = true  # Cost optimization: 1 NAT only

tags = {
  Owner = "SirrDev"
  CostCenter = "Engineering"
}
```

---

#### Initialiser Terraform

```bash
# Initialiser (télécharge providers)
terraform init
```

**Résultat :**

```
Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.31.0...
- Installed hashicorp/aws v5.31.0 (signed by HashiCorp)

Terraform has been successfully initialized!
```

**[OK] Terraform prêt !**

---

**Vérifier configuration :**

```bash
# Valider syntaxe
terraform validate
```

**Résultat :**

```
Success! The configuration is valid.
```

---

**Formatter code (style consistent) :**

```bash
terraform fmt -recursive
```

**Formatte tous les .tf files selon style Terraform standard**

---

#### Plan et Apply

**Voir ce qui sera créé (dry-run) :**

```bash
terraform plan
```

**Résultat :**

```
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_eip.nat[0] will be created
  + resource "aws_eip" "nat" {
      + allocation_id        = (known after apply)
      + domain               = "vpc"
      + id                   = (known after apply)
      + public_ip            = (known after apply)
      + tags                 = {
          + "Environment" = "production"
          + "ManagedBy"   = "Terraform"
          + "Name"        = "production-nat-eip-1"
        }
    }

  # aws_internet_gateway.main will be created
  + resource "aws_internet_gateway" "main" {
      + id       = (known after apply)
      + vpc_id   = (known after apply)
      + tags     = {
          + "Environment" = "production"
          + "ManagedBy"   = "Terraform"
          + "Name"        = "production-igw"
        }
    }

  # aws_vpc.main will be created
  + resource "aws_vpc" "main" {
      + cidr_block           = "10.0.0.0/16"
      + enable_dns_hostnames = true
      + enable_dns_support   = true
      + id                   = (known after apply)
      + tags                 = {
          + "Environment" = "production"
          + "ManagedBy"   = "Terraform"
          + "Name"        = "production-vpc"
        }
    }

  ... (15+ resources total)

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

Changes to Outputs:
  + nat_gateway_ips    = [
      + (known after apply),
      + (known after apply),
    ]
  + private_subnet_ids = [
      + (known after apply),
      + (known after apply),
    ]
  + public_subnet_ids  = [
      + (known after apply),
      + (known after apply),
    ]
  + vpc_cidr           = "10.0.0.0/16"
  + vpc_id             = (known after apply)
```

**Preview complet des changements [OK]**

---

**Appliquer les changements :**

```bash
terraform apply
```

**Terraform demande confirmation :**

```
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
```

**Taper `yes`**

---

**Résultat :**

```
aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 2s [id=vpc-0abc123def456]
aws_internet_gateway.main: Creating...
aws_subnet.public[0]: Creating...
aws_subnet.public[1]: Creating...
aws_subnet.private[0]: Creating...
aws_subnet.private[1]: Creating...
...
aws_nat_gateway.main[0]: Creating...
aws_nat_gateway.main[0]: Still creating... [10s elapsed]
aws_nat_gateway.main[0]: Still creating... [20s elapsed]
...
aws_nat_gateway.main[0]: Creation complete after 1m35s [id=nat-0abc123]

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

Outputs:

nat_gateway_ips = [
  "52.18.123.45",
  "52.18.123.46",
]
private_subnet_ids = [
  "subnet-0abc123",
  "subnet-0def456",
]
public_subnet_ids = [
  "subnet-0ghi789",
  "subnet-0jkl012",
]
vpc_cidr = "10.0.0.0/16"
vpc_id = "vpc-0abc123def456"
```

**[OK] Infrastructure créée avec Terraform !**

**Durée : ~3 minutes (vs 5-10 minutes manuellement)**

---

**Vérifier state :**

```bash
terraform show
```

**Affiche toutes les ressources créées et leurs attributs**

---

**Lister ressources :**

```bash
terraform state list
```

**Résultat :**

```
aws_eip.nat[0]
aws_eip.nat[1]
aws_internet_gateway.main
aws_nat_gateway.main[0]
aws_nat_gateway.main[1]
aws_route.private_nat_gateway[0]
aws_route.private_nat_gateway[1]
aws_route.public_internet_gateway
aws_route_table.private[0]
aws_route_table.private[1]
aws_route_table.public
aws_route_table_association.private[0]
aws_route_table_association.private[1]
aws_route_table_association.public[0]
aws_route_table_association.public[1]
aws_subnet.private[0]
aws_subnet.private[1]
aws_subnet.public[0]
aws_subnet.public[1]
aws_vpc.main
```

---

#### Modifier infrastructure avec Terraform

**Modifier `terraform.tfvars` (ajouter 3ème AZ) :**

```bash
nano terraform.tfvars
```

**Modifier :**

```hcl
availability_zones   = ["eu-west-1a", "eu-west-1b", "eu-west-1c"]  # <- Ajouté 1c
public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]  # <- Ajouté
private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]  # <- Ajouté
```

**Sauvegarder**

---

**Plan des changements :**

```bash
terraform plan
```

**Résultat :**

```
Terraform will perform the following actions:

  # aws_eip.nat[2] will be created
  + resource "aws_eip" "nat" {
      ...
    }

  # aws_nat_gateway.main[2] will be created
  + resource "aws_nat_gateway" "main" {
      ...
    }

  # aws_subnet.private[2] will be created
  + resource "aws_subnet" "private" {
      + cidr_block = "10.0.13.0/24"
      ...
    }

  # aws_subnet.public[2] will be created
  + resource "aws_subnet" "public" {
      + cidr_block = "10.0.3.0/24"
      ...
    }

Plan: 6 to add, 0 to change, 0 to destroy.
```

**6 nouvelles ressources (3ème AZ) [OK]**

---

**Appliquer :**

```bash
terraform apply -auto-approve
```

**(`-auto-approve` = pas de confirmation interactive)**

**Résultat :**

```
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
```

**[OK] 3ème AZ ajoutée !**

---

#### Détruire infrastructure

```bash
terraform destroy
```

**Terraform demande confirmation :**

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

  Enter a value: yes
```

---

**Résultat :**

```
aws_route_table_association.private[2]: Destroying... [id=rtbassoc-abc123]
aws_route_table_association.public[2]: Destroying... [id=rtbassoc-def456]
...
aws_nat_gateway.main[0]: Destroying... [id=nat-0abc123]
aws_nat_gateway.main[0]: Still destroying... [10s elapsed]
...
aws_vpc.main: Destroying... [id=vpc-0abc123]
aws_vpc.main: Destruction complete after 1s

Destroy complete! Resources: 17 destroyed.
```

**[OK] Toute l'infrastructure détruite !**

**[ATTENTION] Irréversible (pas de corbeille)**

---

### ÉTAPE 4 : Terraform Modules

#### Créer module VPC réutilisable

**Structure modules :**

```bash
mkdir -p modules/vpc
cd modules/vpc
```

---

**Créer `modules/vpc/main.tf` :**

```bash
nano main.tf
```

**Contenu :**

```hcl
# ═══════════════════════════════════════════════════════════════
# VPC MODULE
# ═══════════════════════════════════════════════════════════════

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = var.enable_dns_hostnames
  enable_dns_support   = var.enable_dns_support
  
  tags = merge(
    var.tags,
    {
      Name = "${var.name}-vpc"
    }
  )
}

resource "aws_internet_gateway" "this" {
  count = var.create_igw ? 1 : 0
  
  vpc_id = aws_vpc.this.id
  
  tags = merge(
    var.tags,
    {
      Name = "${var.name}-igw"
    }
  )
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)
  
  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.azs[count.index]
  map_public_ip_on_launch = var.map_public_ip_on_launch
  
  tags = merge(
    var.tags,
    {
      Name = "${var.name}-public-${var.azs[count.index]}"
      Type = "Public"
    }
  )
}

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)
  
  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.azs[count.index]
  
  tags = merge(
    var.tags,
    {
      Name = "${var.name}-private-${var.azs[count.index]}"
      Type = "Private"
    }
  )
}

# ... (suite similaire à main.tf précédent)
```

---

**Créer `modules/vpc/variables.tf` :**

```hcl
variable "name" {
  description = "Name prefix for resources"
  type        = string
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string
}

variable "azs" {
  description = "Availability zones"
  type        = list(string)
}

variable "public_subnet_cidrs" {
  description = "Public subnet CIDRs"
  type        = list(string)
  default     = []
}

variable "private_subnet_cidrs" {
  description = "Private subnet CIDRs"
  type        = list(string)
  default     = []
}

variable "enable_nat_gateway" {
  description = "Enable NAT Gateway"
  type        = bool
  default     = true
}

variable "single_nat_gateway" {
  description = "Use single NAT Gateway"
  type        = bool
  default     = false
}

variable "enable_dns_hostnames" {
  description = "Enable DNS hostnames"
  type        = bool
  default     = true
}

variable "enable_dns_support" {
  description = "Enable DNS support"
  type        = bool
  default     = true
}

variable "create_igw" {
  description = "Create Internet Gateway"
  type        = bool
  default     = true
}

variable "map_public_ip_on_launch" {
  description = "Auto-assign public IP"
  type        = bool
  default     = true
}

variable "tags" {
  description = "Tags"
  type        = map(string)
  default     = {}
}
```

---

**Créer `modules/vpc/outputs.tf` :**

```hcl
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.this.id
}

output "vpc_cidr" {
  description = "VPC CIDR"
  value       = aws_vpc.this.cidr_block
}

output "public_subnet_ids" {
  description = "Public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "Private subnet IDs"
  value       = aws_subnet.private[*].id
}

output "nat_gateway_ids" {
  description = "NAT Gateway IDs"
  value       = aws_nat_gateway.this[*].id
}
```

---

**Utiliser module dans main.tf (root) :**

```bash
cd ~/iac-workshop/terraform
nano main.tf
```

**Contenu :**

```hcl
# ═══════════════════════════════════════════════════════════════
# ROOT MAIN.TF - USING MODULES
# ═══════════════════════════════════════════════════════════════

module "vpc" {
  source = "./modules/vpc"
  
  name                 = var.environment
  vpc_cidr             = var.vpc_cidr
  azs                  = var.availability_zones
  public_subnet_cidrs  = var.public_subnet_cidrs
  private_subnet_cidrs = var.private_subnet_cidrs
  enable_nat_gateway   = var.enable_nat_gateway
  single_nat_gateway   = var.single_nat_gateway
  
  tags = merge(
    var.tags,
    {
      Environment = var.environment
    }
  )
}

# Outputs from module
output "vpc_id" {
  value = module.vpc.vpc_id
}

output "public_subnets" {
  value = module.vpc.public_subnet_ids
}

output "private_subnets" {
  value = module.vpc.private_subnet_ids
}
```

---

**Apply avec module :**

```bash
terraform init  # Re-init pour module
terraform plan
terraform apply
```

**[OK] Module VPC réutilisable !**

**Utilisation :**

```hcl
# Production
module "vpc_prod" {
  source = "./modules/vpc"
  name   = "production"
  ...
}

# Dev
module "vpc_dev" {
  source = "./modules/vpc"
  name   = "dev"
  ...
}

# Staging
module "vpc_staging" {
  source = "./modules/vpc"
  name   = "staging"
  ...
}
```

**DRY (Don't Repeat Yourself) [OK]**

---

Je vais continuer dans le prochain message avec Terraform State Management (S3 backend), AWS CDK, comparaisons, CI/CD, et conclusion. Veux-tu que je continue ?

### ÉTAPE 5 : Terraform State Management (S3 Backend)

#### Problème avec state local

**State local (`terraform.tfstate`) :**

```
Problèmes:
  [X] Fichier sensible (contient secrets, IDs ressources)
  [X] Pas de collaboration (conflits si plusieurs devs)
  [X] Pas de versioning automatique
  [X] Pas de locking (concurrent edits = corruption)
  [X] Perdu si laptop crashe/volé
```

**Solution : Remote backend (S3 + DynamoDB)**

```
S3 Backend:
  [OK] State stocké dans S3 (durable, versionné)
  [OK] Encryption at rest (KMS)
  [OK] DynamoDB pour locking (évite conflits)
  [OK] Collaboration équipe
  [OK] Backup automatique (S3 versioning)
```

---

#### Créer infrastructure backend

**Créer `backend-setup.tf` (bootstrap) :**

```bash
cd ~/iac-workshop/terraform
nano backend-setup.tf
```

**Contenu :**

```hcl
# ═══════════════════════════════════════════════════════════════
# TERRAFORM BACKEND INFRASTRUCTURE (Bootstrap)
# ═══════════════════════════════════════════════════════════════
# Run this FIRST to create S3 bucket and DynamoDB table
# Then configure backend in providers.tf
# ═══════════════════════════════════════════════════════════════

# S3 Bucket for Terraform state
resource "aws_s3_bucket" "terraform_state" {
  bucket = "sirrdev-terraform-state-${var.aws_region}"
  
  tags = {
    Name        = "Terraform State Bucket"
    ManagedBy   = "Terraform"
    Purpose     = "Backend"
  }
}

# Enable versioning (rollback capability)
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  versioning_configuration {
    status = "Enabled"
  }
}

# Enable encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
      # Ou utiliser KMS:
      # sse_algorithm     = "aws:kms"
      # kms_master_key_id = aws_kms_key.terraform.arn
    }
  }
}

# Block public access
resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Lifecycle policy (optional, keep old versions limited)
resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  
  rule {
    id     = "expire-old-versions"
    status = "Enabled"
    
    noncurrent_version_expiration {
      noncurrent_days = 90
    }
  }
}

# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"  # On-demand (gratuit Free Tier)
  hash_key     = "LockID"
  
  attribute {
    name = "LockID"
    type = "S"
  }
  
  tags = {
    Name        = "Terraform State Lock Table"
    ManagedBy   = "Terraform"
    Purpose     = "Backend"
  }
}

# Outputs
output "s3_bucket_name" {
  description = "S3 bucket name for Terraform state"
  value       = aws_s3_bucket.terraform_state.id
}

output "dynamodb_table_name" {
  description = "DynamoDB table name for state locking"
  value       = aws_dynamodb_table.terraform_locks.id
}
```

---

**Créer backend infrastructure :**

```bash
# Init et apply (local state pour bootstrap)
terraform init
terraform apply -auto-approve
```

**Résultat :**

```
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:

dynamodb_table_name = "terraform-state-lock"
s3_bucket_name = "sirrdev-terraform-state-eu-west-1"
```

**[OK] Backend infrastructure créée !**

---

#### Configurer Terraform pour utiliser S3 backend

**Éditer `providers.tf` :**

```bash
nano providers.tf
```

**Décommenter et configurer backend :**

```hcl
terraform {
  required_version = ">= 1.6.0"
  
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  
  # S3 Backend configuration
  backend "s3" {
    bucket         = "sirrdev-terraform-state-eu-west-1"
    key            = "production/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
    
    # Optional: KMS encryption
    # kms_key_id = "arn:aws:kms:eu-west-1:123456789012:key/abc-123"
  }
}

provider "aws" {
  region = var.aws_region
  
  default_tags {
    tags = {
      ManagedBy   = "Terraform"
      Environment = var.environment
      Project     = "SirrDev-IaC-Workshop"
    }
  }
}
```

**Sauvegarder**

---

**Migrer state local -> S3 :**

```bash
terraform init -migrate-state
```

**Terraform demande confirmation :**

```
Do you want to migrate all workspaces to "s3"?
  Both the existing "local" backend and the newly configured "s3" backend
  support workspaces. When migrating between backends, Terraform will copy
  all workspaces (with the same names). THIS WILL OVERWRITE any conflicting
  states in the destination.

  Terraform initialization doesn't currently migrate only select workspaces.
  If you want to migrate a select number of workspaces, you must manually
  pull and push those states.

  If you answer "yes", Terraform will migrate all states. If you answer
  "no", Terraform will abort.

  Enter a value: yes
```

**Taper `yes`**

---

**Résultat :**

```
Initializing the backend...
Acquiring state lock. This may take a few moments...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
```

**[OK] State migré vers S3 !**

---

**Vérifier dans S3 :**

```bash
aws s3 ls s3://sirrdev-terraform-state-eu-west-1/production/
```

**Résultat :**

```
2024-12-16 23:00:00      12345 terraform.tfstate
```

**State stocké dans S3 [OK]**

---

**Vérifier versioning :**

```bash
aws s3api list-object-versions \
  --bucket sirrdev-terraform-state-eu-west-1 \
  --prefix production/terraform.tfstate
```

**Résultat :**

```json
{
  "Versions": [
    {
      "Key": "production/terraform.tfstate",
      "VersionId": "abc123...",
      "IsLatest": true,
      "LastModified": "2024-12-16T23:00:00.000Z",
      "Size": 12345
    }
  ]
}
```

**Versioning actif [OK]**

---

#### Tester state locking

**Terminal 1 :**

```bash
terraform plan
# Acquiert lock dans DynamoDB
```

**Terminal 2 (pendant que plan tourne) :**

```bash
terraform plan
```

**Résultat :**

```
Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        abc123-def456-ghi789
  Path:      sirrdev-terraform-state-eu-west-1/production/terraform.tfstate
  Operation: OperationTypePlan
  Who:       sirrdev@laptop
  Version:   1.6.5
  Created:   2024-12-16 23:00:00.123456789 +0000 UTC

Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock=false"
flag, but this is not recommended.
```

**Lock fonctionne ! Concurrent access bloqué [OK]**

---

**Vérifier lock dans DynamoDB :**

```bash
aws dynamodb scan \
  --table-name terraform-state-lock \
  --filter-expression "attribute_exists(LockID)"
```

**Pendant `terraform plan` actif :**

```json
{
  "Items": [
    {
      "LockID": {
        "S": "sirrdev-terraform-state-eu-west-1/production/terraform.tfstate-md5"
      },
      "Info": {
        "S": "{\"ID\":\"abc123\",\"Operation\":\"OperationTypePlan\",\"Who\":\"sirrdev@laptop\",\"Version\":\"1.6.5\",\"Created\":\"2024-12-16T23:00:00.123Z\"}"
      }
    }
  ]
}
```

**Lock visible dans DynamoDB [OK]**

---

#### Multi-environnement avec workspaces

**Workspaces = Multiple states pour même config**

```
Workspace "dev":     dev.tfstate    -> VPC 10.1.0.0/16
Workspace "staging": staging.tfstate -> VPC 10.2.0.0/16
Workspace "prod":    prod.tfstate    -> VPC 10.0.0.0/16
```

---

**Créer workspaces :**

```bash
# Créer workspace dev
terraform workspace new dev

# Créer workspace staging
terraform workspace new staging

# Lister workspaces
terraform workspace list
```

**Résultat :**

```
  default
* dev
  staging
```

**`*` = workspace actuel**

---

**Switcher entre workspaces :**

```bash
# Aller en production
terraform workspace select default
```

---

**Utiliser workspace dans config :**

```hcl
# variables.tf
variable "vpc_cidrs" {
  type = map(string)
  default = {
    dev     = "10.1.0.0/16"
    staging = "10.2.0.0/16"
    default = "10.0.0.0/16"  # production
  }
}

# main.tf
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidrs[terraform.workspace]
  
  tags = {
    Name        = "${terraform.workspace}-vpc"
    Environment = terraform.workspace
  }
}
```

---

**Déployer par environnement :**

```bash
# Dev
terraform workspace select dev
terraform apply -var-file=environments/dev.tfvars

# Staging
terraform workspace select staging
terraform apply -var-file=environments/staging.tfvars

# Production
terraform workspace select default
terraform apply -var-file=environments/production.tfvars
```

**3 environnements isolés, même code [OK]**

---

### ÉTAPE 6 : AWS CDK (Cloud Development Kit)

#### Qu'est-ce que CDK ?

**CDK = Infrastructure as Code avec langages programmation (TypeScript, Python, Java, C#)**

**CDK vs CloudFormation/Terraform :**

```
CloudFormation/Terraform (declarative):
  resource "aws_vpc" "main" {
    cidr_block = "10.0.0.0/16"
  }
  
  -> YAML/HCL = Configuration
  -> Pas de loops, conditions complexes limitées
  -> Verbeux (beaucoup de lignes)

CDK (imperative + abstractions):
  const vpc = new ec2.Vpc(this, 'MainVPC', {
    cidr: '10.0.0.0/16',
    maxAzs: 2,
    natGateways: 1
  });
  
  -> TypeScript/Python = Code
  -> Loops, conditions, classes, functions
  -> Abstractions (1 ligne -> 15 ressources)
  -> Type safety (IDE autocomplete)
```

**CDK synthétise vers CloudFormation :**

```
CDK code (TypeScript)
  v cdk synth
CloudFormation template (JSON)
  v cdk deploy
AWS Resources
```

---

#### Installation CDK

**Prérequis : Node.js 18+**

```bash
# Vérifier Node.js
node --version
# v18.19.0

npm --version
# 10.2.3
```

---

**Installer CDK CLI :**

```bash
npm install -g aws-cdk

# Vérifier
cdk --version
# 2.115.0
```

---

#### Initialiser projet CDK

```bash
mkdir ~/iac-workshop/cdk
cd ~/iac-workshop/cdk

# Initialiser projet TypeScript
cdk init app --language typescript
```

**Résultat :**

```
Applying project template app for typescript
...
[OK] All done!

Project structure:
cdk/
├── bin/
│   └── cdk.ts           # Entry point
├── lib/
│   └── cdk-stack.ts     # Stack definition
├── test/
│   └── cdk.test.ts      # Tests
├── cdk.json             # CDK configuration
├── package.json         # Dependencies
├── tsconfig.json        # TypeScript config
└── README.md
```

---

**Installer dépendances AWS :**

```bash
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-rds @aws-cdk/aws-ecs
```

---

#### Bootstrap CDK (première fois)

**Bootstrap = Créer ressources CDK dans compte AWS**

```bash
cdk bootstrap aws://123456789012/eu-west-1
```

**Résultat :**

```
 [HOURGLASS_WITH_FLOWING_SAND]  Bootstrapping environment aws://123456789012/eu-west-1...
CDKToolkit: creating CloudFormation changeset...
 [OK]  Environment aws://123456789012/eu-west-1 bootstrapped.
```

**Crée :**
- S3 bucket pour assets CDK
- ECR repository pour Docker images
- IAM roles pour déploiements

---

#### Créer VPC Stack avec CDK

**Éditer `lib/cdk-stack.ts` :**

```bash
nano lib/cdk-stack.ts
```

**Contenu :**

```typescript
// ═══════════════════════════════════════════════════════════════
// CDK STACK - VPC
// ═══════════════════════════════════════════════════════════════

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export interface VpcStackProps extends cdk.StackProps {
  environmentName: string;
  vpcCidr: string;
  maxAzs: number;
  natGateways: number;
}

export class VpcStack extends cdk.Stack {
  public readonly vpc: ec2.Vpc;
  
  constructor(scope: Construct, id: string, props: VpcStackProps) {
    super(scope, id, props);
    
    // ─────────────────────────────────────────────────────────
    // VPC with public and private subnets
    // ─────────────────────────────────────────────────────────
    
    this.vpc = new ec2.Vpc(this, 'MainVPC', {
      // IP range
      ipAddresses: ec2.IpAddresses.cidr(props.vpcCidr),
      
      // Number of Availability Zones
      maxAzs: props.maxAzs,
      
      // NAT Gateways (1 = single NAT, save costs)
      natGateways: props.natGateways,
      
      // Subnet configuration
      subnetConfiguration: [
        {
          // Public subnets (with Internet Gateway)
          cidrMask: 24,
          name: 'Public',
          subnetType: ec2.SubnetType.PUBLIC,
        },
        {
          // Private subnets (with NAT Gateway)
          cidrMask: 24,
          name: 'Private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
        {
          // Isolated subnets (no internet access)
          cidrMask: 24,
          name: 'Isolated',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
        },
      ],
      
      // Enable VPC Flow Logs (optional)
      flowLogs: {
        's3': {
          destination: ec2.FlowLogDestination.toS3(),
          trafficType: ec2.FlowLogTrafficType.ALL,
        }
      }
    });
    
    // ─────────────────────────────────────────────────────────
    // Tags
    // ─────────────────────────────────────────────────────────
    
    cdk.Tags.of(this.vpc).add('Name', `${props.environmentName}-vpc`);
    cdk.Tags.of(this.vpc).add('Environment', props.environmentName);
    cdk.Tags.of(this.vpc).add('ManagedBy', 'CDK');
    
    // ─────────────────────────────────────────────────────────
    // Outputs
    // ─────────────────────────────────────────────────────────
    
    new cdk.CfnOutput(this, 'VpcId', {
      value: this.vpc.vpcId,
      description: 'VPC ID',
      exportName: `${props.environmentName}-VpcId`,
    });
    
    new cdk.CfnOutput(this, 'VpcCidr', {
      value: this.vpc.vpcCidrBlock,
      description: 'VPC CIDR Block',
    });
    
    new cdk.CfnOutput(this, 'PublicSubnets', {
      value: this.vpc.publicSubnets.map(s => s.subnetId).join(','),
      description: 'Public Subnet IDs',
    });
    
    new cdk.CfnOutput(this, 'PrivateSubnets', {
      value: this.vpc.privateSubnets.map(s => s.subnetId).join(','),
      description: 'Private Subnet IDs',
    });
  }
}
```

---

**Éditer `bin/cdk.ts` (entry point) :**

```bash
nano bin/cdk.ts
```

**Contenu :**

```typescript
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/cdk-stack';

const app = new cdk.App();

// Production Stack
new VpcStack(app, 'VpcStackProduction', {
  environmentName: 'production',
  vpcCidr: '10.0.0.0/16',
  maxAzs: 2,
  natGateways: 2,  // HA
  
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: 'eu-west-1',
  },
  
  tags: {
    Owner: 'SirrDev',
    CostCenter: 'Engineering',
  }
});

// Dev Stack (cost optimized)
new VpcStack(app, 'VpcStackDev', {
  environmentName: 'dev',
  vpcCidr: '10.1.0.0/16',
  maxAzs: 1,
  natGateways: 1,  // Single NAT
  
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: 'eu-west-1',
  },
  
  tags: {
    Owner: 'SirrDev',
    CostCenter: 'Engineering',
  }
});
```

---

#### Synthétiser et déployer

**Synthétiser CloudFormation template :**

```bash
cdk synth VpcStackProduction
```

**Résultat (CloudFormation YAML généré) :**

```yaml
Resources:
  MainVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: production-vpc
        - Key: Environment
          Value: production
        - Key: ManagedBy
          Value: CDK
  
  MainVPCPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: MainVPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      MapPublicIpOnLaunch: true
  
  ... (50+ resources générées automatiquement)
```

**1 classe CDK -> 50+ ressources CloudFormation [OK]**

---

**Voir diff (changements) :**

```bash
cdk diff VpcStackProduction
```

**Résultat :**

```
Stack VpcStackProduction
Resources
[+] AWS::EC2::VPC MainVPC MainVPC123ABC
[+] AWS::EC2::InternetGateway MainVPCIGW MainVPCIGW456DEF
[+] AWS::EC2::Subnet MainVPCPublicSubnet1 MainVPCPublicSubnet1789GHI
...

Outputs
[+] Output VpcId VpcId: {"Value":{"Ref":"MainVPC"}}
[+] Output VpcCidr VpcCidr: {"Value":{"Fn::GetAtt":["MainVPC","CidrBlock"]}}

Other Changes
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/abc123/version"}
```

---

**Déployer :**

```bash
cdk deploy VpcStackProduction
```

**CDK demande confirmation :**

```
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬─────────────────┬────────┬────────────────┬──────────────────┬───────────┐
│   │ Resource        │ Effect │ Action         │ Principal        │ Condition │
├───┼─────────────────┼────────┼────────────────┼──────────────────┼───────────┤
│ + │ ${MainVPC.Arn}  │ Allow  │ logs:PutLogEv… │ Service:vpc-flow…│           │
└───┴─────────────────┴────────┴────────────────┴──────────────────┴───────────┘

Do you wish to deploy these changes (y/n)? y
```

**Taper `y`**

---

**Résultat :**

```
VpcStackProduction: deploying...
[0%] start: Publishing abc123:current_account-current_region
[100%] success: Published abc123:current_account-current_region

VpcStackProduction: creating CloudFormation changeset...

 [OK]  VpcStackProduction

*  Deployment time: 142.5s

Outputs:
VpcStackProduction.PublicSubnets = subnet-0abc123,subnet-0def456
VpcStackProduction.PrivateSubnets = subnet-0ghi789,subnet-0jkl012
VpcStackProduction.VpcCidr = 10.0.0.0/16
VpcStackProduction.VpcId = vpc-0abc123def456

Stack ARN:
arn:aws:cloudformation:eu-west-1:123456789012:stack/VpcStackProduction/abc123-def456-...
```

**[OK] VPC déployé avec CDK !**

---

#### Constructs de haut niveau (L3)

**CDK a 3 niveaux de constructs :**

**L1 (Low-level CFN resources) :**

```typescript
// Mapping 1:1 avec CloudFormation
new cdk.aws_ec2.CfnVpc(this, 'VPC', {
  cidrBlock: '10.0.0.0/16',
  enableDnsHostnames: true,
});
```

**L2 (Intent-based APIs) :**

```typescript
// APIs avec defaults sensibles
const vpc = new ec2.Vpc(this, 'VPC', {
  ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
  maxAzs: 2,
});
```

**L3 (Patterns - High-level) :**

```typescript
// Patterns complets (1 ligne = architecture complète)
import * as patterns from 'aws-cdk-lib/aws-ecs-patterns';

const fargateService = new patterns.ApplicationLoadBalancedFargateService(this, 'Service', {
  taskImageOptions: {
    image: ecs.ContainerImage.fromRegistry('nginx'),
  },
  desiredCount: 3,
});

// -> Crée automatiquement:
//   - VPC (si pas fourni)
//   - Application Load Balancer
//   - Target Group
//   - ECS Cluster
//   - Fargate Service
//   - Task Definition
//   - Security Groups
//   - IAM Roles
//   - CloudWatch Logs
//   = ~30 ressources en 1 construct [OK]
```

---

#### Créer RDS Stack avec CDK

**Créer `lib/rds-stack.ts` :**

```bash
nano lib/rds-stack.ts
```

**Contenu :**

```typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';

export interface RdsStackProps extends cdk.StackProps {
  vpc: ec2.IVpc;
  environmentName: string;
}

export class RdsStack extends cdk.Stack {
  public readonly database: rds.DatabaseInstance;
  public readonly secret: secretsmanager.Secret;
  
  constructor(scope: Construct, id: string, props: RdsStackProps) {
    super(scope, id, props);
    
    // Create database password secret
    this.secret = new secretsmanager.Secret(this, 'DBSecret', {
      secretName: `${props.environmentName}/db/password`,
      generateSecretString: {
        secretStringTemplate: JSON.stringify({ username: 'postgres' }),
        generateStringKey: 'password',
        excludePunctuation: true,
        includeSpace: false,
        passwordLength: 32,
      },
    });
    
    // RDS Security Group
    const dbSecurityGroup = new ec2.SecurityGroup(this, 'DBSecurityGroup', {
      vpc: props.vpc,
      description: 'Security group for RDS PostgreSQL',
      allowAllOutbound: true,
    });
    
    // PostgreSQL instance
    this.database = new rds.DatabaseInstance(this, 'Database', {
      engine: rds.DatabaseInstanceEngine.postgres({
        version: rds.PostgresEngineVersion.VER_15_5,
      }),
      
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MICRO,
      ),
      
      vpc: props.vpc,
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      },
      
      securityGroups: [dbSecurityGroup],
      
      credentials: rds.Credentials.fromSecret(this.secret),
      
      databaseName: 'todo_production',
      
      allocatedStorage: 20,
      maxAllocatedStorage: 100,
      storageType: rds.StorageType.GP3,
      
      multiAz: props.environmentName === 'production',
      
      backupRetention: cdk.Duration.days(7),
      deleteAutomatedBackups: props.environmentName !== 'production',
      
      removalPolicy: props.environmentName === 'production'
        ? cdk.RemovalPolicy.SNAPSHOT
        : cdk.RemovalPolicy.DESTROY,
      
      deletionProtection: props.environmentName === 'production',
    });
    
    // Outputs
    new cdk.CfnOutput(this, 'DBEndpoint', {
      value: this.database.dbInstanceEndpointAddress,
      description: 'Database endpoint',
    });
    
    new cdk.CfnOutput(this, 'DBSecretArn', {
      value: this.secret.secretArn,
      description: 'Database credentials secret ARN',
    });
  }
}
```

---

**Utiliser dans `bin/cdk.ts` :**

```typescript
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/cdk-stack';
import { RdsStack } from '../lib/rds-stack';

const app = new cdk.App();

// VPC Stack
const vpcStack = new VpcStack(app, 'VpcStackProduction', {
  environmentName: 'production',
  vpcCidr: '10.0.0.0/16',
  maxAzs: 2,
  natGateways: 2,
  env: { region: 'eu-west-1' },
});

// RDS Stack (depends on VPC)
const rdsStack = new RdsStack(app, 'RdsStackProduction', {
  vpc: vpcStack.vpc,
  environmentName: 'production',
  env: { region: 'eu-west-1' },
});

// Add dependency
rdsStack.addDependency(vpcStack);
```

---

**Déployer les deux stacks :**

```bash
cdk deploy --all
```

**Ou stack par stack :**

```bash
cdk deploy VpcStackProduction
cdk deploy RdsStackProduction
```

---

#### Supprimer stacks CDK

```bash
# Supprimer stack spécifique
cdk destroy VpcStackProduction

# Supprimer tous les stacks
cdk destroy --all
```

---

### ÉTAPE 7 : Comparaison CloudFormation vs Terraform vs CDK

#### Tableau comparatif complet

| Critère | CloudFormation | Terraform | CDK |
|---------|----------------|-----------|-----|
| **Provider** | AWS | HashiCorp | AWS |
| **Open Source** | Non | Oui | Oui |
| **Language** | YAML/JSON | HCL | TypeScript/Python/Java/C# |
| **Learning Curve** | Medium | Medium | Easy (devs) / Hard (ops) |
| **Abstraction Level** | Low | Low | High (L3 constructs) |
| **Multi-cloud** | [X] No | [OK] Yes | [X] No (AWS only) |
| **State Management** | AWS-managed [OK] | Self-managed (S3) | AWS-managed [OK] |
| **Drift Detection** | [OK] Yes | [OK] Yes | [OK] Yes (via CFN) |
| **Import Existing** | [OK] Yes | [OK] Yes | [OK] Yes |
| **Rollback** | [OK] Automatic | Manual | [OK] Automatic |
| **Cost** | Free | Free (OSS) | Free |
| **Maturity** | Very High | Very High | Medium |
| **Community** | AWS docs | Huge | Growing |
| **IDE Support** | Limited | Good | Excellent (TypeScript) |
| **Testing** | Limited | Good (Terratest) | Excellent (Jest) |
| **Modularity** | Nested stacks | Modules | Constructs |
| **Resource Coverage** | All AWS | Most cloud providers | All AWS |
| **Change Preview** | Change sets | `terraform plan` | `cdk diff` |
| **Execution Speed** | Medium | Fast | Medium (synth + CFN) |
| **Debugging** | Hard | Medium | Easy (breakpoints) |

---

#### Quand utiliser quoi ?

**CloudFormation - Use cases :**

```
[OK] AWS-only infrastructure
[OK] Besoin rollback automatique
[OK] Équipe préfère YAML/déclaratif
[OK] Integration AWS services (CodePipeline, etc.)
[OK] Compliance (AWS-managed state)

[X] Multi-cloud
[X] Logique complexe (loops, etc.)
[X] Équipe préfère langages programmation
```

---

**Terraform - Use cases :**

```
[OK] Multi-cloud (AWS + Azure + GCP)
[OK] Hybrid cloud
[OK] Équipe DevOps mature
[OK] Besoin state flexible (S3, Terraform Cloud, etc.)
[OK] Grande communauté (modules publics)
[OK] Providers custom

[X] AWS-only et équipe préfère AWS-native
[X] Besoin rollback automatique natif
```

---

**CDK - Use cases :**

```
[OK] Développeurs écrivent infra (pas ops)
[OK] Besoin abstractions (L3 constructs)
[OK] Logique complexe (loops, conditions, OOP)
[OK] Tests unitaires infrastructure
[OK] Type safety (TypeScript)
[OK] AWS-only

[X] Multi-cloud
[X] Équipe ops préfère déclaratif
[X] Besoin stabilité absolue (CDK évolue vite)
```

---

#### Exemple comparatif : Créer VPC

**CloudFormation (50 lignes) :**

```yaml
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
  
  InternetGateway:
    Type: AWS::EC2::InternetGateway
  
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select [0, !GetAZs '']
  
  # ... 40 lignes supplémentaires
```

---

**Terraform (40 lignes) :**

```hcl
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
}

# ... 25 lignes supplémentaires
```

---

**CDK TypeScript (5 lignes) :**

```typescript
const vpc = new ec2.Vpc(this, 'MainVPC', {
  ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
  maxAzs: 2,
  natGateways: 1,
});

// -> Génère automatiquement 50+ ressources CloudFormation [OK]
```

**CDK = 90% moins de code [OK]**

---

### ÉTAPE 8 : CI/CD pour Infrastructure as Code

#### GitHub Actions - Terraform

**Créer `.github/workflows/terraform.yml` :**

```bash
mkdir -p .github/workflows
nano .github/workflows/terraform.yml
```

**Contenu :**

```yaml
# ═══════════════════════════════════════════════════════════════
# GITHUB ACTIONS - TERRAFORM CI/CD
# ═══════════════════════════════════════════════════════════════

name: Terraform CI/CD

on:
  push:
    branches:
      - main
    paths:
      - 'terraform/**'
  pull_request:
    branches:
      - main
    paths:
      - 'terraform/**'

env:
  AWS_REGION: eu-west-1
  TF_VERSION: 1.6.5

jobs:
  # ─────────────────────────────────────────────────────────────
  # Validation Job
  # ─────────────────────────────────────────────────────────────
  
  validate:
    name: Validate Terraform
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
      
      - name: Terraform Format Check
        run: |
          cd terraform
          terraform fmt -check -recursive
      
      - name: Terraform Init
        run: |
          cd terraform
          terraform init -backend=false
      
      - name: Terraform Validate
        run: |
          cd terraform
          terraform validate
      
      - name: Run tflint
        uses: terraform-linters/setup-tflint@v4
        with:
          tflint_version: latest
      
      - name: Lint Terraform
        run: |
          cd terraform
          tflint --init
          tflint
      
      - name: Security Scan (tfsec)
        uses: aquasecurity/tfsec-action@v1.0.0
        with:
          working_directory: terraform
          soft_fail: true
      
      - name: Security Scan (Checkov)
        uses: bridgecrewio/checkov-action@master
        with:
          directory: terraform
          soft_fail: true
  
  # ─────────────────────────────────────────────────────────────
  # Plan Job (Preview changes)
  # ─────────────────────────────────────────────────────────────
  
  plan:
    name: Terraform Plan
    runs-on: ubuntu-latest
    needs: validate
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
      
      - name: Terraform Init
        run: |
          cd terraform
          terraform init
      
      - name: Terraform Plan
        run: |
          cd terraform
          terraform plan -out=tfplan
      
      - name: Upload Plan
        uses: actions/upload-artifact@v3
        with:
          name: tfplan
          path: terraform/tfplan
      
      - name: Cost Estimation (Infracost)
        uses: infracost/actions/setup@v2
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}
      
      - name: Generate Cost Estimate
        run: |
          cd terraform
          infracost breakdown --path . \
            --format json \
            --out-file /tmp/infracost.json
      
      - name: Post Cost Comment (PR only)
        if: github.event_name == 'pull_request'
        uses: infracost/actions/comment@v1
        with:
          path: /tmp/infracost.json
          behavior: update
  
  # ─────────────────────────────────────────────────────────────
  # Apply Job (Deploy changes)
  # ─────────────────────────────────────────────────────────────
  
  apply:
    name: Terraform Apply
    runs-on: ubuntu-latest
    needs: plan
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production  # Requires manual approval
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}
      
      - name: Terraform Init
        run: |
          cd terraform
          terraform init
      
      - name: Download Plan
        uses: actions/download-artifact@v3
        with:
          name: tfplan
          path: terraform
      
      - name: Terraform Apply
        run: |
          cd terraform
          terraform apply -auto-approve tfplan
      
      - name: Slack Notification (Success)
        if: success()
        uses: slackapi/slack-github-action@v1.24.0
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "[OK] Terraform Apply succeeded for ${{ github.repository }}"
            }
      
      - name: Slack Notification (Failure)
        if: failure()
        uses: slackapi/slack-github-action@v1.24.0
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "[X] Terraform Apply failed for ${{ github.repository }}"
            }
```

---

**Secrets à configurer dans GitHub :**

```
GitHub Repository -> Settings -> Secrets and variables -> Actions

Secrets:
  AWS_ACCESS_KEY_ID: AKIA...
  AWS_SECRET_ACCESS_KEY: wJal...
  INFRACOST_API_KEY: ico-...
  SLACK_WEBHOOK_URL: https://hooks.slack.com/...
```

---

**Workflow :**

```
Developer:
  1. Create branch: feature/add-rds
  2. Modify terraform code
  3. git push origin feature/add-rds
  4. Create Pull Request

GitHub Actions (PR):
  1. Validate (fmt, validate, lint, security scan)
  2. Plan (terraform plan)
  3. Cost estimation (Infracost comment on PR)
  
  -> All checks pass [OK]
  -> Team reviews PR
  -> Approves and merges

GitHub Actions (main):
  1. Validate
  2. Plan
  3. Manual approval (environment: production)
  4. Apply (terraform apply)
  5. Slack notification

Infrastructure deployed [OK]
```

---

Je vais continuer dans le prochain message avec les tests, troubleshooting, points clés et conclusion. Veux-tu que je continue ?

### ÉTAPE 9 : Tests d'Infrastructure

#### Tests Terraform avec Terratest

**Terratest = Framework Go pour tester infrastructure**

**Installation :**

```bash
# Créer dossier tests
mkdir -p terraform/test
cd terraform/test

# Init Go module
go mod init github.com/sirrdev/terraform-tests

# Installer Terratest
go get github.com/gruntwork-io/terratest/modules/terraform
go get github.com/stretchr/testify/assert
```

---

**Créer `vpc_test.go` :**

```bash
nano vpc_test.go
```

**Contenu :**

```go
// ═══════════════════════════════════════════════════════════════
// TERRATEST - VPC TESTS
// ═══════════════════════════════════════════════════════════════

package test

import (
	"testing"

	"github.com/gruntwork-io/terratest/modules/terraform"
	"github.com/stretchr/testify/assert"
)

func TestVPCCreation(t *testing.T) {
	t.Parallel()

	// ─────────────────────────────────────────────────────────
	// Setup Terraform options
	// ─────────────────────────────────────────────────────────
	
	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		// Path to Terraform code
		TerraformDir: "../",

		// Variables to pass
		Vars: map[string]interface{}{
			"environment":          "test",
			"vpc_cidr":             "10.99.0.0/16",
			"availability_zones":   []string{"eu-west-1a", "eu-west-1b"},
			"public_subnet_cidrs":  []string{"10.99.1.0/24", "10.99.2.0/24"},
			"private_subnet_cidrs": []string{"10.99.11.0/24", "10.99.12.0/24"},
			"enable_nat_gateway":   false, // Disable NAT to save costs in tests
		},

		// Disable colors in output
		NoColor: true,
	})

	// ─────────────────────────────────────────────────────────
	// Cleanup resources after test
	// ─────────────────────────────────────────────────────────
	
	defer terraform.Destroy(t, terraformOptions)

	// ─────────────────────────────────────────────────────────
	// Run terraform init and apply
	// ─────────────────────────────────────────────────────────
	
	terraform.InitAndApply(t, terraformOptions)

	// ─────────────────────────────────────────────────────────
	// Validate outputs
	// ─────────────────────────────────────────────────────────
	
	// Get VPC ID
	vpcID := terraform.Output(t, terraformOptions, "vpc_id")
	assert.NotEmpty(t, vpcID, "VPC ID should not be empty")
	assert.Regexp(t, "^vpc-[a-f0-9]+$", vpcID, "VPC ID should match pattern vpc-xxxxx")

	// Get VPC CIDR
	vpcCIDR := terraform.Output(t, terraformOptions, "vpc_cidr")
	assert.Equal(t, "10.99.0.0/16", vpcCIDR, "VPC CIDR should match input")

	// Get public subnets
	publicSubnets := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
	assert.Len(t, publicSubnets, 2, "Should create 2 public subnets")

	// Get private subnets
	privateSubnets := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
	assert.Len(t, privateSubnets, 2, "Should create 2 private subnets")
}

func TestVPCWithNATGateway(t *testing.T) {
	t.Parallel()

	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		TerraformDir: "../",

		Vars: map[string]interface{}{
			"environment":          "test-nat",
			"vpc_cidr":             "10.98.0.0/16",
			"availability_zones":   []string{"eu-west-1a"},
			"public_subnet_cidrs":  []string{"10.98.1.0/24"},
			"private_subnet_cidrs": []string{"10.98.11.0/24"},
			"enable_nat_gateway":   true,
			"single_nat_gateway":   true, // Single NAT for cost
		},

		NoColor: true,
	})

	defer terraform.Destroy(t, terraformOptions)

	terraform.InitAndApply(t, terraformOptions)

	// Validate NAT Gateway created
	natGatewayIDs := terraform.OutputList(t, terraformOptions, "nat_gateway_ids")
	assert.Len(t, natGatewayIDs, 1, "Should create 1 NAT Gateway")

	natGatewayIPs := terraform.OutputList(t, terraformOptions, "nat_gateway_ips")
	assert.Len(t, natGatewayIPs, 1, "Should allocate 1 Elastic IP")
	
	// Validate IP format
	assert.Regexp(t, `^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$`, natGatewayIPs[0], 
		"NAT Gateway IP should be valid IPv4")
}
```

---

**Exécuter tests :**

```bash
cd terraform/test

# Run all tests
go test -v -timeout 30m

# Run specific test
go test -v -run TestVPCCreation -timeout 30m
```

**Résultat :**

```
=== RUN   TestVPCCreation
=== PAUSE TestVPCCreation
=== CONT  TestVPCCreation
TestVPCCreation 2024-12-17T00:00:00Z logger.go:66: Running command terraform with args [init -upgrade=false]
TestVPCCreation 2024-12-17T00:00:05Z logger.go:66: Initializing the backend...
TestVPCCreation 2024-12-17T00:00:10Z logger.go:66: Running command terraform with args [apply -input=false -auto-approve]
TestVPCCreation 2024-12-17T00:00:15Z logger.go:66: aws_vpc.main: Creating...
TestVPCCreation 2024-12-17T00:00:17Z logger.go:66: aws_vpc.main: Creation complete [id=vpc-0abc123]
...
TestVPCCreation 2024-12-17T00:03:45Z logger.go:66: Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
TestVPCCreation 2024-12-17T00:03:45Z logger.go:66: Running command terraform with args [output -no-color vpc_id]
TestVPCCreation 2024-12-17T00:03:46Z logger.go:66: vpc-0abc123def456
TestVPCCreation 2024-12-17T00:03:46Z logger.go:66: Running command terraform with args [destroy -auto-approve -input=false]
...
TestVPCCreation 2024-12-17T00:06:30Z logger.go:66: Destroy complete! Resources: 12 destroyed.
--- PASS: TestVPCCreation (390.45s)
PASS
ok  	github.com/sirrdev/terraform-tests	390.500s
```

**[OK] Tests passés, infrastructure validée !**

---

#### Tests CDK avec Jest

**CDK inclut support tests natif**

**Installer Jest :**

```bash
cd ~/iac-workshop/cdk

npm install --save-dev jest @types/jest ts-jest
npm install --save-dev @aws-cdk/assert
```

---

**Configurer Jest (`jest.config.js`) :**

```bash
nano jest.config.js
```

**Contenu :**

```javascript
module.exports = {
  testEnvironment: 'node',
  roots: ['<rootDir>/test'],
  testMatch: ['**/*.test.ts'],
  transform: {
    '^.+\\.tsx?$': 'ts-jest'
  }
};
```

---

**Créer test `test/cdk-stack.test.ts` :**

```bash
nano test/cdk-stack.test.ts
```

**Contenu :**

```typescript
// ═══════════════════════════════════════════════════════════════
// CDK UNIT TESTS
// ═══════════════════════════════════════════════════════════════

import * as cdk from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { VpcStack } from '../lib/cdk-stack';

describe('VPC Stack', () => {
  let app: cdk.App;
  let stack: VpcStack;
  let template: Template;

  beforeEach(() => {
    app = new cdk.App();
    stack = new VpcStack(app, 'TestVpcStack', {
      environmentName: 'test',
      vpcCidr: '10.99.0.0/16',
      maxAzs: 2,
      natGateways: 1,
    });
    template = Template.fromStack(stack);
  });

  // ───────────────────────────────────────────────────────────
  // Test: VPC created
  // ───────────────────────────────────────────────────────────

  test('VPC Created', () => {
    template.hasResourceProperties('AWS::EC2::VPC', {
      CidrBlock: '10.99.0.0/16',
      EnableDnsHostnames: true,
      EnableDnsSupport: true,
    });
  });

  // ───────────────────────────────────────────────────────────
  // Test: Correct number of subnets
  // ───────────────────────────────────────────────────────────

  test('Creates 6 Subnets (2 AZs × 3 types)', () => {
    template.resourceCountIs('AWS::EC2::Subnet', 6);
  });

  // ───────────────────────────────────────────────────────────
  // Test: NAT Gateway created
  // ───────────────────────────────────────────────────────────

  test('Creates NAT Gateway', () => {
    template.resourceCountIs('AWS::EC2::NatGateway', 1);
  });

  // ───────────────────────────────────────────────────────────
  // Test: Internet Gateway created
  // ───────────────────────────────────────────────────────────

  test('Creates Internet Gateway', () => {
    template.resourceCountIs('AWS::EC2::InternetGateway', 1);
  });

  // ───────────────────────────────────────────────────────────
  // Test: Tags applied
  // ───────────────────────────────────────────────────────────

  test('VPC has correct tags', () => {
    template.hasResourceProperties('AWS::EC2::VPC', {
      Tags: Match.arrayWith([
        { Key: 'Name', Value: 'test-vpc' },
        { Key: 'Environment', Value: 'test' },
        { Key: 'ManagedBy', Value: 'CDK' },
      ]),
    });
  });

  // ───────────────────────────────────────────────────────────
  // Test: Outputs exported
  // ───────────────────────────────────────────────────────────

  test('Stack has outputs', () => {
    template.hasOutput('VpcId', {});
    template.hasOutput('VpcCidr', {});
    template.hasOutput('PublicSubnets', {});
    template.hasOutput('PrivateSubnets', {});
  });

  // ───────────────────────────────────────────────────────────
  // Test: No hardcoded values
  // ───────────────────────────────────────────────────────────

  test('No hardcoded availability zones', () => {
    // Subnets should use Fn::Select with Fn::GetAZs
    template.hasResourceProperties('AWS::EC2::Subnet', {
      AvailabilityZone: Match.objectLike({
        'Fn::Select': Match.anyValue(),
      }),
    });
  });
});

describe('VPC Stack - Production Configuration', () => {
  test('Production uses Multi-AZ NAT Gateways', () => {
    const app = new cdk.App();
    const stack = new VpcStack(app, 'ProdVpcStack', {
      environmentName: 'production',
      vpcCidr: '10.0.0.0/16',
      maxAzs: 2,
      natGateways: 2, // HA
    });

    const template = Template.fromStack(stack);
    
    // Should have 2 NAT Gateways for HA
    template.resourceCountIs('AWS::EC2::NatGateway', 2);
  });

  test('Dev uses Single NAT Gateway', () => {
    const app = new cdk.App();
    const stack = new VpcStack(app, 'DevVpcStack', {
      environmentName: 'dev',
      vpcCidr: '10.1.0.0/16',
      maxAzs: 1,
      natGateways: 1, // Cost optimization
    });

    const template = Template.fromStack(stack);
    
    // Should have only 1 NAT Gateway
    template.resourceCountIs('AWS::EC2::NatGateway', 1);
  });
});
```

---

**Exécuter tests CDK :**

```bash
npm test
```

**Résultat :**

```
 PASS  test/cdk-stack.test.ts
  VPC Stack
    [OK] VPC Created (45 ms)
    [OK] Creates 6 Subnets (2 AZs × 3 types) (12 ms)
    [OK] Creates NAT Gateway (8 ms)
    [OK] Creates Internet Gateway (6 ms)
    [OK] VPC has correct tags (15 ms)
    [OK] Stack has outputs (10 ms)
    [OK] No hardcoded availability zones (18 ms)
  VPC Stack - Production Configuration
    [OK] Production uses Multi-AZ NAT Gateways (22 ms)
    [OK] Dev uses Single NAT Gateway (19 ms)

Test Suites: 1 passed, 1 total
Tests:       9 passed, 9 total
Snapshots:   0 total
Time:        2.456 s
```

**[OK] Tous les tests passés !**

---

#### Policy as Code (OPA / Sentinel)

**OPA (Open Policy Agent) = Validation policies pour IaC**

**Exemple : Interdire ressources sans tags**

**Créer `policy/require-tags.rego` :**

```rego
package terraform.analysis

import input as tfplan

# Deny if resource missing required tags
deny[reason] {
  resource := tfplan.resource_changes[_]
  resource.change.actions[_] == "create"
  
  required_tags := ["Environment", "ManagedBy", "Owner"]
  
  resource_tags := object.get(resource.change.after, "tags", {})
  
  missing_tag := required_tags[_]
  not resource_tags[missing_tag]
  
  reason := sprintf(
    "Resource %s is missing required tag: %s",
    [resource.address, missing_tag]
  )
}

# Deny production resources without encryption
deny[reason] {
  resource := tfplan.resource_changes[_]
  resource.type == "aws_s3_bucket"
  resource.change.after.tags.Environment == "production"
  
  not resource.change.after.server_side_encryption_configuration
  
  reason := sprintf(
    "Production S3 bucket %s must have encryption enabled",
    [resource.address]
  )
}
```

---

**Valider avec OPA :**

```bash
# Installer OPA
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64
chmod +x opa
sudo mv opa /usr/local/bin/

# Générer Terraform plan en JSON
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

# Valider avec OPA
opa eval --data policy/require-tags.rego \
         --input tfplan.json \
         --format pretty \
         'data.terraform.analysis.deny'
```

**Si violations :**

```json
[
  "Resource aws_vpc.main is missing required tag: Owner",
  "Production S3 bucket aws_s3_bucket.logs must have encryption enabled"
]
```

**Fix violations, re-run, policy passes [OK]**

---

### ÉTAPE 10 : Troubleshooting IaC

#### Erreur 1 : CloudFormation Stack Stuck

**Symptôme :**

```
Stack: CREATE_IN_PROGRESS
  (30 minutes, aucun progrès)
```

**Cause : Resource dependency cycle ou timeout**

---

**Diagnostic :**

```bash
aws cloudformation describe-stack-events \
  --stack-name vpc-production \
  --query 'StackEvents[?ResourceStatus==`CREATE_IN_PROGRESS`]'
```

**Résultat :**

```json
[
  {
    "ResourceType": "AWS::EC2::NatGateway",
    "ResourceStatus": "CREATE_IN_PROGRESS",
    "ResourceStatusReason": "Resource creation Initiated",
    "Timestamp": "2024-12-17T00:00:00.000Z"
  }
]
```

**NAT Gateway stuck (prend parfois 5-10 min)**

---

**Solution : Attendre ou cancel**

```bash
# Cancel stack creation
aws cloudformation cancel-update-stack --stack-name vpc-production

# Ou delete et retry
aws cloudformation delete-stack --stack-name vpc-production
```

---

#### Erreur 2 : Terraform State Lock

**Symptôme :**

```
Error: Error acquiring the state lock

Lock Info:
  ID: abc123-def456
  Path: s3://bucket/terraform.tfstate
  Operation: OperationTypePlan
  Who: jenkins@ci-server
  Created: 2024-12-17 12:00:00
```

**Cause : Lock pas released (crash, Ctrl+C, etc.)**

---

**Solution : Force unlock**

```bash
# Vérifier que personne utilise vraiment
# Puis force unlock
terraform force-unlock abc123-def456
```

**[ATTENTION] Dangereux si quelqu'un utilise vraiment !**

---

**Prévention : Timeouts automatiques**

**DynamoDB TTL sur locks :**

```hcl
resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
  
  attribute {
    name = "LockID"
    type = "S"
  }
  
  # Auto-expire locks after 1 hour
  ttl {
    attribute_name = "Expires"
    enabled        = true
  }
}
```

---

#### Erreur 3 : Terraform Drift Detection

**Problème : Ressources modifiées manuellement (Console AWS)**

```
Terraform state: Instance type = t3.micro
AWS reality: Instance type = t3.medium (changé dans Console)

-> Drift (divergence)
```

---

**Détecter drift :**

```bash
terraform plan -refresh-only
```

**Résultat :**

```
Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the last "terraform apply":

  # aws_instance.app has changed
  ~ resource "aws_instance" "app" {
        id                    = "i-abc123"
      ~ instance_type         = "t3.medium" # was t3.micro
        # (25 unchanged attributes hidden)
    }

Unless you have made equivalent changes to your configuration, or ignored the relevant
attributes using ignore_changes, the following plan may include actions to undo or
respond to these changes.
```

**Drift détecté [OK]**

---

**Options :**

**1. Import changes (accepter drift) :**

```bash
# Mettre à jour Terraform code pour matcher reality
# main.tf
resource "aws_instance" "app" {
  instance_type = "t3.medium"  # Update to match AWS
}

terraform apply  # Synchronize state
```

---

**2. Revert changes (reset to Terraform state) :**

```bash
# Terraform va recréer/modifier pour matcher code
terraform apply
# Will change instance type back to t3.micro
```

---

**3. Ignore specific attributes :**

```hcl
resource "aws_instance" "app" {
  instance_type = "t3.micro"
  
  lifecycle {
    ignore_changes = [
      instance_type,  # Ignore manual changes to instance type
      tags,
    ]
  }
}
```

---

#### Erreur 4 : CDK Bootstrap Version Mismatch

**Symptôme :**

```
Error: This CDK deployment requires bootstrap stack version '18', 
but the deployed stack version is '14'
```

**Cause : CDK CLI upgraded, bootstrap pas**

---

**Solution : Re-bootstrap**

```bash
cdk bootstrap --force aws://123456789012/eu-west-1
```

**Upgrade bootstrap stack [OK]**

---

#### Erreur 5 : Resource Already Exists

**CloudFormation :**

```
CREATE_FAILED: VPC (vpc-abc123) already exists
```

**Terraform :**

```
Error: VPC already exists
```

---

**Solution : Import existing resource**

**CloudFormation : Use resource import**

```bash
# Create import file
cat > resources-to-import.txt << EOF
VPC,vpc-abc123
EOF

# Import
aws cloudformation create-change-set \
  --stack-name vpc-production \
  --change-set-name import-vpc \
  --change-set-type IMPORT \
  --resources-to-import file://resources-to-import.txt \
  --template-body file://vpc.yaml

# Execute import
aws cloudformation execute-change-set \
  --change-set-name import-vpc \
  --stack-name vpc-production
```

---

**Terraform : terraform import**

```bash
# Import VPC
terraform import aws_vpc.main vpc-abc123

# Import subnet
terraform import aws_subnet.public[0] subnet-def456

# Import all resources one by one
# Then run terraform plan to verify
```

---

#### Erreur 6 : Circular Dependency

**Symptôme :**

```
Error: Cycle: aws_security_group.app, aws_security_group.db

resource "aws_security_group" "app" {
  ingress {
    security_groups = [aws_security_group.db.id]  # App -> DB
  }
}

resource "aws_security_group" "db" {
  ingress {
    security_groups = [aws_security_group.app.id]  # DB -> App
  }
}

-> Circular dependency [X]
```

---

**Solution : Separate rules**

```hcl
# Security Groups (no rules)
resource "aws_security_group" "app" {
  name = "app-sg"
  vpc_id = aws_vpc.main.id
}

resource "aws_security_group" "db" {
  name = "db-sg"
  vpc_id = aws_vpc.main.id
}

# Rules (separate resources)
resource "aws_security_group_rule" "app_to_db" {
  type                     = "egress"
  from_port                = 5432
  to_port                  = 5432
  protocol                 = "tcp"
  security_group_id        = aws_security_group.app.id
  source_security_group_id = aws_security_group.db.id
}

resource "aws_security_group_rule" "db_from_app" {
  type                     = "ingress"
  from_port                = 5432
  to_port                  = 5432
  protocol                 = "tcp"
  security_group_id        = aws_security_group.db.id
  source_security_group_id = aws_security_group.app.id
}
```

**No circular dependency [OK]**

---

### [OK] TESTS DE VALIDATION

**CloudFormation :**
- [ ] Template validé syntaxiquement
- [ ] Stack créé avec succès
- [ ] Change sets utilisés pour updates
- [ ] Nested stacks déployés
- [ ] Outputs exportés correctement
- [ ] Rollback testé

**Terraform :**
- [ ] Configuration validée (`terraform validate`)
- [ ] Code formaté (`terraform fmt`)
- [ ] Plan généré sans erreurs
- [ ] Infrastructure déployée (`terraform apply`)
- [ ] State stocké dans S3
- [ ] State locking fonctionne (DynamoDB)
- [ ] Modules créés et réutilisables
- [ ] Multi-environnement testé (workspaces)
- [ ] Drift detection testé
- [ ] Import de ressources existantes testé

**CDK :**
- [ ] Projet CDK initialisé
- [ ] Bootstrap exécuté
- [ ] Stacks synthétisés (`cdk synth`)
- [ ] Diff affiché (`cdk diff`)
- [ ] Stacks déployés (`cdk deploy`)
- [ ] Tests unitaires passés (Jest)
- [ ] Constructs L2/L3 utilisés
- [ ] CloudFormation généré optimisé

**CI/CD :**
- [ ] GitHub Actions workflow créé
- [ ] Validation automatique (lint, fmt)
- [ ] Security scan configuré (tfsec, checkov)
- [ ] Plan automatique sur PR
- [ ] Cost estimation intégrée (Infracost)
- [ ] Manual approval pour production
- [ ] Apply automatique sur merge
- [ ] Notifications configurées (Slack)

**Tests :**
- [ ] Tests unitaires écrits (Terratest/Jest)
- [ ] Tests d'intégration exécutés
- [ ] Policy as Code validé (OPA)
- [ ] Coverage infrastructure > 80%

**Best Practices :**
- [ ] Code versionné dans Git
- [ ] Documentation à jour (README.md)
- [ ] Secrets stockés sécurisés (pas hardcodés)
- [ ] Tags appliqués sur toutes ressources
- [ ] Naming convention cohérente
- [ ] Multi-environnement configuré
- [ ] State backend sécurisé (encryption)
- [ ] Backup state configuré (versioning)

---

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

**Infrastructure as Code - Principes :**
- IaC = Infrastructure décrite en code (versionnable, reproductible)
- Declarative > Imperative (what vs how)
- Idempotence = Run multiple fois -> même résultat
- State management = Track resources créées
- Drift detection = Détecter changements manuels

**CloudFormation :**
- AWS-native, state managed par AWS
- YAML/JSON format
- Nested stacks pour modularité
- Change sets pour preview
- Rollback automatique
- Gratuit [OK]

**Terraform :**
- Multi-cloud, open-source
- HCL (HashiCorp Configuration Language)
- State self-managed (S3 backend recommandé)
- Modules pour réutilisation
- Plan -> Apply workflow
- Grande communauté

**CDK :**
- Infrastructure en langages programmation
- Abstractions puissantes (L2/L3 constructs)
- Synthétise vers CloudFormation
- Tests unitaires natifs
- Type safety (TypeScript)
- Verbosité réduite (90% moins de code)

**State Management :**
- CloudFormation : AWS-managed (automatic)
- Terraform : S3 + DynamoDB locking
- CDK : AWS-managed (via CloudFormation)
- Versioning crucial (rollback)
- Encryption at rest obligatoire

**Best Practices :**
- Version control (Git)
- Code review (Pull Requests)
- Automated testing (CI/CD)
- Security scanning (tfsec, checkov)
- Cost estimation (Infracost)
- Multi-environment (dev/staging/prod)
- Secrets management (Secrets Manager)
- Tagging strategy
- Documentation
- Backup state

**CI/CD for IaC :**
- Validate -> Plan -> Manual Approval -> Apply
- Automated on PR (validation)
- Automated on merge (deployment)
- Notifications (Slack, email)
- Audit trail (Git history)

**Testing :**
- Unit tests (Terratest, Jest)
- Integration tests (deploy -> validate -> destroy)
- Policy as Code (OPA)
- Security tests (tfsec)
- Cost tests (budget limits)

---

### [RAPIDE] POUR ALLER PLUS LOIN

**1. Terraform Cloud / Enterprise**

**Terraform Cloud = SaaS pour collaboration Terraform**

```
Features:
  - Remote state management (no S3 setup)
  - Remote execution (runs in cloud)
  - Sentinel (policy as code)
  - Private module registry
  - Cost estimation built-in
  - Collaboration (team management)
  - Audit logging
  - RBAC (role-based access)

Pricing:
  - Free: Up to 5 users
  - Team: 20$/user/month
  - Enterprise: Custom
```

---

**2. Pulumi (Infrastructure as Code 2.0)**

**Pulumi = IaC avec langages programmation NATIFS**

```typescript
import * as aws from "@pulumi/aws";

// Create VPC (real TypeScript, not DSL)
const vpc = new aws.ec2.Vpc("main", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
});

// Use standard programming constructs
const subnets = ["10.0.1.0/24", "10.0.2.0/24"].map((cidr, i) => 
    new aws.ec2.Subnet(`public-${i}`, {
        vpcId: vpc.id,
        cidrBlock: cidr,
        availabilityZone: aws.getAvailabilityZones().then(azs => azs.names[i]),
    })
);

// Export outputs
export const vpcId = vpc.id;
export const subnetIds = subnets.map(s => s.id);
```

**Avantages vs CDK :**
- Multi-cloud natif (AWS, Azure, GCP, K8s)
- Pas de synth step (direct)
- Support plus langages (Python, Go, C#, Java)
- State management built-in

---

**3. Terragrunt (DRY Terraform)**

**Terragrunt = Wrapper Terraform pour DRY**

```hcl
# terragrunt.hcl (root)
remote_state {
  backend = "s3"
  config = {
    bucket = "sirrdev-terraform-state"
    key    = "${path_relative_to_include()}/terraform.tfstate"
    region = "eu-west-1"
  }
}

# environments/dev/vpc/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/vpc"
}

inputs = {
  environment = "dev"
  vpc_cidr    = "10.1.0.0/16"
}

# Run all environments at once
terragrunt run-all apply
```

**Évite duplication backend config [OK]**

---

**4. Atlantis (Terraform PR Automation)**

**Atlantis = Bot GitHub/GitLab pour Terraform automation**

```yaml
# atlantis.yaml
version: 3
projects:
- name: vpc-production
  dir: terraform/vpc
  terraform_version: v1.6.5
  autoplan:
    when_modified: ["*.tf", "*.tfvars"]
  workflow: production

workflows:
  production:
    plan:
      steps:
      - init
      - plan
    apply:
      steps:
      - apply
```

**Workflow :**

```
1. Developer: Create PR
2. Atlantis: Auto-comment with terraform plan
3. Team: Review plan in PR comment
4. Developer: Comment "atlantis apply"
5. Atlantis: Runs terraform apply, comments result
6. PR merged

-> Terraform automation in GitHub [OK]
```

---

**5. Crossplane (Kubernetes for Infrastructure)**

**Crossplane = Manage cloud infra via Kubernetes CRDs**

```yaml
apiVersion: ec2.aws.crossplane.io/v1beta1
kind: VPC
metadata:
  name: production-vpc
spec:
  forProvider:
    region: eu-west-1
    cidrBlock: 10.0.0.0/16
    enableDnsHostNames: true
  providerConfigRef:
    name: aws-provider
```

**Apply via kubectl :**

```bash
kubectl apply -f vpc.yaml
```

**Infrastructure = Kubernetes resources [OK]**

---

**6. AWS Proton (Managed IaC Service)**

**Proton = AWS-managed service templates**

```
Features:
  - Pre-built templates (VPC, ECS, etc.)
  - Self-service portal for developers
  - Centralized governance
  - Automated deployments
  - Version control built-in

Use case:
  Platform team: Create templates
  Dev teams: Deploy from templates (no IaC knowledge needed)
```

---

**7. Spacelift (Terraform/Pulumi/CloudFormation CI/CD)**

**Spacelift = Advanced CI/CD for IaC**

```
Features:
  - Multi-IaC support (Terraform, Pulumi, CFN, Ansible)
  - Policy as Code (OPA)
  - Drift detection scheduled
  - Cost estimation
  - Private worker pools
  - RBAC advanced
  - Audit logs
  - Dependencies between stacks

Pricing: ~50$/user/month
```

---

## [COURS] CONCLUSION DE L'EXERCICE 8

**[BRAVO] EXCELLENT ! Tu maîtrises maintenant l'Infrastructure as Code ! [BRAVO]**

**Ce que tu as appris :**
- [OK] Concepts IaC (declarative, idempotent, state)
- [OK] CloudFormation (YAML, nested stacks, change sets)
- [OK] Terraform (HCL, modules, state management)
- [OK] Terraform backend (S3 + DynamoDB)
- [OK] Terraform workspaces (multi-env)
- [OK] AWS CDK (TypeScript, constructs L2/L3)
- [OK] CI/CD for IaC (GitHub Actions)
- [OK] Testing infrastructure (Terratest, Jest)
- [OK] Policy as Code (OPA)
- [OK] Troubleshooting IaC
- [OK] Drift detection
- [OK] Import existing resources

**Compétences acquises :**
- [OK] Infrastructure versionnable
- [OK] Infrastructure reproductible
- [OK] Infrastructure testable
- [OK] Multi-environnement
- [OK] GitOps workflows
- [OK] Collaboration équipe

**Outils maîtrisés :**
```
[OK] AWS CloudFormation
[OK] HashiCorp Terraform
[OK] AWS CDK
[OK] Terratest
[OK] GitHub Actions
[OK] OPA
[OK] Infracost
```

**Architecture déployée (tous outils) :**
```
[OK] VPC multi-AZ (CloudFormation)
[OK] VPC avec modules (Terraform)
[OK] VPC avec constructs (CDK)
[OK] RDS (tous outils)
[OK] ECS (CDK)
[OK] State backend (S3 + DynamoDB)
[OK] CI/CD pipeline
[OK] Tests automatisés
```

**Best practices appliquées :**
- Version control (Git)
- Code review (PR)
- Automated testing
- Security scanning
- Cost estimation
- Documentation
- Tagging strategy
- Multi-environment
- Secrets management
- Backup & versioning

**Comparaison outils :**

| Use Case | Meilleur choix |
|----------|----------------|
| AWS-only, équipe ops | CloudFormation |
| Multi-cloud | Terraform |
| Développeurs écrivent infra | CDK |
| Abstractions maximales | CDK (L3) |
| Communauté large | Terraform |
| Rollback automatique | CloudFormation/CDK |
| Tests unitaires | CDK |

**Temps moyen de réalisation :** 7-8 heures

**Série d'exercices AWS complétée !** [BRAVO]

**Exercices 1-8 recap :**
1. [OK] EC2 + VPC + IAM
2. [OK] S3 + Glacier + Lifecycle
3. [OK] RDS + Multi-AZ + Backups
4. [OK] Lambda + API Gateway + DynamoDB
5. [OK] Advanced Networking (NAT, Peering, Endpoints)
6. [OK] Load Balancing + Auto Scaling
7. [OK] Containers (ECS + Fargate + ECR)
8. [OK] Infrastructure as Code (CFN + Terraform + CDK)

**Tu es maintenant prêt pour :**
- Architecte solutions AWS
- DevOps Engineer
- SRE (Site Reliability Engineer)
- Cloud Infrastructure Engineer
- Certifications AWS (Solutions Architect, DevOps Engineer)

---

**[ATTENTION] NETTOYAGE DES RESSOURCES**

```bash
# ══════════════════════════════════════════════════════════
# CLOUDFORMATION
# ══════════════════════════════════════════════════════════

# Supprimer stacks
aws cloudformation delete-stack --stack-name vpc-production
aws cloudformation delete-stack --stack-name master-production

# Vider et supprimer S3 bucket (templates)
aws s3 rm s3://sirrdev-cloudformation-templates --recursive
aws s3 rb s3://sirrdev-cloudformation-templates

# ══════════════════════════════════════════════════════════
# TERRAFORM
# ══════════════════════════════════════════════════════════

cd ~/iac-workshop/terraform

# Détruire infrastructure (tous workspaces)
terraform workspace select default
terraform destroy -auto-approve

terraform workspace select dev
terraform destroy -auto-approve

terraform workspace select staging
terraform destroy -auto-approve

# Supprimer backend infrastructure
terraform destroy -target=aws_s3_bucket.terraform_state
terraform destroy -target=aws_dynamodb_table.terraform_locks

# Vider S3 state bucket
aws s3 rm s3://sirrdev-terraform-state-eu-west-1 --recursive
aws s3 rb s3://sirrdev-terraform-state-eu-west-1

# ══════════════════════════════════════════════════════════
# CDK
# ══════════════════════════════════════════════════════════

cd ~/iac-workshop/cdk

# Détruire tous les stacks
cdk destroy --all

# Supprimer bootstrap stack (optional, peut être réutilisé)
# aws cloudformation delete-stack --stack-name CDKToolkit

# ══════════════════════════════════════════════════════════
# VERIFICATION FINALE
# ══════════════════════════════════════════════════════════

# Lister toutes les stacks CloudFormation
aws cloudformation list-stacks \
  --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \
  --query 'StackSummaries[*].StackName'

# Doit retourner: [] ou seulement CDKToolkit

# Lister VPCs
aws ec2 describe-vpcs \
  --filters "Name=tag:ManagedBy,Values=Terraform" \
            "Name=tag:ManagedBy,Values=CloudFormation" \
            "Name=tag:ManagedBy,Values=CDK" \
  --query 'Vpcs[*].VpcId'

# Doit retourner: []

# Lister buckets S3
aws s3 ls | grep -E "(terraform|cloudformation|cdk)"

# Doit retourner: vide (ou seulement CDK bootstrap bucket si gardé)
```

**[OK] Ressources nettoyées !**

---

**FIN DE L'EXERCICE 8 ET DE LA SÉRIE AWS**

═══════════════════════════════════════════════════════════════

**Félicitations ! [BRAVO][BRAVO][BRAVO]**

Tu as complété une formation AWS complète couvrant :
- Compute (EC2, Lambda, ECS/Fargate)
- Storage (S3, EBS, Glacier)
- Database (RDS, DynamoDB)
- Networking (VPC, ALB, NAT, Peering)
- Security (IAM, Security Groups, Secrets Manager)
- Monitoring (CloudWatch, X-Ray)
- Infrastructure as Code (CloudFormation, Terraform, CDK)
- CI/CD (CodePipeline, GitHub Actions)
- Containers (Docker, ECS, ECR)
- Serverless (Lambda, API Gateway)

**Prochaines étapes suggérées :**
1. [DOC] Certifications AWS (Solutions Architect, DevOps)
2. [OUTIL] Projets personnels (portfolio)
3. [DOCS] Spécialisation (Kubernetes, Security, Machine Learning)
4. [PRO] Postuler à des postes Cloud Engineer / DevOps

**Bon courage pour la suite ! [RAPIDE]**